一个冷门算法——Floyd判圈算法在Leetcode中的应用
题目
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。示例 1:输入:nums = [1,3,4,2,2]
输出:2
示例 2:输入:nums = [3,1,3,4,2]
输出:3
示例 3 :输入:nums = [3,3,3,3,3]
输出:3
先搬上我的解法——人类的记忆机制。
人类的记忆机制
最自然的想法就是一个一个地去看,从左往右,第一个开始,然后看看以前出现过的元素我见过没。圈起来的应该不算额外的空间复杂度开支吧?(我只是把我见过的元素给圈起来了而已,反而是Python的索引其实是有机制会导致额外的时空复杂度开支)
class Solution:def findDuplicate(self, nums: List[int]) -> int:mem = set()for i,j in enumerate(nums):if j in mem:return jmem.add(j)
我看到一个题解很有意思,顺便学了一下Algorithms书里面不会提到的一些算法。首先我用非常数学的办法分析了一下这个题目的特征。
等价类的划分
这题有一些特征:
- nums长n+1,这意味着其索引的范围是[0,n]
- nums中元素的范围则是[1,n],被[0,n]完全包含
这就意味着可以把nums中的元素拿出来抽“索引”。现在定义一个函数fff:
f(x)=nums[x],x∈numsf(x) = \text{nums}[x], x\in \text{nums}f(x)=nums[x],x∈nums
这就定义了一个nums上的自我变换。
明显这个从nums到nums的变换不是一个双射,至少第一个元素没有任何原像可以映射到它,因此f(x)f(x)f(x)的值域一定是其定义域的真子集。nums中只有一个数字是重复的,很明显就是因为这个重复的数字导致了fff不是双射。我们回顾一下没有重复数字的情况。这种情况下,可以定义nums上的全排列,根据抽象代数的基本知识可知,全排列中的每一个数字都属于某个循环群,这些循环群是全排列对应集合的一个分割。现在我们缩小目光,只考虑fff在其值域上的限制。fff在其值域上的限制如果是个满射,一定是个单射,因此是双射,形成值域内的全排列。所以,fff在其值域上的限制内一定会有循环群(但是未必分割这个值域),如果路径到了循环群,路径可以建模成一个只有一个环的链表。
我们知道从nums[0]出发,肯定是在上述的循环群外面出发。
然后再进入这个循环群。那么这时场景就可以建模成为带有一个环的链表。
龟兔赛跑算法及其在本题上的证明
这是一个在链表上的算法,又称Floyd判圈算法。通过等价类的划分,我们已经知道关于圈一些基本事实,以及为什么可以建模成为带一个环的链表(设这个环的长度为n),那么接下来,就介绍一下这个龟兔赛跑算法。
该算法用了一快一慢两个指针,快指针的速度是2,慢指针的速度则是1。第一步,将两个指针都置于链表的起点。然后,Run起来!
我们标注一下慢指针踏入环的瞬间,不妨标记此时慢指针已经走了l步(但是我们并不知道这个l是多少)。现在按照顺序给环标号,比如慢指针进来的地方是0。显然,这个时刻快指针一定是在环内的。设这个时刻,此时快指针比慢指针在环中快m步(m<n),那么这个时候快指针在环中的位置就是m。我们稍稍看看这个m是多少。如果不考虑环的效应,快指针原来应该领先慢指针l步,现在考虑环的效应,那么有l%n=m。
因为快指针的速度是2,慢指针的速度是1,二者同向,那么速度差为1。慢指针每走一步,快指针与慢指针的距离就会增加1。我们是在环中计算的,这个增加有上限,当增加到n的时候,就是两个指针碰面的时刻。所以,不论原来的m是多少,两个指针一定会碰面。所以,两个指针在环中标号为n-m的地方碰面(快指针在m+2(n-m)%n=n-m的位置)。
下面这个算法要找入口。现在这俩指针的位置一样,把快指针的速度降为1,然后放到链表的起点。原来的慢指针不动。现在继续让它们跑起来。
显然,接下来它们都需要走l步。Floyd判圈算法神奇的地方在于,这时候这两个指针会相遇。让我们验证一下这个结论。圈里的指针位于n-m+l处,而圈外指针走了l步后到达0处。神奇的是(n-m+l)%n=(n+l-m)%n=(n+kn+m-m)%n=0。这就意味着两个指针是相遇的!指针的位置,就是环入口(在环里)的位置。这个值,恰好就是重复的那个数字。
class Solution:def findDuplicate(self, nums: List[int]) -> int:slow = nums[0]fast = nums[0]fast = nums[nums[fast]]slow = nums[slow]while fast!=slow:fast = nums[nums[fast]]slow = nums[slow]fast = nums[0]while fast!=slow:fast = nums[fast]slow = nums[slow]return fast