给定一个包含 n + 1 个整数的数组 nums,其数字都在 [1, n] 范围内。
假设 nums 只有一个重复的整数,请找出这个重复的数。
要求:
nums。示例:
本题要求在不修改数组、只使用 O(1) 额外空间的前提下寻找重复数。
如果使用排序,会修改数组或需要额外空间;如果使用 Set 记录出现过的数字,空间复杂度是 O(n),都不符合要求。
因为数组长度是 n + 1,数字范围是 [1, n],可以把数组看成一张链表:
由于有 n + 1 个位置,却只会指向 [1, n] 中的下标,一定会形成环。重复数字就是这个环的入口。
所以本题可以转化为“链表找环入口”,使用快慢指针解决。
把 nums[i] 看成从下标 i 出发能走到的下一个位置。
使用两个阶段的指针:
slow:慢指针,每次走一步,即 slow = nums[slow]。fast:快指针,每次走两步,即 fast = nums[nums[fast]]。finder:第二阶段从起点 0 出发,用来和 slow 一起寻找环入口。第一阶段让 slow 和 fast 在环内相遇。
第二阶段让 finder 从起点出发,slow 从相遇点出发,两者每次都走一步。它们再次相遇的位置,就是环入口,也就是重复数字。
本题没有滑动窗口,而是维护快慢指针在“数组构成的链表”上的移动。
第一阶段:寻找环内相遇点。
当 slow === fast 时,说明快慢指针已经在环内相遇。
第二阶段:寻找环入口。
finder 从下标 0 出发,slow 从第一阶段相遇点出发。二者每次都走一步,最终会在环入口相遇。
这个入口对应的值就是重复数。
本题不需要在遍历过程中不断更新答案。
答案只在第二阶段 finder === slow 时确定:
因为指针位置本身就是重复数字。
例如 nums = [1,3,4,2,2]:
环入口是 2,所以重复数字就是 2。
边界条件:
[1, n],因此从 0 出发后一定会进入合法下标范围。nums = [1,1],此时重复数字是 1。易错点:
nums[i] 当成普通值来比较次数,本解法中它表示“下一个下标”。finder 要从 0 开始,不是从 nums[0] 开始。finder,不是 nums[finder]。因为环入口下标就是重复数字。nums,否则不满足题目要求。定义三个距离:
图示如下:
环的长度是:
第一阶段中,慢指针每次走 1 步,快指针每次走 2 步。
当它们第一次相遇时,慢指针走了:
快指针走了:
快指针比慢指针多走的路,一定是环长度的整数倍:
所以:
其中 k 是某个正整数。
整理一下:
也就是:
这表示:从相遇点出发走 a 步,等价于先绕若干圈,再走 c 步回到环入口。
所以第二阶段让:
finder 从起点 0 出发。slow 从第一次相遇点出发。当 finder 走了 a 步到达环入口时,slow 也刚好走了 a 步到达环入口,因此它们一定会在环入口相遇。
以 nums = [1,3,4,2,2] 为例:
这里:
环长是:
并且:
所以:
它们都会到达环入口 2,所以重复数字就是 2。
O(n)。快慢指针会先进入环并相遇,第二阶段再走到环入口,总移动次数是线性的。O(1)。只使用了 slow、fast、finder 三个额外变量,没有修改原数组。