数据结构与算法面试宝典 - 前微软资深软件工程师 - 拉勾教育

在上一讲中,我给你介绍了解决链表问题的 “三板斧” 中的第一斧:假头,你知道了带假头的链表一共有 6 种基本的操作,分别是初始化、追加结点、头部插入结点、查找结点、插入指定位置之前和删除结点。

如果说三板斧的第一斧平平淡淡,大巧不工;第二斧就是鬼斧神工,生成新链表后,链表的交换、反转求解都会变得极其简单 ;第三斧则是奇思妙想,双指针也叫快慢指针)用在链表上经常可以解决一些单个指针难以解决的问题。学会了这两种思路,算法面试中的链表题就如同探囊取物了。

注:大部分链表题主要考查动手能力,因此在本讲将不再按照 “分析四步法” 进行讲解。

三板斧的第二斧:新链表

做链表的反转、交换等操作时,我不建议直接在原来的链表上进行操作。一种可取的思路是,把这些操作想象成要生成新的链表,然后借助这些新的链表,完成原本比较复杂的操作。这个方法就是我们今天要讲的“第二斧”——新链表

接下来,我将采用这种新思路,带你解决一些面试中经常会遇到的疑难题目。

例 1:链表反转

题目】输入一个链表的头结点,反转该链表,并返回反转后链表的头结点。

输入:1->2->3

输出:3->2->1

分析】这里借助假头和新链表求解,思路如下:

  • 建立一个新的带假头的空链表;
  • 遍历旧链表,依次取出旧链表中的每个结点;
  • 采用头部插入的方法放到新链表中;
  • 返回 dummy.next。

画图】这里我们利用示意图演示如下:

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图1

代码】对应的代码如下(解析在注释里):

  1. class Solution {
  2. public ListNode reverseList(ListNode head) {
  3. ListNode dummy = new ListNode();
  4. while (head != null) {
  5. ListNode tmp = head.next;
  6. head.next = dummy.next;
  7. dummy.next = head;
  8. head = tmp;
  9. }
  10. return dummy.next;
  11. }
  12. }

代码:Java/C++/Python

复杂度分析:每个结点只遍历一次,所以时间复杂度为 O(N),内存空间只使用了常量空间,因此空间复杂度为 O(1)。

小结】仔细查看代码之后,链表反转的考点就是之前我们学到的基本操作:假头,头部插入法,再结合今天学习的新链表的思路。可以总结如下:

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图2

例 2:删除结点

题目】给定一个链表头及一个整数值,要求把链表里面等于整数值的结点都从链表中移除出去。

输入:1->2->3->2->4, remove = 2

输出:1->3->4。

解释:要移除的整数值是 2。那么移除之后,返回的结果应该是 1->3->4。

分析】这里我们不采用在原来的链表上进行删除的办法,而是采用新链表的操作思路:

  • 建立一个新的带假头的空链表;
  • 遍历旧链表,依次取出旧链表中的每个点,如果不删除这个结点,那么就采用尾部插入方法接到新链表中。

可以发现,在这里没有出现结点交换的操作。采用新链表的思路,避免了在原链表上不停地做结点的删除。为了方便你理解,我制作了动图演示,如下所示:

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图3

代码】基于以上思想,可以写出代码如下(解析在注释里):

  1. class Solution {
  2. public ListNode removeElements(ListNode head, int val) {
  3. ListNode dummy = new ListNode();
  4. ListNode tail = dummy;
  5. ListNode p = head;
  6. while (p != null) {
  7. ListNode back = p.next;
  8. if (p.val != val) {
  9. tail.next = p;
  10. tail = p;
  11. }
  12. p = back;
  13. }
  14. tail.next = null;
  15. return dummy.next;
  16. }
  17. }

代码:Java/C++/Python

复杂度分析:时间复杂度 O(N),空间复杂度 O(1)。

小结】我们将这道题的考点层层剥离之后,就只剩下生成 dummy 新链表,尾巴追加新结点,以及新链表的思路。关于解决这道这类题目的思路、重点以及分析方法,建议你先尝试自己梳理总结,再来看我给出的思维导图:

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图4

如果我们仔细对比链表反转与删除结点,会发现,这两者的不同之处在于:

  • 链表反转使用的是头部插入的方法
  • 删除结点采用的是尾部追加的方法

