1.分类:单链表、双链表、循环链表(可以用来解决约瑟夫环问题)。
2.存储方式:链表的节点在内存中是分散存储的,通过指针连在一起。
3.时间复杂度:链表的增添和删除都是O(1);查找的时间复杂度是O(n)。
public class ListNode {
// 结点的值
int val;
// 下一个结点
ListNode next;
// 节点的构造函数(无参)
public ListNode() {
}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
双指针:
1.首先排除链表 headA 和 headB为空的情况;
2.创建两个指针 pA和 pB,初始时分别指向两个链表的头节点 headA和 headB,然后将两个指针依次遍历两个链表的每个节点。
3.当指针 pA 为空,则将指针 pA\ 移到链表 headB 的头节点;如果指针 pB为空,则将指针 pB移到链表 headA\的头节点。
4.当指针 pA 和 pB指向同一个节点或者都为空时,返回它们指向的节点或者 null。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null || headB == null){
return null;
}
ListNode pA = headA;
ListNode pB = headB;
while(pA != pB){
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
迭代法/双指针:
遍历链表时,将当前节点的 next指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。在更改引用之前,还需要存储后一个节点。最后返回新的头引用。
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while(cur != null){
ListNode next=cur.next;
cur.next=pre;
pre = cur;
cur=next;
}
return pre;
}
时间复杂度:O(n),其中 n是链表的长度。需要遍历链表一次。
空间复杂度:O(1)。
方法一:将值复制到数组列表中,然后用双指针法。时间复杂度:O(n) 空间复杂度:O(n)
方法二:快慢指针
下列代码位恢复链表。要恢复,需要找到中间结点的前一个结点。因此while循环里,改为fast.next != null && fast.next.next !=null,然后反转函数传参变成slow.next。
public boolean isPalindrome(ListNode head) {
if(head == null){
return true;
}
ListNode fast = head;
ListNode slow = head;
//奇数个数时,fast.next==null
//偶数个数时,fast==null
while(fast != null && fast.next != null){
fast=fast.next.next;
slow=slow.next;
}
//奇数情况
if(fast != null){
slow=slow.next;
}
ListNode newHead = reverseList(slow);
while(newHead != null ){
if(head.val != newHead.val){
return false;
}
head = head.next;
newHead=newHead.next;
}
return true;
}
private ListNode reverseList(ListNode head){
ListNode cur = head;
ListNode pre = null;
while(cur != null){
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur =next;
}
return pre;
}
时间复杂度:O(n)
空间复杂度:O(1)
快慢指针(龟兔赛跑算法):
定义两个指针,一快一慢。慢指针每次只移动一步,而快指针每次移动两步。
初始时,慢指针在位置 head,而快指针在位置 head.next。
在移动的过程中,当快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。
注意:快慢指针的初始化与「乌龟」和「兔子」中的叙述不同(都从head开始):
这是为了保证循环的执行,否则,循环一开始就不会执行。
public boolean hasCycle(ListNode head) {
if(head == null || head.next == null){
return false;
}
ListNode fast = head.next;
ListNode slow = head;
while(fast!=slow){
if(fast == null || fast.next == null){
return false;
}
fast=fast.next.next;
slow = slow.next;
}
return true;
}
快慢指针:
与上一题一样,只不过循环变成fast !=null && fast.next!=null。先在slow与fast相遇的时候,一定时在环上相遇。然后初始化一个指针从相遇点一步一步往前走,同时head也一步一步往前,当两者相遇时,就是环的入口。
public ListNode detectCycle(ListNode head) {
if(head == null || head.next==null){
return null;
}
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next !=null){
slow=slow.next;
fast=fast.next.next;
//有环
if(slow == fast){
ListNode p=fast;
while(p != head){
head=head.next;
p=p.next;
}
return p;
}
}
return null;
}
slow 指针走过的距离不会超过链表的总长度;随后寻找入环点时,走过的距离也不会超过链表的总长度。因此,总的执行时间为 O(N)+O(N)=O(N)
迭代(哨兵结点):
1.设定一个哨兵节点 preHead ,便于返回合并后的链表。
2.维护一个 prev 指针,调整它的 next 指针。
3.循环将更小的值添加到pre后面,并把 pre 向后移一位。直到有一个链表的指针指向了 null 。
4.在循环终止的时候,至多有一个链表是非空的。我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if(list1 == null){
return list2;
}else if(list2==null){
return list1;
}
//设定一个哨兵结点,也就是虚拟头结点;
ListNode preHead = new ListNode(-1);
ListNode pre = preHead;
while(list1 != null && list2 !=null){
if(list1.val > list2.val){
pre.next = list2;
list2=list2.next;
}else {
pre.next= list1;
list1=list1.next;
}
pre =pre.next;
}
pre.next = list1==null ? list2 : list1;
return preHead.next;
}
由于输入的两个链表都是逆序存储数字的位数的,因此两个链表中同一位置的数字可以直接相加。
1.同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。
2.如果当前两个链表处相应位置的数字为 n1,n2,进位值为 carry,则它们的和为 n1+n2+carry;其中,答案链表处相应位置的数字为 (n1+n2+carry) mod 10,而新的进位值为 (n1+n2+carry)/10。
3.如果链表遍历结束后,有 carry>0,还需要在答案链表的后面附加一个节点,节点的值为 carry。
注意:其中涉及到很多判断!!!
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = null,tail=null;
int carry=0;
while(l1!=null || l2 !=null){
int n1 = l1 != null ? l1.val : 0;
int n2 = l2 !=null ? l2.val:0;
int sum = n1+n2+carry;
if(head == null){
head = tail = new ListNode(sum%10);
}else{
tail.next = new ListNode(sum%10);
tail=tail.next;
}
carry=sum /10;
if(l1!=null){
l1=l1.next;
}
if(l2!=null){
l2=l2.next;
}
}
if(carry > 0){
tail.next=new ListNode(carry);
}
return head;
}
双指针(哨兵结点)
要删除倒数第n个节点,我们要得到的是倒数第 n 个节点的前驱节点而不是倒数第 n个节点。
我们可以使用两个指针fast和 slow同时对链表进行遍历,并且 fast比 slow超前 n+1 个节点。当 first 遍历到链表的末尾时,slow就恰好处于倒数第 n 个节点的前一个。
具体地,初始时fast指向头节点,slow指向哑节点,。我们首先使用 fast对链表进行遍历,遍历的次数为 n。此时,fast比 slow 超前了 n+1 个节点。
在这之后,我们同时使用fast和 slow 对链表进行遍历。当 fast遍历到链表的末尾(即fast为空指针)时,slow 恰好指向倒数第 n个节点。
public ListNode removeNthFromEnd(ListNode head, int n) {
//哨兵结点
ListNode dummy=new ListNode(0,head);
ListNode slow=dummy;
ListNode fast=head;
for(int i=0; i
时间复杂度:O(n)
空间复杂度:O(1)
迭代:哨兵结点
public ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(0,head);
ListNode pre = dummy;
while(pre.next != null && pre.next.next !=null){
// 记录第一个结点
ListNode n1 = pre.next;
// 记录第二个结点
ListNode n2 =pre.next.next;
// 指针指向第二个结点
pre.next = n2;
// 第一个结点指向第二个节点的后面
n1.next = n2.next;
// 第二个结点指向第一个结点
n2.next=n1;
// 移动指针至下一个位置
pre =n1;
}
return dummy.next;
}
时间复杂度:O(n)
空间复杂度:O(1)
本题难点: 在复制链表的过程中构建新链表各节点的 random
引用指向。遍历复制,无法复制到random指针。
迭代 + 节点拆分:
考虑构建 原节点 1 -> 新节点 1 -> 原节点 2 -> 新节点 2 -> …… 的拼接链表,如此便可在访问原节点的 random 指向节点的同时找到新对应新节点的 random 指向节点。
需要注意原节点的随机指针可能为空,我们需要特别判断这种情况。
当我们完成了拷贝节点的随机指针的赋值,我们只需要将这个链表按照原节点与拷贝节点的种类进行拆分即可,只需要遍历一次。同样需要注意原链表的最后一个节点的后继节点为空,我们需要特别判断这种情况。
public Node copyRandomList(Node head) {
if(head == null){
return null;
}
// 1. 复制各节点,并构建拼接链表
Node cur = head;
while(cur != null){
Node temp = new Node(cur.val);
temp.next = cur.next;
cur.next = temp;
cur=temp.next;
}
// 2. 构建各新节点的 random 指向
cur = head;
while(cur != null){
if(cur.random != null){
cur.next.random=cur.random.next;
}
cur = cur.next.next;
}
// 3. 拆分两链表
cur = head.next;
Node pre = head, res=head.next;
while(cur.next !=null){
pre.next = pre.next.next;
cur.next = cur.next.next;
cur = cur.next;
pre=pre.next;
}
pre.next=null;// 单独处理原链表尾节点
return res;// 返回新链表头节点
}
时间复杂度是 O(nlogn)的排序算法包括归并排序、堆排序和快速排序(快速排序的最差时间复杂度是 O(n^2),其中最适合链表的排序算法是归并排序。
归并排序基于分治算法。最容易想到的实现方式是自顶向下的递归实现,考虑到递归调用的栈空间,自顶向下归并排序的空间复杂度是 O(logn)。如果要达到 O(1)的空间复杂度,则需要使用自底向上的实现方式。
public ListNode sortList(ListNode head) {
if(head == null){
return null;
}
// 遍历得到链表的长度
int len = 0;
ListNode cur = head;
while(cur != null){
len++;
cur = cur.next;
}
// 哨兵结点
ListNode dummy = new ListNode(-1,head);
//将链表拆成若干长度位subLen的子链表,两两一组进行归并排序,直至subLen=len
// subLen加倍增加 subLen *= 2
for (int subLen = 1; subLen < len; subLen <<= 1) {
// 记录头结点
ListNode prev = dummy;
cur = dummy.next;
while (cur != null) {
//记录第一个分组的头节点
ListNode head1 = cur;
//找到第一个分组的尾节点
for (int i = 1; i < subLen && cur.next != null; i++) {
cur = cur.next;
}
//记录下一个分组的头节点
ListNode head2 = cur.next;
//重置cur,并让其指向第二个分组的头结点
cur.next = null;
cur = head2;
//找到下一个分组的尾结点
for (int i = 1; i < subLen && cur != null && cur.next != null; i++) {
cur = cur.next;
}
// 记录下两个分组的开始
ListNode next = null;
if (cur != null) {
next = cur.next;
cur.next = null;
}
// 合并两个组
ListNode merged = merge(head1, head2);
// 记录两个组合并的头结点
prev.next = merged;
while (prev.next != null) {
prev = prev.next;
}
// 开始进入下个循环处理接下来的两组数据
cur = next;
}
}
return dummy.next;
}
public ListNode merge(ListNode list1,ListNode list2){
ListNode preHead = new ListNode(-1);
ListNode pre = preHead;
while(list1 != null && list2 !=null){
if(list1.val > list2.val){
pre.next = list2;
list2=list2.next;
}else {
pre.next= list1;
list1=list1.next;
}
pre =pre.next;
}
pre.next = list1==null ? list2 : list1;
return preHead.next;
}
时间复杂度:O(nlogn)。
空间复杂度:O(1)。