【算法】Leetcode 287 Find the Duplicate Number 找重复元素

题目

原题链接
Given an array nums containing n + 1 integers where each integer is between 1 and n (inclusive), prove that at least one duplicate number must exist. Assume that there is only one duplicate number, find the duplicate one.

给定一个包含 n + 1 n+1 n+1个元素的数组,每个元素都在 1 1 1 n n n之间(闭区间),证明至少一个元素重复,找出重复元素。

随想

这也是我遇到的一个面试题,当时想了很长时间,表现也不太好。
这个题和另外一个题:每个元素出现两次,有一个出现一次(Leetcode 136 Single Number)较为类似,有正好反过来的感觉。Leetcode136那道题异或位运算直接秒之。
证明一定有一个重复元素并不难,根据抽屉原理即可。
下面说如何找到它。
注意:并不要求 1 1 1 n n n中的元素都要出现,也即 [ 2 , 2 , 2 ] [2, 2, 2] [2,2,2]这种输入也是合法的。

思路及代码

方法1 哈希表

用一个hash table去存储,然后查重复的元素。
平均时间复杂度是 O ( n ) \mathcal{O}(n) O(n),空间复杂度是 O ( n ) \mathcal{O}(n) O(n)

优点:

  • 打破元素必须在 1 1 1 n n n的约束,适用范围广
  • 不修改输入参数
  • 时间复杂度低

缺点:

  • 空间复杂度高。

如果要求了空间复杂度是 O ( 1 ) \mathcal{O}(1) O(1),那么该如何做呢?

方法2 排序

对原数组排序,排序后找相邻重复元素。
平均时间复杂度是 O ( n log ⁡ n ) \mathcal{O}(n\log n) O(nlogn),空间复杂度是 O ( 1 ) \mathcal{O}(1) O(1)

优点:

  • 打破元素必须在 1 1 1 n n n的约束,适用范围广
  • 空间复杂度低

缺点:

  • 时间复杂度高
  • 修改了输入参数

方法3 二分查找

对原数组进行二分查找,假如结果确定在在区间范围 [ 1 , n ] [1,n] [1,n]中,则记 m i d = ⌊ ( 1 + n ) 2 ⌋ mid = \left \lfloor \frac{(1 + n)}{2} \right \rfloor mid=2(1+n) ,统计 [ 1 , m i d ] [1,mid] [1,mid]的元素个数是否小于等于 m i d − 1 + 1 mid -1 + 1 mid1+1,如果是,说明结果在 [ m i d + 1 , n ] [mid + 1, n] [mid+1,n],否则说明结果在 [ 1 , m i d ] [1, mid] [1,mid];重复上述过程。
最坏时间复杂度 O ( n log ⁡ n ) \mathcal{O}(n\log n) O(nlogn),空间复杂度 O ( 1 ) \mathcal{O}(1) O(1)

优点:

  • 空间复杂度低
  • 不修改输入参数

缺点:

  • 时间复杂度高

方法3可以看作是方法2的一个改进。
如果即要求空间复杂度空间复杂度 O ( 1 ) \mathcal{O}(1) O(1),又要求时间复杂度 O ( n ) \mathcal{O}(n) O(n),该如何实现呢?

代码

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int left = 1, right = nums.size(), mid, count;
        while (left < right) {
            mid = (left + right) / 2;
            count = 0;
            for (int i = 0; i < nums.size(); ++i) {
                count += (nums[i] <= mid && nums[i] >= left);
            }
            if (count <= (mid - left + 1)) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return left;
    }
};

方法4 利用索引交换

1 1 1 n n n都当作索引使用,例如把元素 a a a放到数组 [ a ] [a] [a]位置上。索引 [ 0 ] [0] [0]是没有元素能放对的,所以可以一直用索引 [ 0 ] [0] [0]中的元素交换它对应位置的元素,直到这两个元素相等,即找到了重复元素。
最坏时间复杂度 O ( n ) \mathcal{O}(n) O(n),空间复杂度 O ( 1 ) \mathcal{O}(1) O(1)。因为每一次交换一定能放对一个元素,一共只有 n n n个元素,所以最坏只需要交换 O ( n ) \mathcal{O}(n) O(n)次。
(当时面试答得是这种方法,虽然后来手写代码写的是错的……emm)

优点:

  • 空间复杂度低
  • 时间复杂度低

缺点:

  • 修改了输入参数

代码:

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        while (nums[0] != nums[nums[0]]) {
            swap(nums[0], nums[nums[0]]);
        }
        return nums[0];
    }
};

方法5 链表判环

这个是非常推荐的一种方法!
把索引当作是地址,0当作是链表头,那么本质上这个题就是在找环入口,而且是在一定有环的情况下。
这样的话用快慢指针即可。类似Leetcode 142 Linked List Cycle II,不会链表找入口的话可以先看下这个题.

最坏时间复杂度 O ( n ) \mathcal{O}(n) O(n),空间复杂度 O ( 1 ) \mathcal{O}(1) O(1)

优点:

  • 空间复杂度低
  • 时间复杂度低

缺点:

暂无

代码:

class Solution {
public:
    int findDuplicate(vector<int>& nums) {
        int fast = 0, slow = 0;
        do {
            fast = nums[fast];
            fast = nums[fast];
            slow = nums[slow];
        } while  (fast != slow);  // 这个do-while的写法非常精巧,可以仔细品一品
        
        fast = 0;
        
        while (fast != slow) {
            fast = nums[fast];
            slow = nums[slow];
        }
        
        return fast;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值