21. 合并两个有序链表
可以和后面的 23. 合并 K 个升序链表
结合在一起看,不过这里只有两个链表,不用优先级队列,简单一比较就好
注意这个
// 这个head是特意造的,只是为了后面插入新节点的时候好插入,可以不用对头节点做特殊判断。最后返回head.next即可 ListNode head = new ListNode(0); // 下一个要插入的节点的前驱节点 ListNode curPre = head;
然后后面就是每次找 cur (两个链表都没完的时候就是两个中较小的那个p,一个已经完了就是另一个还没完的链表的p)
再调整关系即可
// 调整关系
curPre.next = cur;
curPre = cur;
还有一个注意点就是,while 的条件,里面 else if 的条件
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode mergeTwoLists(ListNode list1, ListNode list2) { // 这个head是特意造的,只是为了后面插入新节点的时候好插入,可以不用对头节点做特殊判断。最后返回head.next即可 ListNode head = new ListNode(0); // 下一个要插入的节点的前驱节点 ListNode curPre = head; ListNode p1 = list1; ListNode p2 = list2; // 注意条件是或 while (p1 != null || p2 != null) { ListNode cur = null; // p1 已经完了,现在搞 p2 if (p1 == null) { cur = p2; p2 = p2.next; } // p2 已经完了,现在搞 p1 else if (p2 == null) { cur = p1; p1 = p1.next; } // 两个都没玩,cur=小的那个,同时小的那个往下走一步(注意另一个不用走) else { if (p1.val<=p2.val) { cur = p1; p1 = p1.next; } else { cur = p2; p2 = p2.next; } } // 调整关系 curPre.next = cur; curPre = cur; } return head.next; } }
25. K 个一组翻转链表
写一个反转链表的公共方法 revers(ListNode listHead)
ki 表示走到了 k 组第几个;k 组第一个记为 kStart ; k 组最后一个记为 kEnd;前一个 k 组的尾巴记为 preKTail
每次走到 k 组最后一个的时候
- ki 清零
- kEnd 指向 null ,调用反转方法反转 kStart
- 如果 preKTail 不为空,preKTail指向 kEnd,preKTail 变为 kStart (反转后 start 成了尾)
如果走到了末尾,而 ki >1 说明后面的不够 k 个一组,不用反转。因此 preKTail 直接指向 kStart
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode reverseKGroup(ListNode head, int k) { ListNode kStart = null; ListNode kCur = head; ListNode kEnd = null; ListNode preKtail = null; ListNode resHead = null; int ki = 1; while(kCur != null) { // 每次先缓存下一个节点,防止后面有变化 ListNode nextNode = kCur.next; if (ki == 1) { // k 个一组的起始 kStart = kCur; } if (ki == k) { kEnd = kCur; if (resHead == null) { resHead = kEnd; } ki=0; // 切断与下一个 k 组的联系,使得反转链表的时候有尾指向null kEnd.next = null; // 上一个链表尾指向这个的最后一个(待翻转) if (preKtail != null) { preKtail.next = kEnd; } // 反转链表 reverseList(kStart); // 链表已经反转过,头成了尾巴 preKtail = kStart; } ki++; kCur = nextNode; if (kCur == null && ki > 1) { // 没有任何完整的一组的情况 if (resHead == null) { resHead = kStart; } // 这些剩下的不用反转,因此上一个尾指向这一个头 if (preKtail != null) { preKtail.next = kStart; } break; } } return resHead; } private void reverseList(ListNode head) { ListNode cur = head; ListNode pre = null; while(cur != null) { // 先缓存下一个节点。注意怎么反转链表,这个 Next 指针要每次循环新生成一个,不能定义一个全局的 ListNode next = cur.next; cur.next = pre; pre = cur; cur = next; } } }
剑指 Offer 22. 链表中倒数第k个节点
双指针
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */ class Solution { public ListNode getKthFromEnd(ListNode head, int k) { ListNode p=head; ListNode q=head; // q 先走 k 步 for (int i=0;i<k;i++) { q=q.next; } // p 和 q 再一起走 while(q!=null) { p=p.next; q=q.next; } return p; } }
143. 重排链表
1、找到链表中点(方法:快慢指针,快指针走每两步,慢指针走一步),将链表分为两段
2、将后半段链表倒排(倒排方法要熟练掌握)
3、将前半段链表 和 倒排后的后半段链表 穿插合并
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public void reorderList(ListNode head) { // 找到整个链表的中间节点 ListNode midNode = findMidNode(head); // 反转后半部分 ListNode reversedRightHead = reverseList(midNode); // 将两个列表相间合并 mergeList(head, reversedRightHead); } private ListNode findMidNode(ListNode head) { ListNode fastNode = head; ListNode slowNode = head; // 要找到中间节点的前一个节点,从中间断开 ListNode midPre = null; while (fastNode != null) { // 快节点走2步,慢节点走1步 fastNode = fastNode.next; if (fastNode != null) { fastNode = fastNode.next; } midPre = slowNode; slowNode = slowNode.next; } // 使链表从中间节点断开 midPre.next = null; return slowNode; } private ListNode reverseList(ListNode head) { ListNode tail=null; // 第一个是 head 的话 ListNode cur = head; // 它的 pre 就是 null ListNode pre = null; while (cur != null) { if (cur.next == null) { tail = cur; } // next 不能定义成全局的,要在 while 里面 ListNode next = cur.next; cur.next = pre; // pre = cur 要在 cur = next 的前面 pre = cur; cur = next; } return tail; } // 将两个列表相间合并 private void mergeList(ListNode head1, ListNode head2) { ListNode p = head1; ListNode q = head2; while (p != null && q != null) { // 暂存 p 本来的 next ListNode tmpnext = p.next; // 暂存 q 本来的 next ListNode tmqnext = q.next; // 如 1->2 和 4->3,p=1,q=4 // 那么 p(1)->4 q(4)->2 // 1->4->2->3 p.next = q; q.next = tmpnext; //p 和 q 再指向它们本来的下一个 p = tmpnext; q = tmqnext; } } }
876. 链表的中间结点
快指针每次走两步,慢指针每次走一步
最后返回慢指针
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode middleNode(ListNode head) { ListNode slowNode = head; ListNode fastNode = head; while(fastNode != null) { fastNode = fastNode.next; if (fastNode != null) { fastNode = fastNode.next; slowNode = slowNode.next; } } return slowNode; } }
707. 设计链表
在链表中找到下标为 index 节点的正确方法
Node curNode = head; Node preNode = null; int cnt = 0; // 找到下标为 index 的 curNode while(curNode != null && cnt<index) { preNode = curNode; curNode = curNode.next; cnt++; }
之前用这个错误方法,curNode = head.next 并且判断条件是 curNode != null 这样 curNode 是以第二个节点下标为 1 起始的,而不是以第一个节点下标为 0 起始的
Node curNode = head.next; Node preNode = head; int cnt = 0; // 找到下标为 index 的 curNode while(curNode != null && cnt<index) { curNode = curNode.next; preNode = preNode.next; cnt++; }
- get(int index)
- 就用上面的方法
- addAtHead(int val)
- 特例:链表为空的情况。调整的太简单,不说了。记得 length++ 。记得把后面所有节点的 index +1
- addAtTail(int val)
- 特例:链表为空的情况。调整的太简单,不说了。记得 length++。
- addAtIndex(int index, int val)
- 排除 index < 0 || index > length
- 特例:index == 0 直接调用 addAtHead(val) ;index == length 直接调用 addAtTail(val)
- 找到下标为 index 的节点,和它的 preNode,然后进行调整
- 最后从 index 后面那个节点开始,所有节点的下标 +1
- deleteAtIndex(int index)
- 排除 index < 0 || index > length-1
- 找到下标为 index 的节点进行调整
- 要注意 index==0 即要删头节点时,要改变头节点指向;index == length-1 即要删尾节点时,要改变 tail 指向
- 最后记得释放 delete 的那个节点的内存,并将 delete 的那个节点的后面所有节点的下标 -1
class MyLinkedList { private Node head; private Node tail; private int length; public MyLinkedList() { length = 0; } public class Node { public int index; public int val; public Node next; public Node (int val) { this.val = val; } public Node (int index, int val) { this.index = index; this.val = val; } } public int get(int index) { if (head == null || index <0 || index>length-1) { return -1; } Node curNode = head; int cnt = 0; while (curNode != null && cnt<index) { curNode = curNode.next; cnt++; } return curNode.val; } public void addAtHead(int val) { Node node = new Node(0, val); if (length == 0) { head = node; tail = node; node.next = null; } else { node.next = head; head = node; // 要把后面的 index 都 +1 Node curNode = head.next; while(curNode != null) { curNode.index++; curNode=curNode.next; } } length++; } public void addAtTail(int val) { Node node = new Node(length, val); if (length == 0) { node.next = null; head = node; tail = node; } else { tail.next = node; node.next = null; tail = node; } length++; } public void addAtIndex(int index, int val) { if (index<0 || index>length) { return; } // index 刚好等于 length 插到链表尾 if (index == length) { addAtTail(val); return; } // 有可能插到头节点前面, 这也是一种例外情况 if (index == 0) { addAtHead(val); return; } Node newNode = new Node(index, val); Node curNode = head; Node preNode = null; int cnt = 0; // 找到下标为 index 的 curNode while(curNode != null && cnt<index) { preNode = curNode; curNode = curNode.next; cnt++; } // 插到它的前面 preNode.next = newNode; newNode.next = curNode; // 从 curNode 开始,后面所有节点的 index+1 while(curNode != null) { curNode.index++; curNode = curNode.next; } length++; } public void deleteAtIndex(int index) { if (index<0 || index>length-1) { return; } Node preNode = null; Node curNode = head; int cnt = 0; // 找到下标为 index 的节点 curNode while(curNode != null && cnt<index) { preNode = curNode; curNode = curNode.next; cnt++; } Node indexNext = curNode.next; if (index == 0) { // 释放现在被删掉的头节点的内存 head = null; // 头指向原先的下一个 head = indexNext; } else { // 调整 preNode.next = indexNext; curNode = null; } // 如果下标为 index 的节点是最后一个节点 // 那么删掉后要改变 tail if (index == length-1) { tail = preNode; } length--; // 删完要将它后面所有节点的 下标 -1 curNode = indexNext; while (curNode != null) { curNode.index--; curNode = curNode.next; } } } /** * Your MyLinkedList object will be instantiated and called as such: * MyLinkedList obj = new MyLinkedList(); * int param_1 = obj.get(index); * obj.addAtHead(val); * obj.addAtTail(val); * obj.addAtIndex(index,val); * obj.deleteAtIndex(index); */
160. 相交链表
方法一:哈希集合
从头遍历 A 链表,把所有节点放到 HashSet 中
再从头变脸 B 链表,每次判断节点是否已经在 HashSet 中。第一个已经在 HashSet 中的节点就是相交节点
- 时间复杂度:O(m+n)
- 空间复杂度:O(m)
方法二:双指针
将2个链表在末尾加上对方,碰到第一个相同的点,要么是交点,要么是末尾
- 相交
-
A: 1 -> 2 -> 3 -> C -> 4 -> 5 -> null
-
B: 6 -> 7 -> C -> 4 -> 5 -> null
-
A + B: 1 -> 2 -> 3 -> C -> 4 -> 5 -> 6 -> 7 -> C -> 4 -> 5 -> null
-
B + A: 6 -> 7 -> C -> 4 -> 5 -> 1 -> 2 -> 3 -> C -> 4 -> 5 -> null
-
-------------------------------------------------------------------------------------------
- 不相交
-
A: 1 -> 2 -> 3 -> 4 -> 5 -> null
-
B: 6 -> 7 -> 8 -> 3 -> null
-
A + B: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 3 -> null
-
B + A: 6 -> 7 -> 8 -> 3 -> 1 -> 2 -> 3 -> 4 -> 5 -> null
不用物理意义的加到末尾,只需要 pA pB 两指针:
- pA 不为空,pA 每次走到下一步 next ;pB 不为空,pB 每次走到下一步 next
- pA 为空时,pA 去 B 的头节点 headB;pB 为空时,pB 去 A 的头节点 headA
- 当连着相等时:如果不为 null ,说明走到了相交节点返回;如果为 null,说明两链表没有相交返回 null
此解法可以降低空间复杂度
- 时间复杂度:O(m+n)
- 空间复杂度:O(1)
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */ public class Solution { public ListNode getIntersectionNode(ListNode headA, ListNode headB) { ListNode pA = headA; ListNode pB = headB; while (true) { if (pA == pB) { if (pA == null) { return null; } else { return pA; } } pA = (pA == null ? headB:pA.next); pB = (pB == null ? headA:pB.next); } } }
206. 反转链表
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode reverseList(ListNode head) { ListNode pre = null; ListNode p = head; while (p != null) { ListNode next = p.next; p.next = pre; pre = p; p = next; } return pre; } }
234. 回文链表
方法一:
找到中间节点,反转链表的后半部分
p 指向前半部分头(即原链表头),q 指向反转后的后半链表头,依次往后比较
方法二:
slow fast 找中间节点的同时,把前半部分链表的值加入 stack
后半链表用 slow 来遍历,与 stack 顶部元素比较,如果不相等就返回 false。相等就出栈并继续往下比较。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public boolean isPalindrome(ListNode head) { Stack<Integer> stack = new Stack(); ListNode slow = head; ListNode fast = head; // 把前半部分元素放入栈中。当 fast == null 时退出循环 while(fast != null) { fast = fast.next; if (fast != null) { fast = fast.next; // 把 stack 放在里面,也是为了奇数时最中间那个元素不被加进去 stack.push(slow.val); } // 如果 slow 在 if (fast != null) 的里面,那么如果是奇数个元素,返回的就是最中间的那个 // 如果把 slow 放在 if (fast != null) 的外面,那么如果是奇数个元素,返回的就是最中间的下一个 slow = slow.next; } // 后半部分元素与栈中的逐个比较 while(slow != null) { int top = stack.peek(); if (top != slow.val) { return false; } else { stack.pop(); slow = slow.next; } } return true; } public boolean isPalindrome2(ListNode head) { if (head.next == null) { return true; } // 找到中间节点 ListNode midNode = findMidNode(head); // 翻转后半部分 ListNode newMidNode = reversList(midNode); // p指向前半部分开头。q指向后半部分开头,依次比较 ListNode p = head; ListNode q = newMidNode; while(q != null) { if (p.val != q.val) { return false; } p = p.next; q = q.next; } return true; } private ListNode findMidNode(ListNode head) { ListNode slow = head; ListNode fast = head; while (fast != null) { fast = fast.next; if (fast == null) { return slow; } fast = fast.next; slow = slow.next; } return slow; } private ListNode reversList(ListNode head) { ListNode pre = null; ListNode p = head; while (p != null) { ListNode next = p.next; p.next = pre; pre = p; p = next; } return pre; } }
141. 环形链表
方法一:哈希表
最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。
具体地,我们可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。
复杂度分析
- 时间复杂度:O(N) 最坏情况下我们需要遍历每个节点一次。
- 空间复杂度:O(N) 主要为哈希表的开销,最坏情况下我们需要将每个节点插入到哈希表中一次。
方法二:快慢指针
本方法需要读者对「Floyd 判圈算法」(又称龟兔赛跑算法)有所了解。
假想「乌龟」和「兔子」在链表上移动,「兔子」跑得快,「乌龟」跑得慢。当「乌龟」和「兔子」从链表上的同一个节点开始移动时,如果该链表中没有环,那么「兔子」将一直处于「乌龟」的前方;如果该链表中有环,那么「兔子」会先于「乌龟」进入环,并且一直在环内移动。等到「乌龟」进入环时,由于「兔子」的速度快,它一定会在某个时刻与乌龟相遇,即套了「乌龟」若干圈。
我们可以根据上述思路来解决本题。具体地,我们定义两个指针,一快一慢。慢指针每次只移动一步,而快指针每次移动两步。初始时,慢指针在位置 head,而快指针在位置 head.next。这样一来,如果在移动的过程中,快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。
时间复杂度:O(N)
- 当链表中不存在环时,快指针将先于慢指针到达链表尾部,链表中每个节点至多被访问两次。
- 当链表中存在环时,每一轮移动后,快慢指针的距离将减小一。而初始距离为环的长度,因此至多移动 N 轮
空间复杂度:O(1)。我们只使用了两个指针的额外空间。
/** * Definition for singly-linked list. * class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */ public class Solution { public boolean hasCycle(ListNode head) { // 因为最后返回的是 slow==fast。而只有一个节点时,slow和fast都是初始的head不会往下走,会返回true,但其实应该是false if (head == null || head.next == null) { return false; } ListNode slow = head; ListNode fast = head; // slow 走一步,fast 走两步 do { slow = slow.next; fast = fast.next; if (fast != null) { fast = fast.next; } } // do while 是因为,一开始 slow和fast 都在 head,如果是 while 的话就往下走不了了 while (slow != fast && fast != null); // slow 和 fast 相遇的话说明有环 return slow == fast; } }
142. 环形链表 II
环外部分长度为 a
slow 进入环以后,又走了 b 与 fast 相遇。slow 走过的距离为 a+b(slow 一定是未满一圈就和 fast 相遇的,可以证明)
相遇时 fast 已经走完了环的 n 圈,fast 走过的距离为 a+n(b+c)+b = a+(n+1)b+nc
任意时刻,fast 走过的长度都为 slow 的两倍,即 a+(n+1)b+nc = 2(a+b) ——> a = c+(n-1)(b+c)
由 a = c+(n-1)(b+c) 可知,所求的入环点距离 a 为 相遇距离c 加上 n-1 圈的环长
因此,当发现 slow 和 fast 相遇后,再有一个新指针指向头 head,和 slow 每次同步移动一步,最后它们会在所求入环点相遇
/** * Definition for singly-linked list. * class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */ public class Solution { public ListNode detectCycle(ListNode head) { if (head == null || head.next == null) { return null; } ListNode slow = head; ListNode fast = head; // 快指针走两步,慢指针走一步 do { slow = slow.next; fast = fast.next; if (fast != null) { fast = fast.next; } } // do while 是因为刚开始 slow 和 fast 都为 head,如果 while 的话没法下去 while (slow != fast && fast!= null); // slow 和 fast 相遇说明有环,没相遇,直接返回 null if (slow != fast) { return null; } // 相遇后让一个新指针指向头部 ListNode newp = head; // 新指针和慢指针同步走一步 while (slow != newp) { slow = slow.next; newp = newp.next; } // 最后新指针和慢指针会在入环点相遇 return newp; } }
19. 删除链表的倒数第 N 个结点
快指针先走 n 步,慢指针再跟着一起走
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode fast = head; ListNode slow = fast; ListNode slowPre = null; for (int i=0;i<n;i++) { fast = fast.next; } while (fast != null) { slowPre = slow; slow = slow.next; fast = fast.next; } if (slowPre != null) { slowPre.next = slow.next; slow = null; } else { // 倒数第 n 个可能就是头节点 head = slow.next; slow = null; } return head; } }
24. 两两交换链表中的节点
与 k 个一组翻转链表类似,只是这里 k=2
写一个反转链表的公共方法 revers(ListNode listHead)
ki 表示走到了 k 组第几个;k 组第一个记为 kStart ; k 组最后一个记为 kEnd;前一个 k 组的尾巴记为 preKTail
每次走到 k 组最后一个的时候
- ki 清零
- kEnd 指向 null ,调用反转方法反转 kStart
- 如果 preKTail 不为空,preKTail指向 kEnd,preKTail 变为 kStart (反转后 start 成了尾)
如果走到了末尾,而 ki >1 说明后面的不够 k 个一组,不用反转。因此 preKTail 直接指向 kStart
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode swapPairs(ListNode head) { ListNode resHead = null; ListNode preKEnd = null; ListNode kp = head; ListNode kStart = head; ListNode kEnd = null; ListNode nextKStart = head; int k=2; boolean isFirstK = true; while (true) { kStart = nextKStart; int ki=0; for (int i=0;i<k;i++) { if (kp != null) { kp = kp.next; if (i==k-2) { kEnd = kp; } } else { break; } ki++; } if (ki == k) { nextKStart = kp; kEnd.next = null; reverseList(kStart); if (preKEnd != null) { preKEnd.next = kEnd; } preKEnd = kStart; if (isFirstK) { resHead = kEnd; } } else { if (preKEnd != null) { preKEnd.next = kStart; } if (isFirstK) { resHead = kStart; } break; } isFirstK = false; } return resHead; } private ListNode reverseList(ListNode head) { ListNode pre = null; ListNode p = head; while (p != null) { ListNode next = p.next; p.next = pre; pre = p; p = next; } return pre; } }
138. 复制带随机指针的链表
方法一:
有一个 Map<Node,Node> 来存所有 旧节点-新节点 的对应
先遍历一次,复制链表中所有的节点,复制的过程中把 旧节点-新节点 放入 map
再遍历一次:
Node curCopy = oldNode2NewNodeMap.get(curOld);
curCopy.next = oldNode2NewNodeMap.get(curOld.next);
curCopy.random = oldNode2NewNodeMap.get(curOld.random);
方法二:
比上面进步的是,上面空间复杂度 O(n),这个是 O(1)
Node randomOri = cur.random;
Node curCopy = cur.next;
curCopy.random = randomOri.next;
/* // Definition for a Node. class Node { int val; Node next; Node random; public Node(int val) { this.val = val; this.next = null; this.random = null; } } */ class Solution { /* 时间复杂度 O(n) 空间复杂度 O(n) */ public Node copyRandomList2(Node head) { // 旧节点与新节点一一对应 Map<Node, Node> oldNode2NewNodeMap = new HashMap(); for (Node curOld=head;curOld!=null;curOld=curOld.next) { Node newNode = new Node(curOld.val); oldNode2NewNodeMap.put(curOld, newNode); } for (Node curOld=head;curOld!=null;curOld=curOld.next) { Node curCopy = oldNode2NewNodeMap.get(curOld); curCopy.next = oldNode2NewNodeMap.get(curOld.next); curCopy.random = oldNode2NewNodeMap.get(curOld.random); } return oldNode2NewNodeMap.get(head); } /* 时间复杂度 O(n) 空间复杂度 O(1) */ public Node copyRandomList(Node head) { if (head == null) { return null; } // 1.每个节点的后面搞一个 S'。注意S'在遍历过程中就加进去了,所以每次走两步 cur=cur.next.next for (Node cur=head;cur!=null;cur=cur.next.next) { Node newNode = new Node(cur.val); newNode.next = cur.next; cur.next = newNode; } // 2.random指针 for (Node cur=head;cur!=null;cur=cur.next.next) { Node randomOri = cur.random; Node curCopy = cur.next; if (randomOri != null) { curCopy.random = randomOri.next; } } // 3.分开成两个链表。注意S'在遍历过程中就去掉了,所以每次走一步 cur=cur.next Node resHead = head.next; for (Node cur=head;cur!=null;cur=cur.next) { Node curCopy = cur.next; cur.next = curCopy.next; if (curCopy.next != null) { curCopy.next = curCopy.next.next; } } return resHead; } }
23. 合并 K 个升序链表
优先队列(原理是堆排序,每次加入和取出的时间复杂度都是 logk)
要重写compare方法,确保每次从队列中取出来的,都是ListNode val最小的那个。
因为要重写 compareTo 方法,而 leetcode 已经定义好好的 ListNode 我们是不能改变更别说重写 compareTo 的。因此新定义一个类把 ListNode 包进去,再重写 compareTo 方法。这个类要 implements Comparable<Status>,然后重写 compareTo() 方法
1.初始化优先队列:将所有链表的头节点加入队列
2.定义:
-
- ListNode head = new ListNode(0) 这个head是特意造的,只是为了后面插入新节点的时候好插入,可以不用对头节点做特殊判断。最后结果返回head.next即可
- ListNode curPre = head 每次插入位置的前驱节点
3.while队列不为空
-
- ListNode cur = queue.poll().node 即从优先队列中取出最小的那个node
- curPre.next=cur;
- curPre = cur;
- 再把这个 cur 在它所在链表的下一个节点加入优先级队列
时间复杂度 O(kn * logk)
优先队列元素不超过k个,每个元素插入和删除复杂度都是 logk。所有链表的所有元素一共是 kn 个,它们每个都要在优先列表中插入和删除一次,所以一共 O(kn * logk)
空间夫再度 O(k)
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { class Status implements Comparable<Status> { public ListNode node; // 构造方法 public Status(ListNode node) { this.node = node; } // leetcode 已经定义好好的 ListNode 我们是不能改变更别说重写 compareTo 的。因此新定义一个类把 ListNode 包进去再重写 public int compareTo(Status status2) { return this.node.val - status2.node.val; } } private PriorityQueue<Status> queue = new PriorityQueue(); public ListNode mergeKLists(ListNode[] lists) { // 1.初始化优先队列:把所有链表的头节点加进去 for(ListNode listHead : lists) { if (listHead != null) { queue.offer(new Status(listHead)); } } // 这个head是特意造的,只是为了后面插入新节点的时候好插入,可以不用对头节点做特殊判断。最后返回head.next即可 ListNode head = new ListNode(0); // curPre,下一个要插入位置的前驱 ListNode curPre = head; while (!queue.isEmpty()) { // 队首的即为所有链表中最小的那个 ListNode cur = queue.poll().node; // 把它加到 curPre 的后面 curPre.next = cur; curPre = cur; // 把刚刚那个cur在它所在链表的next加到queue中 if (cur.next != null) { queue.offer(new Status(cur.next)); } } return head.next; } }
148. 排序链表
一般都用归并排序,因为是单向链表,其它排序算法根据下标找元素,向前遍历等都比较困难
主函数流程是:
- 如果 head==null || head.next==null return head。因为 head.next == null 即只有一个元素时,不用再划分了,而且一个元素本身也是有序的,所以返回就返回这一个元素
- 通过找链表中点算法找到 midNode
- 左半数组是 head~midNode,右半数组是 midNode.next~结尾null 。因此令 rightListHead=midNode.next。此外为了使坐半链表有正确的边界,结尾指向null,把链表从中间分割开来,令 midNode.next = null
- 左半递归 rightHead = sortList(head) 右半递归 rightHead = sortList(rightListHead)
- 最后合并左半和右半两个有序链表 return merge2OrderList(leftHead, rightHead)
过程中会用到链表的两个经典算法:
- 合并两个有序链表:虚拟头节点,ListNode head = new ListNode(0); curPre = head 最后返回 head.next。while(p1!=null || p2!=null) if(p1==null)......
- 找链表中点:快指针走两步,慢指针走一步,但又一点于以前不同: 我们希望奇数个如 1 2 3 时返回 2,偶数个如 1 2 3 4 时返回 2 。因为我们认为 mid.next 是右半的起点,并且在这时候会令 mid.next = null。这与之前的不同,之前偶数个如 1 2 3 4 时希望返回 3
之前的找链表中点:奇数个如 1 2 3 时返回 2,偶数个如 1 2 3 4 时返回 3 。
private static ListNode findMidNode(ListNode head) { ListNode fast = head; ListNode slow = head; while (fast != null) { fast = fast.next; if (fast != null) { fast = fast.next; slow = slow.next; } } return slow; }
- 奇数 1 2 3:初始 fast=1 slow=1 ;第一次循环:fast=2 fast=3 slow=2 ;第二次循环 fast=3 ;最后返回 slow=2
- 偶数 1 2 3 3:初始 fast=1 slow=1 ;第一次循环:fast=2 fast=3 slow=2 ;第二次循环 fast=4 fast=null slow=3 ;最后返回 slow=3
现在的找链表中点:奇数个如 1 2 3 时返回 2,偶数个如 1 2 3 4 时返回 2 。
private static ListNode findMidNode(ListNode head) { ListNode fast = head; ListNode slow = head; while (fast != null) { fast = fast.next; // 这里要上一个 && fast.next != null 的条件 if (fast != null && fast.next != null) { fast = fast.next; slow = slow.next; } } return slow; }
- 奇数 1 2 3:初始 fast=1 slow=1 ;第一次循环:fast=2 fast=3 slow=2 ;第二次循环 fast=3 ;最后返回 slow=2
- 偶数 1 2 3 3:初始 fast=1 slow=1;第一次循环:fast=2 fast=3 slow=2 ;第二次循环 fast=4 因为fast.next==null退出了循环;最后返回 slow=2
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode sortList(ListNode head) { if (head == null || head.next == null) { // head.next == null 即只有一个元素时,不用再划分了,而且一个元素本身也是有序的,所以返回就返回这一个元素 // 而且只有一个元素时,也没法找中点 return head; } ListNode midNode = findMidNode(head); // 右半数组的起始是中间节点的下一个 ListNode rightListHead = midNode.next; // 为了使链表有正常的边界,从中间节点断开 midNode.next = null; ListNode leftHead = sortList(head); ListNode rightHead = sortList(rightListHead); // 合并两个有序链表 ListNode list = merge2OrderList(leftHead, rightHead); return list; } /* 经典链表算法:合并两个有序链表 */ private static ListNode merge2OrderList(ListNode head1, ListNode head2) { // 这个 head 是可以造的,只是为了后面可以不对头节点做特殊判断 ListNode head = new ListNode(0); ListNode curPre = head; ListNode p1 = head1; ListNode p2 = head2; // 两个有一个没完,就继续循环,注意是 || 不是 && while (p1 != null || p2 != null) { // list1完了。list2没完 // 请注意:【刚开始写成了p1!=null】 应该是【p1==null】 if (p1 == null) { curPre.next = p2; curPre = p2; p2 = p2.next; } // list2完了。list1没完 else if (p2 == null) { curPre.next = p1; curPre = p1; p1 = p1.next; } else { if (p1.val <= p2.val) { curPre.next = p1; curPre = p1; p1 = p1.next; } else { curPre.next = p2; curPre = p2; p2 = p2.next; } } } // 最后返回 head.next,因为 head 就是个虚拟结点 return head.next; } /* 经典链表算法:合并两个有序链表 */ private static ListNode findMidNode(ListNode head) { // 我们希望奇数个如 1 2 3 时返回 2,偶数个如 1 2 3 4 时返回 2 // 因为我们认为 mid.next 是右半的起点,并且在这时候会令 mid.next = null // 这与之前的不同,之前偶数个如 1 2 3 4 时希望返回 3 ListNode fast = head; ListNode slow = head; while (fast != null) { fast = fast.next; // 所以这里要上一个 && fast.next != null 的条件 if (fast != null && fast.next != null) { fast = fast.next; slow = slow.next; } } return slow; } }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· DeepSeek “源神”启动!「GitHub 热点速览」
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· NetPad:一个.NET开源、跨平台的C#编辑器