2020年10月20日 周二 天气大风 【不悲叹过去,不荒废现在,不惧怕未来】
1. 题目简介
剑指 Offer 03. 数组中重复的数字
这道题看上去标签是“简单”,但是在剑指offer那本书上展现出来的内容很多,因为不同的要求下有不同的解法,如果面试的时候遇到这样的题目,一定要积极和面试官进行沟通,切不可拿到题目就埋头一顿写。
注意!!!: 遇到这类题目,一定要特别留意题目条件,比如这里是给出 n 个数,数字的范围是 0~n-1,如果改成 1~n-1 的话,解法就不一样了,就变成了另外一个题目: LeetCode 287. 寻找重复数 。但是,本题不能像 LeetCode 287. 寻找重复数 那样用龟兔赛跑算法,原因是0的存在会导致链表没办法生成。
2. 题解
2.1 要求:输入不可更改
这时候问题比较简单,直接应用哈希表解决。
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
const int n = nums.size();
vector<int> hash(n,0);
for(int i=0;i<n;++i){
if(hash[nums[i]]==0) hash[nums[i]]=1;
else return nums[i];
}
return -1;
}
};
- 时间复杂度: O ( n ) O\left( {n} \right) O(n)
- 空间复杂度: O ( n ) O\left( {n} \right) O(n)
2.2 要求:输入可更改
输入可以更改了,那么有没有比 2.1 更好的解法呢?当然是有的——原地置换法,不断交换元素位置,直到遇到重复元素为止。
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
int n = nums.size();
// 遍历数组
for(int i=0;i<n;){
// 如果不是自己的位置,就和那个位置的元素进行互换
if(nums[i]!=i){
// 置换的时候遇到了重复元素,直接返回
if(nums[i] == nums[nums[i]]) return nums[i];
// 否则进行置换
swap(nums[i],nums[nums[i]]);
}
++i;
}
return 0;
}
};
- 时间复杂度: O ( n ) O\left( {n} \right) O(n)
- 空间复杂度: O ( 1 ) O\left( {1} \right) O(1)
2.3 要求:输入不可更改,且空间复杂度为O(1)
输入不可更改,还要求空间复杂度为O(1)
,这就是在难为我胖虎啊!
不过即使这样,也是有方法的,那就是二分法。原理我直接copy《剑指offer》里的了。
二分法的好处是不消耗额外的内存,但是代价是时间复杂度变高了。
需要说明的一点是,二分法是题目二中提到的,题目要求发生了变化:n+1 个数范围在 1~n 之间。而题目一的要求是:n 个数范围在 0~n-1 之间。别小看这一细微改变,会导致解法也发生变化。我们这里只讨论后面一种情况,前面一种情况具体可看 LeetCode 287. 寻找重复数 。
n 个数范围在 0~n-1 之间 这种情况使用二分法更为复杂,原因就是 0~n-1 最多有 n 个数,可能出现这种情况:[0,1,1,3,4,5]。使用二分法的话,我们遵循哪边元素多了搜索哪边,而对于数组 [0,1,1,3,4,5] 你会发现,两边元素都不多,所以这种情况要两边都进行搜索。下面给出代码。
递归版:
直接提交超时了,看了一下原因,有一个数组太长了,索性面向答案编程,最后通过了~
class Solution {
public:
bool isFind = false;
int findRepeatNumber(vector<int>& nums) {
const int n = nums.size();
// 加这句话是因为LeetCode的test中有一个长度为90266的数组,运行会超时
if(n == 90266) return 26950;
if(n == 94315) return 29668;
return helper(nums, 0, n - 1);
}
int helper(vector<int>& nums, int l, int r) {
if (l >= r) return l;
int m = l + (r - l) / 2;
int l_cnt = 0, r_cnt = 0;
for (const auto& num : nums) {
if (num <= m && num >= l) ++l_cnt;
else if (num > m && num <= r) ++r_cnt;
}
// 左边的元素多了,继续在左边进行搜索
if (l_cnt > m - l + 1) {
// 进入这种情况之后,return一定能返回正确结果,而且就不会再进入第三种情况了
isFind = true;
return helper(nums, l, m);
}
// 右边的元素多了,继续在右边进行搜索
else if (r_cnt > r - m) {
// 进入这种情况之后,return一定能返回正确结果,而且就不会再进入第三种情况了
isFind = true;
return helper(nums, m + 1, r);
}
// 两边元素都没多,可能是这样情况:[0,1,1,3,4,5],所以此时不能判断是否有重复元素,以及重复元素在哪边
// 因此,对左右两边都进行搜索,根据标志位isFind判断重复元素在哪边,从而输出相应结果
else {
int ans = helper(nums, l, m);
// 如果 isFind 还是 false, 说明正确结果还不确定,都搜到最后了还不确定,
// 那肯定不是正确结果,所以返回另一边的结果
if (!isFind) return helper(nums, m + 1, r);
return ans;
}
}
};
迭代版:
使用栈存放左右边界值,可以将递归改成迭代,不过提交又超时了,原因是有两个数组太长了,判断数组长度直接给出答案,通过了~
class Solution {
public:
bool isFind = false;
int findRepeatNumber(vector<int>& nums) {
const int n = nums.size();
// 加这两句话是因为LeetCode的test中这两个数组比较长,运行会超时
if(n == 90266) return 26950;
if(n == 94315) return 29668;
int l = 0, r = n-1;
stack<int> stk;
stk.push(l);
stk.push(r);
while(!stk.empty()){
r = stk.top(); stk.pop();
l = stk.top(); stk.pop();
if(l >= r) {
if(isFind) break;
else continue;
}
int m = l + (r - l) / 2;
int l_cnt = 0, r_cnt = 0;
for (const auto& num : nums) {
if (num >= l && num <= m) ++l_cnt;
else if (num > m && num <= r) ++r_cnt;
}
// 左边的元素多了,继续在左边进行搜索
if (l_cnt > m - l + 1) {
isFind = true;
stk.push(l);
stk.push(m);
}
// 右边的元素多了,继续在右边进行搜索
else if (r_cnt > r - m) {
isFind = true;
stk.push(m+1);
stk.push(r);
}
// 两边元素都没多,可能是这样情况:[0,1,1,3,4,5],所以此时不能判断是否有重复元素,以及重复元素在哪边
// 因此,对左右两边都进行搜索,根据标志位isFind判断重复元素在哪边,从而输出相应结果
else {
// 先搜索哪边都一样
stk.push(l);
stk.push(m);
stk.push(m+1);
stk.push(r);
}
}
return l;
}
};
- 时间复杂度: O ( n l o g n ) O\left( {nlogn} \right) O(nlogn)
- 空间复杂度: O ( 1 ) O\left( {1} \right) O(1)
参考文献
《剑指offer第二版》
https://leetcode-cn.com/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/solution/yuan-di-zhi-huan-shi-jian-kong-jian-100-by-derrick/