1,题目要求
Given an array nums containing n + 1 integers where each integer is between 1 and n (inclusive), prove that at least one duplicate number must exist. Assume that there is only one duplicate number, find the duplicate one.
Example 1:
- Input: [1,3,4,2,2]
- Output: 2
Example 2:
- Input: [3,1,3,4,2]
- Output: 3
Note:
- You must not modify the array (assume the array is read only).
- You must use only constant, O(1) extra space.
- Your runtime complexity should be less than O(n2).
- There is only one duplicate number in the array, but it could be repeated more than once.
给定包含n + 1个整数的数组nums,其中每个整数在1和n之间(包括1和n),证明必须存在至少一个重复的数字。 假设只有一个重复的数字,找到重复的数字。
2,题目思路
对于这道题,需要找到一个数组中出现了两次的数字,值得注意的是,数组的长度为n+1,而所有的数字的范围是1-n,因此就一定有一个数字,出现了两次。
在实现这个目的上,如果我们直接使用类似hash的策略是可行的,但是我们可以从题目要求中看到,我们只能使用O(1)的额外空间,因此hash的方法在这里不可以。
另外一种策略是基于对原数组的修改。即在遍历的过程,将对应索引位置的数字放到它对应的位置上,因此, 一定会出现一个位置对应了两个甚至三个以上相同数字的情况,这时,我们就找到了对应的出现了两次以上的重复数字。不过题目又要求了我们不不可以对数组进行修改,因此,这种方法在这里又不能用了。
因此,我们需要找到其他的方法。
第一种方法,是基于搜索的办法。
这种方法,是基于以下事实:
假如说对于n,我们设置为7,则所有的数字应该是:
- 1 2 3 4 5 6 7
但是,数组长度为n+1,因此一定会有一个数字出现两次。
此时,我们的方法是对于这个7,可以分成两部分:
- 1 2 3 4
- 5 6 7
当然,1 2 3 和4 5 6 7也是一样的。
在定义了两个数据范围后,我们可以分别对整个数组进行遍历,查看数组中这几个数字出现的次数。
比如,对于 1 2 3 4的搜索,我们定义完这个搜索的范围后,对整个数字进行一次遍历,如果这四个数字只在数组中出现过一次,那么我们的搜索结果一定是搜索范围的长度,也就是4。
这时,出现两次的数字,一定是5 6 7这几个之内。
于是,重新设置搜索的总范围为5 6 7,并再对其利用二分进行搜索即可。
因此,这种基于二分搜索的策略,是搜索的值范围的二分,其中判断成立与否的结果为搜索的数字个数与搜索的值的数字个数的关系。
另外一种方法,则是比较巧妙的、类似于链表中的求带环链表相关的问题。
具体原理是,对于题目所给出的这种形式的数组,如果我们将每一位数字作为下一个取值的索引,我们可以发现:
- 如果数组是n个数字,数字范围是1-n,那么,这样建立起来的“链表”,一定不存在环,因为每一位数字所代表的索引,指向的另一个元素一定不会和之前的数字相同,也即不会有相同的索引。
- 然而,如果是n+1个数字,而数字范围还是1-n,那么这个“链表”一定存在环。原因也很好理解,正如上一条所述。相同的数字也代表了相同的索引,于是,这个环的开始位置也就是重复的数字。
因此,这道题就转化成了求一个有环链表的环连接点的问题。之前写过一篇相关的博客,跳转链接如下:
链表与有环链表
于是,这里涉及到两个注意点:
1,链表的node->next
和node->next->next
,在数组中的实现可以是nums[slow]
和nums[nums[fast]]
。
2,第一次碰撞点Pos到连接点Join的距离 = 头结点到连接点Join的距离。
因此,分别从第一次碰撞点Pos、头指针head开始走,相遇的那个点就是连接点。
在数组的实现上,就是在第一次快慢指针相遇后,让快指针回到nums[0],并和slow一起一次走一步,再次相遇时,就是对应的连接点,即重复的数字。
3,代码实现
1,搜索范围二分
static auto speedup = [](){
ios::sync_with_stdio(false);
cin.tie(nullptr);
return nullptr;
}();
class Solution {
public:
int findDuplicate(vector<int>& nums) {
if(nums.size() == 2)
return 1;
int start = 1;
int end = nums.size()-1;
while(start<=end){
int middle = (end - start)/2 +start;
int count = countRange(nums, start, middle);
if(start == end)
{
if(count>1)
return start;
else
break;
}
if(count > middle-start+1)
end = middle;
else
start = middle+1;
}
return -1;
}
//统计从start到end范围内的数字在数组中出现的次数
int countRange(vector<int> &nums, int start, int end)
{
int count = 0;
for(int i = 0;i < nums.size();i++)
{
if(nums[i]>=start && nums[i]<=end)
count++;
}
return count;
}
};
2,类-快慢指针
int x = []() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
return 0;
}();
class Solution {
public:
int findDuplicate(vector<int>& nums) {
if(nums.size() == 2)
return 1;
int slow = nums[0];
int fast = nums[nums[0]];
while(slow!=fast){
slow = nums[slow];
fast = nums[nums[fast]];
}
//找到了相遇点
//fast回到起点
fast = 0;
while(slow!=fast){
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
};