《剑指 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 之外的数字。

法一:快速排序#

解法思想#

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

解题代码#

Copy Highlighter-hljs
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 容器实现计数操作,原理也是一样的。

解题代码#

Copy Highlighter-hljs
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]],说明出现了重复的数据,求得了这个问题的解。

解题代码#

Copy Highlighter-hljs
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) 的数字存在重复。反复使用二分的思想缩小解空间的范围,最终就可以找到重复的数据元素。

解题代码#

Copy Highlighter-hljs
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 @   乌漆WhiteMoon  阅读(102)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示
CONTENTS