只是换了一个考点,题目就完全大变样了。如果我们再严格地对比这两个题目,可以发现:

  • 链表反转时,头部插入是条件的
  • 删除结点时,尾部 append 是条件的

这种条件的千变万化,会带来很多有趣的题目。比如下面这道练习题。

练习题 1:给定一个排序链表,删除重复出现的元素,使得每个元素只出现一次。

输入: 1->1->2->3->3

输出: 1->2->3

代码:Java/C++/Python

练习题 2:给定一个排序链表,删除重复出现的元素,只留下没有重复出现的元素。

输入:1->1->2->3->3

输出:2

代码:Java/C++/Python

你可以把答案或者思考的过程写在评论区,我们一起讨论。

例 3 :合并

题目】合并给定的两个有序链表。

输入:a = 1->4->6, b = 3->5->7

输出:1->3->4->5->6->7

分析】首先应该是生成一个带假头的新链表 C,然后把 A,B 中的元素从小到大,依次添加到新生成的链表 C 中。因此,我们还需要使用到尾部插入法

具体操作方法如下。

  • 第一步:A,B 两个指针分别指向 A,B 链表的表头。
  • 第二步:依次取出 A,B 两个指针中更小的值加入新链表中。
  • 第三步:返回 C 链表假头的 next。

具体演示如下所示:

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图5

代码】有了前面的思路,可以写出代码如下(解析在注释里):

  1. class Solution {
  2. public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
  3. ListNode dummy = new ListNode();
  4. ListNode tail = dummy;
  5. while (l1 != null || l2 != null) {
  6. if (l2 == null || l1 != null && l1.val < l2.val) {
  7. tail.next = l1;
  8. tail = l1;
  9. l1 = l1.next;
  10. } else {
  11. tail.next = l2;
  12. tail = l2;
  13. l2 = l2.next;
  14. }
  15. }
  16. tail.next = null;
  17. return dummy.next;
  18. }
  19. }

代码:Java/C++/Python

复杂度分析:时间复杂度 O(N),空间复杂度 O(1)。

小结】如果我们再分析一下这道题目,可以发现考点仍然是:

  • 生成 dummy 新链表
  • 选择结点往新链表尾部追加数据

此时的尾部 append 是条件的:需要从两个链表头中选择一个较小的数据进行追加。当然,有条件的 append 还可以变成各种其他的条件来操作。不过即使千变万化,只要你看清楚题的考点,就能轻松应对、。

那么这里我们不妨再选择其中一个考点 “选择较小的数” 进行练习。在原题中,只有两个链表,所以可以直接通过比较得到较小的结点。可是如果有 k 个链表要合并的时候,又应该怎么做呢?比如下面这道练习题:

练习题 3:给定 k 个有序链表,合并成一个有序链表

输入:[1->4->5,1->3->4, 2->6]

输出:[1->1->2->3->4->4->5->6]

代码:Java/C++/Python

你可以把答案或者思考的过程写在评论区,我们一起讨论。

例 4:交换链表中的结点

题目】给定一个链表,需要将里面的结点两两交换。

输入:[1->2->3->4->5->6]

输出:[2->1->4->3->6->5]

【分析】经过观察发现,只不过把偶数位置与奇数位置的结点进行了交换。为了避免在原始链表中进行结点间的交换操作,我们可以采用如下方法:

  1. 生成两个新链表,一个用来存放奇数位置结点的链表 odd,一个用来存放偶数位置结点的链表 even;
  2. 遍历旧链表,并且把奇数位置上的结点放到 odd 链表中,把偶数位置的结点放到链表 even 中;
  3. 合并 odd 链表与 even 链表。

为了方便你理解,我同样制作了动图演示,如下:

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图6

到这里,新增链表已经从一条变成了两条,来帮助我们解决这道题目。

