原题链接:数组中重复的数字
一、描述:
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
二、样例:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:
2 或 3
三、数据范围:
2 <= n <= 100000
四、题解
1. 利用下标索引
即通过创建额外的数组空间用来记录各个数出现的次数,具体实现方法是:
(1)创建数组,并将数组元素初始化为0
(2)遍历原数组,以出现的数作为用于计数的数组的下标,该下标所对应的计数数组中的元素++
(3)计数数组中的元素若大于1,则该元素所对应的下标就是原数组中那个重复的数字。
以样例为例:
设计数数组为count[100001]
,则遍历原数组后可得到:
count[0] == 1;
count[1] == 1;
count[2] == 2;
count[3] == 2;
count[5] == 1;
故2或3就是原数组中重复的数
- 完整代码:
int findRepeatNumber(int* nums, int numsSize)
{
int hash[100001] = {0};
int i = 0;
for(i = 0; i < numsSize; i++)
{
if(++hash[nums[i]] > 1)
return nums[i];
}
return 0;
}
复杂度分析:
使用了额外的空间,空间复杂度O(n);
遍历整个数组,时间复杂度O(n)。
2. 原地置换(重点学习)
顾名思义,算法的主要思想就是在原数组的基础上根据不同索引值进行数据的置换,然后根据置换的结果,就可以判断出那个重复的数,下面详细介绍。
介绍之前,先总结列出文中相关句段所对应的代码段,可能会便于理解一些:
设循环遍量为 i ,则:
当前元素或者说以 i 为下标索引得到当前元素表示为:nums[i]
以循环变量 i 作为直接索引得到当前元素表示为:i == nums[i]
以当前元素本身 作为数组下标索引得到当前元素表示为: nums[nums[i]] == nums[i]
这里再规定一下 “正确的位置” 的含义以便说明:
“正确的位置” 代表该元素正好就是以该元素本身为数组下标索引所得的元素。如:数组arr[5] = {0, 2, 1, 3, 6}
,其中的元素0与元素3就处于 “正确的位置” ,因为arr[0] == 0
,arr[3] == 3
。
(1)算法思想:遍历数组,每遇到一个元素就检查该元素是否位于正确的位置上:
- 先以循环变量
i
作为直接索引进行检查,即if(i == nums[i])
,若符合条件则说明,此时元素恰好位于正确的位置上(原来在数组中就处于正确位置或交换后处于正确位置),那么就执行i++
跳过而检查下一个元素; - 再以该元素本身作为下标索引进行检查,即
if(nums[nums[i]] == nums[i])
,若符合条件则说明,此元素在之前已经出现过,出现时的位置要么恰好是正确的位置,要么是经过交换来到了正确的位置,因此该元素就为重复元素; - 若都不符合两种检查方式的条件,则说明元素不在正确的位置上,那么需与相对当前元素来说的正确位置上的元素进行交换,从而让该元素元素处于正确位置;但需要注意的是,交换可以让当前元素处于正确的位置上,但不能保证被交换的元素处于正确的位置上,所以在执行完交换后
i
不进行++
,直到使两个交换的元素都处于正确的位置上后,才由条件if(i == nums[i])
来控制i的++。(后面有例)
由如上描述可知,符合第一种情况的一定符合第二种情况,即:若有i == nums[i]
,则必有i == nums[i] == nums[nums[i]]
;反之则不一定成立,这个不一定成立的情况,也正是重复元素的情况,即有 nums[i] == nums[nums[i]]
,但同时i != nums[i]
总结来说就是:利用原地置换处理的数组,对于数组中的不重复的元素必有i == nums[i]
; 当不满足这个条件,且满足 nums[i] == nums[nums[i]]
条件时,nums[i]
就是重复的元素
(2)样例说明:对于数组 [2, 3, 1, 0, 2, 5, 3],原地置换的过程如下:
- 当
i = 0
时,当前元素nums[i] = 2
, 元素 2 不在正确的位置上,且以该元素本身为下标索引找到的元素 1 不等于当前元素,所以将其与以2为下标的数组元素进行交换,交换结果为: [1, 3, 2, 0, 2, 5, 3]; - 交换完后发现当前元素
nums[i] = 1
,元素 1 不在正确的位置上,且以该元素本身为下标索引找到的元素 3 不等于当前元素执行交换,交换结果为:[3, 1, 2, 0, 2, 5, 3] - 交换完后发现当前元素
nums[i] = 3
,元素 3 不在正确的位置上,且以该元素本身为下标索引找到的元素 0 不等于当前元素,执行交换,交换结果为:[0, 1, 2, 3, 2, 5, 3] - 交换完后发现当前元素位于正确位置,
i++
跳过,进行下一个数的检查,由此,i一直++到4; - 当
i = 4
时,当前元素nums[i] = 2
不在正确的位置上,但以该元素本身为下标索引能到的元素 2 等于当前元素,说明当前元素为重复元素,返回当前元素 2 - (3)形象理解:
此形象理解来源于原题题解下的评论,这里转述一下,供大家参考:
这个原地交换法就相当于分配工作,每个索引代表一个工作岗位,每个岗位必须专业对口,即0索引必须是0元素的“专业人才”才能上岗。而我们的目的就是通过专业对口的方式找出溢出的人才。我们先从0索引岗位开始遍历,首先我们看0索引岗位是不是已经专业对口了,如果已经专业对口即nums[0]=0,那我们就跳过0岗位看1岗位。如果0索引没有专业对口,那么我们看现在0索引上的人才调整到他对应的岗位上,比如num[0]=2,那我们就把2这个元素挪到他对应的岗位num[2]上,这个时候有两种情况:1、num[2]岗位上已经有专业对口的人才了,既num[2]=2,这就说明刚刚那个在num[0]上的2是溢出的人才,我们直接将其返回即可。2、num[2]上的不是专业对口的人才,那我们将num[0]上的元素和num[2]上的元素交换,这样num[2]就找到专业对口的人才了。之后重复这个过程直到帮num[0]找到专业对口的人才,然后以此类推帮num[1]找人才、帮num[2]找人才,直到找到溢出的人才。
(4)算法实现注意事项:
- 检查的次序问题:如上所述,应先检查
if(i == nums[i])
,再检查if(nums[nums[i]] == nums[i])
- 交换注意问题:这里用一个错误的代码作为例子,请看:
tmp = nums[i];
nums[i] = nums[nums[i]];
nums[nums[i]] = tmp;
这样的交换存在问题:在执行完第二句语句时,nums[i]
的值已被改为nums[nums[i]]
,故在第三句语句用nums[i]
来作为数组下标索引时就达不到我们预期的效果,最后一句语句我们想的把nums[i]
的值赋给nums[nums[i]]
,而如上代码相当于是把nums[i]
的值赋给了nums[nums[nums[i]]]
,这就出现问题了。
正确更改方式为,把最后一句语句中的nums[i]
作为下标索引改为tmp
作为下标索引,即:nums[tmp] = tmp;
复杂度分析:
使用了常数复杂度的空间,空间复杂度O(1);
遍历整个数组,时间复杂度O(n)。
- 完整代码:
int findRepeatNumber(int* nums, int numsSize)
{
int i = 0;
int tmp = 0;
while(i < numsSize)
{
if(i == nums[i])
{
i++;
continue;
}
if(nums[i] == nums[nums[i]])
return nums[i];
if(i != nums[i])
{
tmp = nums[i];
nums[i] = nums[nums[i]];
nums[tmp] = tmp;
//nums[i]的值已变,故要用tmp索引
}
}
return -1; //没有重复的元素返回-1
}
看完觉得有觉得帮助的话不妨点赞收藏鼓励一下,有疑问或看不懂的地方或有可优化的部分还恳请朋友们留个评论,多多指点,谢谢朋友们!🌹🌹🌹