【leetcode】287 寻找重复数(查找)

题目链接:https://leetcode-cn.com/problems/find-the-duplicate-number/

题目描述

给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。

示例 1:

输入: [1,3,4,2,2]
输出: 2

示例 2:

输入: [3,1,3,4,2]
输出: 3

说明:

  • 不能更改原数组(假设数组是只读的)。
  • 只能使用额外的 O(1) 的空间。
  • 时间复杂度小于 O(n2) 。
  • 数组中只有一个重复的数字,但它可能不止重复出现一次。

代码

1 快慢指针

参考:http://bookshadow.com/weblog/2015/09/28/leetcode-find-duplicate-number/
将数组看成链表,数组元素值看是下个节点的地址;由于元素限定在1~n范围内,所以数组访问不会越界。
(1)设置快慢指针,slow每次走1步,fast每次走2步;当fast和slow相遇时必然在环内
(2)确定环的入口:slow指针继续走, 且另设第三个指针从每次走一步,两个指针必定在入口处相遇

假设环的入口和起点的距离时m
当第三个指针走了m步到环的入口时
slow刚好走了n + m步,换句话说时饶了环i圈(环的周长为n/i)加m步(起点到入口的距离)
得到相遇的是环的入口,入口元素即为重复元素

【笔记】这道题(据说)花费了计算机科学界的传奇人物Don Knuth 24小时才解出来。并且我只见过一个人(注:Keith Amling)用更短时间解出此题。

快慢指针,一个时间复杂度为O(N)的算法。

其一,对于链表问题,使用快慢指针可以判断是否有环。

其二,本题可以使用数组配合下标,抽象成链表问题。但是难点是要定位环的入口位置。

举个例子:nums = [2,5, 9 ,6,9,3,8, 9 ,7,1],构造成链表就是:2->[9]->1->5->3->6->8->7->[9],也就是在[9]处循环。

其三,快慢指针问题,会在环内的[9]->1->5->3->6->8->7->[9]任何一个节点追上,不一定是在[9]处相碰,事实上会在7处碰上。

其四,必须另起一个for循环定位环入口位置[9]。这里需要数学证明。

http://bookshadow.com/weblog/2015/09/28/leetcode-find-duplicate-number/

对“其四”简单说明一下,既然快慢指针在环内的某处已经相碰了。那么,第二个for循环遍历时,res指针还是在不停的绕环走,但是必定和i指针在环入口处相碰。

以下是具体的证明过程

首先,令c为进入环的链的长度,然后令l为环的长度。接下来,令l’为大于c的l的倍数的最小值。可以得出结论:对于上文定义的任意ρ型序列的l’,都有

 x_{l'} = x_{2l'}

证明实际上非常直观并且具有自明性 - 这是计算机科学中我最喜欢的证明之一。思路就是由于l’至少为c,它一定包含在环内。同时,由于l’是环长度的倍数,我们可以将其写作ml,其中m为常数。如果我们从位置x_{l’}开始(其在环内),然后再走l’步到达x_{2l’},则我们恰好绕环m次,正好回到起点。

Floyd算法的一个关键点就是即使我们不明确知道c的值,依然可以在O(l’)时间内找到值l’。思路如下。我们追踪两个值"slow"和"fast",均从x_0开始。然后迭代计算

 slow = f(slow)
 fast = f(f(fast))

我们重复此步骤直到slow与fast彼此相等。此时,我们可知存在j满足slow = x_j,并且fast = x_{2j}。 由于x_j = x_{2j},可知j一定至少为c,因为此时已经在环中。另外,可知j一定是l的倍数,因为x_j = x_{2j}意味着在环内再走j步会得到同样的结果。最后,j一定是大于c的l的最小倍数,因为如果存在一个更小的大于c的l的倍数,我们一定会在到达j之前到达那里。所以,我们一定有j = l’,意味着我们可以在不知道环的长度或者形状的情况下找到l’。

