对于二分题,其实就是设定一个中间值 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 吧,毕竟这个听有意思的。
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 没有精确匹配后的值;
};
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 的平方根
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;
};
分析
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;
}
分析
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;
};
分析
参考视频:传送门
var search = function (nums, target) {
let left = 0,
right = nums.length - 1;
while (left <= right) {
const mid = ((right - left) >> 1) + left;
if (nums[mid] === target) return mid;
if (nums[mid] >= nums[left]) {
// [left,mid] 是有序的
if (nums[left] <= target && target < nums[mid]) {
// target 在[left , mid) 中
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// [mid,right] 是有序的
if (nums[mid] < target && target <= nums[right]) {
// target 在(mid , right] 中
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
};
相比于模板 1,模板 2 中不是仅有一个符合条件值,而是一系列值,我们需要找到符合要求的那个 极值
,比方说是符合条件的最大值/第一个值
等;
var search = function (fn) {
let left = 最小值,
right = 最大值;
while (left <= right) {
// 取 mid 值
const mid = ((right - left) >> 1) + left;
//这里的 fn 可能是函数,也可能只是数组取值,反正就是可以取得一个值去跟 target 比较
const bool = fn(mid);
if (bool) {
// 成功了,要向还没成功的地方寻找
right = mid - 1;
} else {
left = mid + 1;
}
}
return 特定的值;
};
分析
正常 -> 错误
,所以这里是根据错误向左逼近var solution = function (isBadVersion) {
return function (n) {
let left = 1,
right = n;
while (left <= right) {
const mid = ((right - left) >> 1) + left;
if (isBadVersion(mid)) {
// 如果是错误版本
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
};
};
分析
var findPeakElement = function (nums) {
nums[-1] = nums[nums.length] = -Infinity; // 设置边界值,这样保证在边缘的时候也只需要两个值就能判极值
let left = 0,
right = nums.length - 1;
while (left <= right) {
const mid = ((right - left) >> 1) + left;
// 找出一个有峰值的区间
if (nums[mid] > nums[mid + 1]) {
// [mid,right] 局部下降,而[-1,mid] 是局部上升的,比较有一个最低值
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
};
分析
var findMin = function (nums) {
let len = nums.length;
nums[-1] = nums[len] = Infinity;
let left = 0,
right = len - 1;
while (left <= right) {
const mid = ((right - left) >> 1) + left;
if (nums[mid - 1] > nums[mid] && nums[mid] < nums[mid + 1])
return nums[mid]; //谷值
if(nums[mid]<=nums[right]){
// [mid,right] 单增
// 注意这个等号为啥要加,可以考虑一下如果 left 和 right 相等时,对应的 mid 也是这个点,那么是让 right 走,还是让 left 走;这里我们最后返回值是 left,所以让 right 走一步结束战斗
right = mid-1
}else{
left = mid+1
}
}
return nums[left];
};
var findMin = function(nums) {
const len = nums.length
nums[-1] = nums[len] = Infinity
let left = 0,right = len-1
while(left<=right){
const mid = ((right-left)>>1) + left
// 将右侧与 mid 同值的值删掉
while(nums[right] === nums[mid] && right>mid){
right --
}
// 由于存在重复值,所以拐点值右侧可以是直线,而不一定是单增
if(nums[mid-1]>nums[mid] && nums[mid]<=nums[mid+1]) return nums[mid]
if(nums[mid]<=nums[right]){
// 上一题这里的等号是当 left 和 right 重合时的特殊情况
// 现在由于值可能重复,所以不能直接判断出 [mid,right] 是递增的区间了,所以要先为右侧相同的值进行删减,然后再进行即可
right = mid-1
}else{
left = mid+1
}
}
return nums[left]
};
分析
var searchRange = function(nums, target) {
if(!nums) return [-1,-1]
let left = 0,right = nums.length-1
let ret = []
// 找左节点
while(left<=right){
const mid = ((right-left)>>1) + left
if(nums[mid]<target){
left = mid+1
}else{
right = mid-1
}
}
if(nums[left]!== target) return [-1,-1]
// 找右节点
let l = left,r = nums.length-1
while(l<=r){
const mid = ((r-l)>>1) + l
if(nums[mid]>target){
r = mid-1
}else{
l = mid+1
}
}
return [left,r]
};
分析
var findClosestElements = function (arr, k, x) {
let l = 0,
r = arr.length - 1;
while (l <= r) {
const mid = ((r - l) >> 1) + l;
if (arr[mid] > x) {
r = mid - 1;
} else {
l = mid + 1;
}
}
// 这个时候 r 是左侧最靠近 x 的值下标,l 是右侧最靠近 x 的值
let lIndex = r,
rIndex = l; // 防止混淆
let ret = [];
while (k--) {
let isLeft = true;
if (lIndex >= 0 && rIndex < arr.length) {
isLeft = x - arr[lIndex] <= arr[rIndex] - x;
} else if (rIndex < arr.length) {
isLeft = false;
}
if (isLeft) {
ret.unshift(arr[lIndex]);
lIndex--;
} else {
ret.push(arr[rIndex]);
rIndex++;
}
}
return ret;
};
分析
var myPow = function (x, n) {
const recursion = (n) => {
if (n === 0) return 1;
// const y = recursion(n >> 1);
const y = recursion(Math.floor(n / 2));
return n % 2 ? y * y * x : y * y;
};
return n < 0 ? 1 / recursion(-n) : recursion(n);
};
console.log(myPow(2.1, 3));
console.log(myPow(2, -2));
/** * @分析 * 1. 概念:若 x*x = y , 则 y 是完全平方数 */
var isPerfectSquare = function (num) {
let left = 0,
right = num;
while (left <= right) {
const mid = ((right - left) >> 1) + left;
const temp = mid * mid;
if (temp === num) return true;
if (temp > num) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return false;
};
分析:
var nextGreatestLetter = function(letters, target) {
const targetIndex = target.charCodeAt()
let left = 0,right = numbers.length-1
while(left <= right){
const mid = ((right-left) >> 1) + left
if(letters[mid].charCodeAt()>targetIndex){
// 只要是符合的,都返回往左走,知道走不了
right = mid-1
}else{
left= mid+1
}
}
if(left>=numbers.length) return letters[0]
return letters[left]
};
分析:
var intersection = function (nums1, nums2) {
const l1 = nums1.length,
l2 = nums2.length;
if (!l1 || !l2) return [];
// 先排序
nums1.sort((a, b) => a - b);
nums2.sort((a, b) => a - b);
const ret = [];
for (let i = 0;i<l1;i++) {
if(i>0 && nums1[i-1] === nums1[i]) continue // 重复值跳过
const n = nums1[i];
let left = 0,
right = l2-1;
while (left <= right) {
const mid = ((right - left) >> 1) + left;
if (nums2[mid] === n) {
ret.push(n);
break;
}
if (nums2[mid] > n) {
right = mid - 1;
} else {
left = mid + 1;
}
}
}
return ret
};
var intersect = function (nums1, nums2) {
const map = new Map(); //将长数组的值存储一份
for (let item of nums2) {
if (map.has(item)) {
map.set(item, map.get(item) + 1);
} else {
map.set(item, 1);
}
}
let ret = [];
for(let item of nums1){
if(!!map.get(item)){
map.set(item,map.get(item)-1)
ret.push(item)
}
}
return ret;
};
分析
var twoSum = function (numbers, target) {
for (let i = 0; i < numbers.length-1; i++) {
const temp = target - numbers[i];
let l = i + 1,
r = numbers.length - 1;
while (l <= r) {
const mid = ((r - l) >> 1) + l;
if (numbers[mid] === temp) return [i + 1, mid + 1];
if (numbers[mid] < temp) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
};
分析
var findDuplicate = function (nums) {
let left = 1,
right = nums.length - 1; // 值是 1 - n
while (left < right) {
const mid = ((right - left) >> 1) + left;
const count = nums.reduce((prev, cur) => (cur <= mid ? prev + 1 : prev), 0); // 小于等于 count 的值
if (count > mid) {
// 如果 [1,mid] 这个数组满值的情况才只有 mid 个,现在 count 如果比这个还大,证明重复的值在这里面
right = mid;
} else {
left = mid + 1;
}
}
return left;
};
分析
var findMedianSortedArrays = function (nums1, nums2) {
const n = nums1.length,
m = nums2.length;
let sum = n + m; // 如果是偶数,取两个值 prev,cur ,取中间值
let k = (sum + 1) / 2; // 可以是非整数
let cur;
while (k >= 1) {
if (!nums1.length) {
cur = nums2.shift();
} else if (!nums2.length) {
cur = nums1.shift();
} else {
if (nums1[0] <= nums2[0]) {
cur = nums1.shift();
} else {
cur = nums2.shift();
}
}
k--;
}
let next;
if (k !== 0) {
// 这里用 ?? 而不是用 || , 是因为判断 nums[0] 是否为 undefined,而如果是 0 的时候,取 0 而非切换到 Infinity;
next = Math.min(nums1[0] ?? Infinity, nums2[0] ?? Infinity);
return (cur + next) / 2;
}
return cur;
};
分析
var splitArray = function (nums, m) {
// 先找到 left 和 right
let left = 0,
right = 0;
for (let num of nums) {
left = Math.max(num, left);
right += num;
}
// 切割最大值不超过 max 的数组,如果切出来的数组数量少于 m,则证明可以切得更新,发挥 true
// 如果切除来的数组数量超出了 m, 证明只且 m 组的时候,最小值要超出 max 了,返回 false
function check(max) {
let ret = 0,
sum = 0;
let i = 0;
while (i < nums.length) {
sum += nums[i];
if (sum >max) {
// 一旦超出 max,则分组结束,sum 重新设置为 nums[i]
ret++;
sum = nums[i];
}
i++
}
// 如果最后还有剩,单独成团
ret = sum ? ret + 1 : ret;
return ret<=m
}
while (left <= right) {
const mid = ((right - left) >> 1) + left;
if (check(mid)) {
// 如果能找到,向左逼近 -- right 最后得到的是一个不成功的值,因为只要成功它就要发生改变
right = mid - 1;
} else {
// left 最终出去的时候,肯定代表一个成功的值
left = mid + 1;
}
}
return left;
};
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。