代码】有了思路,我们可以写出代码如下(解析在注释里):

  1. class Solution {
  2. private ListNode mergeList(ListNode a, ListNode b) {
  3. ListNode dummy = new ListNode();
  4. ListNode tail = dummy;
  5. while (a != null || b != null) {
  6. if (a != null) {
  7. tail.next = a;
  8. tail = a;
  9. a = a.next;
  10. }
  11. if (b != null) {
  12. tail.next = b;
  13. tail = b;
  14. b = b.next;
  15. }
  16. }
  17. tail.next = null;
  18. return dummy.next;
  19. }
  20. public ListNode swapPairs(ListNode head) {
  21. ListNode oddDummy = new ListNode();
  22. ListNode oddTail = oddDummy;
  23. ListNode evenDummy = new ListNode();
  24. ListNode evenTail = evenDummy;
  25. int index = 0;
  26. ListNode p = head;
  27. while (p != null) {
  28. ListNode back = p.next;
  29. if ((index & 0x01) == 0) {
  30. evenTail.next = p;
  31. evenTail = p;
  32. } else {
  33. oddTail.next = p;
  34. oddTail = p;
  35. }
  36. index++;
  37. p = back;
  38. }
  39. oddTail.next = null;
  40. evenTail.next = null;
  41. return mergeList(oddDummy.next, evenDummy.next);
  42. }
  43. }

代码:Java/C++/Python

复杂度分析:每个结点会访问两次,拆分一次,合并一次,所以时间复杂度为 O(N),空间复杂度为 O(1)。

小结】这道题的考点也比较明确了,可以拆分出以下考点:

  • 拆分链表
  • 新链表的思路
  • 合并链表的操作

尤其需要注意的是,使用新链表思路时,可以通过生成多条新链表来解决以前处理起来比较麻烦的问题。至此,我们一起进一步扩展了链表知识。此外,还发现了一些小型的组合操作,比如:拆分链表,合并链表。在合并时,如果按照不同的条件合并,就需要写出不一样的合并代码,结合前面例 3,可以知道合并分两种:

  • 有序合并
  • 先后合并

到这里可以总结出我们更加丰富的知识路线图,如下图所示:

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图7

在这道题中,链表是两两成对进行了反转,那么如果是 k 个一组进行反转应该怎么办呢?我们再来看看与交换有关的练习题。

练习题 4:给定一个链表,要求将链表 k 个一组进行反转,如果最后一组不足 k 个,那么不反转。返回反转之后的链表。

输入:A = [1, 2, 3, 4, 5], k = 2

输出: [2, 1, 4, 3, 5]

代码:Java/C++/Python

练习题 5:给定一个链表,从链表尾部开始,k 个一组进行反转,如果左边的分组不足 k 个,那么不反转。返回反转之后的链表。

输入:A = [1, 2, 3, 4, 5], k = 2

输出:[1, 3, 2, 5, 4]

解释:注意是从链表的尾部开始 k 个一组的。所以这里是[1], [2, 3], [4, 5]这样分组来进行反转。

代码:Java/C++/Python

三板斧的第三斧:双指针

虽然新链表的思路非常有趣,但是关于它的更多探索还是应该留给你自己。收拾好行囊,我们将要去看更加瑰丽的奇景——双指针

双指针,顾名思义就是两个指针在链表上移动。实际上,我们在前面链表的查找中已经使用过双指针了:比如链表中指定位置插入一个新结点,就使用了两个指针,一前一后两个指针在链表上前进

其实两个指针在链表上前进时,有很多种形式,常见的主要有以下两种。

  1. 间隔指针:前面的指针先走一步,然后后面的指针再一起走;前面的指针先走 k 步,后面的指针再一起走。
  2. 快慢指针:两个指针的速度一快一慢前进,比如一个每次走一步,一个每次走两步。

接下来,我们来看看双指针能解决什么类型的问题。

例 5:链表的倒数第 k 个结点

题目】给定一个链表,删除链表中的倒数第 k 个结点。这里我们认为最后一个结点是倒数第 1 个

输入:1->2->3, k = 2

输出: 1->3

【分析】首先第一种常规思路是,先统计出整个链表的长度 len, 再去取第 len-k 结点的前驱进行删除。

但是,面试的时候,面试官往往会加一个限制条件 :只能遍历链表一次

以后凡是遇到链表题,看到这句话,实际上就是在告诉你 “用双指针吧”。思路如下:

  1. 在原链表前面加上 dummy,变成带假头的链表
  2. front 指针从 dummy 开始,走 k 步,然后停下来
  3. back 指针指向链表 dummy 假头
  4. 然后两个指针再一起走
  5. 当 front 指针指向最后一个结点时,back 指针刚好指向倒数第 k 个结点的前驱。

