第一节介绍了常用的三种链表:单链表、双向链表、环形链表,一节各自的增删遍历方式,作为基础的回顾。
第二节12道力扣经典的链表题练手,链表题没有统一的套路、但有许多必须掌握的常识 :链表拼接、哑巴结点、快慢指针、反转链表、寻找中间结点等。反正就是多练。
第三节是简单的总结,没什么卵用。
[1]原理部分参考了码农StayUp 的文章https://segmentfault.com/a/1190000038252047
[3]啊我写到一半看到力扣上一个超牛批的题解文章,再看我总结的,,,,这不就是shi么,,贴个链接以后可以回看:作者Time-Limit 链接:https://leetcode-cn.com/problems/linked-list-cycle/solution/yi-wen-gao-ding-chang-jian-de-lian-biao-wen-ti-h-2/
除文中标注处以外,文字、代码都是自我理解写的,不对之处敬请指出,保留权利。
1 链表结构介绍
此节图片来源于[1],谢谢谢谢谢(我就是懒)
当我再一次打开链表的简单题,发现我连链表的遍历都忘了的时候我就知道,“脑子是不顶用的,但键盘可以”。所以只是做做笔记、方便闲的dan疼的时候回看。
链表是以节点(Node)的方式来存储,其节点的逻辑顺序与物理顺序(内存)可以不一致[1]
相比数组,链表的增删快而查找慢。当数据结构的大小容量未知、查找少的场合 推荐使用链表储存
链表分为单链表、双链表和环形链表。双链表改善单链表的查询时间复杂度,环形链表改善空间复杂度
1.1 单链表 单链表的遍历:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ListNode cur = head; while (cur!=null ){ System.out.printlin(cur.val); cur = cur.next; }
单链表的插入操作:
1 2 3 4 5 6 7 8 9 10 11 12 ListNode nodeNew = ListNode('X' ); ListNode cur = head; while (cur!=null ){ if (cur.val=='A1' ){ nodeNew.next = cur.next; cur.next = nodeNew; break ; } cur = cur.next; }
单链表的删除操作:
删除操作一般处理当前节点的下一个节点,所以遍历循环条件为cur.next!=null
1 2 3 4 5 6 7 8 9 ListNode cur = head; while (cur.next!=null ){ if (cur.next.val=='A1' ){ cur.next = cur.next.next; break ; } cur = cur.next; }
技巧一:运用哑巴节点
链表的头节点也可能被删除,这里要用哑巴节点dummy node辅助删除
1 2 3 4 5 6 7 8 9 10 11 ListNode dummyNode= new ListNode(0 ); dummyNode.next = head; head = dummyNode; while (head.next!=null ){ ... head = head.next; } return dummyNode.next;
技巧二:反转链表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Solution { public ListNode ReverseList (ListNode head) { ListNode prev=null ; ListNode cur=head; while (cur!=null ){ ListNode tempNode = cur.next; cur.next = prev; prev = cur; cur = tempNode; } return prev; } }
1.2 双向链表 双向链表就是双车道了两头都可以跑,如果知道某一个节点的索引与数组最大索引/2的大小关系,就可以判断应该正向遍历或者反向遍历查找,这其实是一种以空间换取时间的策略 ,将原先查找O(n)的时间复杂度变为O(n/2),应该还是能快一些。(我一月份背的八股居然还没忘/狗头)
双向链表的首尾节点 和中间节点 是不一样的,具体怎么定义、我还没看…这里只考虑对中间节点的操作
双向链表的遍历
两个方向都可以,还是只写一个方向的 (我瞎写的也不知道对不对,反正考的少)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Node cur = first; while (cur!=null ){ System.out.println(cur.val); cur = cur.next; if (cur.next.equals(cur.prev) ) break ; }
双向链表的插入
向双链表中插入一个新节点,需要通过调整两次prev
指向和两次next
指向来完成[1]。一定要注意顺序、参考文献[1]这里有些误人子弟了,口诀是 **”0点开始顺时针”**(我他娘真是个天才)
1 2 3 4 5 6 7 8 9 10 11 12 13 Node nodeNew = new Node('X' ); Node cur = first; while (cur!=null ){ if (cur.val=='A1' ){ nodeNew.next = cur.next; cur.next.prev = nodeNew; nodeNew.prev = cur; cur.next = nodeNew; } cur = cur.next; if (cur.next.equals(cur.prev) ) break ; }
双向链表的删除
删除节点分为两步,“三点开始顺时针”
删除操作一般处理当前节点的下一个节点,所以遍历循环条件为cur.next!=null
1 2 3 4 5 6 7 8 9 10 11 12 13 Node cur = first; while (cur.next!=null ){ if (cur.next.val=='A2' ){ cur.next.next.prev = cur; cur.next = cur.next.next; nodeNew.prev = cur; cur.next = nodeNew; } cur = cur.next; if (cur.next.equals(cur.prev) ) break ; }
1.3 单向环形链表 与单链表的唯一区别是尾部节点的next
不再为空,则是指向了头部节点,这样便形成了一个环。[1]
环形链表的遍历
环形链表主类里面必然有两个特殊的首尾指针节点first
和 last
,实现起来挺困难,特别是插入删除均要考虑相对的位置,这里只列遍历吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ListNode cur = first; while (cut!=last){ System.out.println(cur.val); cur = cur.next; } System.out.println(cur.val);
2 力扣题解_链表 开始搞题,理论部分搞不动了。2.1-2.4先把快慢指针或者说双指针搞爽。
2.1 剑指offer22.链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6
个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6
。这个链表的倒数第 3
个节点是值为 4
的节点。
思路:前后两个指针left、right
它们相距k的长度,同步的移动,当right
到达链表尾部时,left
到达倒数第k个节点处。(图片源于[3])
代码:画一下就能知道两指针的索引相减等于k-1
,所以第一步right
只需移动k-1
格就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution { public ListNode getKthFromEnd (ListNode head, int k) { ListNode right = head; ListNode left = head; for (int i=1 ;i<k;i++){ right = right.next; } while (right.next!=null ){ right = right.next; left = left.next; } return left; } }
2.2 力扣867.链表的中间结点
给定一个头结点为 head
的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
思路:快慢指针,快指针步长为2,慢指针步长为1,当快指针指向链表末尾时,慢指针指向中间节点。 特别的当结点个数为奇数时,慢指针指向中间结点;当偶数时,慢指针指向中间两个结点中靠前或者靠后的一个(用一个6结点的链表画一下图就清楚了)下图来源于[3]
当fast=head slow=head
时,偶数情况将返回第二个中间结点
当fast=head.next slow=head
,偶数情况将返回第一个中间结点
代码一:返回第二个中间结点
1 2 3 4 5 6 7 8 9 10 11 class Solution { public ListNode middleNode (ListNode head) { ListNode fast = head; ListNode slow = head; while (fast!=null && fast.next!=null ){ fast = fast.next.next; slow = slow.next; } return slow; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution { public ListNode middleNode (ListNode head) { ListNode fast = head.next; ListNode slow = head; while (fast!=null && fast.next!=null ){ fast = fast.next.next; slow = slow.next; } if (fast!=null ) return slow.next; return slow; } }
代码二:返回第一个中间结点
1 2 3 4 5 6 7 8 9 10 11 12 class Solution { public ListNode middleNode (ListNode head) { ListNode fast = head.next; ListNode slow = head; while (fast!=null && fast.next!=null ){ fast = fast.next.next; slow = slow.next; } return slow; } }
2.3 力扣 环形链表 来两道环形链表的力扣题体验一下,实际上也是双指针快慢指针的题。
141环形链表Ⅰ
给定一个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
如果链表中存在环,则返回 true 。 否则,返回 false 。
示例 1:
1 2 3 输入:head = [3,2,0,-4], pos = 1 输出:true 解释:链表中有一个环,其尾部连接到第二个节点。
思路:
1、判断是否成环,可以用hashset
集合存每个节点,如果遍历过程发现该节点已经存在于集合中就表示成环了
2、链表题目想要常数的空间复杂度、好像就是双指针 莽起来。双指针是一种思想:有快慢指针、首尾指针、等间隔指针等。
代码实现:快慢指针
对于成环链表,进入链表环内的两个快慢指针终会相遇。(就像是跑操场,跑得快的人必然会追上跑得慢的人)
其实效率有一点需要考量:
因为快指针有些节点永远都不能到达、当第一圈两指针接近但没有重合,就只能再多跑一圈才能真正相遇。
慢指针步长设为1,快指针步长设为2;虽然如果快指针步长设更大时会追赶的更快,但是会出现更多不能到达的节点,有可能得不偿失
快指针步长为2 注意while循环的判断条件是fast&fast.next
都不为空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Solution { public boolean hasCycle (ListNode head) { if (head==null ) return false ; ListNode fast = head.next; ListNode slow = head; while (fast!=null && fast.next!=null ){ if (fast==slow) return true ; fast = fast.next.next; slow = slow.next; } return false ; } }
代码实现:set集合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Solution { public boolean hasCycle (ListNode head) { Set<ListNode> visited = new HashSet<>(); ListNode cur = head; while (cur!=null ){ if (visited.contains(cur)) return true ; visited.add(cur); cur = cur.next; } return false ; } }
142环形链表Ⅱ
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。你是否可以使用 O(1) 空间解决此题?
示例 1:
1 2 3 输入:head = [3,2,0,-4], pos = 1 输出:返回索引为 1 的链表节点 解释:链表中有一个环,其尾部连接到第二个节点。
思路一:用set集合存节点,找到第一个重复出现的节点就返回
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Solution { public ListNode detectCycle (ListNode head) { Set<ListNode> visited = new HashSet<>(); ListNode cur = head; while (cur!=null ){ if (visited.contains(cur)) return cur; visited.add(cur); cur = cur.next; } return null ; } }
思路二:快慢指针 [图片来源于力扣官方题解]
解释一下,慢指针走过a+b
,快指针走过a+b+k(b+c)
,且快指针走过的距离是慢指针的两倍,所以有a+b=k(b+c)
,可以得到a=(k-1)(b+c)+c
。也就是说如果知道相遇点了(如图紫色的位置)。放指针1从头开始走、指针2从相遇点开始走, 指针1走过a
步,指针2走过k-1圈+c
步后,两指针会相遇在链表环路的起点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Solution { public ListNode detectCycle (ListNode head) { if (head==null ) return null ; ListNode fast = head.next; ListNode slow = head; while (fast!=null && fast.next!=null ){ if (fast == slow) break ; fast = fast.next.next; slow = slow.next; } fast = head; slow = slow.next; while (fast.next!=null && slow.next!=null ){ if (fast == slow) return fast; fast = fast.next; slow = slow.next; } return null ; } }
2.4 剑指Offer52. 两个链表的第一个公共节点
输入两个链表,找出它们的第一个公共节点。
如下面的两个链表:
在节点 c1 开始相交。
思路
两个链表长度分别为L1+C、L2+C, C为公共部分的长度
cur1 cur2都走了 L1+L2 +C +1步之后相遇(1是null的位置)
要多走一步null 以便不相交时也能跳出循环 (当cur1 cur2都走过了L1+L2的长度时都为null,此时跳出循环)
代码
1 2 3 4 5 6 7 8 9 10 11 public class Solution { public ListNode getIntersectionNode (ListNode headA, ListNode headB) { ListNode cur1 = headA; ListNode cur2 = headB; while (cur1!=cur2){ if (cur1==null ) cur1 = headB; else cur1 = cur1.next; if (cur2==null ) cur2 = headA; else cur2 = cur2.next; } return cur1; } }
2.5 力扣83/82.删除排序链表中的重复元素 删除排序链表中的重复元素Ⅰ
存在一个按升序排列的链表,给你这个链表的头节点 head
,请你删除所有重复的元素,使每个元素 只出现一次 。
返回同样按升序排列的结果链表。
头节点不用动,删除节点只需要移动指针,注意连续相同的元素用while判断一次性全部删除。
这题判断条件容易出错,因为可能有多个重复元素,内部需要循环判断是否重复
只要某循环里面要用到cur.next
,就一定要在循环判断条件里面加上cur.next!=null
1 2 3 4 5 6 7 8 9 10 11 12 class Solution { public ListNode deleteDuplicates (ListNode head) { ListNode temp = head; while (temp!=null ){ while (temp.next!=null && temp.val==temp.next.val) temp.next = temp.next.next; temp = temp.next; } return head; } }
删除排序链表中的重复元素Ⅱ
存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中 没有重复出现 的数字。
返回同样按升序排列的结果链表。
**头节点可能会动掉,搞个哑巴节点dummyNode
**。这题也很容易出错啊、我做第三遍又没调试好。。
要想删完重复的(如a b b b c)、先判断是不是有重复、有重复就内部while
循环,cur
要指向a,记录b的值 每次删一个b。
还有head
节点如果没用了、可以直接拿来做指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution { public ListNode deleteDuplicates (ListNode head) { ListNode dummyNode =new ListNode(0 ); dummyNode.next = head; head = dummyNode; while (head.next!=null &&head.next.next!=null ){ if (head.next.val==head.next.next.val){ int tempVal = head.next.val; while (head.next!=null &&head.next.val==tempVal) head.next=head.next.next; }else { head = head.next; } return dummyNode.next; } }
2.6 力扣92.反转链表Ⅱ(中等) 思路
反转链表的升级版,区别就是链表分为三段,只有第二段需要翻转,翻转完之后将三段链表再次连接起来。
想要连接起来 必须有第一段的尾temp1
, 第二段的头prev
,第二段的尾temp2
,第三段的头cur
因为头节点有可能动 所以用哑巴节点 dummyNode
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution { public ListNode reverseBetween (ListNode head, int left, int right) { ListNode dummyNode = new ListNode(-1 ); dummyNode.next = head; head = dummyNode; int count = 0 ; while (count<left-1 ){ head = head.next; count++; } ListNode temp1 = head; head = head.next;count++; ListNode temp2 = head; ListNode prev = null ; while (count<=right){ ListNode tempNode = head.next; head.next = prev; prev = head; head = tempNode; count++; } temp1.next = prev; temp2.next = head; return dummyNode.next; } }
2.7 力扣21.合并两个有序链表(简单) 思路:
两个链表两个当前节点指针,哪个小就先加哪个喽
头节点不确定所以哑巴节点
可以直接使用两个子链表的头节点l1 l2
作为指针
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution { public ListNode mergeTwoLists (ListNode l1, ListNode l2) { ListNode dummyNode = new ListNode(-1 ); ListNode temp = dummyNode; while (l1!=null && l2!=null ){ if (l1.val<=l2.val){ temp.next = l1; l1 = l1.next; }else { temp.next = l2; l2 = l2.next; } temp = temp.next; } if (l1==null ) temp.next=l2; else temp.next=l1; return dummyNode.next; } }
2.8 力扣86分隔链表(中等)
给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
示例 :
1 2 输入:head = [1,4,3,2,5,2], x = 3 输出:[1,2,2,4,3,5]
思路一:大佬题解
将大于等于x值得节点取出放到另一个链表,然后连接两个链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution { public ListNode partition (ListNode head, int x) { ListNode headDummy = new ListNode(0 ); ListNode tailDummy = new ListNode(0 ); ListNode tail = tailDummy; headDummy.next = head; head = headDummy; while (head.next!=null ){ if (head.next.val>=x){ ListNode nodeOut = head.next; head.next=head.next.next; tail.next = nodeOut; tail=tail.next; }else { head=head.next; } } tail.next=null ; head.next=tailDummy.next; return headDummy.next; } }
思路二:我的思路
开始以为不能做,没想到也可以跑通,是比较质朴的思路。
指针定位,节点的删除和插入操作
一个指针flag
定位第一个大于等于x的节点(小于的元素都要插到flag前)
一个指针location
定位flag
前最后一个节点,新元素插到其后,每一次插入后移一格
flag
和location
用来定位 再用一个指针head
用来遍历即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class Solution { public ListNode partition (ListNode head, int x) { ListNode dummyNode = new ListNode(-1 ); dummyNode.next = head; head = dummyNode; ListNode flag = dummyNode; ListNode location = dummyNode; while (head.next!=null ){ if (head.next.val>=x){ flag = head.next; location = head; break ; } head = head.next; } if (flag==dummyNode) return dummyNode.next; head = flag; while (head.next!=null ){ if (head.next.val<x){ ListNode temp = head.next; head.next = head.next.next; temp.next = flag; location.next = temp; location = location.next; }else { head = head.next; } } return dummyNode.next; } }
2.9 力扣143.重排链表
给定一个单链表 L:L0→L1→…→Ln-1→Ln , 将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→…
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
示例 1:
给定链表 1->2->3->4, 重新排列为 1->4->2->3.
一道比较综合的题,用到了查找中间节点、翻转链表、链表的插入操作
思路:
先找中间结点阶段,中间结点之后(不包括)的为链表2
链表2翻转得到链表3,
然后遍历链表3 直到它为null,,每次都做插入操作,插入之后注意链表1的指针移动
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Solution { public void reorderList (ListNode head) { ListNode fast = head; ListNode slow = head; while (fast!=null &&fast.next!=null ){ fast=fast.next.next; slow=slow.next; } ListNode head2 = slow.next; slow.next = null ; ListNode head3 = null ; while (head2!=null ){ ListNode tmepNode = head2.next; head2.next = head3; head3 = head2; head2 = tmepNode; } slow = head; while (head3!=null ){ ListNode temp = head3; head3 = head3.next; temp.next = slow.next; slow.next = temp; slow = slow.next.next; } } }
2.10 力扣234.回文链表
请判断一个链表是否为回文链表。
示例 1:
示例 2:
重排链表会做了这题就简单了
思路:
1找中间节点,分隔成链表1 链表2
2翻转链表2
3遍历比较每个元素值是否相同
代码:
不要被示例误导,回文串也有奇数个元素的。head2
和head1
的长度可能不一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Solution { public boolean isPalindrome (ListNode head) { if (head==null ) return true ; ListNode fast = head.next; ListNode slow = head; while (fast!=null &&fast.next!=null ){ fast = fast.next.next; slow = slow.next; } ListNode head2 = slow.next; slow.next = null ; head2 = reverseNode(head2); while (head!=null &&head2!=null ){ if (head.val!=head2.val) return false ; head = head.next; head2 = head2.next; } return true ; } public ListNode reverseNode (ListNode head) { if (head==null ||head.next==null ) return head; ListNode prev = null ; ListNode cur = head; while (cur!=null ){ ListNode tempNode = cur.next; cur.next = prev; prev = cur; cur = tempNode; } return prev; } }
2.11 力扣138. 复制带随机指针的链表
给你一个长度为 n
的链表,每个节点包含一个额外增加的随机指针 random
,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random –> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random –> y 。
返回复制链表的头节点。
思路一:map集合赋值新旧节点
hashmap
储存复制的节点,键为旧节点 值为新节点
第一次遍历将每个节点的值复制进map中 (此时next random
指针都为空)
第二次遍历将每个节点的两个指针赋值给新节点
新节点要指向新节点所以要用map.get()
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution { public Node copyRandomList (Node head) { if (head==null ) return null ; Map<Node,Node> map =new HashMap<>(); Node cur = head; while (cur!=null ){ Node newNode = new Node(cur.val); map.put(cur,newNode); cur = cur.next; } cur = head; while (cur!=null ){ Node newNode = map.get(cur); if (cur.next!=null ) newNode.next = map.get(cur.next); if (cur.random!=null ) newNode.random = map.get(cur.random); cur = cur.next; } return map.get(head); } }
思路二 链表原地复制
1在每个节点后原地创建复制节点,此时节点中只有value
2用来设置新结点的随机指针, 即新节点的random
指向对应旧结点随机指针的下一个
3只要将两个链表分离,返回新链表。集体操作就是一个新链表尾部添加、一个旧链表删除
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution { public Node copyRandomList (Node head) { Node cur = head; while (cur!=null ){ Node newNode = new Node(cur.val); newNode.next = cur.next; cur.next=newNode; cur = cur.next.next; } cur = head; while (cur!=null && cur.next!=null ){ if (cur.random!=null ) cur.next.random = cur.random.next; cur = cur.next.next; } Node dummyNode = new Node(-1 ); Node cur2 = dummyNode; cur = head; while (cur!=null &&cur.next!=null ){ cur2.next = cur.next; cur2 = cur2.next; cur.next = cur.next.next; cur = cur.next; } cur2.next = null ; return dummyNode.next; } }
2.12 力扣148.排序链表(中等)
给你链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
你可以在 O(n log n)
时间复杂度和常数级空间复杂度下,对链表进行排序吗?
示例 1:
1 2 >输入:head = [4,2,1,3] >输出:[1,2,3,4]
思路:
归并排序的思想 :“假设初始序列含有n 个记录,则可以看成是n 个有序(内部有序)的子序列,每个子序列的长度为 1 , 然后两两归并,得到[n /2] ([x ]表示不小于x 的最小整数)个长度为2 或 1 的有序子序列;再两两归并,……,如此重复,直至得到一个长度为n 的有序序列为止。“
需要两个函数:一个函数MergeSort
用来递归,将原链表平分为两个部分、分别递归调用生成有序链表;一个函数Merge
合并两个有序子链表,主要采用两个指针轮流后移的方法。
递归的出口是子链表的长度为1
1先将一个长链表分为两个等长(或差1)的子链表
2分别对两个子链表进行归并排序
3合并两个已经有序的子链表
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 class Solution { public ListNode sortList (ListNode head) { return mergeSort(head); } private ListNode mergeSort (ListNode head) { if (head==null || head.next==null ) return head; ListNode midNode = findMid(head); ListNode head2 = midNode.next; midNode.next = null ; head = mergeSort(head); head2 = mergeSort(head2); ListNode res = merge(head,head2); return res; } private ListNode findMid (ListNode head) { ListNode fast = head.next; ListNode slow = head; while (fast!=null &&fast.next!=null ){ fast = fast.next.next; slow = slow.next; } return slow; } private ListNode merge (ListNode head1, ListNode head2) { ListNode dummyNode = new ListNode(-1 ); ListNode cur = dummyNode; while (head1!=null && head2!=null ){ if (head1.val<=head2.val){ cur.next = head1; cur = cur.next; head1 = head1.next; }else { cur.next = head2; cur = cur.next; head2 = head2.next; } } if (head1==null ) cur.next=head2; else cur.next = head1; return dummyNode.next; } }
为什么题解直接用归并,而不是用快排、堆排序? 翻了一下还真有用快排和堆排序的题解,直接贴出来、以后再看。
链表的快排
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution { public static ListNode sortList (ListNode head) { return quickSort(head ,null ); } public static ListNode quickSort (ListNode head ,ListNode end) { if (head ==end || head.next ==end) return head; ListNode lhead = head ,utail = head ,p = head.next; while (p != end){ ListNode next = p.next; if (p.val < head.val){ p.next = lhead; lhead = p; } else { utail.next = p; utail = p; } p = next; } utail.next = end; ListNode node = quickSort(lhead, head); head.next = quickSort(head.next, end); return node; } }
链表的堆排(利用优先队列)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution { public ListNode sortList (ListNode head) { if (head == null ) return null ; PriorityQueue<Integer> pq = new PriorityQueue<>(); ListNode p = head; while (p != null ) { pq.offer(p.val); p = p.next; } ListNode q = new ListNode(0 ); ListNode resHead = q; while (pq.size() > 0 ) { q.val = pq.poll(); if (pq.size() > 0 ) { ListNode temp = new ListNode(0 ); q.next = temp; } q = q.next; } return resHead; } }
3 总结 链表啊链表,虽然双向链表和环形链表很难、但是基本不考(美滋滋)。单链表的一些考察点凭印象总结一下:
最常用的遍历,如果要用到哪一个结点必须先判断其不为空,,处理完要记得指针后移
最基础的 增删改拼接,增加要3点钟顺时针 ;删除直接改指针;有重复元素时用while删,并且要注意循环内部的结点不能为空(可用一个整型存值判断)
快慢指针 解决 倒数k结点、找中间元素、环形链表、第一个公共结点 问题。要注意每段的长度以及走多少步之后能相遇,草稿列一下公式画一下图;
反转链表 常规操作了、反转部分链表注意链表之间的拼接就好(多设几个指针)
重排链表和回文链表 ,基本就是 找中间元素+反转链表的组合题
复制随机指针的链表 ,遍历两次第一次复制节点值,第二次复制指针
排序链表 是 归并排序+找中间结点+已排好序的链表拼接 的组合题
链表的题目变化还是挺大的、手生了基本做不了、调试也很难通过。多练吧少年!奥里给
4 新的题目 新遇到的常考题或者做不出来的题都会放在这里!!
结果链表构造是要用一个移动指针cur 所以要用哑巴节点保存头节点的位置
因为要考虑进位的问题,其实链表之所以个位在前就是方便你进位!!
遍历完两个链表、如果短链表为空了 就以0代替之
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution { public ListNode addTwoNumbers (ListNode l1, ListNode l2) { ListNode dummyNode = new ListNode(-1 ); ListNode cur = dummyNode; 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; carry = sum / 10 ; sum = sum % 10 ; cur.next = new ListNode(sum); cur = cur.next; if (l1!=null )l1 = l1.next; if (l2!=null )l2 = l2.next; } if (carry==1 ) cur.next = new ListNode(1 ); return dummyNode.next; } }