二分查找针对的是一个有序的数据集合。
二分查找非常高效,时间复杂度是 O( logn )。
假设数据大小是 n,每次查找后数据都会缩小为原来的一半,也就是会除以 2。最坏情况下,直到查找区间被缩小为空,才停止。
二分查找模板:
function search(nums, target) {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
let mid = left + ((right - left) >> 1);
if (nums[mid] === target) { // 刚好是二分中点
return mid;
} else { // 否则缩小查找范围
if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
}
return -1;
}
注意以下三点:
-
求中点的写法应是: mid = left + ((right - left) >> 1)
-
mid = ( left + right )/ 2 写法的弊端:溢出,如果 left 和 right 比较大的话,两者之和就有可能会溢出。
-
用位运算代替除法运算,性能更好。
-
-
循环退出的条件,是 left <= right,而不是 left < right
-
left 和 right 的更新,left = mid + 1, right = mid - 1
- 注意这里的 +1 和 -1,如果直接写成 left = mid 或者 high = mid,就可能会发生死循环。比如,当 left = 3,right = 3 时,如果 nums[3] 不等于 target,就会导致一直循环不退出。
纯粹的二分查找很简单,相对较难的是二分查找的变形问题:
如果数组中出现多个目标值,要求找出第一个或者最后一个。
如有序数组[1, 2, 3, 3, 3, 3, 7, 8],请找出第一个等于 3 的数组元素下标。
-
求第一个的模板:
找到 target 时检查边界情况,是否为第一个元素?前一个元素是否为 target?
对于符合边界判断的结果则返回,否则继续缩小搜索范围。
while (left <= right) { let mid = left + ((right - left) >> 1); if (nums[mid] === target) { /* 找到 target 时,需要检查边界情况 */ if (mid === 0 || nums[mid - 1] !== target) return mid; else right = mid - 1; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid - 1; } } -
求最后一个的模板:
找到 target 时检查边界情况,是否为最后一个元素?后一个元素是否为 target?
对于符合边界判断的结果则返回,否则继续缩小搜索范围。
while (left <= right) { let mid = left + ((right - left) >> 1); if (nums[mid] === target) { /* 找到 target 时,需要检查边界情况 */ if (mid === nums.length - 1 || nums[mid + 1] !== target) return mid; else left = mid + 1; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid - 1; } }
两道 leetcode 题目练习巩固:
-
const search = function(nums, target) { let left = 0; let right = nums.length - 1; while (left <= right) { let mid = left + ((right - left) >> 1); if (nums[middle] === target) return middle; // 否则缩小查找范围 if (nums[left] <= nums[middle]) { // 左边有序 if (nums[left] <= target && nums[middle] >= target) { // 在左 right = middle; } else { // 在右 left = middle + 1; } } else { // 右边有序 if (nums[middle + 1] <= target && nums[right] >= target) { left = middle + 1; } else { right = middle; } } } return -1; }; -
const searchRange = function(nums, target) { return [searchFrist(nums, target), searchLast(nums, target)]; }; const searchFrist = function(nums, target) { let left = 0; let right = nums.length - 1; while (left <= right) { let mid = left + ((right - left) >> 1); if (nums[mid] === target) { // 找到 target 时,需要检查边界情况 if (mid === 0 || nums[mid - 1] !== target) return mid; else right = mid - 1; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return -1; } const searchLast = function(nums, target) { let left = 0; let right = nums.length - 1; while (left <= right) { let mid = left + ((right - left) >> 1); if (nums[mid] === target) { if (mid === nums.length - 1 || nums[mid + 1] !== target) return mid; else left = mid + 1; } else if (nums[mid] < target) { left = mid + 1; } else { right = mid - 1; } } return -1; }