在计算middle时没有考虑到溢出的问题,导致第一次测试失败
左闭右闭
var search = function (nums, target) {
let left, middle = 0, right = nums.length - 1;
while (left <= right) {
// 注意溢出问题
middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle - 1; // target在左区间[left, middle - 1]
} else if (nums[middle] < target){
left = middle + 1; // target在右区间[middle + 1, right]
} else {
return middle;
}
}
return - 1;
};
左闭右开
var search = function (nums, target) {
let left, middle = 0, right = nums.length - 1;
while (left < right) {
middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle;// target在左区间[left, middle)
} else if (nums[middle] < target) {
left = middle + 1;// target在右区间[middle + 1, right)
} else {
return middle;
}
}
return -1;
};
二分查找最重要的是处理好区间的边界,是使用左闭右闭还是左闭右开?一旦确定了边界,题目就变得清晰了。
1.左闭右闭
在这种情况下,如果判断出 target 在左区间(也就是nums[middle] > target
时),那么nums[middle] ≠ target
,此时左区间应该为[left, middle - 1]
;
如果判断出 target 在右区间(也就是nums[middle] < target
时),那么nums[middle] ≠ target
,此时右区间应该为[middle + 1, right]
;
当nums[middle] = target
,此时middle就是要找的数组下标,返回 middle 即可。
2.左闭右开
在这种情况下,如果判断出 target 在左区间(也就是nums[middle] > target
时),那么nums[middle] ≠ target
,而且是左闭右开的区间,实际对比时并不对比右边界,因此此时左区间应该为[left, middle)
;
如果判断出 target 在右区间(也就是nums[middle] < target
时),那么nums[middle] ≠ target
,而且是左闭右开的区间,实际对比时并不对比右边界,因此此时右区间应该为[middle + 1, right)
;
当nums[middle] = target
,此时middle就是要找的数组下标,返回 middle 即可。
3.浮点数溢出问题
额外总结一下溢出问题。
在JS中,如果一个整型数据经过除运算后的结果是一个浮点数,那么会JS不会默认向下取整,例如:
console.log(10 / 3);
// 输出:3.3333333333333335
因此,在本题中为了取到正确的 middle 值,需要使用更为精确的计算方法,例如使用位运算。
在本题中使用了右移运算符,它的规则是这样:
各二进制位全部右移若干位,正数高位补0,负数高位补1,低位丢弃
举个例子:
// 正数的位运算
12 >> 2 = 3
1100 -> 12
0011 -> 12 >> 2 = 3
因为12是正数,右移过程中高位补上两个0,低位丢弃,结果就是3
// 负数的位运算
-12 >> 2 = -3
0100 -> -12
1101 -> -12 >> 2 = -3
因为-12是负数,右移过程中高位补1,低位丢弃,结果就是-3
明白了右移运算符的规则,回到本题,middle的计算是要计算出一组连续数据最中间的数,也就是传统意义上的(left + right) / 2
。
我们看几个例子来理解右移 >> 1的特殊含义:
10 >> 1 = 5
1010 -> 10
0101 -> 10 >> 1 = 5
8 >> 1 = 4
1000 -> 8
0100 -> 8 >> 1 = 4
11 >> 1 = 5
1011 -> 11
0101 -> 11 >> 1 = 5
发现了吗,十进制数进行 >> 1的位运算就相当于一个除以2的十进制运算。
也就是说,10 >> 1 等同于10 / 2。
这样我们就能拆分了:
(left + right) / 2 = left / 2 + right / 2
= left + right / 2 - left / 2
= left + ((right - left) / 2)
= left + ((right - left) >> 1)
这就是本题中的写法,这种写法避免了浮点数溢出的问题,同时也比直接使用/
计算更快一些。
理解了之后,以后在工作中就可以直接使用了。
无
暴力解法
var removeElement = function (nums, val) {
let len = nums.length;
for (let i = 0; i < len; i++) {
if(nums[i] == val) { // 找到需要移除的元素,将数组整体左移一位
for (let j = i + 1; j < len; j++) {
nums[j - 1] = nums[j];
}
i--; // 数组左移一位,因此下标i也要左移
len--; // 此时已经完成一次删除,数组长度也要减1
}
}
return len;
};
双指针法
var removeElement = function(nums, val) {
let len = nums.length;
let i, j = 0; // 定义快慢指针
for (; i < len; i++) { // 快指针遍历数组,寻找非移除元素
if (nums[i] != val) { // 找到非移除元素,慢指针指向不含移除元素val的新数组的下标
nums[j++] = nums[i];
}
}
return j; // 遍历结束后,慢指针就代表新数组的长度
};
暴力解法看似简单粗暴,实则要考虑的细节比较多,远不如双指针好用。
1.暴力解法
在找到要移除的val后,数组整体左移进行删除,此时在进入下一次循环时需要考虑到指针i的位置发生了变化,数组的长度也发生了变化,因此需要更新二者后再进入循环。
2.双指针法
定义快慢指针,快指针遍历数组,寻找val之外的元素,组成不含val元素的新数组;
慢指针指向新数组的元素,一次循环后需要右移一位以便接收下次循环快指针找到的元素,当快指针遍历完nums,快指针的下标实际上就是新数组的长度。