龟兔赛跑算法

一、题目

给定一个长度为 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 的范围内,所以数组中不存在负数,也不存在索引超出数组范围的情况。

原理:

当我们把数组中的元素看作链表中的节点时,题目要求找到数组中的任意一个重复数,实际上就是在链表中找到环的入口点。快慢指针算法的核心思想是利用两个不同速度的指针,如果存在环,这两个指针最终会相遇。

下面是算法的基本思路:

  1. 初始化:使用两个指针,一个慢指针 slow 和一个快指针 fast,初始时都指向数组的第一个元素 nums[0]

  2. 寻找相遇点:快指针每次前进两步,慢指针每次前进一步,直到两者相遇。相遇时,说明链表中存在环。

  3. 重置一个指针:将其中一个指针(例如慢指针)重置到数组的第一个元素 nums[0],而另一个指针保持在相遇点。

  4. 寻找环的入口点:两个指针再以相同速度前进,直到它们再次相遇。相遇点即为环的入口点,也即重复的数。

这个原理的关键在于,当两个指针相遇时,说明链表中存在环。在寻找环的入口点时,将一个指针重置到链表头,然后两个指针以相同的速度前进,它们再次相遇的点就是环的入口点。在这个问题中,环的入口点对应于数组中的重复数。

这个算法的时间复杂度为 O(n),其中 n 是数组的长度。算法的空间复杂度为 O(1),因为只使用了常数额外的空间。

下面是用C语言实现的代码:

#include 

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:让我们来解释一下为什么这个过程能找到环的入口点:

  1. 首次相遇点:当快指针和慢指针首次相遇时,它们分别走过的步数之间存在关系,快指针走过的步数是慢指针的两倍。假设两者相遇时,慢指针走了 k 步,则快指针走了 2k 步,其中 k 是环的长度的整数倍。

  2. 重置指针位置:将慢指针重置到数组的第一个元素 nums[0],保持快指针在相遇点不动。

  3. 再次相遇:此时,慢指针从数组头部开始,而快指针还停留在相遇点。它们以相同的速度前进,当慢指针再次走了 k 步时,快指针走了 2k 步,即正好走完了环的若干圈,同时再次相遇。

  4. 相遇点即为入口点:再次相遇的点就是环的入口点。这是因为在第一次相遇后,慢指针已经在环内走了若干圈,而重置后再次相遇时,慢指针还需走若干圈才能达到入口点,而快指针已经在环内等待,因此它们在入口点相遇。

这个过程的本质是根据快慢指针相遇时,快指针已经走过的步数是慢指针的两倍的特性,找到环的入口点。这个算法的关键在于数学上的推理,而实际上这种方法是基于龟兔赛跑算法的原理。

你可能感兴趣的:(ACWING每日一题,算法)