文档讲解:代码随想录
704 二分查找
刚开始的思路是遍历nums数组,比较值相等返回位置。这么做肯定是超时的,原因是并没有接触过二分查找,脑子里面没有这个概念。
二分查找需要注意的点:
1.数组为有序数组;
2.数组中无重复元素
因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。
二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 while(left < right)
还是 while(left <= right)
,到底是right = middle
呢,还是要right = middle - 1
呢?
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
class Solution {
public int search(int[] nums, int target) {
if(target < nums[0] || target > nums[nums.length -1]){
return -1;
}
int left = 0;
int right = nums.length - 1;
while(left <= right){
int mid = left + ((right - left) >> 1);
if(target < nums[mid]){
right = mid - 1;
}else if(target > nums[mid]){
left = mid + 1;
}else if(target == nums[mid]){
return mid;
}
}
return -1;
}
}
结果:
代码解释:
首先,这是一个名为
Solution
的类,其中包含了一个名为search
的公共方法,该方法接受一个整数数组nums
和一个目标整数target
作为参数,返回目标整数在数组中的索引,如果不存在则返回-1。在方法的开头,有一个条件判断:
if (target < nums[0] || target > nums[nums.length - 1])
。这个判断用于优化搜索过程。如果目标整数小于数组的第一个元素或者大于数组的最后一个元素,那么目标整数肯定不在数组中,直接返回-1,避免不必要的循环运算。接下来,代码定义了两个变量:
left
和right
,它们分别代表了当前查找范围的左边界和右边界。初始时,left
被设为0,表示查找范围的起始位置,而right
被设为nums.length - 1
,表示查找范围的结束位置。然后,代码进入一个循环体,条件是
left <= right
,即只要查找范围仍然有效,就会继续进行查找。在循环中,首先计算中间索引
mid
,这是通过将left
和right
相加再除以2来得到的。这样可以获得当前查找范围的中间位置。接下来,代码使用
nums[mid]
与目标整数target
进行比较:
- 如果
nums[mid]
等于target
,则找到了目标整数,直接返回mid
作为目标整数在数组中的索引。- 如果
nums[mid]
小于target
,则说明目标整数可能在当前中间元素的右侧,因此将left
更新为mid + 1
,缩小查找范围。- 如果
nums[mid]
大于target
,则说明目标整数可能在当前中间元素的左侧,因此将right
更新为mid - 1
,同样是为了缩小查找范围。循环会不断根据比较结果缩小查找范围,直到查找范围变为无效(
left > right
),此时退出循环。最后,如果循环结束时仍未找到目标整数,说明目标整数不在数组中,返回-1作为结果。
关于计算中间位置mid这行代码的解释:
int mid = left + ((right - left) >> 1);
eg: 1 + (99-1)/2 = 50
97 + (99 - 97)/2 = 98
这里使用了位运算,具体来说是右移运算
>>
,用于对(right - left)
的结果进行右移一位。这个位运算的效果相当于将(right - left)
的值除以 2,但是比直接使用除法更加高效。解释这个表达式的步骤如下:
(right - left)
:计算当前查找范围的长度,即右边界减去左边界,得到的值表示当前查找范围内元素的个数。
((right - left) >> 1)
:将上一步计算的查找范围长度右移一位,相当于将其除以 2。这是因为右移一位等价于除以 2 的操作,只是在二进制表示中更高效。
left + ((right - left) >> 1)
:将右移后的结果加上左边界left
,得到的就是当前查找范围的中间索引mid
。这种方式计算中间索引是为了避免整数溢出问题,同时保持查找范围的均衡性。通过这种方法,可以在每次迭代中确定中间索引,将查找范围分成两半,以便在数组中进行更精确的查找操作。
为什么不使用: int mid = (right + left) >> 1;来进行计算呢?
虽然
(right + left) >> 1
在某些情况下可能会产生正确的结果,但在其他情况下可能会导致错误的结果或者整数溢出的问题。这是因为(right + left)
的求和操作可能会导致整数溢出,尤其是在right
和left
很大的情况下。二进制位运算
>>
可以理解为将数值向右移动,并且在右边补零。但是如果(right + left)
发生了溢出,移位后的结果可能不再是正确的中间索引,因为溢出后的数值可能与原来的意图不符。相比之下,使用
(right - left) >> 1
的方式,即先计算差值再右移,可以减少整数溢出的风险。因为当right
和left
都是正整数时,其差值不会超过right
的值,从而避免了溢出的问题。因此,在进行二分查找时,使用
(right - left) >> 1
来计算中间索引是更为安全和可靠的方法,可以有效避免溢出问题,同时保持算法的正确性和稳定性。
27 移除元素
这道题在力扣上暴力解法是可以通过的,我们先来看第一次写的暴力解法的问题(没过):
class Solution {
public int removeElement(int[] nums, int val) {
if(nums.length > 0){
for(int i = 0;i <= nums.length;i++){
if(val == nums[i]){
for(int j = i+1;j < nums.length;j++){
nums[j-1] = nums[j];
}
}
}
}
System.out.print(nums);
return nums.length;
}
}
问题:
1. 循环条件错误:在外层循环中,i 的范围应该是 i < nums.length 而不是 i <= nums.length。
2. 返回值问题:虽然代码中返回了 nums.length,但是数组中被修改的部分可能不再包含有效元素,这会导致返回的长度大于实际有效长度。
修改代码之后又发现问题:在数组中的数据往前替换了之后,i的值也需要往前走一位,不然就会出现轮空的情况。并且i和j的循环要小于size,而不是nums的长度,否则会出现size为负数的情况,时间也会超出限制。
以上是暴力解法需要注意的地方。
下面我们看一下使用双指针法来进行计算。
双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
定义快慢指针
- 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
- 慢指针:指向更新 新数组下标的位置
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。
class Solution {
public int removeElement(int[] nums, int val) {
// 快慢指针
int slowIndex = 0;
for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
if (nums[fastIndex] != val) {
nums[slowIndex] = nums[fastIndex];
slowIndex++;
}
}
return slowIndex;
}
}