287.寻找重复数
给定一个包含
n + 1
个整数的数组nums
,其数字都在[1, n]
范围内(包括1
和n
),可知至少存在一个重复的整数。假设
nums
只有 一个重复的整数 ,返回 这个重复的数 。你设计的解决方案必须 不修改 数组
nums
且只用常量级O(1)
的额外空间。示例 1:
输入:nums = [1,3,4,2,2] 输出:2示例 2:
输入:nums = [3,1,3,4,2] 输出:3提示:
1 <= n <= 105
nums.length == n + 1
1 <= nums[i] <= n
nums
中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次进阶:
- 如何证明
nums
中至少存在一个重复的数字?- 你可以设计一个线性级时间复杂度
O(n)
的解决方案吗?
1.前言
经学长推荐发现这道题,在拿到手后想到了2种方法。
1.循环遍历
int findDuplicate(int* nums, int numsSize)
{
for (int i = 0; i < = numsSize-1; i++)//第一层循环表示某一个元素
{
for (int j = i + 1 ; j < = numsSize-1; j++)//第二层循环表示遍历剩下的元素
{
if (nums[i] == nums[j])//若两个元素相等则使标记点加一
{
return nums[i];
}
}
}
return 0;
}
2.数组排序法
int findDuplicate(int* nums, int numsSize)
{
for(int i=0;i<numsSize-1;i++)
{
for(int j=0;j<numsSize-i-1;j++)
{
if(nums[j]<nums[j+1])
{
int t=nums[j];
nums[j]=nums[j+1];
nums[j+1]=t;
}
}
}//冒泡排序排成有序数列
for(int i=0;i<numsSize;i++)//遍历数组
{
if(nums[i]==nums[i+1])//只要有元素相等就将该元素值赋给标记
return nums[i];
}
return 0;
}
上述两种方法出来的结果如下:
可见,该两种方法都是超时的方法。
2.正确思路
1.快排解决
int cmp(const void* a, const void* b)
{
return *(int*)a - *(int*)b;
}
int findDuplicate(int* nums, int numsSize)
{
qsort(nums, numsSize,sizeof(int), cmp);
for (int i = 1; i < numsSize; ++i)
{
if (nums[i] == nums[i - 1])
{
return nums[i];
}
}
return 0;
}
引用c中的qsort函数与自定义的cmp函数通过快速排序直接比较。
可见通过了测试,但是无论是时间还是内存占用都比较大。
2.哈希表方法解决
int findDuplicate(int* nums, int numsSize) {
int hash[100001] = {0}; // 创建一个大小为100001的哈希表,初始值都为0
for (int i = 0; i < numsSize; ++i) {
if (hash[nums[i]]) { // 如果哈希表中nums[i]位置的值不为0,说明该数字已经出现过
return nums[i]; // 返回重复的数字
}
hash[nums[i]] = 1; // 将哈希表中nums[i]位置的值设为1,表示该数字已经出现过
}
return -1; // 如果没有找到重复的数字,返回-1
}
当然,哈希表并不是本文介绍的重点
接下来将使用更好的办法,也是将要介绍的算法
弗洛伊德判圈思想(快慢指针法)
1.算法描述:
如果有限状态机、迭代函数或者链表存在环,那么一定存在一个起点可以到达某个环的某处(这个起点也可以在某个环上)。
2.实现思路:
使用了两个指针,一个慢指针(龟)每次前进一步,快指针(兔)指针每次前进两步(两步或多步效果是等价的,只要一个比另一个快就行。但是如果移动步数增加,算法的复杂度可能增加)。如果两者在链表头以外(不包含开始情况)的某一点相遇(即相等)了,那么说明链表有环,否则,如果(快指针)到达了链表的结尾(如果存在结尾,肯定无环),那么说明没环。
3.在本题中运用该算法的思路:
如果将数组的下标和值都当成一个点,从下标引一条线指向值,再从值引条线指向下标
此时由下标索引元素,再由元素的值索引下标,当出现重复值的时候就会进入环
如图所示,我们发现在出现重复数字的时候出现了一个环,正好符合弗洛伊德判圈的思想。
那么此时,我们的目标就从找重复数字变化成了找环的起点。
4.找环起点:
给定两个指针, 分别命名为 slow 和 fast,起始位置在链表的开头。每次 fast 前进两步,slow 前进一步。如果 fast 可以走到尽头,那么说明没有环路;如果fast可以无限走下去,那么说明一定有环路,且一定存 在一个时刻slow和fast相遇。当slow和fast第一次相遇时,我们将slow重新移动到链表开头,并让slow和fast每次都前进一步。当slow和fast第二次相遇时,相遇的节点即为环路的开始点。
所以我们以如下代码来尝试实现。
int findDuplicate(int* nums, int numsSize)
{
int slow = 0, fast = 0;//定义快慢指针
do {
slow = nums[slow];
//slow初始代表上图的下标,nums[slow]则为上图中的值,此处即改变一次,也就是慢指针走一步
fast = nums[nums[fast]];
//同上,但此处相当于进行了两次赋值,也就是前进两步
} while (slow != fast);//此处为快慢指针第一次相遇
slow = 0;//将慢指针拖回起点
while (slow != fast) //第二次相遇
{
slow = nums[slow];
fast = nums[fast];//快慢指针均走一步
}
//最终相遇时相遇的地方是环开始的地方
return slow;//返回此时slow或fast的值均可,此时得到的即是重复的数字
}
补充:异或法
运用位运算中的^(异或运算符)
二进制下相同为0,相异为1,便有
a^a=0;自己异或自己等于0
a^0=a;任何数字和0异或等于他自己
a^b^c=a^c^b;异或具有交换律
此处仅仅一种思路