三种方法:二分、位运算、快慢指针
/**
* 方法一:二分法 假设重复的数字为target,对于[1,N]的数而言 ,整个nums中,[1,target-1]中的数i满足, 小于等于i的数的数目
* cnt[i] <= i , [target, N]中的数j满足, 小于等于j的数的数目 cnt[j] > j;
* @param Integer[] $nums
* @return Integer
*/
function findDuplicate($nums) {
$left = 1;
$right = count($nums) - 1;
while ($left < $right) {
$mid = $left + intdiv($right-$left, 2);
$cnt = 0;
foreach ($nums as $num) {
// 统计nums中有多少个数小于等于mid
if ($num <= $mid) {
$cnt++;
}
}
// 根据mid的值,调整左右端点
if ($cnt <= $mid) {
$left = $mid + 1;
} else {
$right = $mid;
}
}
return $left;
}
/**
* 方法二:利用位运算还原target
* 设 x 表示nums中各元素二进制数第i位上1的个数, y表示[1,n]中各元素二进制数第i位上1的个数
* 1.当target只重复1次时,即1~N各数均在nums中出现了一次,当target第i位上的数是1时,则 x 必 大于 y
* 而当target第i位上的数是0时, x = y
* 2.当target重复大于1次时, 相当于用target去替换了1~N上的某个数,记为replace, 此时有4种情况
* replace i位上的数是1, target i位上的数是1, 则 x > y (因为在替换前 x > y, 替换后相当于x没变)
* replace i位上的数是0, target i位上的数是1, 则 x > y (因为在替换前 x > y, 替换后相当于x+1)
* replace i位上的数是1, target i位上的数是0, 则 x <= y (因为在替换前 x = y, 替换后相当于x-1)
* replace i位上的数是0, target i位上的数是0, 则 x <= y (因为在替换前 x = y, 替换后相当于x没变)
* 综上可见 只有在target i位上的数是1时, 才有 x > y, 于是可以利用x, y的关系,判定target在i位上是1还是0,从而推算出target的值。
* @param $nums
*/
function findDuplicateV2($nums) {
$n = count($nums) - 1;
$i = 0;
$ans = 0;
while ($n !== 0) {
$x = 0;
$y = 0;
foreach ($nums as $k => $num) {
if (($num & (1 << $i)) !== 0) {
$x++;
}
if ($k != 0 && ($k & (1 << $i)) !== 0) {
$y++;
}
}
if ($x > $y) {
$ans |= 1 << $i;
}
$i++;
$n = $n >> 1;
}
return $ans;
}
/**
* 方法三:快慢指针法,参考floyd判圈法,将i -> nums[i] 看成一条边,则因为有重复元素,则构成的图定是有环的,
* 且重复元素target指向了环的入口。
* @param $nums
*/
function findDuplicateV3($nums) {
$fast = 0;
$slow = 0;
do {
$slow = $nums[$slow];
$fast = $nums[$fast];
$fast = $nums[$fast];
} while ($fast != $slow);
$slow = 0;
do {
$slow = $nums[$slow];
$fast = $nums[$fast];
} while ($fast != $slow);
return $fast;
}