题目描述
给定一个包含 n + 1
个整数的数组 nums
,其数字都在 [1, n]
范围内(包括 1
和 n
),可知至少存在一个重复的整数。
假设 nums
只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums
且只用常量级 O(1)
的额外空间。
示例 1:
输入:nums = [1,3,4,2,2] 输出:2
示例 2:
输入:nums = [3,1,3,4,2] 输出:3
提示:
1 <= n <= 105
nums.length == n + 1
1 <= nums[i] <= n
nums
中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次
进阶:
如何证明
nums
中至少存在一个重复的数字?你可以设计一个线性级时间复杂度
O(n)
的解决方案吗?
解题思路
1.暴力for循环(超时):
时空复杂度: O(n^2) O(1)
我们直接使用两层for循环遍历,从前往后判断当前元素和基准元素是否相等,十分暴力的方法。这个方法没什么好说的了,只能说干就完了。
public int BrustMethod(int[] nums) {
for(int i = 0; i < nums.length; i ++) {
for(int j = i + 1; j < nums.length; j ++) {
if(nums[i] == nums[j]){
return nums[i];
}
}
}
return 0;
}
2.使用哈希表:
时空复杂度: O(n) O(n)
遇事不决哈希表,这个方法应该是比较好想到的了吧,用空间换时间。先遍历一次数组把所有元素存入哈希表中,然后再遍历一次,判断是否出现在哈希表中即可,也是十分暴力。其实本人当时也只想到这个方法(手动狗头)。
直接上代码:
public int hashMethod(int[] nums) {
//存入哈希表
Set<Integer> set = new HashSet();
for(int i = 0; i < nums.length; i ++) {
if(set.contains(nums[i])){
return nums[i];
}
set.add(nums[i]);
}
return 0;
}
3.二分法:
时空复杂度: O(nlogn) O(1)
我们可以根据题目所给条件有n + 1
个整数,数字范围在[1 , n]
,我们可以利用小于等于当前数字的元素个数,进行二分查找
这里我使用leetcode案例一中的数据进行分析
nums = [1,3,4,2,2]
当为这个数组的时候
我们可以列出一个小于等于当前元素的表格,
1 | 2 | 3 | 4 |
---|---|---|---|
1 | 3 | 4 | 5 |
通过观察我们可以发现一个规律,即如果存在多个重复元素,会导致一种情况.
在这里我假设重复数字为target,用 sum[target] 代表小于等于target的数的个数,则有 [1,target - 1] <= nums[target] 和 nums[targe,n] > [target,n]
这就是我们二分单调性的依据,即重复元素会影响数组中元素整体的计数情况
我们只需要二分出来sum[target] > target的那个临界点即可
上代码
/**
要注意我们要二分的是[1,nums.length - 1],题目已经告诉我们数字范围在[1,nums.length - 1]
而不是二分nums[mid],这里要清楚
**/
public int binSearch(int[] nums) {
//定义两个指针
int l = 1, r = nums.length - 1;
//当l < r的时候进行二分
while(l < r) {
//中点为 (l + r) / 2,这里用的位运算,右移一位代表除以2
int mid = l + r >> 1;
//定义一个统计小于等于mid的的变量
int cnt = 0;
//统计小于等于mid的元素个数
for(int i = 0; i < nums.length; i ++) {
if(nums[i] <= mid) {
cnt ++;
}
}
//如果cnt <= mid则继续二分右边的
if(cnt <= mid) {
l = mid + 1;
} else {
//否则继续二分左边
r = mid;
}
}
return r;
}
4.快慢指针法:
时空复杂度: O(n) O(1)
这种方法属实牛逼(手动狗头),具体的实现思路其实和LeetCode上142题<<环形链表>>非常相似,具体使用的是Floyd判圈算法。
具体思路是定义一个慢指针,一个快指针,让快指针一次移动两个节点,慢指针一次移动一个节点,当链表中存在环的时候,快慢指针会在环中相遇,因为快指针相对于慢指针每次移动一个节点,相当于在追赶慢指针,只要存在环,就一定能追上。
这里贴出来链接,需要了解为什么有环一定能追上快指针的小伙伴可以自行查看。Leetcode 142.环形链表
public int dualPointMethod(int[] nums) {
//直接定义快慢指针,这里使用int变量模拟指针
int slow = nums[0];
int fast = nums[0];
//循环遍历数组,类似遍历链表,找到快慢指针的交点
do{
slow = nums[slow];
fast = nums[nums[fast]];
}while(slow != fast);
//当找到交点的时候,利用从头节点开始的指针和交点开始的指针来查找数组中的初始交点
slow = nums[0];
int p = fast;
while(slow != p) {
slow = nums[slow];
p = nums[p];
}
return slow;
}
总结
这道题目有多种解题方法,还是非常考验经验积累和思路的,不然像作者一样只会使用哈希表和暴力了,第四种方法快慢指针,使用的Floyd判圈算法真的十分巧妙,将链表中的判断是否有环的思路运用到数组中去,也能实现同样的效果,只能说是异曲同工之妙了!
当没有什么思路的时候,不妨将各种数据结构都想一遍,看能不能找到合适的数据结构解题!
路漫漫其修远兮,吾将上下而求索!与君共勉!