《剑指 Offer》学习记录:题 3:数组中的重复数字
题 3_1:找出数组中重复的数字#
题干#
在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为 7 的数组{2, 3, 1, 0, 2, 5, 3},那么对应的输出是重复的数字 2 或者 3。——《剑指 Offer》P39
测试用例#
- 长度为 n 的数组里包含一个或多个重复的数字;
- 数组中不包含重复的数字;
- 无效输入测试用例,例如输入空指针、数组中包含 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
测试用例#
- 长度为 n 的数组里包含一个或多个重复的数字;
- 数组中不包含重复的数字;
- 无效输入测试用例(输入空指针)。
解法思想#
这道题目是 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版)》,何海涛 著,电子工业出版社
把数组视为哈希表,找到重复的数就是发生了哈希冲突
算法:排序
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)