代码随想录算法训练营第1天 | 704.二分查找、27.去除元素
文章目录
一.题目链接–704
力扣题目链接:704.二分查找
代码随想录704链接
二.解题思路
当该数组为有序,无重复数字时,进行查找只需要确定搜索范围即左右边界,然后不断缩小范围。
左边界永远不变,只需要判断右边界
2.1 右边界为right = nums.length - 1; 左闭右闭[left, right]
2.2 右边界为right = nums.length; 左闭右开[left, right)
注意边界的作用是为了让后面的left和right的取值符合区间定义,比如[1,1] 和[1,1)
三.遇到问题和注意事项
- 3.1 注意先判断当值不在数组中,即超出的值需要如何处理,此处我们设置当值小于最小和大于最大时候就令返回值为1
- 3.2 取中间值时候,为了避免int的数值过大导致溢出问题,所以用小值加上差值,再用二进制向右移一位的方法 left + ((right - left) >> 1
四.实现代码
4.1 左闭右闭
//方法一:左闭右闭区间
public int search(int[] nums, int target) {
if (target < nums[0] || target > nums[nums.length - 1]){
return -1;
}
int left = 0, right = nums.length - 1;
while(left <= right){
int middle = left + ((right - left) >> 1);
if (target == nums[middle]){
return middle;
}
//目标值在中间值的左边,因为左闭右闭,左边不动,
//右边的值能取到,但是上一步已经排除相等,所以右边要减一
else if (target < nums[middle]){
right = middle - 1;
}
//目标值在中间值的右边,因为左闭右闭,右边不动,
//左边的值能取到,但是上一步已经排除相等,所以左边要加一
else if(target > nums[middle]){
left = middle + 1;
}
}
return -1;
}
4.2 左闭右开
//方法二:左闭右开区间
public int search(int[] nums, int target) {
if (target < nums[0] || target > nums[nums.length - 1]){
return -1;
}
int left = 0, right = nums.length;
while(left < right) {
int middle = left + ((right - left) >> 1);
if(target == nums[middle]){
return middle;
}
// 目标值在中间值的左边,因为左闭右开,左边不动,
//右边的值不能取到,所以可以设置右边的边界为middle
else if(target < nums[middle]){
right = middle;
}
// 目标值在中间值的右边,因为左闭右开,右边不动,
// 左边的值能取到,但是上一步已经排除相等,所以左边要加一
else if(target > nums[middle]){
left = middle + 1;
}
}
return -1;
}
五.随想录知识点
关于 二分查找
● 最重要的就是分类讨论好二分,二分看着好写边界 case 还是需要测试的哈
● 什么是区间不变量? 比如 区间取左闭右闭的话 那么每次区间二分 范围都是新区间的左闭右闭 后面做判断时 要一直基于这个左闭右闭的区间
● 其实区间定义成开或者闭都没有什么关系 只是要明确每次收缩范围后 范围内的元素是哪些 注意会不会漏掉边界就好
● 大家需要注意二分的几种情况
○ 当l = 0, r = n的时候因为r这个值我们在数组中无法取到,while(l < r) 是正确写法
○ 当l = 0, r = n - 1的时候因为r这个值我们在数组中可以取到,while(l <= r) 是正确写法 主要看能不能取到这个值
● 二分法有多种写法,末尾是开区间闭区间都可以解出寻找单个元素和寻找边界的题目,只需要注意相应的是l < r还是l <= r,每次取mid还是取mid加减一即可。建议理解后背熟一套模板,不要搞混。
● 其实二分还有很多应用场景,有着许多变体,比如说查找第一个大于target的元素或者第一个满足条件的元素,都是一样的,根据是否满足题目的条件来缩小答案所在的区间,这个就是二分的本质。另外需要注意,二分的使用前提:有序数组
● 二分的最大优势是在于其时间复杂度是O(logn),因此看到有序数组都要第一时间反问自己是否可以使用二分。
● 关于二分mid溢出问题解答:
○ mid = (l + r) / 2时,如果l + r 大于 INT_MAX(C++内,就是int整型的上限),那么就会产生溢出问题(int类型无法表示该数)
○ 所以写成 mid = l + (r - l) / 2或者 mid = l + ((r - l) >> 1) 可以避免溢出问题
● 对于二进制的正数来说,右移x位相当于除以2的x几次方,所以右移一位等于➗2,用位运算的好处是比直接相除的操作快
一.题目链接–27
力扣题目链接:27.移除元素
代码随想录27链接
二.解题思路
题目要求在原数组里修改 不能再new一个新的数组,最后返回一个长度。
2.1暴力解法
暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组
暴力解法的时间复杂度是O(n^2)
2.2双指针(快慢指针)
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
最差需要遍历两次数组
定义快慢指针
快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
慢指针:指向更新 新数组下标的位置
也可以令快指针为right 慢指针为left
但是注意此时的right和left都是最左边开始的值
2.3双指针(相向双指针)
相向双指针方法,基于元素顺序可以改变的题目描述改变了元素相对位置,确保了移动最少元素
最差需要遍历一次数组
将最右边的先找到不为value所需要的下标,
再从左边开始依次将最右边的赋值给最左边,同时最右边仍然在判断是否不为value
2.4双指针 (官方给的优化双指针,类似上面的双向双指针)
三.遇到问题和注意事项
注意在暴力解法里面的设置两层循环条件时,将i < nums.length 改为在外面设置一个参数size赋值为nums.length,再令 i < size ,因为每次两层循环都是调用nums.length,会导致超时,所以直接先在外面给长度赋值为一个变量就好了。
四.实现代码
4.1暴力解法
//方法一:暴力解法
// 时间复杂度:O(n^2)
// 空间复杂度:O(1)
public int removeElement(int[] nums, int val) {
int size = nums.length;
for(int i=0; i<size; i++){
if(nums[i] == val){// 发现需要移除的元素,就将数组集体向前移动一位
for(int j = i+1; j<size; j++){
nums[j-1] = nums[j];
}
i--;// 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位
size--;// 此时数组的大小-1
}
}
return size;
}
4.2双指针(快慢指针)
//方法二:双指针法(快慢指针法)
// 时间复杂度:O(n)
// 空间复杂度:O(1)
public int removeElement(int[] nums, int val) {
int slowIndex = 0;
int size = nums.length;
for(int fastIndex = 0; fastIndex < size; fastIndex++){
if(nums[fastIndex] != val){
nums[slowIndex] = nums[fastIndex];
slowIndex++;
}
}
return slowIndex;
}
4.3双指针(相向双指针)
//方法三:相向双指针法
public int removeElement(int[] nums, int val) {
int left = 0;
int right = nums.length - 1;
while(right >= 0 && nums[right] == val) right--;//将right移到从右数第一个值不为val的位置
while(left <= right){
if(nums[left] == val){ //left位置的元素需要移除
将right位置的元素移到left(覆盖),right位置移除
nums[left] = nums[right];
right--;
}
left++;
while(right >= 0 && nums[right] == val) right--; //将right再次移到从右数第一个值不为val的位置
}
return left;
}
4.4双指针 (官方给的优化双指针,类似上面的双向双指针)
//官方的答案:双指针优化
public int removeElement(int[] nums, int val) {
int left = 0;
int right = nums.length;
while (left < right) {
if (nums[left] == val) {
nums[left] = nums[right - 1];
right--;
} else {
left++;
}
}
return left;
}
五.随想录知识点
关于 移除元素
● 快指针可以理解成在旧数组中找非目标元素,然后赋值给慢指针指向的新数组,虽然都指向一个数组
● 关于二分法和移除元素的共性思考
这两题之间有点类似的,他们都是在不断缩小 left 和 right 之间的距离,每次需要判断的都是 left 和 right 之间的数是否满足特定条件。对于「移除元素」这个写法本质上还可以理解为,我们拿 right 的元素也就是右边的元素,去填补 left 元素也就是左边的元素的坑,坑就是 left 从左到右遍历过程中遇到的需要删除的数,因为题目最后说超过数组长度的右边的数可以不用理,所以其实我们的视角是以 left 为主,这样想可能更直观一点。用填补的思想的话可能会修改元素相对位置,这个也是题目所允许的。
● fast < nums.size() 和 fast <= nums.size()-1 没什么区别,那为什么第二个会在空数组时报数组越界的错误?
vector的size()函数返回值是无符号整数,空数组时返回了0,再减个一会溢出