《剑指 Offer》学习记录:题 3:数组中的重复数字

题 3_1:找出数组中重复的数字

题干

在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为 7 的数组{2, 3, 1, 0, 2, 5, 3},那么对应的输出是重复的数字 2 或者 3。——《剑指 Offer》P39

测试用例

  1. 长度为 n 的数组里包含一个或多个重复的数字;
  2. 数组中不包含重复的数字;
  3. 无效输入测试用例,例如输入空指针、数组中包含 0~n-1 之外的数字。

法一:快速排序

解法思想

这种方法非常简单粗暴,也就是无论数组的结构如何,直接使用某种排序算法对数组进行排序。这么做可以直接把相同的数据聚集到一起,只需要对数组进行遍历即可求解。

解题代码

int findRepeatNumber(vector<int>& nums) {
    //调用 sort 函数对 vector 容器排序
    sort(nums.begin(),nums.end());
    for(int i = 0;i < nums.size()- 1; i++)
    {
        if(nums[i] == nums[i + 1])
        {
            return nums[i];
        }
    }
    return -1;
}

时空复杂度

时间复杂度取决于使用的排序算法,例如使用冒泡、选择排序时间复杂度是 O(n^2),使用快速排序或堆排序时间复杂度是 O(n㏒n)。遍历数组的时空复杂度为 O(n),因此得到算法的 T(n㏒n + n),时间复杂度为 O(n)。
由于这种解法直接对原数组进行修改,没有借助其他的辅助空间,因此空间复杂度为 O(1)。

法二:hash

解法思想

hash 就是数据保存的存储位置和关键字之间存在一个映射关系,使得关键字可以和存储位置直接对应起来。用人话讲,也就是可以另外开辟一个长度为 n 的数组作为计数表,由于传入的数组的每个数据元素的范围在 0~n-1 之间,因此可以用这个数组的 n 个位置的 index 分别对应一个数字,用 num[index] 存储某个数字出现的次数。

制作出这样的 hash 表之后就很简单了,只需要遍历传入的数组并计数,若某个数字统计到了 2 次即为重复数据。当然也可以用 Set 容器或 Map 容器实现计数操作,原理也是一样的。

解题代码

int findRepeatNumber(vector<int>& nums) {
    int hash[nums.size()];    //构造 nums.size 大小的 hash
    memset(hash, 0, sizeof hash);    //初始化

    for(int i = 0; i < nums.size(); i++)
    {
        if(++hash[nums[i]] > 1)
        {
            return nums[i];    //返回数量大于 1 的数据元素
        }
    }
    return -1;
}

时空复杂度

由于这种解法使用了空间换时间的思想,仅需要对传入的数组进行遍历,因此时间复杂度为 O(n)。
这种写法需要申请一个和传入数组相同大小的数组作为辅助空间,辅助空间的大小为 nums.size(),因此空间复杂度为 O(n)。

法三:元素置换

解法思想

这种方法比较巧妙,不需要申请额外空间的情况下可以实现时间复杂度为 O(n)。由于传入的数组大小为 n,而数据范围在 0 ~ n-1。因此当我们对数组进行排序时,若数组中没有重复的数据,数组中的每一个数据元素将会和 index 相等。根据这个结论,可以对数组进行遍历,将数组中的每个元素通过交换的方式放到对应的 index 位置上。设数组中的某个元素为 num[i],若 num[i] == num[num[i]] 则说明数组中存在重复数据。
例如输入长度为 7 的数组{2, 3, 1, 0, 2, 5, 3},则遍历数组当 index = 0 时需要执行 4 次交换操作才能把数据 0 移动到 index = 0 的位置上。

当 index = 1 时,由于 num[1] = 1 已经归位,可以直接向下遍历。

同理当 index = 2、3 时,由于 num[2] = 2、num[3] = 3 已经归位,可以直接向下遍历。

当 index = 4 时,由于 num[4] = 2 = num[2] = num[num[4]],说明出现了重复的数据,求得了这个问题的解。

解题代码

int findRepeatNumber(vector<int>& nums) {
    int temp = 0;

    for(int i = 0; i < nums.size(); i++)
    {
        while(i != nums[i])
        {
            if(nums[nums[i]] == nums[i])    //数据重复
            {
                return nums[i];
            }
            //将 num[i] 通过交换的方式放回 num[num[i]]
            temp = nums[i];
            nums[i] = nums[temp];
            nums[temp] = temp;
        }
    }
    return -1;
}

时空复杂度

这种写法本质上是把每个数据元素通过交换的方式,替换回和其相等的 index 的位置,因此需要进行 num.size 次交换操作,时间复杂度为 O(n)。
由于这种解法直接对原数组进行修改,没有借助其他的辅助空间,因此空间复杂度为 O(1)。

题 3_2:找出数组中重复的数字

题干

在一个长度为 n+1 的数组里的所有数字都在 0 到 n-1 的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,如果输入长度为 8 的数组{2, 3, 5, 4, 3, 2, 6, 7},那么对应的输出是重复的数字 2 或者 3。——《剑指 Offer》P41

测试用例

  1. 长度为 n 的数组里包含一个或多个重复的数字;
  2. 数组中不包含重复的数字;
  3. 无效输入测试用例(输入空指针)。

解法思想

这道题目是 3_1 的变种,此时数组中至少有一个重复的数据,但是不能对数组进行修改。可以把数组拷贝一份之后做快速排序,但是这么做就达不到解题的目的了。使用 hash 进行计数也是不错的方法,这种方法的时间复杂度是 O(n),空间复杂度也是 O(n)。如果不借助辅助空间的话,可以利用二分查找思想进行搜索。假设数组 nums 的长度为 n,若数组中数字范围在 [0,n/2) 的数字元素个数等于 n/2,就说明数字范围在 [0,n/2) 的数字不存在重复,在 (n/2,n] 范围内有重复。若数组中数字范围在 [0,n/2) 的数字元素个数大于 n/2,就说明数字范围在 [0,n/2) 的数字存在重复。反复使用二分的思想缩小解空间的范围,最终就可以找到重复的数据元素。

解题代码

int findRepeatNumber(vector<int>& nums) {
    int low = 0;
    int high = nums.size() - 1;
    int mid = (low + high) / 2;
    int count;    

    while(low <= high)
    {
        count = 0;
        for(int i = 0; i < nums.size(); i++)
        {
            //统计 low ~ mid 范围内元素的个数
            if(nums[i] >= low && nums[i] <= mid)
                count++;
        }
        if(low == high)    //找到重复或数组无重复
            break;
        else if(count <= (mid - low + 1))    //low ~ mid 范围内无重复
            low = mid + 1;
        else    //low ~ mid 范围内有重复
            high = mid;
    }
    if(count > 1)    //找到重复数据
        return nums[mid];
    else    //数组无重复
        return -1;
}

时空复杂度

这种写法是按照二分查找的思想进行求解的,因此总共会遍历 ㏒n 次数组,每次遍历数组的时间复杂度为 O(n),因此总的时间复杂度为 O(n㏒n)。
由于这种解法没有修改原数组,也没有借助其他的辅助空间,因此空间复杂度为 O(1)。

参考资料

《剑指 Offer(第2版)》,何海涛 著,电子工业出版社
把数组视为哈希表,找到重复的数就是发生了哈希冲突
算法:排序

posted @ 2021-03-26 23:06  乌漆WhiteMoon  阅读(99)  评论(0编辑  收藏  举报