二分查找算法题的技巧总结

0 阅读5分钟

一、解题思维总结

1. 何时使用二分查找?

场景解法
有序数组中查找目标值基础二分查找
有序矩阵中查找目标值先定位行再二分查找
寻找满足条件的最小值/最大值二分查找边界
旋转排序数组中的查找先找旋转点再二分查找
基于时间戳的键值存储二分查找时间戳

2. 复杂度分析

  • 时间复杂度:O(log n)
  • 空间复杂度:O(1)(递归实现可能为 O(log n))

3. 常用技巧

  1. 初始化指针let left = 0, right = nums.length - 1;
  2. 计算中间值const mid = left + Math.floor((right - left) / 2);(避免溢出)
  3. 循环条件left <= right(查找存在性)或 left < right(查找最值)
  4. 更新指针:根据比较结果调整 left 或 right

二、核心技术

技巧一:基础二分查找

适用场景:有序数组中查找目标值

核心要点

  • 有序数组是前提
  • 循环条件left <= right
  • 找到目标值立即返回

典型例题二分查找

var search = function(nums, target) {
    let left = 0;
    let right = nums.length - 1;
    while (left <= right) {
        let mid = left + Math.floor((right - left) / 2);
        if (nums[mid] === target) {
            return mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
};

技巧二:二维矩阵中的二分查找

适用场景:每行有序且每行首元素大于前一行尾元素的矩阵

核心要点

  • 先定位可能包含目标的行
  • 再在该行内进行二分查找

典型例题搜索二维矩阵

var searchMatrix = function(matrix, target) {
    let rowIndex = 0, rowStart = 0, rowEnd = matrix.length - 1;
    while (rowStart <= rowEnd) {
        rowIndex = rowStart + Math.floor((rowEnd - rowStart) / 2);
        if (matrix[rowIndex][0] <= target &&
            matrix[rowIndex][matrix[rowIndex].length - 1] >= target) {
            break;
        }
        if (matrix[rowIndex][0] < target) {
            rowStart = rowIndex + 1;
            continue;
        }
        rowEnd = rowIndex - 1;
    }
    if (rowStart > rowEnd) {
        return false;
    }
    let colLeft = 0, colRight = matrix[rowIndex].length - 1;
    while (colLeft <= colRight) {
        const middle = colLeft + Math.floor((colRight - colLeft) / 2);
        if (matrix[rowIndex][middle] === target) {
            return true;
        }
        if (matrix[rowIndex][middle] < target) {
            colLeft = middle + 1;
            continue;
        }
        colRight = middle - 1;
    }
    return false;
};

技巧三:寻找满足条件的最小值

适用场景:需要找到满足条件的最小速度/值

核心要点

  • 确定左右边界(如最小速度为 1,最大速度为数组最大值)
  • 循环条件left < right
  • 找到满足条件的mid后,调整right = mid以寻找更小值

典型例题爱吃香蕉的珂珂

var minEatingSpeed = function (piles, h) {
    let low = 1;
    let high = Math.max(...piles);

    while (low < high) {
        let mid = Math.floor((low + high) / 2);
        let hoursNeeded = 0;

        for (let pile of piles) {
            hoursNeeded += Math.ceil(pile / mid);
        }

        if (hoursNeeded <= h) {
            high = mid;
        } else {
            low = mid + 1;
        }
    }

    return low;
};

技巧四:旋转排序数组中的最小值

适用场景:旋转后的有序数组(元素互不相同)

核心要点

  • 比较中间元素和右边界元素
  • nums[mid] >= nums[right],则最小值在右半部分
  • 否则在左半部分(包括 mid)

典型例题寻找旋转排序数组中的最小值

var findMin = function(nums) {
    let left = 0, right = nums.length - 1;
    while (left < right) {
        const middle = left + Math.floor((right - left) / 2);
        if (nums[middle] >= nums[right]) {
            left = middle + 1;
            continue;
        }
        right = middle;
    }
    return nums[left];
};

技巧五:旋转排序数组中的目标查找

适用场景:旋转后的有序数组中查找目标值

核心要点

  • 先找到旋转点(最小值位置)
  • 再根据目标值与首元素的比较确定查找范围
  • 在对应范围内进行二分查找

典型例题搜索旋转排序数组

var search = function(nums, target) {
    const pivot = findPivot(nums);
    if (pivot === 0) {
        return binarySearch(nums, 0, nums.length - 1, target);
    }
    if (target >= nums[0]) {
        return binarySearch(nums, 0, pivot, target);
    } else {
        return binarySearch(nums, pivot, nums.length - 1, target);
    }
};

// 查找支点
function findPivot(arr) {
    let left = 0, right = arr.length - 1;
    while (left < right) {
        const middle = left + Math.floor((right - left) / 2);
        if (arr[middle] > arr[right]) {
            left = middle + 1;
            continue;
        }
        right = middle;
    }
    return left;
}

// 二分查找
function binarySearch(arr, left, right, target) {
    let index = -1;
    while (left <= right) {
        const middle = left + Math.floor((right - left) / 2);
        if (arr[middle] === target) {
            index = middle;
            break;
        }
        if (arr[middle] > target) {
            right = middle - 1;
            continue;
        }
        left = middle + 1;
    }
    return index;
}

技巧六:基于时间的键值存储

适用场景:需要根据时间戳检索值

核心要点

  • 使用 Map 存储键对应的时间戳-值对列表
  • set 操作追加到列表(时间戳严格递增)
  • get 操作使用二分查找找到最大的不大于目标时间戳的值

典型例题基于时间的键值存储

var TimeMap = function() {
    this.map = new Map();
};

TimeMap.prototype.set = function(key, value, timestamp) {
    if (!this.map.has(key)) {
        this.map.set(key, []);
    }
    this.map.get(key).push([timestamp, value]);
};

TimeMap.prototype.get = function(key, timestamp) {
    const arr = this.map.get(key);
    if (!arr) {
        return '';
    }
    let left = 0, right = arr.length - 1;
    while (left <= right) {
        const middle = left + Math.floor((right - left) / 2);
        const [t, v] = arr[middle];
        if (t === timestamp) {
            return v;
        }
        if (t < timestamp) {
            left = middle + 1;
            continue;
        }
        right = middle - 1;
    }
    return arr[right] ? arr[right][1] : '';
};

技巧七:两个正序数组的中位数

适用场景:找到两个正序数组的中位数(要求 O(log(m+n)) 复杂度)

核心要点

  • 转化为寻找第 k 小的元素
  • 递归或迭代地缩小查找范围
  • 处理边界情况

典型例题找两个正序数组的中位数

// 基础解法(O(m+n)复杂度)
var findMedianSortedArrays = function(nums1, nums2) {
    const arr = [];
    let i = 0, j = 0;
    while (i < nums1.length || j < nums2.length) {
        if (i < nums1.length && j < nums2.length) {
            if (nums1[i] <= nums2[j]) {
                arr.push(nums1[i]);
                ++i;
            } else {
                arr.push(nums2[j]);
                ++j;
            }
            continue;
        }
        if (i < nums1.length) {
            arr.push(nums1[i]);
            ++i;
        } else if (j < nums2.length) {
            arr.push(nums2[j]);
            ++j;
        }
    }
    let index = arr.length / 2;
    if (parseInt(index + '') === index) {
        return (arr[index] + arr[index - 1]) / 2;
    }
    index = Math.floor(index);
    return arr[index];
};

三、易错点提醒

  1. 计算 mid 时避免溢出:使用left + Math.floor((right - left)/2)而非(left+right)/2
  2. 循环条件的选择:查找存在性用left <= right,查找最值用left < right
  3. 更新指针时注意 +1 或 -1:避免死循环
  4. 旋转数组中比较的是中间元素和右边界元素:而非左边界元素
  5. 基于时间的键值存储中 set 操作的效率:直接 push 比创建新数组更高效

四、学习心得

二分查找的优势

  1. 高效:时间复杂度 O(log n),远优于线性查找
  2. 灵活:可应用于多种类型的问题
  3. 简洁:代码实现相对简单

解题思维模式

  1. 确定边界:明确查找范围的左右边界
  2. 定义条件:确定如何根据中间值调整边界
  3. 循环终止:明确循环结束的条件和返回结果