解题思路有了,还有两个细节需要你特别注意。

细节 1】你需要小心处理三种情况:

  1. 链表长度 < k,此时什么也不做;
  2. 链表长度 == k,此时删除原来的链表头结点;
  3. 链表长度 > k,此时找到倒数第 k 个结点的前驱,然后删除倒数第 k 个结点

接下来,我们分别讨论这三种情况。

情况 1:链表长度小于 k。front 指针会先走 k 步,如果链表长度小于 k,那么必然会导致 front 指针行走的步数小于 k,此时应该什么也不做。

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图8

情况 2:链表长度等于 k。此时需要删除倒数第 k 个结点,也就是旧链表的 head 结点。

当 front 指针先走完 k 步之后,back 指针刚好位于 dummy 结点。而 dummy 结点就是倒数第 k+1 个结点,那么此时可以直接通过 back 指针删除它后面的结点(刚好是 head,也就是倒数第 k 个)。

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图9

情况 3:链表长度大于 k。back 指针刚好位于倒数第 k+1 个结点,此时可以直接通过 back 指针删除它后面的结点(刚好是倒数第 k 个)。

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图10

我们发现:情况 2 和情况 3 实际上都是用 back 指针来删除后面的结点。因此,这两种情况可以一起处理。

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图11

细节 2】任何时候,front 最后停下来的位置一定要位于链表的最后一个结点。这是因为:要想删除倒数第 k 个结点的前驱结点,需要 back 刚好指向倒数第 k+1 个结点,那么就必须要让 front 非空,即指向倒数第一个结点。

代码】有了思路以及相应的细节,我们就可以利用代码来解决问题了(解析在注释里):

  1. class Solution {
  2. public ListNode removeNthFromEnd(ListNode head, int k) {
  3. ListNode dummy = new ListNode();
  4. dummy.next = head;
  5. int preWalkedSteps = 0;
  6. ListNode front = dummy;
  7. while (preWalkedSteps < k &&
  8. front != null && front.next != null) {
  9. front = front.next;
  10. preWalkedSteps++;
  11. }
  12. ListNode back = dummy;
  13. while (front != null && front.next != null) {
  14. back = back.next;
  15. front = front.next;
  16. }
  17. if (preWalkedSteps == k) {
  18. back.next = back.next.next;
  19. }
  20. return dummy.next;
  21. }
  22. }

代码:Java/C++/Python

复杂度分析:时间复杂度 O(N),空间复杂度 O(1)

小结】当做完这道题之后,我们可以进一步完善双指针的技巧,总结的思维导图如下:

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图12

然后,我们再来总结一下这道题目的考点。首先除了思路 “双指针” 以外,你还需要注意写代码的技巧。

  • 将旧链表改造成带 dummy 结点的链表,方便删除 head 结点。这是能让情况 2 和情况 3 统一处理的关键。
  • 让指针指向链表最后一个结点的 while 语句的写法。
  • 利用移动步数来判断链表长度与 k 的关系。

接下来我们一起看一下双指针的另外一种形式,快慢指针

例 6:拆分链表

题目】给定一个链表,需要把链表从中间拆分成长度相等的两半(如果链表长度为奇数,那么拆分之后,前半部分长度更长一点)。

输入:[1->2->3->4->5]

输出:[1->2->3, 4->5]

分析】我们需要分为 2 步:

  1. 找到链表的中间结点
  2. 从中间结点把链表分为两半

那么问题是,如何找到中间结点呢?如果是首先求出链表的长度,然后再利用 getPreNode(len/2) 函数的前驱,再把链表拆分成两半。

但是,这可能不是面试官想要的解法,因为这种解法会将链表遍历两遍,面试官可能会说:“只能遍历一次”。又听到了这个声音,这就是告诉你需要用双指针了。

所以问题的关键就是如何使用双指针找到链表的中间结点,可以采用如下办法:

  • 假设链表头在左边,尾巴在右边,两个指针 s1、s2 从链表头开始往右走;
  • s1 表示每次只往前走一步,s2 则表示每次只往前走 2 步;
  • 在同样的时间内,当 s2 指向链表的末尾,s1 指针便指向链表的中间结点。

只是在写代码的时候,需要特别注意以下 2 点:

  1. 当有偶数个结点,s2 是空指针,此时,s1 位于后半部分指针的头部,因此需要返回s1 的前驱

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图13

  1. 当有奇数个结点,s2 是最后一个结点,此时 s1 指针位于前半部分的最后,直接返回 s1即可。

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图14

如果找到了中间结点,那么就可以直接进行拆分了。

代码】接下来我们就实现拆分链表的逻辑,代码如下(解析在注释里):

  1. class Solution {
  2. private ListNode findMiddleNode(ListNode head) {
  3. ListNode dummy = new ListNode();
  4. dummy.next = head;
  5. ListNode s2 = head;
  6. ListNode s1 = head;
  7. ListNode pre = dummy;
  8. while (s2 != null && s2.next != null) {
  9. pre = s1;
  10. s1 = s1.next;
  11. s2 = s2.next.next;
  12. }
  13. return s2 != null ? s1 : pre;
  14. }
  15. public ListNode[] split(ListNode head) {
  16. ListNode mid = findMiddleNode(head);
  17. ListNode back = mid.next;
  18. mid.next = null;
  19. return new ListNode[]{head, back};
  20. }
  21. }

代码:Java/C++/Python

复杂度分析:时间复杂度 O(N),空间复杂度 O(1)。

小结】这道题的核心就是如何通过双指针找到链表的中间结点。考点还是清晰明了,我们可以再将双指针的分析要点总结如下:

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图15

不过对于这道题,我想给你留几个有趣的小问题,可以帮助你加深代码的理解,希望你可以尝试回答以下两个问题,并写在留言区,我们一起讨论。

  • 为什么没有判断空链表,对于空链表的支持是怎么完成的?
  • 为什么 s1, s2 要从 head 开始走,如果从 dummy 开始走可以吗?如果可以,会有什么样的代码改动?

练习题 6: 将一个链表进行重排,如果我们用 L[x] 表示链表的第 x 个结点(从 0 开始)。将链表 L[0]->L[1]->L[2]->L[3]-> …. ->L[N-1] 重新排列为 L[0]->L[N-1]->L[1]->L[N-2]->L[2]->L[N-3]…..。

输入:1->2->3->4->5

输出:1->5->2->4->3

代码:Java/C++/Python

例 7:链表环问题

题目】给定一个链表,原本的链表尾巴如果不为空,并且指向了链表的中间结点,这样我们就认为这个链表存在一个环。给定一个链表,判断链表中是否存在环?

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图16

分析】首先,如果链表中存在环,只用一个指针遍历肯定是永无止境的,这一个指针会在环里面打转。因此,我们可以再次利用双指针,s1,s2 两个指针都从链表头开始,s1 指针表示每次只往前走一步,s2 指针则是每次只往前走两步。那么链表最终只有两种情况:

1.s1 == s2,这个时候链表存在环;

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图17

2.s1 != s2,这个时候链表不存在环。

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图18

不过我们还是需要处理两种边界条件

  1. 当为空链表的时候,s1 == s2,但是实际上此时链表无环;
  2. 当链表中只存在一个结点,并且无环的时候,运行的结果也会是 s1 == s2。

这两种边界条件的处理,只需要特殊判断一下即可。

代码】有了前面了思路,那么我们就可以写出解问题的代码了(解析在注释里):

  1. public class Solution {
  2. public boolean hasCycle(ListNode head) {
  3. if (head == null || head.next == null) {
  4. return false;
  5. }
  6. ListNode s1 = head;
  7. ListNode s2 = head;
  8. while (s2 != null && s2.next != null) {
  9. s2 = s2.next.next;
  10. s1 = s1.next;
  11. if (s1 == s2) {
  12. break;
  13. }
  14. }
  15. return s1 == s2;
  16. }
  17. }

代码:Java/C++/Python

复杂度分析:时间复杂度为 O(N),空间复杂度为 O(1)。

小结】至此,我们完成了快慢指针的学习,可以在知识图谱中加上链表环问题了,如下图所示:

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图19

这里我还想给你留一个小问题:在寻找链表环的过程中,对于两种特殊情况,我们实际上进行了特殊判断,那么有没有什么办法可以避免这种特殊的断呢?

小提示:想想我们之前学习过的假头。

老规矩,希望你尝试思考并把想法写在留言区,期待和你一起讨论。另外,我也会根据大家的留言反馈,不定时输出加餐内容,比如练习题详解、留言区问题点评等。

扩展】在面试中,伴随着链表环问题的,往往还有后招:如果链表中存在环,能不能把形成环的那个结点找出来?

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图20

我们可以把这个问题转化成一个数学问题。我们一起看一下下面这张图:

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图21

这里我们只考虑链表存在环的情况。假设 s1 慢指针与 s2 快指针在环中某个位置相遇。此时:

  • s1 指针走过的路径长度为 a = x + y
  • s2 指针走过的路径长度为 b = x + y + n * (y + z)

由于两个指针都是从同一个地点出发,s2 指针走得更快,那么走的长度肯定是 s1 指针的两倍。所以可以得到 b = 2a,即 b = x + y + n * (y + z) = 2x + 2y

由此,可以推导出 x = n (y + z) - y = (n-1)(y+z) + z,即 x - z = (n-1) * (y + z)

从 x-z 表达式可以看出,如果有两个指针同时从头结点,相遇结点这两个地方出发,它们肯定会在环形入口相遇。因为它们之间的差值刚好是圆环长度的整数倍(更加严格一点的证明可以用数学归纳法)。

经过数学证明,我们可以写出求解代码如下(解析在注释里):

  1. public class Solution {
  2. public ListNode detectCycle(ListNode head) {
  3. if (head == null || head.next == null) {
  4. return null;
  5. }
  6. ListNode s1 = head;
  7. ListNode s2 = head;
  8. while (s2 != null && s2.next != null) {
  9. s1 = s1.next;
  10. s2 = s2.next.next;
  11. if (s1 == s2) {
  12. break;
  13. }
  14. }
  15. if (s1 != s2) {
  16. return null;
  17. }
  18. s1 = head;
  19. while (s1 != s2) {
  20. s1 = s1.next;
  21. s2 = s2.next;
  22. }
  23. return s1;
  24. }
  25. }

代码:Java/C++/Python

复杂度分析:时间复杂度为 O(N),空间复杂度为 O(1)。

总结与延伸

经过这两讲的学习,你终于可以用这三板斧来毒打链表题了,在抄家伙之前,我们一起回想下每招式的作用吧。

  • 第一斧:假头。假头的作用主要是避免关于空链表的判断与讨论,假头还可以用来避免检查前驱结点为空的情况。
  • 第二斧:新链表。新链表的引入是为了解决在旧链表中进行原地的交换、插入、删除,把复杂的操作变成在新链表中头部插入或者尾部添加。
  • 第三斧:双指针。双指针主要是用于寻找链表中的特定结点,双指针的走法可以一次一步,可以有快有慢,出发点也可以有前有后。

了解了思路,你还需要深入理解操作的代码模板,然后就可以成功地进行解题实战了。这里我已经为你总结好了《链表题通关路线图》,请参照此地图来通关链表题吧。

05 | 链表:如何利用“假头、新链表、双指针”解决链表题?(下) - 图22

链表操作是很多其他复杂算法的基础,需要你熟练掌握,比如 LRU,跳表等数据结构里面都会用到链表。希望你课后能熟练地运行本讲介绍的思路

此外,从算法的难度上来说,实际上链表题并不算太难,但是非常考验基本功。我在处理链表题时,经常把文中介绍的题目作为模板深刻理解,达到熟练记忆的程度。我希望你在理解解题思路的基础上,也能够熟练记忆这些模板,逐渐建立一个系统的知识体系。

思考题

最后,我再给你留一道思考题。

链表排序:给定一个单向链表,如何给这个链表排序,要求复杂度达到 O(nlogn)。

  • 你能使用所讲的创建新链表 + 快排的思想吗?
  • 你能使用快慢指针 + 合并排序的思想来解决吗?

解法 1:Java/C++/Python
解法 2:Java/C++/Python

学会了链表的三板斧,处理链表问题,变得越来越容易了。不过我们可不能总是待在舒适区,还有很多算法与数结构等着我们去征服。下一讲将介绍 06 | 树:如何深度运用树的遍历?记得按时来探险。