0、絮絮叨叨
守护全网讲的最好的二分法博主
注:本文用JavaScript写的~
1、伪代码
//二分查找伪代码模版
let left = 0, right = length - 1;①
while (left <= right) {②
mid = (left + right) / 2;③
if (array[mid] === target) return result;
else if (array[mid] < target) left = mid + 1;④
else if (array[mid] > target) right = mid - 1;
}
return -1;⑤
2、容易出错的地方
1.right = length 还是 length - 1?
答:取哪个对应的是相应的搜索区间
比如取right = length - 1;对应区间[0, length - 1],后边left = mid + 1;
比如取right = length;对应区间[0, length)。本质是一样的,后边left = mid;
2.whild(left <= right),<= 和 < 有什么区别?
答1:因为初始化 right 的赋值是 nums.length - 1,即最后一个元素的索引,而不是nums.length,所以可以等于right。
这二者可能出现在不同功能的二分查找中,区别是:前者 (<=)相当于两端都闭区间 [left, right],后者相当于左闭右开区间 [left, right),因为索引大小为 nums.length 是越界的。
答2:还有一种时候就是 left = right 的时候有意义,比如153, < 和 <= 写出来的结果是不一样的
3.mid = (left + right) / 2 这么写为啥不好?
答:用位运算最好:mid = left + ((right - left) >> 1) 速度快,记得加括号,是弱运算
4.为啥 left = mid + 1?
答1:因为我每次搜的都是闭区间(right = length - 1),所以每次循环到left或者right的时候,说明nums[mid] !== target,mid对应的数都不对,都不对了我还取left或者right=mid干啥,所以就取 left = mid + 1, 或者 right = mid - 1;
答2:left等于mid或者是mid+1,就看nums[mid] 的取值有没有意义,能不能是最终答案,要能是的话就left = mid
5.最后返回的是right还是left?
答1:看题,最后的求的结果一定在答案附近,看看是取左边还是右边,比如35题就取左边(较大的一个),69就取右边(较小的一个)
答2:题做多了,返回的是啥还真不一定,看题目要求的是啥。
6,在做了一些题之后的感受1:
1)有时候会把target变成动态的 比如:153
2)有时候会把比较的双方都变成动态的,但是if判断条件里总有nums[mid] 比如:153
3)有时候会是二维矩阵,你要拆开来比较 比如:1351
4)有的时候二分法不是那么明显,需要一系列数学公式的引导 比如:441
5)有时候所有的部分都是变量,很绕 :153 ,33
6)有时候比较的区间需要先判断比如,153,33
7.感受2
想要用二分法做题,最重要的就是知道 f(mid) 和 target 之间的关系,这两个都有可能都是动态的,而且还得找一下才有。
3、三种类型的模板*(后边做多了,发现都是类型一的变形)*
类型1:正常查找,找到了就返回,没找到就返回-1
function binary_search(nums, target) {
let left = 0, right = nums.length - 1;
while(left <= right) {
let mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if(nums[mid] == target) {
return mid; //直接返回
}
}
return -1; // 直接返回
}
类型2:left = *** eg:找数组[1,2,2,2,3]最右边的2的位置序号
function right_bound(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
let mid = left + (right - left) / 2;
if (nums[mid] == target) {
left = mid + 1; // 这里改成收缩左侧边界即可
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
}
}
// 这里改为检查 right 越界的情况,见下图
if (right < 0 || nums[right] != target) return -1;
return right;
}
类型3:right = *** eg:找数组[1,2,2,2,3]最左边的2的位置序号
function left_bound(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
let mid = left + (right - left) / 2;
if (nums[mid] == target) {
right = mid - 1; // 这里改成收缩右侧边界即可
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
}
}
// 最后要检查 left 越界的情况,越界情况可以看上边链接详情
if (left >= nums.length || nums[left] != target) return -1;
return left;
}
4、例题
类型1:大多数都是这个最基础类型的变形
1)704. 二分查找:最正常的
// 递归
var search = function (nums, target) {
return _search(nums, target, 0, nums.length - 1)
};
var _search = function (nums, target, left, right) {
if (left > right) return -1;
let mid = Math.floor((left + right) / 2);
if (nums[mid] === target) {
return mid
}
else if (nums[mid] < target) {
return _search(nums, target, mid + 1, right)
} else {
return _search(nums, target, left, right - 1)
}
}
//非递归,后来发现这才是常用的方法
var search = function (nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
let mid = Math.floor((left + right) / 2);
if (nums[mid] === target) {
return mid
} else if (nums[mid] < target) {
left = mid + 1
} else {
right = mid - 1
}
}
return -1
}
2)374. 猜数字大小:也挺正常的
var guessNumber = function (n) {
let left = 1, right = n;//left 是从 1 开始的
while (left <= right) {
let mid = left + ((right - left) >> 1);
// guess 是个函数 所以输入的是参数,用小括号
if (guess(mid) == 0) {
return mid
} else if (guess(mid) == 1) {
left = mid + 1;
} else if (guess(mid) == -1) {
right = mid - 1;
}
// return -1 //return 放在这里不对
}
return -1
};
3)852:山脉数组:mid和mid+1、mid-1之间比较
var peakIndexInMountainArray = function(arr) {
let left = 0, right = arr.length - 1;
while(left <= right){
let mid = left + ((right - left) >> 1);
if( arr[mid - 1] < arr[mid] && arr[mid] > arr[mid + 1]){
// 上边这块不能连着写 会错 arr[mid + 1] < arr[mid] < arr[mid + 1]
return mid
}else if(arr[mid] < arr[mid + 1]){
left = mid + 1
}else if(arr[mid] > arr[mid + 1]){
right = mid -1
}
}
return -1
// 这个return 写不写都行 没啥用 因为就给的条件而言一定会返回
};
4)35. 搜索插入位置:正常的思考,在多一点思考
var searchInsert = function(nums, target) {
let left = 0, right = nums.length - 1;
while(left <= right){
let mid = left + ((right - left) >> 1);
if(nums[mid] == target){
return mid //这个也是类型1的变形
}else if(nums[mid] < target){
left = mid + 1;
}else if(nums[mid] > target){
right = mid - 1;
}
}
return left //代码中,0代表位置1,所以返回大的那个
};
5)1385. 两个数组间的距离值:题目描述很傻屌。涉及到二维了已经
//距离值的意思是说在arr1中符合特定条件的元素数量,什么特定条件呢?
//这个特定条件就是 arr1中的这个元素和arr2中任何一个元素相减再求绝对值后都是大于d的.
var findTheDistanceValue = function (arr1, arr2, d) {
// 用二分法先排序
arr1.sort((a, b) => a - b);
arr2.sort((a, b) => a - b);
let ans = 0;
// 对于每个arr1,arr2的所有值都拿来对比
for (let i = 0; i < arr1.length; i++) {
let left = 0, right = arr2.length - 1;
while (left <= right) {
// 这里其实取巧了,arr2 数量大的时候,有一些值是被忽略的,不过不影响最终结果
// 因为只要有一个错误,就break 了
let mid = left + ((right - left) >> 1);
if (arr2[mid] <= arr1[i] + d && arr2[mid] >= arr1[i] - d) {
break // 进入到这个圈内就是失败的意思,break了之后,下边的ans++不会触发
} else if (arr2[mid] + d < arr1[i]) {
left = mid + 1; // 因为已经排序好了,第二个数组加上间距还够不着第一个数组,那就让第二个数组往右移
} else if (arr2[mid] - d > arr1[i]) {
right = mid - 1;
}
}
// while能结束 说明是正确的了 计数器 加1
if (left > right) {
ans++
}
}
return ans
};
6)367. 有效的完全平方数:就跟做数学题一样,看完答案大明白
var isPerfectSquare = function(num) {
let left = 0, right = num ;
while(left <= right){
let mid = left + ((right - left) >> 1);
if(mid * mid == num){
return true
}else if(mid *mid < num ){
left = mid + 1;
}else if (mid * mid > num ){
right = mid - 1;
}
}
return false
};
7)69. x 的平方根
var mySqrt = function(x) {
let left = 0, right = x;
while(left <= right){
let mid = left + ((right - left) >> 1)
if(mid*mid === x){ // mid === x/mid :这么写 x是0时不行
return mid
}else if(mid < x/mid){
left = mid + 1;
}else {
right = mid -1;
}
}
return right
};
- 问:为什么return right
- 答:跳出来的时候一定是在平方根附近的,最后判断一下如果平方大于x的话就返回它前面的一个值
8)35. 搜索插入位置
var searchInsert = function (nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
let mid = left + ((right - left) >> 1);
if (nums[mid] === target) {
return mid
} else if (nums[mid] < target) {
left = mid + 1
} else {
right = mid - 1
}
}
return left
};
- 问:为什么要return left
- 答:返回的时候一定是在结果附近的,返回的时候 左比右多一,又数组下标的特性 0 表示位置1,所以返回的是left
var solution = function(isBadVersion) {
/**
* @param {integer} n Total versions
* @return {integer} The first bad version
*/
return function(n) {
// 左等于0或者1都没有问题,但是这里为了有实际意义,我让他等于1
let left = 1, right = n;
while(left <= right){
let mid = left + ((right - left) >> 1);
if(isBadVersion(mid) == false) {
left = mid + 1;// false 是版本低了 双否表肯定
}else if(isBadVersion(mid) == true){
right = mid - 1;
}
}// 可以找个栗子试一下 为什么返回left
return left
};
};
10)441. 排列硬币:有的时候可能需要用含有mid的式子和target比较
var arrangeCoins = function(n) {
let left = 0, right = n;
while(left <= right){
let mid = left + ((right - left) >> 1);
// 这块是我看答案看的 真没想到,但是我发现了,要用二分法需要有两个值
// 进行比较 这两个值可能有一静一动,也有可能两个都是动态的
let now_num = ((mid + 1)*mid)/2;
if(now_num == n){
return mid
}else if(now_num < n){
left = mid + 1;
}else if(now_num > n){
right = mid - 1;
}
}
return right // 这块我是举一个具体的例子搞的
};
11)167. 两数之和 II - 输入有序数组:这题就是需要自己弄一个含有mid的表达式和target比较。
//题目的意思就是从一个数组中找个两个数,使之和等于target
var twoSum = function (numbers, target) {
// 固定一个,找另一个 n^2
for (let i = 0; i < numbers.length - 1; i++) {
let left = i + 1, right = numbers.length - 1;
while (left <= right) {
let mid = left + ((right - left) >> 1);
if (numbers[i] + numbers[mid] == target) {
return [i+ 1, mid + 1]
} else if (numbers[i] + numbers[mid] < target) {
left = mid + 1;
} else if (numbers[i] + numbers[mid] > target) {
right = mid - 1;
}
}
}
};
var specialArray = function (nums) {
// 这个题就只能取right = nums.length
let left = 0, right = nums.length;
while (left <= right) {
let mid = left + ((right - left) >> 1);
if (mid == bigger(mid, nums)) {
return mid
} else if (mid < bigger(mid, nums)) {
left = mid + 1;
} else if (mid > bigger(mid, nums)) {
right = mid - 1;
}
}
return -1
};
// 这个函数的作用是,看看在nums这个数组里大于n的有几个,返回数字
var bigger = function (n, num) {
let result = 0;
for (let i = 0; i < num.length; i++) {
if (num[i] >= n) {
result++
}
}
return result
}
var countNegatives = function (grid) {
// 第二步:一共m行数组,for循环累加
let sum = 0, m = grid.length;
for (let i = 0; i < m; i++) {
sum = sum + findFirstNegative(grid[i])
}
return sum
};
// 第一步:先判断一个数组:查找一个递减数组小于0的个数
var findFirstNegative = function (row) {
let left = 0, right = row.length - 1, count = 0;
while (left <= right) {
let mid = left + ((right - left) >> 1);
if (row[left] < 0) {
count++;
left++;
} else if (row[mid] <= 0) {
left ++
} else if (row[mid] >= 0) {
left = mid + 1;
}
}
return count
};
14)74:搜索二维矩阵:题目的意思是看看矩阵里有没有target,送分题
var searchMatrix = function (matrix, target) {
// 第一步,把二维变成一维的arr数组
let m = matrix.length, n = matrix[0].length;
let arr = [];
for (let i = 0; i < m; i++) {
// concat不会改变原来的数组,所以需要重新赋值
arr = arr.concat(matrix[i])
}
// 第二步,正常的二分法
let left = 0, right = arr.length - 1;
while(left <= right){
let mid = left + ((right - left) >> 1);
if(arr[mid] == target){
return true
}else if(arr[mid] < target){
left = mid + 1;
}else if (arr[mid] > target){
right = mid - 1
}
}
return false
};
15)633:平方数之和
var judgeSquareSum = function (c) {
// 这也是一到二分法的题,只不过这个数组需要你自己想
let a = 0, b = Math.floor(Math.sqrt(c));
while (a <= b) {
let cur = a * a + b * b;
if (cur === c) {
return true
} else if (cur < c) {
a++
} else if (cur > c) {
b--
}
}
return false
};
// 关于为什么每次左加一以及右减一不会错过正确答案的问题,参考:
// https://leetcode.cn/problems/sum-of-square-numbers/solution/shuang-zhi-zhen-de-ben-zhi-er-wei-ju-zhe-ebn3/
var findMin = function (nums) {
// 法一:暴力枚举法
let min = nums[0];
for(let i = 1; i< nums.length; i++){
if(nums[i] < min){
min = nums[i]
}
}
return min
// 法二:
let left = 0, right = nums.length - 1;
while (left <= right) {
let mid = left + ((right - left) >> 1);
if (nums[mid] > nums[right]) {
left = mid + 1 // 因为中值 > 右值,中值肯定不是最小值,左边界可以跨过mid
} else if (nums[mid] < nums[right]) {
right = mid // 因为中值 < 右值,中值也可能是最小值,右边界只能取到mid处
} else if (nums[mid] == nums[right] && nums[mid] == nums[left]) {
return nums[left] // 等于的时候,其实就找出答案了,此时左和右一样
}
}
// return nums[right] 这个返回也就没必要了
//法二的变形
let left = 0, right = nums.length - 1;
while (left < right) {// 这题如果判断条件加一个=号,当中值和右值相等的时候,就是结果
let mid = left + ((right - left) >> 1);
if (nums[mid] > nums[right]) {
left = mid + 1 // 因为中值 > 右值,中值肯定不是最小值,左边界可以跨过mid
} else if (nums[mid] < nums[right]) {
right = mid // 因为中值 < 右值,中值也可能是最小值,右边界只能取到mid处
}
}
return nums[right]
};
var search = function (nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const mid = Math.floor(left + (right - left) / 2)
if (nums[mid] === target) {
return mid
} else if (nums[left] <= nums[mid]) {// 如果nums[left] <= nums[mid],说明是左半段是有序的
if (nums[left] <= target && target < nums[mid]) { // nums[mid]的等于号可以省略,因为等于的时候就退出了
right = mid - 1// 此时有两种情况,若target在左递增区间,则排除mid右边的所有值
} else if (target < nums[left] || target > nums[mid]) {
left = mid + 1// 若不在则排除mid左边的所有值
}
} else if (nums[mid] <= nums[right]) {// 如果nums[mid]<=nums[right],说明是右半段是有序的
if (nums[mid] < target && target <= nums[right]) {
left = mid + 1// 此时有两种情况,若target在右递增区间,则排除mid左边的所有值
} else if (target < nums[mid] || target > nums[right]) {
right = mid - 1// 若不在则排除mid右边的所有值。
}
}
}
return -1
};
类型2:相等的时候,left = ***
var nextGreatestLetter = function (letters, target) {
let left = 0, right = letters.length - 1;
while (left <= right) {
let mid = left + ((right - left) >> 1);
if (letters[mid] == target) {
left = mid + 1 // 相等的时候,说明差不多就到中间了
} else if (letters[mid] < target) {
left = mid + 1
} else if (letters[mid] > target) {
right = mid - 1
}
}
// return letters[left % letters.length] // 另一种思路
return left == letters.length ? letters[0] : letters[left];
};
类型3:相等的时候,right = ***
var searchRange = function (nums, target) {
var right_fun = function (nums, target) {
let left = 0, right = nums.length - 1;
let rightmax = -1;// 这块的思路挺好的 我想了一大气
while (left <= right) {
let mid = left + ((right - left) >> 1);
if (nums[mid] == target) {
rightmax = mid;
left = mid + 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
}
}
return rightmax
}
var left_fun = function (nums, target) {
let left = 0, right = nums.length - 1;
let leftmin = -1;
while (left <= right) {
let mid = left + ((right - left) >> 1);
if (nums[mid] == target) {
leftmin = mid;
right = mid - 1;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
}
}
return leftmin
}
return [left_fun(nums, target), right_fun(nums, target)]
}