文章目录
面试题 03:数组中重复的数字
题目一:找出数值中重复的数字
题目描述
在一个长度为 n
的数组里的所有数字都在 0 ~ n-1
的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组 {2, 3, 1, 0, 2, 5, 3}
,那么对应的输出是第一个重复的数字 2
。
思路一:排序
解决这个问题的一个简单的方法就是先把输入数组排序。从排序的数组中找出重复的数字只需要从头到尾扫描一遍排序后的数组即可。
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
思路二:哈希表
从头到尾扫描数组中的每个数字,然后判断哈希表里是否已经包含了这个数字。如果哈希表里还没有这个数字,就把它加入哈希表。如果哈希表中已经存在该数字,就找到一个重复的数字。
可以用 Set
来判断是否存在,也可以用 HashMap
来计数判断是否出现过。
时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)
思路三:交换元素
注意已知条件,长度为 n
的数组 nums[]
,元素取值范围在 0 ~ n-1
,比较巧妙的想法就是交换元素,将 nums[i]
放在下标为 nums[i]
的位置,如果交换之前已经满足 nums[nums[i]] = nums[i]
,说明该元素已经被交换过,那么就是一个重复元素。
class Solution {
public int duplicateInArray(int[] nums) {
if (nums == null || nums.length < 2) {
return -1;
}
int n = nums.length;
// 元素合法性检查
for(int i = 0; i<n; ++i){
if(nums[i]<0 || nums[i]>=n)
return -1;
}
for (int i = 0; i < n; ++i) {
while (nums[i] != i) {
int val = nums[nums[i]];
if (nums[i] == val) {
return val;
}
swap(nums, i, nums[i]);
}
}
return -1;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
时间复杂度: O ( n ) O(n) O(n)
题目二:不修改数组找出重复的数字
LeetCode 链接: 287. Find the Duplicate Number
题目描述
给定一个长度为 n+1
的数组 nums
,数组中所有的数均在 1∼n
的范围内,其中 n≥1
。
请找出数组中任意一个重复的数,但不能修改输入的数组。
样例
给定 nums = [2, 3, 5, 4, 3, 2, 6, 7]。
返回 2 或 3。
思考题: 如果只能使用 O ( 1 ) O(1) O(1) 的额外空间,该怎么做呢?
思路一:抽屉原理+分治法
抽屉原理: n+1
个苹果放在 n
个抽屉里,那么至少有一个抽屉中会放两个苹果
在本题中,一共有 n+1
个数,每个数的取值范围是 [1, n]
,所以至少会有一个数出现两次。
另外,还有两点需要注意:
- 不能修改原数组
- 使用常数额外空间
所以跟前面一题的思路有些不同。我们采用分治的思想,将每个数的取值的区间 [1, n]
划分成 [1, n/2]
和 [n/2+1, n]
两个子区间,然后分别统计两个区间中数的个数。
注意这里的区间是指 元素的取值范围,即 nums[i]
,而 不是数组下标。
划分之后,左右两个区间里一定至少存在一个区间,区间中元素的个数大于区间长度。
这个可以用反证法来说明:如果两个区间中元素的个数都小于等于区间长度,那么整个区间中元素的个数就小于等于 n
,和有 n+1
个数矛盾。
因此我们可以把问题划归到左右两个子区间中的一个,而且由于区间中数的个数大于区间长度,根据抽屉原理,在这个子区间中一定存在某个数出现了两次。
依次类推,每次我们可以把区间长度缩小一半,直到区间长度为 1
时,我们就找到了答案。
class Solution {
public int duplicateInArray(int[] nums) {
if (nums == null || nums.length < 2)
return 0;
// 区间范围[1,n]
int l = 1, r = nums.length - 1;
while (l < r) {
// 划分的区间:[l, mid], [mid + 1, r]
int mid = (l + r) >>> 1;
int count = 0;
// 统计[l,mid]区间内元素个数
for (int num : nums) {
if (num >= l && num <= mid)
++count;
}
if (count > mid - l + 1) {
// 说明重复元素在[l,mid] 中
r = mid;
} else {
l = mid + 1;
}
}
return l;
}
}
思路二:找到“环”的入口
类似链表找环的入口,数组中的元素 val
看作是结点值,nums[val]
看作是 next域
,因为必然存在重复元素,所以必然存在“环”。
初始时 慢指针 slow
和 快指针 fast
均等于 0
,而数组中的元素是在 [1,n]
范围内,这相当于链表中的 dummy
虚假头结点,即 dummy.next = head
class Solution {
public int duplicateInArray(int[] nums) {
if (nums == null || nums.length < 2)
return 0;
int slow = 0, fast = 0;
while (fast < nums.length) {
slow = nums[slow];
fast = nums[nums[fast]];
if (slow == fast) {
slow = 0;
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
}
return -1;
}
}
我们可以看到外层 while
循环条件是一定成立的,因为“环”一定存在,所以也必然能有循环结束条件。但是要注意,如果初始用 slow = nums[0]
,fast = nums[nums[0]]
,可能会出现死循环,例如 nums = {1, 1}