剑指offer(第二版)面试题3. 数组中重复的数字,leetcode 287. Find the Duplicate Number

欢迎访问我的剑指offer(第二版)题解目录
对应的leetcode题目可点击leetcode287. 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.

散列(时间 O ( n ) O(n) ,空间 O ( n ) O(n)

算法设计

由于有n+1个数字,数字范围是从1~n,因此可以建立一个哈希表flag,标记1~n这n个数字是否出现过。扫描整个数组,假设当前扫描到的数字为m,如果flag[m]==true,表示数字m出现了重复,返回m即可。否则令flag[m]==true,继续扫描数组。

C++代码

class Solution {
   public:
    int findDuplicate(vector<int>& nums) {
        bool flag[nums.size()] = {};
        for (int m : nums)
            if (flag[m])
                return m;
            else
                flag[m] = true;
        return 0;  //没有重复数字
    }
};

修改数组元素(时间 O ( n ) O(n) ,空间 O ( 1 ) O(1)

算法设计

可以采用书中的算法:

现在让我们重排这个数组。从头到尾依次扫描这个数组中的每个数字。当扫描到下标为i的数字时,首先比较这个数字(用m表示)是不是等于i。如果是,则接着扫描下一个数字;如果不是,则再拿它和第m个数字进行比较。如果它和第m个数字相等,就找到了一个重复的数字(该数字在下标为i和m的位置都出现了);如果它和第m个数字不相等,就把第i个数字和第m个数字交换,把m放到属于它的位置。接下来再重复这个比较、交换的过程,直到我们发现一一个重复的数字。

leetcode这道题和书中的题目描述略有不同,因为leetcode这道题是有n+1个数字,数字范围是从1~n。因此当扫描到下标为i的数字时,要比较这个数字是不是等于i+1

C++代码

class Solution {
   public:
    int findDuplicate(vector<int>& nums) {
        for (int i = 0; i < nums.size() - 1;) {
            if (nums[i] == i + 1) {
                ++i;
                continue;
            }
            if (nums[i] == nums[nums[i] - 1])
                return nums[i];
            swap(nums[i], nums[nums[i] - 1]);
        }
        return 0;  //没有重复数字
    }
};

不修改数组元素(时间 O ( n l o g n ) O(nlogn) ,空间 O ( 1 ) O(1)

算法设计

可以采用书中的算法:

我们把从1~n的数字从中间的数字m分为两部分,前面一半为1~m,后面一半为m+1~n。如果1~m的数字的数目超过m,那么这一半的区间里一定包含重复的数字;否则,另一半m+1~n的区间里一定包含重复的数字。我们可以继续把包含重复数字的区间一分为二,直到找到一个重复的数字。这个过程和二分查找算法很类似,只是多了一步统计区间里数字的数目。

C++代码

class Solution {
   public:
    int findDuplicate(vector<int>& nums) {
        int left = 1, right = nums.size() - 1;
        while (left < right) {
            int mid = left + (right - left) / 2, k = count_if(nums.begin(), nums.end(), 
            [left, mid](int a) {return a >= left and a <= mid;});
            if (k > mid - left + 1)
                right = mid;
            else
                left = mid + 1;
        }
        return left;
    }
};

不修改数组元素(时间 O ( n ) O(n) ,空间 O ( 1 ) O(1)

算法设计

这个算法有些难懂,因为它是将这道题目转换成了求一个有环链表中环的第一个结点的题目。我建议你先解决leetcode上的这两道题:141. Linked List Cycle142. Linked List Cycle II。我们现在需要解决的是如何将这道题转化成求一个有环链表中环的第一个结点。
举一个具体例子,假设数组存储的元素是这样的:

下标 0 1 2 3 4 5 6
元素 1 4 6 6 6 2 3

我们可以把它看成是一个静态链表的存储方法,即下标代表链表中的一个结点编号,元素表示链表结点的指针,那么上面的数组代表的链表应该是这样的:

0
1
4
2
6
3
5

我们可以把它看成一个有向图,那么很明显,这个有向图每个结点出度都为1,那么这样的有向图中,有n+1个结点必然有环。那么我们想求得重复数字,它的入度至少为2,这样的结点必然处于环内。因而这道题就相当于求一个有环链表中环的第一个结点。具体算法可以参考142. Linked List Cycle II的讨论区。

C++代码

class Solution {
   public:
    int findDuplicate(vector<int>& nums) {
        int i = 0, j = 0, k = 0;
        do {
            i = nums[i];
            j = nums[nums[j]];
        } while (i != j);
        while (i != k) {
            i = nums[i];
            k = nums[k];
        }
        return i;
    }
};
发布了528 篇原创文章 · 获赞 1015 · 访问量 37万+

猜你喜欢

转载自blog.csdn.net/richenyunqi/article/details/103381539