一、题目
给定一个长度为 n+1 的数组nums
,数组中所有的数均在 1∼n1 的范围内,其中 n≥1。
请找出数组中任意一个重复的数。
样例
给定 nums = [2, 3, 5, 4, 3, 2, 6, 7]。
返回 2 或 3。
二、解析
解决这个问题的一种有效方法是使用快慢指针,也称为龟兔赛跑算法(Floyd's Cycle Detection Algorithm)。该算法的基本思想是在一个循环链表中,快指针和慢指针分别以不同的速度移动,如果存在环,则两者最终会相遇。
在这个问题中,可以将数组视为一个链表,数组中的元素值作为下一个节点的索引,构成一个链表。因为题目保证了数组中的元素值在 1 到 n 的范围内,所以数组中不存在负数,也不存在索引超出数组范围的情况。
原理:
当我们把数组中的元素看作链表中的节点时,题目要求找到数组中的任意一个重复数,实际上就是在链表中找到环的入口点。快慢指针算法的核心思想是利用两个不同速度的指针,如果存在环,这两个指针最终会相遇。
下面是算法的基本思路:
-
初始化:使用两个指针,一个慢指针
slow
和一个快指针fast
,初始时都指向数组的第一个元素nums[0]
。 -
寻找相遇点:快指针每次前进两步,慢指针每次前进一步,直到两者相遇。相遇时,说明链表中存在环。
-
重置一个指针:将其中一个指针(例如慢指针)重置到数组的第一个元素
nums[0]
,而另一个指针保持在相遇点。 -
寻找环的入口点:两个指针再以相同速度前进,直到它们再次相遇。相遇点即为环的入口点,也即重复的数。
这个原理的关键在于,当两个指针相遇时,说明链表中存在环。在寻找环的入口点时,将一个指针重置到链表头,然后两个指针以相同的速度前进,它们再次相遇的点就是环的入口点。在这个问题中,环的入口点对应于数组中的重复数。
这个算法的时间复杂度为 O(n),其中 n 是数组的长度。算法的空间复杂度为 O(1),因为只使用了常数额外的空间。
下面是用C语言实现的代码:
#include <stdio.h>
int findDuplicate(int* nums, int numsSize) {
// 初始化快慢指针
int slow = nums[0];
int fast = nums[0];
// 寻找相遇点
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
// 重置其中一个指针,并寻找环的入口点——这里可以想一想为什么:A
slow = nums[0];
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
// 返回环的入口点,即重复的数
return slow;
}
int main() {
// 示例用法
int nums[] = {1, 3, 4, 2, 2};
int numsSize = sizeof(nums) / sizeof(nums[0]);
int duplicate = findDuplicate(nums, numsSize);
printf("Duplicate: %d\n", duplicate);
return 0;
}
A:让我们来解释一下为什么这个过程能找到环的入口点:
-
首次相遇点:当快指针和慢指针首次相遇时,它们分别走过的步数之间存在关系,快指针走过的步数是慢指针的两倍。假设两者相遇时,慢指针走了 k 步,则快指针走了 2k 步,其中 k 是环的长度的整数倍。
-
重置指针位置:将慢指针重置到数组的第一个元素
nums[0]
,保持快指针在相遇点不动。 -
再次相遇:此时,慢指针从数组头部开始,而快指针还停留在相遇点。它们以相同的速度前进,当慢指针再次走了 k 步时,快指针走了 2k 步,即正好走完了环的若干圈,同时再次相遇。
-
相遇点即为入口点:再次相遇的点就是环的入口点。这是因为在第一次相遇后,慢指针已经在环内走了若干圈,而重置后再次相遇时,慢指针还需走若干圈才能达到入口点,而快指针已经在环内等待,因此它们在入口点相遇。
这个过程的本质是根据快慢指针相遇时,快指针已经走过的步数是慢指针的两倍的特性,找到环的入口点。这个算法的关键在于数学上的推理,而实际上这种方法是基于龟兔赛跑算法的原理。