如何快速确认链表上是否存在环

这个问题遇到过很多次了,但是并不是说每次都很清楚。所以这次用golang的代码来实现一遍,加深理解与记忆。

如果一个链表上不存在环,那么一定能够遍历完链表中所有的Node节点;如果存在环,那么可以想象成存在一个圆形操场。在一个圆里面,如果有两个人,行走的速度不一样,那么一定会有相遇的那一刻。最佳的解法便是基于此想法,当然也有其他的解法,比如说将每个遍历的节点放进一个set里面,如果存在过,那么就存在环。

数据结构:

1
2
3
4
type Node struct {
index int
next *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
func createNodeList() *Node{
rand.Seed(time.Now().Unix())
length := rand.Intn(100) + 1

head := new(Node)
var tail *Node

// 构建链表
for idx ,curNode := 1, head; idx <= length; idx ++ {
curNode.index = idx
if idx != length {
curNode.next = new(Node)
curNode = curNode.next
} else {
tail = curNode
}
}

buildRing := rand.Intn(2)
if buildRing == 0 {
// 构建环
enterRingIndex := rand.Intn(length) + 1
enterRingNode := head
for idx:= 2; idx <= enterRingIndex; idx ++{
enterRingNode = enterRingNode.next
}
tail.next = enterRingNode
fmt.Println("length : ", length, ", enter ring index : ", enterRingIndex, ", Ring length : ", length - enterRingIndex + 1)
} else {
fmt.Println("don't build ring")
}

return head
}

输出链表,用于debug

1
2
3
4
5
6
func outputNodeList(head * Node) {
for head != nil {
fmt.Print(head.index, " ")
head = head.next
}
}

使用两个下标,用于抽象表示圆形操场中的两个人,leader走前面,每次走两步,follower走后面,每次走一步,如果相遇,返回相遇时的Node节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func nodeListExistRing(head * Node) *Node {
leader, follower := head, head
for leader != nil && follower != nil {
leader = leader.next
if leader == nil {
return nil
}
leader = leader.next

follower = follower.next

if (leader == follower) {
return leader
}
}
return nil
}

如果链表中存在环,那么环的长度就等于:follower再走一圈时走过的步长。

1
2
3
4
5
6
7
8
9
func calRingLength(meetNode * Node) int {
curNode := meetNode
curNode = curNode.next
count := 1
for ; curNode != meetNode; count ++ {
curNode = curNode.next;
}
return count
}

如果链表存在环,那么入环点等于将leader指向head后,并以每次一步的速度往前,直到与follower再次相遇,此时的节点便是入环点。

如何证明上面的步骤是正确的?

也就是说,入环时走过的路程,等于S2加上(S1+S2)的整数倍,如果leader步长变成1,并且从A点出发到达B点,那么走过的路程D,一定会等于S2+(n-1)(S1 + S2),也就是说一定会在B点相遇。B点即入环点。

1
2
3
4
5
6
7
8
9
func calEnterRingIndex(meetNode * Node, head * Node) int {
var enterRingIndex = 0;
for head != meetNode {
head = head.next
meetNode = meetNode.next
enterRingIndex ++
}
return enterRingIndex + 1
}

启动类,将前面过程,按照想要的逻辑拼装在一起。

1
2
3
4
5
6
7
8
9
func main() {
head := createNodeList()
meetNode := nodeListExistRing(head)
if meetNode == nil {
outputNodeList(head)
} else {
fmt.Println("RING EXIST!", ", Ring length: ", calRingLength(meetNode), ", Enter Ring Index : ", calEnterRingIndex(meetNode, head))
}
}

附录:

文中公式、图及推算过程的latex源码:

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
\begin{tikzpicture}
% \draw[dashed, file=blue!20]{0, 0} circle(0.5cm);
\draw(0, 0) circle (1.5cm);
\coordinate[label = above:$O$] (O) at (0, 0);
\node at(0, 0)[circle,fill,inner sep=1pt]{};

\coordinate[label = right:$C$] (C) at (-30:1.5);
\node at(C)[circle,fill,inner sep=1pt]{};

\coordinate[label = below right:$S_1$] (S1) at (-60:1.5);
\coordinate[label = left:$S_2$] (S2) at (120:1.5);

\coordinate[label = below: $B$] (B) at (-90:1.5);
\node at(B)[circle,fill,inner sep=1pt]{};

\coordinate[label = below: $A$] (A) at (-7, -1.5);
\node at(A)[circle,fill,inner sep=1pt]{};

\draw (A) -- (B) node[above, midway] (line) {$D$};
\end{tikzpicture}

设n为leader与follower在相遇前,比follower多走的圈数,那么便存在
\[ 2(D + S_1) = D + S_1 + n * (S_1 + S_2) \]

化简得:
\[ 2*D + 2*S_1 = D + S_1 + n * (S_1 + S_2) \]
\[ D + S_1 = n * (S_1 + S_2) \]
\[ D = n * (S_1 + S_2) - S_1 \]
\[ D = (n - 1) * (S_1 + S_2) + S_1 + S_2 - S_1 \]

即:
\[ D = (n - 1) * (S_1 + S_2) + S_2 \]

参考:https://zouyu4524.github.io/blog/tikz-plot/

如何快速确认链表上是否存在环

https://eucham.me/2020/03/16/a3995576f721.html

作者

遇寻

发布于

2020-03-16

更新于

2022-04-21

许可协议

评论