-
题目1:03.数组中重复的数字
一个长为n的数组,其中所有元素都位于[ 0,n-1 ]内;
其中有数字是重复的,且重复数字不确定有几个,且重复了几次也不确定;
返回数组中任意一个重复数字; -
思路:
相当于n个位置放n种数,若每种数放一个则刚好没重复元素,题目说有若干种数重复;
1.遍历一次:O(n),O(1)
把遍历过的数字都放在它们应在的坑里,每次++i或swap都将一个数放在了它应在的坑里,直到发现我应在的坑里已经有一个我了;
缺点:修改了原数组
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
int len = nums.size();
for (int i = 0; i < len; ++i) {
while (i != nums[i] && nums[i] != nums[nums[i]]) { //若nums[i] == i就往后遍历
swap(nums[i], nums[nums[i]]); //每交换一次都使下标Nums[i]处匹配
}
if (nums[i] != i && nums[nums[i]] == nums[i]) { //我不在对应位置,我应该在的位置已经有一个我了
return nums[i];
}
}
return -1;
}
};
2.把应在的坑内的数取反,若下次又发现坑内的数已经为负,则说明该坑的索引重复O(n),O(1)
缺点:①数组内的数必须>0:若有负数,很好理解,不再解释 ; 若有0,则重复数字应在的坑内放了一个0,则无法确定重复数字是几,例如3,3,2,0 ② 同样修改了原数组
对于本题,长为n的数组,取值均为[ 0,n - 1 ],因此无法直接使用,若想使用此方法,可以把所有数提前+1,就不会出现0了,但要注意需要-1才是应放的坑;
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
for (int& x : nums) x += 1;//提前把所有数+1,防止0的出现
for (int i = 0; i < nums.size(); ++i) {
//在没发现重复数字之前,遍历到的坑内可能放的是已经取反的数
//需要减1是因为提前把所有数加了1,因此需要减1才是我应在的坑
int index = abs(nums[i]) - 1;
if (nums[index] < 0) {
return index;//说明有两个重复数index+1(这两个数原本是index)都放到了index这个坑内
}
nums[index] *= (-1);
}
return
}
};
3.unordered_set边遍历边存,发现集合中已经有了说明重复:O(n):遍历一次,O(n):额外集合空间,最坏情况重复数字位于数组的末尾两个,此时需要O(n)
优点:无需修改原数组,但缺点是花费O(n)额外空间
class Solution {
public:
int findRepeatNumber(vector<int>& nums) {
unordered_set<int> us;
for (int i = 0; i < nums.size(); ++i) {
if (us.find(nums[i]) != us.end()) return nums[i];
us.insert(nums[i]);
}
return -1;
}
};
- 题目2:不修改数组找出重复的数字
一个长度为n的数组,数组值均为[1,n-1 ],找出任一重复数字;
1:抽屉原理 + 二分:O(nlogn):每次二分都需要遍历一次数组,来统计值位于[ l,mid ]的数目,O(1)
n个坑放n-1个数,因此一定有重复数字;
class Solution {
public:
int duplicateInArray(vector<int>& nums) {
int n = nums.size() - 1;
int l = 1, r = n;
while (l < r) {
int mid = l + ((r - l) >> 1);
//[l, mid], [mid + 1, r]
int count = 0;
for (int x : nums)
if (x >= l && x <= mid) ++count;
if (count > mid - l + 1) r = mid;//[l, mid]长度为mid-l+1,但位于此区间的值的个数超过此长度,说明重复数字位于此区间
else l = mid + 1;
}
return l;//出循环的时候l==r
}
};
2.(最优解)等价于链表找环:O(n):慢指针每次走一格,刚好遍历到链表尾部(即环起点)处结束,O(1)
①我们可以将数组视为一个(或多个链表),每个元素都是一个节点,元素的下标代表节点地址,元素的值代表next指针,因此,重复的元素意味着两个节点的next指针一样,即指向同一个节点,因此存在环,且环的起点即重复的元素。
②为了找到任意一个环的起点(重复元素),我们只需要拿到一个链表的首部,然后利用前置知识即可解决问题。显然,0一定是一个链表的首部,因为所有元素值的范围在1 - n-1之间,即没有节点指向0节点。
题解流程即为:从0开始,快慢指针分别以2、1的速度向前遍历,当它们相遇时,将快指针置为0,继续分别以1、1的速度向前遍历,当它们再次相遇时,此时它们的下标就是题解。
//链表找环很好理解,但本题怎么等价过去的没理解好,为何下标0就是链表首节点有点晕
class Solution {
public:
int duplicateInArray(vector<int>& nums) {
int f = 0, s = 0;
while (f == 0 || f != s) {//从0开始,快慢指针分别以2、1的速度向前遍历,直到相遇
f = nums[nums[f]];
s = nums[s];
}
f = 0;//将快指针置为0,继续分别以1、1的速度向前遍历,当它们再次相遇时,此时它们的下标就是题解。
while (f != s) {
f = nums[f];
s = nums[s];
}
return s;
}
};
- 总结:
这道题长度为n的数组,数组值取[0,n - 1],[1,n-1] 采用的方法都不同,后者因为n个坑放n-1种数因此想到抽屉原理,进而对值二分;前者n个坑放n种树,通常想到用下标映射值的方式,一个萝卜一个坑;
根据不同的条件要有条件反射性的想到对应思路;