数组二分查找到二分法的感悟笔记
Author:zhoulujun Date:
二分查找是一只思维模式,最简单就是:https://leetcode.cn/problems/binary-search/description/
这个题目之前也会写,但是看了
https://www.bilibili.com/video/BV1fA4y1o715/?vd_source=34e0c98f309ef76d2ea130113e548f5e
https://github.com/CyC2018/CS-Notes/blob/master/notes/Leetcode%20题解%20-%20二分查找.md
二分查找(Binary Search)也叫作折半查找,前提是查找的顺序结构是有序的,我们一般在数组上进行二分查找。
二分查找就好像猜数字大小游戏一样。假设要数字目标值属于 [1, 1000] 范围内,当我们猜的数字小于这个目标值时("Too low"),我们需要往大去猜;反之大于这个目标值时("Too high"),我们需要往小去猜。当然这里猜的方式并不是盲目的,我们每次都取中间值去猜,每猜一次可以缩小当前一半的范围,这样可以大大提高效率。二分查找本质上也是这样的过程,时间复杂度为 O(logn) ,在较大数据情况下相比线性查找要快非常多。
定义一个左指针 left 标记当前查找区间的左边界,一个右指针 right 标记当前查找范围的右边界。
每次取 mid 来判断当前取的值是否等于目标值 target。
如果等于 target 就直接返回 mid ;
如果小于目标值 target ,那么将左边界变为 mid + 1,缩小区间范围继续在 [mid + 1, right] 范围内进行二分查找
如果大于目标值 target ,那么将右边界变为 mid - 1,缩小区间范围继续在 [left, mid - 1] 范围内进行二分查找。
最后出现了 left > right 的情况,说明区间范围大小缩小到 0 都无法找到该目标值,那么很明显数组中不存在这个目标值 target,此时退出循环,返回 -1 。
例如在数组:1,2,3,4,7,9,10中查找元素2,如图所示:
开闭区间:
在二分查找法中,开区间和闭区间在写法上只有一个区别:
闭区间的循环条件是 while (left <= right)。
开区间的循环条件是 while (left < right)
开区间和闭区间的区别在于是否包含边界值。比如区间只有 [left, right],即:left<=target<=right
[left, right] ,[left, right]) ,(left, right])],(left, right])
[left, right] 表示包含left和right的闭区间。
[left, right) 表示包含left但不包含right的开区间。
(left, right] 表示不包含left但包含right的开区间。
(left, right) 表示不包含left也不包含right的开区间。
闭区区间核心代码是:
if (nums[middle] > target) { right = middle - 1; // target 在左区间,所以[left, middle - 1] } else if (nums[middle] < target) { left = middle + 1; // target 在右区间,所以[middle + 1, right] } else { // nums[middle] == target return middle; // 数组中找到目标值,直接返回下标 }
之很容易搞混 ,
如果目标值等于中间元素,那么我们找到了目标值,可以直接返回结果。
如果目标值大于中间元素,那么我们可以排除掉左半部分,因为左半部分的所有元素都小于中间元素,而目标值大于中间元素。因此,我们将搜索范围更新为右半部分,即将 left 更新为 mid + 1。
如果目标值小于中间元素,那么我们可以排除掉右半部分,因为右半部分的所有元素都大于中间元素,而目标值小于中间元素。因此,我们将搜索范围更新为左半部分,即将 right 更新为 mid - 1。
在每次迭代中缩小搜索范围,直到找到目标值或搜索范围为空。
左闭右闭、左开右闭,都可以是这个写法
左闭右开、左开右开得换一下
左闭右开代码
var search = function(nums, target) { let mid, left = 0, right = nums.length - 1; // 因为left == right的时候,在[left, right)是无效的空间,所以使用 < while (left< right) { // 位运算 + 防止大数溢出 mid = Math.floor((left+right)/2); // 如果中间数大于目标值,要把中间数排除查找范围,所以右边界更新为mid;如果右边界更新为mid-1,那中间数还在下次查找范围内 if (nums[mid] > target) { right = mid; // 去左面闭区间寻找, target 在左区间,在[left, middle)中 } else if (nums[mid]< target) { left = mid + 1; // 去右面闭区间寻找 } else { return mid; } } return -1; };
理论上,在所有情况下使用 right = mid + 1 都是可以的。但是,在左闭右开区间中,使用 right = mid + 1 会导致效率降低。
在右闭区间中,目标值可能等于right。因此,需要将right更新为mid的下一个位置,以确保目标值在下次查找中不会被排除。
在右开区间中,目标值不可能等于right。因此,将right更新为mid即可。
总结:只要右闭,就是right =mid+1,否则right = mid
递归写法
function binarySearch(nums, target, left, right) { if (left > right) { return -1; } const mid = left + Math.floor((right - left) / 2); if (nums[mid] === target) { return mid; } else if (nums[mid]< target) { return binarySearch(nums, target, mid + 1, right); } else { return binarySearch(nums, target, left, mid - 1); } } // 使用示例 const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]; const target = 5; const index = binarySearch(nums, target, 0, nums.length - 1);
递归看起来更好理解写。
二分查找考题:
运包裹
https://leetcode.cn/problems/capacity-to-ship-packages-within-d-days/description/
function test(weights, days) { // 最小装载能力就是数组的最大值(因为包裹不能拆分呀) let left = weights[weights.length-1]; // 最大装载能力就是数组的总和(极限情况,一次性装完) let right = weights.reduce((a, b) => a + b, 0); // 然后从开始二分查找 while (left < right) { const mid = Math.floor((left + right) / 2); let day = 1; let cur = 0; for (let weight of weights) { //如果当前重量加上当前物品重量大于中间值,天数加1,当前重量重置为0 if (cur + weight > mid) { day += 1; cur = 0; } // 将当前物品重量加到当前重量 cur += weight; } // 如果装载的天数少于实际天数,说明装载量大量,可以缩小装载量(指针右移),因为是右闭,所以是<= if (day <= days) { right = mid; } else { left = mid + 1;// 否则加大装载 } } return left; } console.log(test([3, 2, 2, 4, 1, 4], 3));
分段计费
一遍面试不会考这么简单,比如分段收费(营销措施,买的越多,越便宜)
const config = [ { range: [1, 5], fee: 30 }, { range: [5, 10], fee: 20 }, { range: [10, 30], fee: 15 }, { range: [30, 60], fee: 10 }, { range: [60, 120], fee: 5 }, { range: [120, Infinity], fee: 1 }, ]; function calculate(num){ // 缓存迭代结果 let count = 0; let total = 0; for(const {range:[min,max],fee} of config ){ // 已经循环过的,直接推出 if(num<min){ break; } // 当前值不在区间范围,缓存,继续查找 if(num>max){ count = (max-min)*fee; } if(min<num&&num<max){ if(count===0){ total = num*fee; }else { total = count + (num-min)*fee; } break; } } } // 上面的代码每次都要变量一遍,太耗费时间, 可以以空间换时间的 let count =0; const cache = [0] const temp = config.slice() temp.pop(); temp.forEach(({range:[min,max],fee},index)=>{ count+=(max-min)*fee cache.push(count) }) console.log(cache) function calculate2(num){ let left = 0; let right = config.length -1; let total; while (left<=right){ const mid = Math.floor((left+right)/2); const {range:[min,max],fee} = config[mid]; if(min<num&&num<max){ const cacheFee = cache[mid]; if(mid===0){ total = num*fee; }else { total = cacheFee+(num-min)*fee; } break; } if(num>max){ left = mid+1 } if(num<min){ right = mid-1; } console.log(mid) } return total; } console.log(calculate2(6))
二分查找,其实就是双指针,更多关于双指针的,推举阅读 《双指针相关问题(滑动窗口/反转字符串/回文字串)总结》,
转载本站文章《数组二分查找到二分法的感悟笔记》,
请注明出处:https://www.zhoulujun.cn/html/theory/algorithm/SortingAlgorithms/9092.html