属于双指针的一种典型应用。
前提: 无重复元素,(必须)递增。
二分查找对具有指定左索引和右索引的连续序列进行操作。与中间索引对比,改变左/右指针从而缩小一般的查找空间。
时间:O(log n)
—— 算法时间
空间:O(1)
—— 常量空间
练习:
剑指 Offer 53 - II. 0~n-1中缺失的数字
var missingNumber = function(nums) {
let left = 0;
let right = nums.length - 1;
while (left < right) {
let mid = Math.floor((left + right) / 2);
let cur = nums[mid]
if (cur == mid) {
left = mid + 1;
} else {
right = mid
}
}
return left == nums[left] ? nums.length : left // 细节处理
};
剑指 Offer 11. 旋转数组的最小数字(支持重复)
⚠️ 以左边为判断依据, 相等情况下仅移动一位
var minArray = function(numbers) {
let left = 0;
let right = numbers.length - 1;
while (left < right) {
let mid = Math.floor((left + right) / 2);
if (numbers[mid] > numbers[right]) {
left = mid + 1;
} else if (numbers[mid] < numbers[right]) {
right = mid
} else {
right--;
}
}
return numbers[left]
};
练习1:
标准的二分查找模板
- 不与两侧比较
- 仅比较, 不做后处理, 至到底部
伪代码
- 初始条件:left = 0, right = length-1
- 终止:left > right
- 向左查找:right = mid-1
- 向右查找:left = mid+1
704. 二分查找
function search(nums: number[], target: number): number {
if (nums.length == 0) return -1;
if (nums.length == 1) return nums[0] == target ? 0 : -1;
let left: number = 0;
let right: number = nums.length - 1;
while (left <= right) {
let mid: number = Math.floor((left + right) / 2);
let cur = nums[mid];
if (cur == target) {
return mid;
} else if (cur > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
};
744. 寻找比目标字母大的最小字母
case
console.log(nextGreatestLetter( ["c", "f", "j"], 'a'));
console.log(nextGreatestLetter( ["c", "f", "j"], 'j'));
console.log(nextGreatestLetter( ["c", "f", "j"], 'g'));
// c
// c
// j
注意: 非法校验
function nextGreatestLetter(letters: string[], target: string): string {
let n = letters.length;
let x = target.charCodeAt(0);
if (x >= letters[n - 1].charCodeAt(0)) return letters[0];
let left = 0, right = letters.length - 1;
while (left < right) {
let mid = (left + right) >> 1;
if (x < letters[mid].charCodeAt(0)) {
right = mid;
} else {
left = mid + 1;
}
}
return letters[left];
};
268. 丢失的数字
var missingNumber = function(nums) {
nums.sort((a, b) => a - b);
let left = 0;
let right = nums.length-1;
let mid = Math.round((left + right) / 2);
while (nums.includes(mid)) {
if (nums[mid] == mid) {
left = mid+1;
} else {
right = mid-1;
}
mid = Math.round((left + right) / 2);
}
return mid;
};
374. 猜数字大小
var guessNumber = function(n) {
let left = 1;
let right = n;
while (left <= right) {
let middle = Math.floor((left + right) / 2)
let curRes = guess(middle)
if (curRes == 0) {
return middle
} else if (curRes < 0) {
right = middle - 1
} else {
left = middle + 1
}
}
return middle
};
33. 搜索旋转排序数组
function search(nums: number[], target: number): number {
if (nums.length == 0) return -1;
let left: number = 0;
let right: number = nums.length -1;
while (left <= right) {
let mid: number = Math.floor((left + right) / 2);
let midNum: number = nums[mid];
if (midNum == target) {
return mid
} else if (nums[left] <= midNum) {
// 左侧为升序
if (nums[left] <= target && target < midNum) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// 右侧为升序
if (midNum < target && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
};
练习2:(高级方法)
- 需要访问元素的直接右邻居
- 每一步中至少有 2 个元素
- 剩下 1 个元素时,循环 / 递归结束
伪代码:
- 初始条件:left = 0, right = length
- 终止:left == right
- 向左查找:right = mid
- 向右查找:left = mid+1
278. 第一个错误的版本
[1, n]区间二分, left == right, 返回任意值均可。 注意除2 使用 >> 1会超时, 使用>>> 1
var solution = function(isBadVersion: any) {
return function(n: number): number {
let left = 1, right = n;
while (left < right) {
let mid = (left + right) >>> 1;
let valied = isBadVersion(mid);
if (valied) {
right = mid;
} else {
left = mid + 1;
}
}
// return right; // 均可
return left;
};
};
162. 寻找峰值
读题关键: 假设 nums[-1] = nums[n] = -∞
left = right
return left/right
function findPeakElement(nums: number[]): number {
let left = 0, right = nums.length - 1;
while (left < right) {
let mid: number = (left + right) >> 1;
if (nums[mid] <= nums[mid+1]) {
left = mid +1;
} else {
right = mid;
}
}
return left;
};
540. 有序数组中的单一元素
function singleNonDuplicate(nums: number[]): number {
let left = 0, right = nums.length - 1;
while (left < right) {
let mid = (left + right) >> 1;
if ((mid & 1) == 1) --mid;
if (nums[mid] == nums[mid + 1]) {
left = mid + 2;
} else {
right = mid;
}
}
return nums[left];
};
153. 寻找旋转排序数组中的最小值
function findMin(nums: number[]): number {
let left: number = 0;
let right: number = nums.length -1;
while (left < right) {
let mid: number = Math.floor((left + right) / 2);
if (nums[mid] > nums[right]) {
left = mid +1;
} else {
right = mid;
}
}
return nums[right];
};
练习3:(独特形式)
- 需要访问元素的直接左右邻居
- 每个步骤中至少有 3 个元素
- 当剩下 2 个元素时,循环 / 递归结束
伪代码:
- 初始条件:left = 0, right = length-1
- 终止:left + 1 == right
- 向左查找:right = mid
- 向右查找:left = mid
34. 在排序数组中查找元素的第一个和最后一个位置
寻找左侧边界
left = 0
right = nums.length
while (left < right) {
if (mid == target) {
right = mid
} else (mid < target) {
left = mid +1;
} else {
right = mid;
}
}
return left
寻找右侧边界
left = 0
right = nums.length
while (left < right) {
if (mid == target) {
// right = mid
left = mid+1;
} else (mid < target) {
left = mid +1;
} else {
right = mid;
}
}
return left - 1;
关键: 合并左右边界查找在一个函数中
function searchRange(nums: number[], target: number): number[] {
let res: number[] = [-1, -1];
let firstIndex: number = helper(nums, target, true);
if (firstIndex == nums.length || nums[firstIndex] != target) {
return res;
}
res[0] = firstIndex;
res[1] = helper(nums, target, false)-1;
return res;
};
function helper(nums: number[], target: number, isFirst: boolean) {
let left: number = 0;
let right: number = nums.length;
while (left < right) {
let mid: number = Math.floor((left + right) / 2);
let midNum: number = nums[mid];
if (midNum > target) {
right = mid;
} else if (isFirst && midNum == target) {
right = mid;
} else {
left = mid+1;
}
}
return left;
}
658. 找到 K 个最接近的元素
练习4:双数组的二分查找
4. 寻找两个正序数组的中位数
练习5:
50. Pow(x, n)
367. 有效的完全平方数
744. 寻找比目标字母大的最小字母
练习6:
weekly:
5764. 准时到达的列车最小时速
var minSpeedOnTime = function(dist, hour) {
if (dist.length > Math.ceil(hour)) return -1;
let left = 1, right = 10 ** 7;
while (left < right) {
let mid = (left + right) >> 1;
if (arriveOnTime(dist, mid, hour)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
};
function arriveOnTime (dist, speed, hour) {
let res = 0.0;
let n = dist.length;
for (let i = 0; i < n; i++) {
let cost = parseFloat(dist[i]) / speed;
if (i != n - 1) {
cost = Math.ceil(cost);
}
res += cost;
}
return res <= hour;
}
1889. 装包裹的最小浪费空间
function minWastedSpace(packages: number[], boxes: number[][]): number {
const MOD = 10 ** 9 + 7;
packages.sort((a, b) => a - b);
const max_package = packages[packages.length - 1];
const total = packages.reduce((a, c) => a + c, 0);
let res = Infinity;
for (let box of boxes) {
box.sort((a, b) => a - b);
if (max_package > box[box.length - 1]) continue;
let left = 0, sum = 0;
for (let capacity of box) {
let right = searchRight(packages, capacity, left);
sum += (right - left) * capacity;
left = right;
}
res = Math.min(res, sum);
}
return res == Infinity ? -1 : (res - total) % MOD;
};
辅助函数
function searchRight(packages: number[], target: number, left: number): number {
let right = packages.length;
while (left < right) {
let mid = (left + right) >> 1;
if (packages[mid] <= target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
console.log(searchRight([3,5,8,10,11,12], 10, 0));
// 4
mid特殊处理的二分
69. x 的平方根
function mySqrt(x: number): number {
if (x == 0) return 0;
let left = 1, right = x;
while (left < right) {
let mid = left + ((right - left + 1) >> 1);
if ((mid ** 2) <= x) {
left = mid;
} else {
right = mid - 1;
}
}
return left;
};
标记
1760. 袋子里最少数目的球
Minimum Light Radius
常规方法
function solve(nums: Array<number>): number {
const n = nums.length;
if (n <= 3) return 0;
nums.sort((a, b) => a - b);
let left = 0, right = nums[n - 1] - nums[0];
while (left <= right) {
let mid = (left + right) >> 1;
if (isCover(nums, mid)) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left / 2;
}
function isCover(nums: Array<number>, radius: number): boolean {
let left = nums[0], right = left + radius;
for (let i = 0; i < 3; i++) {
let j = searchRight(nums, right);
if (j >= nums.length) return true;
left = nums[j], right = left + radius;
}
return false;
}
function searchRight(nums: number[], target: number, left: number = 0, right: number = nums.length): number {
if (left < 0) throw ('left must be non-negative');
while (left < right) {
let mid = (left + right) >> 1;
if (target < nums[mid]) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
console.log(solve([3, 4, 5, 6]));
// 0.5
其他方法
function solve(nums: Array<number>): number {
if (!nums.length) return 0;
const n = nums.length;
nums.sort((a, b) => a - b);
let left = 0, right = nums[n - 1] - nums[0];
for (let i = 0; i < 40; i++) {
let mid = (left + right) / 2;
if (validate(nums, mid) <= 3) {
right = mid;
} else {
left = mid;
}
}
return right;
}
function validate(nums: Array<number>, radius: number): number {
let ans = 0;
for (let i = 0; i < nums.length; ) {
ans++;
let distance = nums[i] + 2 * radius;
let j = i + 1;
while (j < nums.length && nums[j] <= distance) j++;
i = j;
}
return ans;
}
console.log(solve([3, 4, 5, 6]));
参考: