141. 环形链表

image.png
image.png

思路一:双指针

判断是否有环

可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。
为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢
首先第一点: fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。
那么来看一下,为什么fast指针和slow指针一定会相遇呢?
可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。
会发现最终都是这种情况, 如下图:
image.png
fast和slow各自再走一步, fast和slow就相遇了
这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。
动画如下:
环形链表 - 图4
C++代码如下:

  1. class Solution {
  2. public:
  3. bool hasCycle(ListNode *head) {
  4. ListNode* slow = head;
  5. ListNode* fast = head;
  6. while (fast && fast->next) {
  7. slow = slow->next;
  8. fast = fast->next->next;
  9. if (fast == slow)
  10. return true;
  11. }
  12. return false;
  13. }
  14. };

image.png

思路二:哈希表

可以使用哈希表来存储所有已经访问过的节点。每次我们到达一个节点,如果该节点已经存在于哈希表中,则说明该链表是环形链表,否则就将该节点加入哈希表中。重复这一过程,直到我们遍历完整个链表即可。

  1. class Solution {
  2. public:
  3. bool hasCycle(ListNode *head) {
  4. unordered_set<ListNode*> see;
  5. while (head) {
  6. if (see.count(head)) {
  7. return true;
  8. }
  9. see.insert(head);
  10. head = head->next;
  11. }
  12. return false;
  13. }
  14. };

使用集合保存访问过的节点,还要每次查找节点是否存在,时间和空间复杂度都不低
image.png

142. 环形链表 II

image.png
image.png

思路一:双指针

这道题目,不仅考察对链表的操作,而且还需要一些数学运算。
主要考察两知识点:

  • 判断链表是否环
  • 如果有环,如何找到这个环的入口

判断链表是否环

通过141题,已经知道了如何判断是否有环

如果有环,如何找到这个环的入口

此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:
image.png

相遇时,slow走过的节点数量环形链表 - 图10,fast走过的节点数/公式环形链表 - 图11,其中n为fast指针在环内绕的圈数
由于fast的速度是slow的两倍,那么fast和slow相遇时,fast访问的节点个数也是slow的两倍,有:
环形链表 - 图12

两边消掉一个环形链表 - 图13得:
环形链表 - 图14
因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。
所以要求x ,将x单独放在左面:
环形链表 - 图15
再从环形链表 - 图16中提出一个 环形链表 - 图17来,整理公式之后为如下公式:
环形链表 - 图18
n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

这个公式说明什么?
先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了slow指针了。
当 n为1的时候,公式就化为
环形链表 - 图19
这就意味着,从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点
也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。
让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。
动画如下:
环形链表 - 图20
那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针
其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。

直接使用slow和fast之一,只定义一个head出发的节点就行了

  1. class Solution {
  2. public:
  3. ListNode *detectCycle(ListNode *head) {
  4. ListNode* fast = head;
  5. ListNode* slow = head;
  6. while (fast && fast->next) {
  7. slow = slow->next;
  8. fast = fast->next->next;
  9. if (slow == fast) { // 有环
  10. ListNode* index = head;// 定义一个节点,让它和slow同时出发
  11. while (slow != index) { // 当slow和index相遇时,就找到了环的入口
  12. slow = slow->next;
  13. index = index->next;
  14. }
  15. return slow;
  16. }
  17. }
  18. return NULL;
  19. }
  20. };

补充

为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?
首先fast一定是先进入环的,当slow也进环后,slow跑一圈的时间,fast可以跑两圈,fast和slow相对速度为1,相当于fast以每次一个节点的速度接近slow,假设环的长度为A,考虑两种极端情况:

  • 环入口就是相遇的节点,那么他俩差正好一圈,也就是slow还没开始走第一圈呢,就被追上了,y=0
  • slow第一次达到环的入口,fast刚好在slow的前面,这时候,fast追上slow需要用最多的时间,相当于slow领先了A-1步,由相对速度为1,当slow走A-1步时,fast会追上slow,这时候b = A - 1

也就是说,无论如何,slow进入环的第一圈,一定会被fast追上!!

Carl哥的思路:
image.png
首先slow进环的时候,fast一定是先进环来了。
如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:
image.png
可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。
重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:
image.png
那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。
因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。
也就是说slow一定没有走到环入口3,而fast已经到环入口3了
这说明什么呢?
在slow开始走的那一环已经和fast相遇了
那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去

剑指offer思路

先通过快慢指针找到相遇的节点,然后统计环中的节点个数

环形链表 - 图24
记环长为n,头节点到入口之前共有x个节点,那么链表的总长度为 x + n,现在快指针先走n步,距离环的入口刚还还剩下x个节点,由此:
用两个指针:

  • 快指针在头节点开始向前走n个节点
  • 慢指针在头节点开始,和第一个指针同时走,他们相遇的位置就是环的入口

    1. /**
    2. * Definition for singly-linked list.
    3. * struct ListNode {
    4. * int val;
    5. * ListNode *next;
    6. * ListNode(int x) : val(x), next(NULL) {}
    7. * };
    8. */
    9. class Solution {
    10. public:
    11. ListNode *detectCycle(ListNode *head) {
    12. // 先找到相遇节点
    13. ListNode* fast = head;
    14. ListNode* slow = head;
    15. ListNode* meeting = new ListNode();
    16. while (fast && fast->next) {
    17. slow = slow->next;
    18. fast = fast->next->next;
    19. // 如果有环
    20. if (fast == slow) {
    21. meeting = fast;
    22. // 统计环中节点个数
    23. int nodeOfLoop = 1;
    24. while (fast->next != meeting) {
    25. fast = fast->next;
    26. nodeOfLoop++;
    27. }
    28. // 找环的入口
    29. // 先移动fast
    30. fast = head;
    31. for (int i = 0; i < nodeOfLoop; i++) {
    32. fast = fast->next;
    33. }
    34. // fast 和 slow 同时移动
    35. slow = head;
    36. while (fast != slow) {
    37. fast = fast->next;
    38. slow = slow->next;
    39. }
    40. return fast;
    41. }
    42. }
    43. return NULL;
    44. }
    45. };

    思路二:哈希表

    可以在扫描链表的过程中,将链表的节点放入集合中,在放入节点之前检查,集合中是否存在该节点,发现第一个已经存在的节点,返回这个节点,这个节点就是环的入口。

    1. class Solution {
    2. public:
    3. ListNode *detectCycle(ListNode *head) {
    4. unordered_set<ListNode*> visited;
    5. while (head) {
    6. if (visited.count(head))
    7. return head;
    8. visited.insert(head);
    9. head = head->next;
    10. }
    11. return NULL;
    12. }
    13. };