floyd判圈算法

  1. 1. 1 判断是否有环
  2. 2. 2 求环的长度
  3. 3. 3. 求环的起点

floyd判圈算法(龟兔赛跑算法)简介

floyd算法判断链表是否有环
floyd算法计算环的长度
floyd算法寻找环的起点

  floyd算法主要基于两个快慢指针,一个慢指针slow,快指针fast。慢指针每次移动1,快指针每次移动2。算法的时间复杂度为O(N)

1 判断是否有环

  慢指针每次移动一步,快指针每次移动两步,若fast指向的对象与slow指针指向同一个对象,则说明有环。
证明:反证法:
  如果没有环,则slow指针永远不可能追上fast指针,指向同一个对象。(此足以证明有环)

问题2:fast指针会越过slow指针不相遇吗?从而增加时间复杂度?
  不可能。假设fast越过slow,则slow = k + 1, fast = k + 2, 从而错过,往前推一步可知,他们必定在k的位置相遇。

时间复杂度:O(N),在慢指针未走完一圈的时候,快慢指针必定能相遇。(如果无环,则O(N)不必证明)
证明:

  1. 快指针quick入环。之后,经过一段步骤,慢指针到达环的起点,准备入环。
  2. 此时,假设快指针距离慢指针x,如果在起点相遇,则x=0
  3. 设环的周长为L,那么之后就是快指针追赶慢指针,追赶距离为L-x
  4. 快指针的速度为每次2个距离,慢指针为每次一个距离,则快指针每次追赶2-1个距离,那么需要追赶(2-1) * (L-x) = L-x步。
  5. 在追赶L-x步的时间里,慢指针走了L-x的距离,由于x>=0,所以在慢指针最多走完一圈(L)的距离时,必定已经相遇。

202. 快乐数 - 力扣(LeetCode)、(287. 寻找重复数 - 力扣(LeetCode))
  如果是快乐数,则最后会变为1,如果不是,则会进入几个数的循环。因此不为1时,判断是否有环即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public boolean isHappy(int n) {
int slowNum = n;
int quickNum = getNextValue(n);
// 这里用quickNum != slowNum 作为出循环条件,无论是都为1,还是有环都可以跳出循环
while (quickNum != slowNum) {
slowNum = getNextValue(slowNum);
quickNum = getNextValue(getNextValue(quickNum));
}
return quickNum == 1;
}

public int getNextValue(int num) {
int res = 0;
while (num > 0) {
res += (num % 10) * (num % 10);
num = num / 10;
}
return res;
}
}

2 求环的长度

  当slow和fast相遇后,slow和fast必定在环上,只要让其中一个不动,另一个继续走,并计数,直到两者再次相遇,则可以得到环的长度

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
class Solution {
public boolean circleLen(int n) {
int slowNum = n;
int quickNum = getNextValue(n);
// 这里用quickNum != slowNum 作为出循环条件,无论是都为1,还是有环都可以跳出循环
while (quickNum != slowNum) {
slowNum = getNextValue(slowNum);
quickNum = getNextValue(getNextValue(quickNum));
}
// 出循环则 quickNum == slowNum
int len = 1;
slowNum = getNextValue(slowNum);
while (slowNum != quickNum) {
slowNum = getNextValue(slowNum);
len++;
}
return len;
}

public int getNextValue(int num) {
int res = 0;
while (num > 0) {
res += (num % 10) * (num % 10);
num = num / 10;
}
return res;
}
}

3. 求环的起点

有环链表

如图为一个有环链表,对于该链表有如下定义:

  • A为环的起点,S为链表的起点,B为slow与quick指针相遇的点。m为S到A的距离,n为A到B的距离
  • slow指针走过的所有节点为i,则quick指针必定走过节点2i。因为quick的速度是slow的2倍。

则有:

  1. 对于slow指针:i = m + n + aL (a为slow走过环的圈数,L为环的节点总数)
  2. 对于quick指针:2i = m + n + bL (b为quick走过的圈数,L同上)
  3. 第2点 - 第1点则有:i = (b-a)L, 则i 必定为L的整数倍。
  4. 带入第1点则有:m + n = (b - 2a) L,则m + n 必定为L的整数倍(b, a为整数,且m+n > 0, 所以有 b-2a > 0),m+n是一个完整的环
  5. 所以当从B点继续走m步,则必定停在A点(因为4,m+n是一个完整的环)。
  6. 但是m是多大,不清楚。此时,只需要一个指针从S点出发,另一个从B出发。如果走了m,S必定到达A点,B也必定到达A点,则当两个指针第一次相遇的时候,就是起点A。我们就不需要处理m的问题了。

142. 环形链表 II(LeetCode)

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
/**
* 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 || getNext(head) == null) {
return null;
}
// 慢指针
ListNode slowNode = getNext(head);
// 快指针
ListNode quickNode = getNext(getNext(head));
// 如果有节点为null,则说明能走到末尾,则必定无环,此时直接跳出循环,或者在环内相遇,也跳出循环
while (slowNode != null && quickNode != null && getNext(quickNode) != null && getNext(getNext(quickNode)) != null && slowNode != quickNode) {
slowNode = getNext(slowNode);
quickNode = getNext(getNext(quickNode));
}
// 有节点走到末尾,则说明无环,
if (quickNode == null || getNext(quickNode) == null || getNext(getNext(quickNode)) == null) {
return null;
}

// 将慢指针置于链表头节点,再次相遇,则为起点
slowNode = head;
while (slowNode != quickNode) {
slowNode = getNext(slowNode);
quickNode = getNext(quickNode);
}
return quickNode;
}

public ListNode getNext(ListNode node) {
return node.next;
}
}

注:此题也可以用哈希表做,用一个指针遍历,将走过的节点存储起来,如果有环,则哈希表中必定能找到这个点,并且第一次找到的点就为起点(因为环的起点在前面)。如果到达节点末尾,则说明无环。
哈希表的时间复杂度也是O(N),但是空间复杂度为O(N),floyd判圈的空间复杂度为O(1)。