要完成整个过程,我们需要明白如何使用l’来找到环的入口(记为x_c)。要做到这一步,我们再用最后一个变量,记为"finder",从x_0出发。然后迭代重复执行过程:

finder = f(finder)
slow   = f(slow)

直到finder = slow为止。我们可知:(1) 两者一定会相遇 (2) 它们会在环的入口相遇。 要理解这两点,我们注意由于slow位于x_{l’},如果我们向前走c步,那么slow会到达位置x_{l’ + c}。由于l’是环长度的倍数,相当于向前走了c步,然后绕环几圈回到原位。换言之,x_{l’ + c} = x_c。另外,考虑finder变量在行进c步之后的位置。 它由x_0出发,因此c步之后会到达x_c。这证明了(1)和(2),由此我们已经证明两者最终会相遇,并且相遇点就是环的入口。

算法的美妙之处在于它只用O(1)的额外存储空间来记录两个不同的指针 - slow指针和fast指针(第一部分),以及finder指针(第二部分)。但是在此之上,运行时间是O(n)的。要明白这一点,注意slow指针追上fast指针的时间是O(l’)。由于l’是大于c的l的最小倍数,有两种情况需要考虑。首先,如果l > c,那么就是l。 否则,如果l < c,那么我们可知一定存在l的倍数介于c与2c之间。要证明这一点,注意在范围c到2c内,有c个不同的值,由于l < c,其中一定有值对l取模运算等于0。最后,寻找环起点的时间为O( c)。这给出了总的运行时间至多为O(c + max{l, 2c})。所有这些值至多为n,因此算法的运行时间复杂度为O(n)。

复杂度分析
时间复杂度:O(n)
空间复杂度:O(1)

class Solution {
public:
    /*
     * 将数组看成链表,val是节点值也是下个节点的地址
     * 问题转换为判断链表有环
     * 时间复杂度O(n)
     */
    int findDuplicate(vector<int>& nums) {
        int slow = 0;
        // 当快慢指针相遇则在环内,但不一定是在环的入口相遇
        for (int fast = 0; slow != fast || fast == 0 ; ) {
            slow = nums[slow];
            fast = nums[nums[fast]];
        }

        // 确定环的入口
        for (int i = 0; i!=slow ; i = nums[i]) {
            slow = nums[slow];
        }
        return slow;
    }
};

2 二分查找+ 鸽笼原理

二分查找(Binary Search)+ 鸽笼原理(Pigeonhole Principle)
“不允许修改数组” 与 “常数空间复杂度”这两个限制条件意味着:禁止排序,并且不能使用Map等数据结构

小于O(n2)的运行时间复杂度可以联想到使用二分将其中的一个n化简为log n

二分枚举答案范围,使用鸽笼原理进行检验

根据鸽笼原理,给定n + 1个范围[1, n]的整数,其中一定存在数字出现至少两次。

(1)假设枚举的数字为 n / 2:
(2)遍历数组,若数组中不大于n / 2的数字个数超过n / 2,则可以确定[1, n /2]范围内一定有解,
(3)否则可以确定解落在(n / 2, n]范围内。

终止条件是low==high;因为最后区间会不断缩小,最终在重复值处相遇;
复杂度分析
时间复杂度:O(nlogn)
空间复杂度:O(1)

class SolutionII {
public:
    /*
     * 二分查找+鸽笼原理
     * 时间复杂度O(nlog n)
     */
    int findDuplicate(vector<int>& nums) {
        int low = 1, high = nums.size(); // 1和n
        while(low < high){
            int mid = (low + high) /2;
            int cnt = 0;
            //计算区间[l,mid]之间有多少个数
            for (auto num:nums) {
                if (num <= mid) ++cnt;
            }
            //区间[l,mid]之间应该有mid个数,若大于这个数,
            //那证明[1,mid]之间一定有重复的,所以搜索区间缩小为[l,mid]
            if (cnt > mid)
                high = mid;
            else
                low = mid + 1;
        }
        return low;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值