287. 寻找重复数
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
示例 1:
输入: [1,3,4,2,2]
输出: 2
示例 2:
输入: [3,1,3,4,2]
输出: 3
说明:
不能更改原数组(假设数组是只读的)。
只能使用额外的 O(1) 的空间。
时间复杂度小于 O(n2) 。
数组中只有一个重复的数字,但它可能不止重复出现一次。
解法一:二分查找(想到了乐鑫的变态笔试题)
由于该题的数组元素限制在1~n之中,所以使用二分查找重复元素;
确定那边区间存在重复元素,然后到该区间接着使用二分法;
以 [1, 2, 2, 3, 4, 5, 6, 7] 为例,一共 8 个数,n + 1 = 8,n = 7,根据题目意思,每个数都在 1 和 7 之间。
例如:区间 [1, 7] 的中位数是 4,遍历整个数组,统计小于等于 4 的整数的个数,至多应该为 4 个。换句话说,整个数组里小于等于 4 的整数的个数如果严格大于 4 个,就说明重复的数存在于区间 [1, 4],它的反面是:重复的数存在于区间 [5, 7]。
于是,二分法的思路是先猜一个数(有效范围 [left, right]里的中间数 mid),然后统计原始数组中小于等于这个中间数的元素的个数 cnt,如果 cnt 严格大于 mid,(注意我加了着重号的部分“小于等于”、“严格大于”)依然根据抽屉原理,重复元素就应该在区间 [left, mid] 里。
说明:下面的算法运行时间肯定不会高,因为这个算法是空间敏感的,「用时间换空间」是反常规做法,大家了解一下即可。
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int len = nums.size();
int left = 1;//初始
int right = len-1;
while(left <= right)
{
int mid = left+(right-left)/2;
int count = 0;
for(auto num:nums)
{
if(num <= mid)
count += 1;
}
if(count > mid)
right = mid-1;
else
left = mid+1;
}
return left;
}
};
复杂度分析:
时间复杂度:O(NlogN),二分法的时间复杂度为 O(logN),在二分法的内部,执行了一次 for 循环,时间复杂度为 O(N),故时间复杂度为 O(NlogN)。
空间复杂度:O(1),使用了一个 cnt 变量,因此空间复杂度为 O(1)。
解法二:找环——快慢指针
先看141. 环形链表——判断是否有环
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head==NULL || head->next==NULL)
return false;
ListNode* slow = head;
ListNode* fast = head;//快慢指针
while(fast!=NULL && fast->next!=NULL){//指针合法
slow = slow->next;//慢指针走一步
fast = fast->next->next;//快指针走两步
if(slow == fast){
return true;//两指针相遇,即有环;没有环的话,是不可能相遇的
}
}
return false;
}
};
再看142: 环形链表 II——找环的起点
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。
思路
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* slow=head;
ListNode* fast=head;
while(fast!=NULL && fast->next!=NULL){//保证指针合法
slow=slow->next;
fast=fast->next->next;
if(slow==fast){//快慢指针相遇,表示有环
slow=head;//慢指针返回头结点,两个指针同步向后走,再次相遇的结点就是环的入口
while(slow!=fast){
slow=slow->next;
fast=fast->next;
}
return fast;
}
}
return NULL;
}
};
最后再细品本题:寻找重复数
对于数组中的每个元素,可以将它们看作next结点;
从理论上讲,数组中如果有重复的数,那么就会产生多对一的映射,这样,形成的链表就一定会有环路了,
综上
1.数组中有一个重复的整数 <——> 链表中存在环
2.找到数组中的重复整数 <——> 找到链表的环入口
至此,问题转换为142题。那么针对此题,快、慢指针该如何走呢。根据上述数组转链表的映射关系,可推出
142题中慢指针走一步slow = slow.next ——> 本题 slow = nums[slow]
142题中快指针走两步fast = fast.next.next ——> 本题 fast = nums[nums[fast]]
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int slow = 0;
int fast = 0;
slow = nums[slow];
fast = nums[nums[fast]];//注意开始循环时,快慢指针不是在同一个起点了
//判断是否有环,
while(slow != fast){
slow = nums[slow];
fast = nums[nums[fast]];
}
//找到环,将慢指针返回起点,快慢指针同步前进,
//再次相遇时,就是环的入口,即本题的而重复数
slow =0;
while(slow != fast)
{
fast = nums[fast];
slow = nums[slow];
}
return slow;
}
};