题目要求
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例:
输入:[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof
下面展示的解法代表了我对题目的处理思路——首先是自己思考的解法(可能是暴力解法),然后综合leetcode部分题解和论坛上的其他回答写出的较优化解法。
一、基础解法——排序后比较相邻元素
拿到题目第一瞬间想到的就是排序后看相邻两元素是否相等,比如示例数组排序后: {0, 1, 2, 2, 3, 3, 5}。依次遍历数组中的元素,并比较该元素(i)与相邻元素(i+1),代码如下:
int findRepeatNumber_m01(vector<int>& nums)
{
//标准库函数排序
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;
}
思路比较简单,只是需要在完成排序的基础上,再进行一次遍历,时间复杂度是排序的复杂度N(nlogn)。但是很明显,该方法一个显著的缺陷是会破坏原有数组的原有顺序。针对这个缺陷,可以应用辅助数组来改进,但会造成额外的开销(空间占用,数组拷贝)。
二、取巧做法——原地置换
针对本道题,其实本道题给的条件很特殊,即数据的大小都小于数组的长度,且只需要返回某一个重复的值,所以没有必要对数组整体做排序。
原地置换的核心思想就是为每个数据找寻他的“正确”位置,即将nums[i]这个数据放到nums[i]为下标的位置,而原先在nums[i]为下标的位置的数据放到下标为i的位置(nums[i]数据原先所在的位置),即交换nums[i]与nums[nums[i]]。当然如果nums[i]刚巧就在“正确”的位置(nums[i] = i),则不需要执行操作,而继续对下一个位置的元素做该检测。
代码如下:
int findRepeatNumber_m03(vector<int>& nums)
{
for (int i = 0; i < nums.size(); i++)
{
while (nums[i] != i)
{
if (nums[i] == nums[nums[i]])
{
return nums[i];
}
swap(nums[i], nums[nums[i]]);
}
}
return -1;
}
时间复杂度:遍历需要O(n),同时需要O(1)来做交换。
三、通用做法——哈希法
通过构建哈希表来解决这个问题,通过监视键值对(key-value)中的value值可以第一时间发现重复元素。
对数组{2, 3, 1, 0, 2, 5, 3}构建哈希表的构建过程如下:
key | value |
---|---|
2 | 1 |
3 | 1 |
1 | 1 |
0 | 1 |
在遇到重复的第一个2后,返回此时的键值key
key | value |
---|---|
2 | 2 |
3 | 1 |
1 | 1 |
0 | 1 |
示例代码如下:
int findRepeatNumber_m02(const vector<int>& nums)
{
unordered_map<int, int> hsm;
for (int i = 0; i < nums.size() -1; i++)
{
hsm[nums[i]]++;
if (hsm[nums[i]] > 1)
{
return nums[i];
}
}
return -1;
}
时间复杂度O(n)。
四、完整测试代码
#include <iostream>
#include <vector>
#include <unordered_map>
#include <algorithm>
using namespace std;
//打印方法
void print(const vector<int> &v)
{
for (auto item: v)
{
cout << item << "\t";
}
cout << endl;
}
//方法一:排序后比较相邻数字
int findRepeatNumber_m01(vector<int>& nums)
{
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;
}
//方法二:哈希法
int findRepeatNumber_m02(const vector<int>& nums)
{
unordered_map<int, int> hsm;
for (int i = 0; i < nums.size(); i++)
{
hsm[nums[i]]++;
if (hsm[nums[i]] > 1)
{
return nums[i];
}
}
return -1;
}
//方法三:原地置换
int findRepeatNumber_m03(vector<int>& nums)
{
for (int i = 0; i < nums.size(); i++)
{
while (nums[i] != i)
{
if (nums[i] == nums[nums[i]])
{
return nums[i];
}
swap(nums[i], nums[nums[i]]);
}
}
return -1;
}
int main()
{
/*
测试用例
输入:[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
*/
vector<int> vTest = { 2,3,1,0,2,5,3 };
//方法一:排序后比较相邻数字
//cout << findRepeatNumber_m01(vTest) << endl;
//print(vTest);
//方法二:哈希法
cout << findRepeatNumber_m02(vTest) << endl;
//方法三:原地置换
//cout << findRepeatNumber_m03(vTest) << endl;
system("pause");
return 0;
}
总结
简单题其实不简单,原地交换的做法是建立在对数据特性有着清晰认识的基础上,所以哈希法是我认为这类型题的通用解法。