对于二分题,其实就是设定一个中间值 mid, 然后通过这个值进行一个判断 check(mid), 通过这个函数的返回值,判断将不可能的一半剪切掉;
在刷题的时候需要注意主要是两部分,check 函数的定义以及边界的选择(等号的选择,以及最后是 return left 还是 right)
这次主要是 LC 的二分专题,里面的简单题基本都是比较显性的提示了 check 函数的构建,比方说直接找出某个值,而难题一般都是 check 函数比较难想的,这个时候就需要经验了;
广义上只要是排好序(局部排序),只要是找某个值,大部分都可以考虑用二分,这样复杂度可以降低很多;
对于边界,我的循环结束条件是 left <= right
, 因为如果要多记很多模板,怕会出问题,所以退出条件基本都按这个,然后无论是那种模块,都基于这个结束条件来判断,这样可以把问题收缩都循环里的判定的 check 函数,多做了就会发现端倪;
然后关于退出之后 left 还是 right ,这个是具体问题具体分析;由于我的结束判定条件是 left<=right
,所以如果没用中间返回,那么必然存在 left === right 的时候,这个时候根据判定条件,就知道 right 在 left 的前面,而到底是左逼近,还是右逼近,都比较好判断了,因为这个时候已经退出去了,left 和 right 所代表的 check 的状态也是显而易见的,那么看题目要求什么,给什么即可;
对于二分,我觉得这个专题就基本足够了,简单居多,难题也有两个;如果是第一次学习二分,那么按照专栏的三个模板去记忆也 ok, 别人的经验终归是适合别人自己,做题最重要是把握住自己的节奏,记忆自己最熟悉的那个点,强行模仿别人反而落了下乘;
当然那个男人那么强,我的做题就是模仿的他,慢慢大佬的解法就是我自己的节奏了,毕竟模仿多了,其实就是自己的了,除了算法,其他的工程化学习也是一样的;
那么,周末快乐,下周开 dp 吧,毕竟这个听有意思的。
模板 1
- 目标值是一个固定的 target,在二分过程中需要不断的判断,如果成功就返回对应的值,否则直接返回失败的值
- 返回值如果是向下取,返回 right,如果向上取,则返回 left,还有可能返回一个特定给的失败值;
var search = function (fn, target) {
let left = 最小值,
right = 最大值;
while (left <= right) {
// 取 mid 值
const mid = ((right - left) >> 1) + left;
//这里的 fn 可能是函数,也可能只是数组取值,反正就是可以取得一个值去跟 target 比较
const temp = fn(mid);
if (temp === target) return mid;
if (temp < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return 没有精确匹配后的值;
};
704. 二分查找
var search = function (nums, target) {
const len = nums.length;
if (!len) return -1;
let left = 0,
right = len - 1;
while (left <= right) {
const mid = ((right - left) >> 1) + left;
if (nums[mid] === target) return mid;
if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
};
69. x 的平方根
// 69. x 的平方根
var mySqrt = function (x) {
let left = 0,
right = x;
while (left <= right) {
const mid = ((right - left) >> 1) + left;
const sqrt = mid * mid;
if (sqrt === x) return mid;
if (sqrt < x) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// 向下取整
return right;
};
374. 猜数字大小
分析
- 这里内置一个函数 guess(n), 返回值是 -1 0 1, -1 是 targt 值更小
var guessNumber = function (n) {
let left = 1,
right = n;
while (left <= right) {
const mid = ((right - left) >> 1) + left;
if (guess(mid) === 0) return mid;
if (guess(mid) > 0) {
// 这个时候 mid < pick
left = mid + 1;
} else {
right = mid - 1;
}
}
};
// 自己模拟一下这个 guess 函数吧 -- 假定第二个参数就是目标猜的数字,我们可以用它来初始化,默认是5
function guess(num, pick = 5) {
if (num === pick) return 0;
if (pick < num) return -1;
if (pick > num) return 1;
}
参考视频:传送门
441. 排列硬币
分析
- 这里求的是一个左侧极值的二分法,是向右逼近的二分
- 累计值算法是小学数学题 sum = (first+end)*count/2
- 每次取中间层数,求出到这个层数需要的币数 sum,然后和目标值 n 比较
- 如果刚好符合,直接返回(这里可以收缩到左侧判定条件中);如果 count 比较少,则 left 要提到 mid+1,否则 right 要提到 mid-1
- 由于最后要返回的是最逼近 n 的层数,所以判断一下当 left === right 情况,如果小于 n,则 left = mid+1,这个时候 right 符合要求,所以跳出循环后,返回的是 right
- 时间复杂度O(logN)
var arrangeCoins = function (n) {
let left = 0,
right = n;
while (left <= right) {
const mid = left + ((right - left) >> 1);
// mid 层的时候满的硬币数
const sum = ((1 + mid) * mid) / 2;
if (sum === n) return mid;
if (sum < n) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return right;
};
33. 搜索旋转排序数组
分析
- 已知:原始数组 nums 是生序排序的,且数组中的值不一样的
- 入参的 nums 是在某个下标 k 的作用下发生了重置,使得 nums 现在是先升序数组 [k,len-1]然后断裂后,再一个升序数组[0,k-1]
- 这是一个局部排好序的数组,所以可以用二分处理,返回的是 target 值的下标或者 -1
- 所以每次都用排好序的一半来作为判断依据,如果在排好序这边,则删除另外,反之亦然
- 时间复杂度 O(logn)
var search = function (nums