「这是我参与11月更文挑战的第三天,活动详情查看:2021最后一次更文挑战」
各位小伙伴大家好啊,今天是第四天了。坚持就是胜利,打卡~~
第一题 剑指 Offer 03. 数组中重复的数字
找出数组中重复的数字。在一个长度为 n 的数组 nums里的所有数字都在 0~n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例 1:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
限制:
2 <= n <= 100000
来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/sh…
解题思路:
暴力
暴力解法就利用两次for循环,第一个指针指向一个数字,第二个指针遍历一次数组,是否有和第一个指针指向相同的值。这里提一下,暴力解放的第二个指针可以不用从头开始遍历,因为前面的数字已经判断过了,不可能存在相同的数字。由于暴力法不是很好,这里就不给出实现了。
时间复杂度:O(n^2),空间复杂度:O(1)
哈希表
我们也可以利用哈希表来重新存储这些数字。如果存储过程中有冲突,那么就说明即将存入的那个数字已经重复了。题目中指出,找出任意重复的数字即可,所以就可以将当前数字返回。因为要引入额外的hash数组,所以空间复杂度O(n)。
时间复杂度:O(n),空间复杂度:O(n)
优化哈希表
因为题目明确指出nums里的所有数字都在0~n-1的范围内,如何来利用这个条件呢?其实在hash表的解法中,我已经使用了这个条件,将数组的索引作为哈希的索引,值和索引一一对应。例如2这个数据应放在数组索引2的位置下。这样一来,就将条件用上了。但是用的还不够好,因为哈希表额外使用了空间。所以,我们也可以在原数组修改。将每个值直接放入原数组中对应的位置,如果要存放的数据的索引里已经是这个数据,就说明该元素重复,return 它。
为什么明明用了两次循环实现,该方法的时间复杂度依然是O(n)?我们只是把所有元素放到了对应位置,如果已经放在了对应位置,第二层循环就不会进入,所以每一个元素只会放置最多一次,那么我们的时间复杂度就应该是O(n)。
时间复杂度:O(n),空间复杂度:O(1)
代码实现:
// 普通哈希表
var findRepeatNumber = function(nums) {
let hash = [];
for(let val of nums) {
// 如果hash中已经存在该值
if(hash[val] === val) {
return val;
} else {
// 否则将值存入hash
hash[val] = val;
}
}
};
// 优化后的哈希表
var findRepeatNumber = function(nums) {
for(let i=0; i<nums.length; i++) {
// 当前位置值和位置不相同时,需要把正确的值放在当前位置才行。
while(nums[i] !== i) {
// nums[i] 当前值
// nums[nums[i]] 当前值应该放入的位置
// 如果当前值和对应位置的值一致,说明该值重复
if(nums[i] === nums[nums[i]]) {
return nums[i];
}
let temp = nums[i];
nums[i] = nums[temp];
nums[temp] = temp;
}
}
};
第二题 剑指 Offer 53 - I. 在排序数组中查找数字 I
统计一个数字在排序数组中出现的次数。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: 2
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: 0
限制:
0 <= nums.length <= 10^5-10^9 <= nums[i] <= 10^9nums 是一个非递减数组-10^9 <= target <= 10^9
来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/za…
解题思路:阅读完题目,抓取题目关键信息——排序数组。现在我们还是从最简单的方式说起,最简单的方式肯定是暴力解法。
暴力
设置一个计数器count,循环遍历一次数组,当遇到target值的时候计数器加一,返回count。
时间复杂度:O(n),空间复杂度:O(1)
二分查找
题目既然说到了是排序数组,那么我们可以试一试二分法。我们一般都知道二分法是用来查找数组中某个元素的位置。那怎么利用二分法查找该元素的出现次数呢?因为是有序数组,所以我们先找到该元素首次出现的位置,再找到该元素最后出现的位置。用最后出现的位置减去首次出现的位置,得到的结果就是该元素出现的次数。
时间复杂度:O(log n),空间复杂度:O(1)
代码实现:
// 二分查找
var search = function(nums, target) {
if(nums.length === 0) {
return 0;
}
let first = findFirstIndex(nums, target);
// 如果不存在该元素
if (first === -1) {
return 0;
}
let last = findLastIndex(nums, target);
return last - first + 1;
};
function findFirstIndex(nums, target) {
let left = 0, right = nums.length - 1;
let mid;
while (left < right) {
mid = left + Math.floor((right - left) / 2);
if(nums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return nums[left] === target ? left : -1;
}
function findLastIndex(nums, target) {
let left = 0, right = nums.length - 1;
let mid;
while (left < right) {
mid = left + Math.floor((right - left + 1) / 2);
if(nums[mid] <= target) {
left = mid;
} else {
right = mid - 1;
}
}
return nums[left] === target ? left : -1;
}
注意:查找的时候,你会遇见这样的情形,偶数数组。那么此时的问题是,中间的值到底是哪一个?查找首次出现的位置,应该向左边偏移,而查找最后出现的位置,我们应该向右偏移,因为最后出现的位置应该在最右边。
第三题 剑指 Offer 53 - II. 0~n-1中缺失的数字
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
示例 1:
输入: [0,1,3]
输出: 2
示例 2:
输入: [0,1,2,3,4,5,6,7,9]
输出: 8
限制:
1 <= 数组长度 <= 10000
来源:力扣(LeetCode) 链接:leetcode-cn.com/problems/qu…
解题思路:题目又提到了递增排序数组和每个数字的范围都在0~n-1,那么有了第一题和第二题的经验,我们可以用二分查找来实现。
二分查找
我们需要找出当前缺失的元素,缺失了元素的特点为后面的值会变大。中间的值等于其索引值,那么说明中间以及之前的值并没有丢失,将left指针指向mid + 1。中间的值大于其索引值,那么说明前面有值丢失,将right指针指向mid,因为有可能刚好是mid的前面一个值丢失了,所以不能置为mid-1。最后,遍历完之后,如果指针指向最后一个元素,并且值也是正确的,那么就说明丢失了n这一个元素,返回left+1,否则返回left。
暴力
这题暴力也是可以解决的,在此就不提了,因为有更好的方法就尽量使用更好的方法。
代码实现:
// 二分搜索
var missingNumber = function(nums) {
let left = 0, right = nums.length - 1;
let mid;
while(left < right) {
mid = Math.floor((left + right) / 2);
if(nums[mid] === mid) {
left = mid + 1;
} else if (nums[mid] > mid) {
right = mid;
}
}
return (left === nums.length - 1 && nums[left] === left) ? left + 1 : left;
};
总结
今天的题主要考查了二分查找这个知识点。一般来说,题目中如果有有序数组这样类似的词语,我们就可以想想是否可以使用二分查找。它的时间复杂度为O(log n),空间复杂度为O(1),所以比一般的暴力查找快。如果有什么不对的地方,或者有更好的方案,欢迎提出,大家一起讨论。