位运算题目:寻找重复数
文章目录
- 题目
- 标题和出处
- 难度
- 题目描述
- 要求
- 示例
- 数据范围
- 进阶
- 前言
- 解法一
- 思路和算法
- 代码
- 复杂度分析
- 解法二
- 思路和算法
- 代码
- 复杂度分析
- 解法三
- 思路和算法
- 代码
- 复杂度分析
题目
标题和出处
标题:寻找重复数
出处:287. 寻找重复数
难度
6 级
题目描述
要求
给定一个包含 n + 1 \texttt{n} + \texttt{1} n+1 个整数的数组 nums \texttt{nums} nums,每个整数都在范围 [1, n] \texttt{[1, n]} [1, n] 内(包括 1 \texttt{1} 1 和 n \texttt{n} n)。
数组 nums \texttt{nums} nums 只有一个重复的整数,返回这个重复的数。
要求不修改数组 nums \texttt{nums} nums 且只用常量级的额外空间。
示例
示例 1:
输入: nums = [1,3,4,2,2] \texttt{nums = [1,3,4,2,2]} nums = [1,3,4,2,2]
输出: 2 \texttt{2} 2
示例 2:
输入: nums = [3,1,3,4,2] \texttt{nums = [3,1,3,4,2]} nums = [3,1,3,4,2]
输出: 3 \texttt{3} 3
示例 3:
输入: nums = [3,3,3,3,3] \texttt{nums = [3,3,3,3,3]} nums = [3,3,3,3,3]
输出: 3 \texttt{3} 3
数据范围
- 1 ≤ n ≤ 10 5 \texttt{1} \le \texttt{n} \le \texttt{10}^\texttt{5} 1≤n≤105
- nums.length = n + 1 \texttt{nums.length} = \texttt{n} + \texttt{1} nums.length=n+1
- 1 ≤ nums[i] ≤ n \texttt{1} \le \texttt{nums[i]} \le \texttt{n} 1≤nums[i]≤n
- nums \texttt{nums} nums 中的所有整数都只出现一次,除了一个整数出现两次或多次
进阶
- 如何证明 nums \texttt{nums} nums 中至少存在一个重复的数字?
- 你可以设计一个线性级时间复杂度的解决方案吗?
前言
由于数组的长度是 n + 1 n + 1 n+1,且最多包含 n n n 个不同的整数。根据抽屉原理(或鸽笼原理)可知,将数组的 n + 1 n + 1 n+1 个位置分配到 n n n 个不同的整数,至少有两个位置分配到同一个整数,即该整数在数组中重复。因此数组中至少存在一个重复的数字。
由于这道题要求不修改数组且空间复杂度是 O ( 1 ) O(1) O(1),因此排序、哈希表等解法都是不允许的,需要使用其他解法寻找重复的数字。
解法一
思路和算法
每个正整数的二进制表示中至少有一位是 1 1 1。对于二进制表示的每一位,分别考虑数组 nums \textit{nums} nums 中的所有整数在该位的 1 1 1 的次数 countArr \textit{countArr} countArr 和范围 [ 1 , n ] [1, n] [1,n] 中的所有整数在该位的 1 1 1 的次数 countNum \textit{countNum} countNum。假设整数 x x x 在数组 nums \textit{nums} nums 中出现超过一次,则 x x x 的二进制表示的每个 1 1 1 所在的位都满足 countArr > countNum \textit{countArr} > \textit{countNum} countArr>countNum。理由如下。
-
如果 x x x 出现两次,则范围 [ 1 , n ] [1, n] [1,n] 中的所有整数都在数组 nums \textit{nums} nums 中出现,因此对于 x x x 中等于 1 1 1 的每一位都有 countArr > countNum \textit{countArr} > \textit{countNum} countArr>countNum。
-
如果 x x x 出现超过两次,则范围 [ 1 , n ] [1, n] [1,n] 中的部分整数不在数组 nums \textit{nums} nums 中出现,可以看成 x x x 替代了这部分整数。对于 x x x 中等于 1 1 1 的每一位,被替代的整数在相应位的值等于 0 0 0 或 1 1 1。替代之前有 countArr > countNum \textit{countArr} > \textit{countNum} countArr>countNum,替代之后同样有 countArr > countNum \textit{countArr} > \textit{countNum} countArr>countNum。
除了 x x x 的二进制表示的每个 1 1 1 所在的位以外,其余位都不满足 countArr > countNum \textit{countArr} > \textit{countNum} countArr>countNum。因此上述结论的逆命题也成立:假设所有满足 countArr > countNum \textit{countArr} > \textit{countNum} countArr>countNum 的位组成的整数是 x x x,则 x x x 在数组 nums \textit{nums} nums 中出现超过一次。
根据上述分析,可以使用位运算寻找重复数。首先找到 n n n 的二进制表示的最高有效位,即最高位 1 1 1 所在的位数,然后依次遍历最低有效位到最高有效位,对于每一位计算 countArr \textit{countArr} countArr 和 countNum \textit{countNum} countNum,如果 countArr > countNum \textit{countArr} > \textit{countNum} countArr>countNum 则将该位加到重复数中。遍历结束之后即可得到重复数。
代码
class Solution {public int findDuplicate(int[] nums) {int n = nums.length - 1;int highBit = 0;int temp = n;while (temp != 0) {highBit = temp & (-temp);temp -= highBit;}int duplicate = 0;for (int i = 1; i <= highBit; i <<= 1) {int countArr = 0, countNum = 0;for (int num : nums) {int bit = num & i;if (bit != 0) {countArr++;}}for (int j = 1; j <= n; j++) {int bit = j & i;if (bit != 0) {countNum++;}}if (countArr > countNum) {duplicate += i;}}return duplicate;}
}
复杂度分析
-
时间复杂度: O ( n log n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度减 1 1 1。需要遍历的二进制表示位数是 O ( log n ) O(\log n) O(logn),对于每一位都需要 O ( n ) O(n) O(n) 的时间计算该位在重复数中是否等于 1 1 1,时间复杂度是 O ( n log n ) O(n \log n) O(nlogn)。
-
空间复杂度: O ( 1 ) O(1) O(1)。
解法二
思路和算法
假设有一个长度为 n + 1 n + 1 n+1 的数组 counts \textit{counts} counts,其中 counts [ i ] \textit{counts}[i] counts[i] 表示数组 nums \textit{nums} nums 中的不超过 i i i 的整数个数。以下用 x x x 表示重复数。
如果 x x x 出现两次,则当 i < x i < x i<x 时 counts [ i ] = i \textit{counts}[i] = i counts[i]=i,当 i ≥ x i \ge x i≥x 时 counts [ i ] = i + 1 > i \textit{counts}[i] = i + 1 > i counts[i]=i+1>i。
如果 x x x 出现超过两次,则范围 [ 1 , n ] [1, n] [1,n] 中的部分整数不在数组 nums \textit{nums} nums 中出现,可以看成 x x x 替代了这部分整数,假设只有一个整数被替代,用 j j j 表示被替代的整数,分别考虑 j < x j < x j<x 和 j > x j > x j>x 的情况。
-
如果 j < x j < x j<x,则对于 i < x i < x i<x,不超过 i i i 的整数个数不变或减少,因此 counts [ i ] ≤ i \textit{counts}[i] \le i counts[i]≤i,对于 i ≥ x i \ge x i≥x,不超过 i i i 的整数个数不变,因此 counts [ i ] > i \textit{counts}[i] > i counts[i]>i。
-
如果 j > x j > x j>x,则对于 i < x i < x i<x,不超过 i i i 的整数个数不变,因此 counts [ i ] = i \textit{counts}[i] = i counts[i]=i,对于 i ≥ x i \ge x i≥x,不超过 i i i 的整数个数不变或增加,因此 counts [ i ] > i \textit{counts}[i] > i counts[i]>i。
因此当 i < x i < x i<x 时 counts [ i ] ≤ i \textit{counts}[i] \le i counts[i]≤i,当 i ≥ x i \ge x i≥x 时 counts [ i ] > i \textit{counts}[i] > i counts[i]>i。如果被替代的整数有多个,该结论仍成立。
重复数 x x x 是使得 counts [ x ] > x \textit{counts}[x] > x counts[x]>x 成立的最小整数 x x x,可以使用二分查找的方法寻找重复数。
用 low \textit{low} low 和 high \textit{high} high 分别表示二分查找的下界和上界。由于 x x x 在范围 [ 1 , n ] [1, n] [1,n] 中,因此初始时 low = 1 \textit{low} = 1 low=1, high = n \textit{high} = n high=n。
每次查找时,取 mid \textit{mid} mid 为 low \textit{low} low 和 high \textit{high} high 的平均数向下取整,计算数组 nums \textit{nums} nums 中的不超过 mid \textit{mid} mid 的整数个数 counts [ mid ] \textit{counts}[\textit{mid}] counts[mid],执行如下操作。
-
如果 counts [ mid ] ≤ mid \textit{counts}[\textit{mid}] \le \textit{mid} counts[mid]≤mid,则重复数 x x x 小于等于 mid \textit{mid} mid,因此在 [ low , mid ] [\textit{low}, \textit{mid}] [low,mid] 中继续查找。
-
如果 counts [ mid ] > mid \textit{counts}[\textit{mid}] > \textit{mid} counts[mid]>mid,则重复数 x x x 大于 mid \textit{mid} mid,因此在 [ mid + 1 , high ] [\textit{mid} + 1, \textit{high}] [mid+1,high] 中继续查找。
当 low = high \textit{low} = \textit{high} low=high 时,查找结束,此时 low \textit{low} low 即为重复数 x x x。
实现方面,并不需要显性创建数组 counts \textit{counts} counts,而是可以在二分查找的过程中根据当前的 mid \textit{mid} mid 遍历数组 nums \textit{nums} nums 计算不超过 mid \textit{mid} mid 的整数个数,因此空间复杂度是 O ( 1 ) O(1) O(1)。
代码
class Solution {public int findDuplicate(int[] nums) {int n = nums.length - 1;int low = 1, high = n;while (low < high) {int mid = low + (high - low) / 2;int count = 0;for (int num : nums) {if (num <= mid) {count++;}}if (count > mid) {high = mid;} else {low = mid + 1;}}return low;}
}
复杂度分析
-
时间复杂度: O ( n log n ) O(n \log n) O(nlogn),其中 n n n 是数组 nums \textit{nums} nums 的长度减 1 1 1。需要执行 O ( log n ) O(\log n) O(logn) 次二分查找,每次二分查找需要 O ( n ) O(n) O(n) 的时间遍历数组 nums \textit{nums} nums 计算不超过特定值的整数个数,时间复杂度是 O ( n log n ) O(n \log n) O(nlogn)。
-
空间复杂度: O ( 1 ) O(1) O(1)。
解法三
思路和算法
将数组看成有向图,范围 [ 0 , n ] [0, n] [0,n] 中的每个整数是一个结点,对于 0 ≤ i ≤ n 0 \le i \le n 0≤i≤n 的每个下标 i i i,存在一条从 i i i 指向 nums [ i ] \textit{nums}[i] nums[i] 的有向边。对于重复数 x x x,存在至少两条指向 x x x 的边,因此有向图中存在环。
这道题可以看成在有向图中寻找环的入口,可以使用「环形链表 II」的快慢指针做法。以下只说明使用快慢指针寻找重复数的做法,正确性证明见「环形链表 II 的题解」。
寻找重复数分成两步。
第一步是使用快慢指针遍历有向图寻找相遇点。
初始时,快指针和慢指针都位于整数 0 0 0。每次将快指针移动两步,慢指针移动一步,在至少移动一次的情况下,当快指针和慢指针相遇时,相遇的位置为相遇点。
第二步是将两个指针分别从起点和相遇点开始遍历有向图寻找重复数。
初始时,两个指针分别位于整数 0 0 0 和相遇点。每次将两个指针各移动一步,两个指针相遇的整数即为重复数。
代码
class Solution {public int findDuplicate(int[] nums) {int fast = 0, slow = 0;int meet = -1;while (meet < 0) {fast = nums[nums[fast]];slow = nums[slow];if (fast == slow) {meet = fast;}}int pointer1 = 0, pointer2 = meet;while (pointer1 != pointer2) {pointer1 = nums[pointer1];pointer2 = nums[pointer2];}return pointer1;}
}
复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums \textit{nums} nums 的长度减 1 1 1。快慢指针的时间复杂度是 O ( n ) O(n) O(n)。
-
空间复杂度: O ( 1 ) O(1) O(1)。