1.方法1
简单来说就是开辟一个空间为n的向量用来做数字出现次数的统计。由于题目只要求返回任意一个重复的数字,因此只需遍历统计向量找到首个记录出现次数大于1的数字即可。
具体代码如下:
class Solution {
public:
int duplicateInArray(vector<int>& nums) {
int n = nums.size();
vector<int> v(n, 0);
//检验越界元素并统计出现次数
for(auto &e: nums){
if(e < 0 || e >= n) return -1;
v[e]++;
}
//遍历统计结果,若有满足条件的则返回
for(int i = 0; i <n; i++){
if(v[i] > 1) return i;
}
//未发现则说明所有元素均不重复
return -1;
}
};
2.方法2
本方法参考题解。相比于上一个方法而言,此方法仍为线性的时间复杂度,不过空间复杂度却为常数。具体思路是:首先判断是否存在越界的元素,有则直接返回-1。没有则采用“循环交换”的方式来确定是否存在重复元素。
2.1“循环交换”原理
在理解循环交换之前,先思考这样一个问题。
Q2.1.1
我们假设所有的元素均满足以下条件:
1.全部都在规定的范围内且互不相同;
2.在数组中的次序是完全混乱的。
题目要求元素范围是从0到n-1的,恰好数组的下标也是从0到n-1的。因此,我们可以考虑将数组的下标与元素一一对应(下标0的位置存放数据0,下标1的位置存放数据1……以此类推)。那么对于一个元素次序完全混乱的数组,我们要怎样才能在O(1)的空间复杂度内让每个元素都能存放到自己对应的位置呢?
答案就是“交换”。首先我们按顺序遍历数组,如果当前位置的元素x就是本该对应此位置的元素则移到下一个位置。如果不是那么就将元素x与x本该对应的位置的元素进行交换,不断重复此交换操作,直到对应x原来所处位置的元素归位。如此进行就可以令所有的元素都能存放到对应的位置上。下面举个例子。
eg2.1.1
假设数组中元素次序为[0, 4, 1, 2, 3],那么整个处理过程如下:
1.[0, 4, 1, 2, 3] -> [0, 4, 1, 2, 3],因为0号位存放元素就是0,故数组不变;
2.[0, 4, 1, 2, 3] -> [0, 3, 1, 2, 4] -> [0, 2, 1, 3, 4] -> [0, 1, 2, 3, 4];
3.[0, 1, 2, 3, 4] -> [0, 1, 2, 3, 4];
4.[0, 1, 2, 3, 4] -> [0, 1, 2, 3, 4];
5.[0, 1, 2, 3, 4] -> [0, 1, 2, 3, 4];
而且,还可以证明:只要数组中的元素满足全部都在规定范围内且全部互异的条件。那么以上算法就一定可以将所有元素放置到其对应的位置上。
如果用代码来实现的话,那就是如下所示的样子:
for(int i = 0; i < n; i++){
while(i != nums[i]) swap(nums[i], nums[nums[i]]);
}
while循环的含义是:当当前位置与其存放的元素不匹配时,便不断对此位置的元素进行交换操作。
那么假如我们破坏了互异的条件,再按照以上算法处理的话会怎样呢?再来看个例子。
eg2.1.2
假设数组中元素次序为[0, 4, 1, 2, 2],那么整个处理过程如下:
1.[0, 4, 1, 2, 2] -> [0, 4, 1, 2, 2];
2.[0, 4, 1, 2, 2] -> [0, 2, 1, 2, 4] -> [0, 1, 2, 2, 4];
3.[0, 1, 2, 2, 4] -> [0, 1, 2, 2, 4];
4.[0, 1, 2, 2, 4] -> [0, 1, 2, 2, 4] -> [0, 1, 2, 2, 4] ->……进入死循环!
从上面的例子可以看到,出于3号位的元素2不属于3号位,那么按照算法它应当与自己本属于的2号位的元素进行交换。然而2号位的元素已经是2了,即使交换过来了仍然不满足条件。于是两个2之间就不断地进行交换,从而让程序陷入了死循环。由此可以看出:如果数组中的元素满足全部都在规定范围。则“数组中存在重复的元素”与“以上算法在某一步会陷入死循环”互为充要条件。而这个结论正是循环交换找出重复元素的关键!
对于原来的问题而言,只要能检测是否发生交换死循环就可以判定数组中是否存在重复元素,而且导致发生死循环的元素也正是要找的重复的元素。
因此,我们需要对上面的for循环代码进行一些修改(确切来说是for里面的while循环)来满足我们的要求。我们的要求是:若没有死循环则正常进行交换操作,否则立即退出循环。于是,代码修改如下:
for(int i = 0; i < n; i++){
while(i != nums[i] && nums[nums[i]] != nums[i]) swap(nums[i], nums[nums[i]]);
}
代码的修改其实就是添加了一段逻辑判断。逻辑与的前一部分仍是原来的含义,后一部分的意思是只有当当前位置元素与同其交换的元素不同时应进行交换。此时,如果退出了while循环则说明发生了两种情况:当前位置的元素已经归位或者是当前位置元素与同其交换的元素相同。
接下来要做的就是对这两种情况进行判断,如果发生的是第一种情况,则将遍历指针移到下一个位置(即contiune语句)。如果是第二种情况,则将当前位置的元素返回即可。将两个判断逻辑合并后就得到了如下的for循环结构体:
for(int i = 0; i < n; i++){
while(i != nums[i] && nums[nums[i]] != nums[i]) swap(nums[i], nums[nums[i]]);
if(i != nums[i] && nums[nums[i]] == nums[i]) return nums[i];
}
这样,结合我们的问题Q2.1.1和以上的分析,就可以轻而易举地理解“循环交换”的原理了。从而得到了解决问题的代码。
2.2实现
具体代码如下:
class Solution {
public:
int duplicateInArray(vector<int>& nums) {
int n = nums.size();
for (auto x : nums) //检查是否存在范围以外的元素
if (x < 0 || x >= n)
return -1;
for (int i = 0; i < n; i ++ ) {
while (nums[i] != i && nums[nums[i]] != nums[i])
swap(nums[i], nums[nums[i]]);
if (nums[i] != i && nums[nums[i]] == nums[i])
return nums[i];
}
return -1; //for循环过程中未出现返回说明所有元素均互异
}
};
2.3其他问题
如果大家有仔细将我的代码与题解的代码进行比对,会发现题解的while语句的逻辑判断语句中并没有我的while语句中的前一个逻辑。实际上从本题的解法上来说while语句中是否判断了当前位置元素是否归位并不影响程序的正确性。大家可以用上面的例子来对比两处语句的不同。
另外就是将while后面的if语句中的逻辑判断改写成一下形式:
if (nums[i] < i) return nums[i];
仍然不影响程序的正确性。大家可以思考一下这是为什么?