你可以通过简单地在列表中运行两个指针来检测它,这个过程在同名寓言中被称为龟兔算法:
- 首先,检查列表是否为空(
head 是null)。如果是这样,则不存在循环,因此请立即停止。
- 否则,在第一个节点
head上启动第一个指针tortoise,在第二个节点head.next上启动第二个指针hare。
- 然后连续循环直到
hare 是null(这在单元素列表中可能已经是正确的),在每次迭代中将tortoise 提前1 和hare 2。保证兔子首先到达终点(如果有 终点),因为它开始领先并且跑得更快。
- 如果没有结束(即,如果有一个循环),它们最终将指向同一个节点,您可以停止,因为您知道您在循环内某处找到了一个节点。
考虑以下从3 开始的循环:
head -> 1 -> 2 -> 3 -> 4 -> 5
^ |
| V
8 <- 7 <- 6
tortoise 从 1 开始,hare 从 2 开始,它们具有以下值:
(tortoise,hare) = (1,2) (2,4) (3,6) (4,8) (5,4) (6,6)
因为它们在(6,6) 处相等,并且由于hare 应该总是在非循环列表中超出tortoise,这意味着您发现了一个循环。
伪代码如下所示:
def hasLoop (head):
return false if head = null # Empty list has no loop.
tortoise = head # tortoise initially first element.
hare = tortoise.next # Set hare to second element.
while hare != null: # Go until hare reaches end.
return false if hare.next = null # Check enough left for hare move.
hare = hare.next.next # Move hare forward two.
tortoise = tortoise.next # Move tortoise forward one.
return true if hare = tortoise # Same means loop found.
endwhile
return false # Loop exit means no loop.
enddef
该算法的时间复杂度为O(n),因为访问的节点数(龟兔赛跑)与节点数成正比。
一旦您知道循环中的节点,还有一个O(n) 保证方法可以找到循环的开始。
当你在循环的某处找到一个元素但你不确定循环的开始在哪里之后,让我们回到原来的位置。
head -> 1 -> 2 -> 3 -> 4 -> 5
^ |
| V
8 <- 7 <- 6
\
x (where hare and tortoise met).
这是要遵循的过程:
- 提前
hare 并将size 设置为1。
- 然后,只要
hare和tortoise不同,继续前进hare,每次增加size。这最终给出了循环的大小,在本例中为 6。
- 此时,如果
size 是1,这意味着您必须已经处于循环的开始(在大小为 1 的循环中,只有一个可能的节点可以在循环中,所以它必须是第一个)。在这种情况下,您只需返回 hare 作为开始,然后跳过下面的其余步骤。
- 否则,将
hare 和tortoise 设置为列表的第一个 元素,并将hare 精确地推进size 次(在本例中为7)。这给出了两个指针,它们恰好循环的大小不同。
- 然后,只要
hare 和 tortoise 不同,就将它们一起推进(兔子以更平稳的速度奔跑,与乌龟的速度相同 - 我猜它从第一次奔跑就累了)。由于它们将始终完全保持 size 元素彼此分开,tortoise 将到达循环的开始恰好与hare 返回循环开始。
您可以通过以下演练看到这一点:
size tortoise hare comment
---- -------- ---- -------
6 1 1 initial state
7 advance hare by six
2 8 1/7 different, so advance both together
3 3 2/8 different, so advance both together
3/3 same, so exit loop
因此3 是循环的起点,并且由于这两个操作(循环检测和循环开始发现)都是O(n) 并按顺序执行的,所以整个事情加在一起也是O(n)。
如果您想要一个更正式的证明,可以查看以下资源:
如果您只是在支持该方法(不是正式证明),您可以运行以下 Python 3 程序,该程序评估其在大量尺寸(循环中有多少元素)和导入(循环开始之前的元素)。
你会发现它总是找到两个指针相交的点:
def nextp(p, ld, sz):
if p == ld + sz:
return ld
return p + 1
for size in range(1,1001):
for lead in range(1001):
p1 = 0
p2 = 0
while True:
p1 = nextp(p1, lead, size)
p2 = nextp(nextp(p2, lead, size), lead, size)
if p1 == p2:
print("sz = %d, ld = %d, found = %d" % (size, lead, p1))
break