【算法】【学习笔记】二分查找

二分查找

二分查找(Binary Search),又称折半查找,是一种在有序数组中查找特定元素的高效算法。它通过不断将搜索范围缩小一半,实现 \(O(\log N)\) 的时间复杂度。

核心前提:一定要是单调的数据,才可以使用。

基本思路

以递增数据作为例子

使用双指针的方法(左指针left,右指针right),一个指首,一个指尾,来维护查询区间:

  1. 获取中间索引(mid = left + ((right - left) >> 1),这个方法避免leftright直接相加可能导致的溢出);
  2. 左闭右闭[left, right]时:( left = 0, right = n-1 )
    1. 比较中间索引的值和目标值target
      • 相等:则已经找到目标索引,跳出循环;
      • 小于目标值:说明中间值偏小,目标值在右区间,将左指针left往右收缩为mid+1
      • 大于目标值:说明中间值偏大,目标值在左区间,将右指针right往左收缩为mid-1
    2. 若左指针left > right,说明没有找到目标,返回-1
  3. 左闭右开[left, right)时:( left = 0, right = n )
    1. 比较中间索引的值和目标值target
      • 相等:则已经找到目标索引,跳出循环;
      • 小于目标值:说明中间值偏小,目标值在右区间,将左指针left往右收缩为mid+1
      • 大于目标值:说明中间值偏大,目标值在左区间,将右指针right往左收缩为mid
    2. 若左指针left >= right,说明没有找到目标,返回-1
  4. 左开右闭(left, right]时:( left = -1, right = n-1 )
    1. 比较中间索引的值和目标值target
      • 相等:则已经找到目标索引,跳出循环;
      • 小于目标值:说明中间值偏小,目标值在右区间,将左指针left往右收缩为mid
      • 大于目标值:说明中间值偏大,目标值在左区间,将右指针right往左收缩为mid-1
    2. 若左指针left >= right,说明没有找到目标,返回-1

边界处理要点

区间定义:左闭右闭、左闭右开、左开右闭会影响 mid 的更新方式。

溢出预防:使用 mid = left + ((right - left) >> 1)而非 mid = (right + left) >> 1)

循环终止条件left > right 说明无法找到。

代码示例

下为使用TypeScript写的左右闭合的区间,并且单调递减的例子。

function binarySearch(nums:number[], target:number){
	// nums是单调递减的
	const n = nums.length;
	let left = 0, right = n-1, res = -1;
	while(left <= right){
		let mid = left + ((right - left) >> 1);
		
		if(nums[mid] < target){
			// 太小了,说明在左边大的数中
			right = mid-1;
		}else if(nums[mid] > target){
			// 太大了,说明在右边小的数中
			left = mid+1;
		}else{
			res = mid;
			break;
		}
	}
	return res;
}

优缺点

  • 优点:可以快速处理大量数据
  • 缺点:
    • 一定要有序
    • 数据结构需要支持 \(O(1)\) 随机访问(如数组)。
      如果是单向链表,即使有序,也无法快速通过索引拿到中间值 mid,时间复杂度会退化成 \(O(N)\)

题目

1. 山脉数组的峰顶索引

【点击跳转】山脉数组的峰顶索引——力扣

解题思路

可以观察得出,山脉数组本质上是一个左侧严格递增、右侧严格递减的数组,满足局部单调性,因此可以使用二分查找定位最高点。

1. 比较逻辑简化
不需要同时比较 mid-1midmid+1 三个位置,只需比较相邻的 arr[mid]arr[mid + 1]

  • arr[mid] < arr[mid + 1]:处于上坡段,峰顶必在 mid 右侧 \(\rightarrow\) l = mid + 1
  • arr[mid] > arr[mid + 1]:处于下坡段或峰顶,峰顶在 mid 或其左侧 \(\rightarrow\) r = mid

2. 边界安全性证明

  • 防越界:取 mid 时向下取整,在 l < r 的循环条件下,mid 最大只能取到 \(N - 2\),因此 mid + 1 最高为 \(N - 1\),访问 arr[mid + 1] 绝对安全。
  • 防死循环l = mid + 1 确保左边界严格右移,r = mid 确保右边界缩小,两者配合向下取整的 mid 绝对不会死循环。

实现

public class Solution {
    public int PeakIndexInMountainArray(int[] arr) {
        int l = 0, r = arr.Length - 1;

        while (l < r) {
            int mid = l + ((r - l) >> 1);

            // 比较 mid 和 mid + 1
            if (arr[mid] < arr[mid + 1]) {
                l = mid + 1; // 上坡:峰顶在右侧
            } else {
                r = mid;     // 下坡或峰顶:峰顶在左侧(包含 mid)
            }
        }

        // 循环结束时 l == r,正好停在峰顶索引上
        return l;
    }
}
function peakIndexInMountainArray(arr: number[]): number {
    let l = 0, r = arr.length-1;
    while(l < r){
        let mid = l + ((r - l) >> 1);
        if(arr[mid] < arr[mid+1]){
            l = mid+1;
        }else{
            r = mid;
        }
    }
    return r;
};
class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        int l = 0, r = arr.size()-1;
        while(l < r){
            int mid = l + ((r - l) >> 1);
            if(arr[mid] < arr[mid+1]){
                l = mid+1;
            }else{
                r = mid;
            }
        }
        return r;
    }
};

复杂度分析

  • 时间复杂度\(O(\log N)\)
  • 空间复杂度\(O(1)\)

2. 二分查找

【点击跳转】二分查找——力扣

解题思路

双分支二分查找模板(排除法)

  1. 向下取整取 mid ,判断 nums[mid] < target
    • 若成立:说明 mid 及其左侧元素一定不是目标,目标在右侧 \(\rightarrow\) l = mid + 1
    • 若不成立:说明 mid 可能是目标,或者目标在 mid 左侧 \(\rightarrow\) r = mid
  2. 防死循环与边界保证mid 向下取整搭配 l = mid + 1r = mid,区间严格收缩,绝对不会死循环。
  3. 退出循环时 l == r,只需在最后校验 nums[r] == target 即可判定是否存在。

在这个题目还可以使用三分支,提前跳出循环。

实现

function search(nums: number[], target: number): number {
    const  n = nums.length;
    let l = 0, r = n-1;
    while(l < r){
        const mid = l + ((r-l) >> 1);
        if(nums[mid] < target){
            l = mid+1;
        }else{
            r = mid;
        }
    }
    return nums[r] === target ? r : -1;
};
public class Solution {
    public int Search(int[] nums, int target) {
        int n = nums.Length;
        int l = 0, r = n-1;
        while(l < r){
            int mid = l + ((r - l) >> 1);
            if(nums[mid] < target){
                l = mid+1;
            }else{
                r = mid;
            }
        }
        return nums[r] == target ? r : -1;
    }
}
// 三分支法
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n = nums.size();
        int l = 0, r = n-1;
        while(l <= r){
            int mid = l + ((r - l) >> 1);
            if(nums[mid] < target){
                l = mid+1;
            }else if(nums[mid] == target){
                return mid;
            }else{
                r = mid-1;
            }
        }
        return -1;
    }
};

复杂度分析

  • 时间复杂度\(O(\log N)\)
  • 空间复杂度\(O(1)\)

3. 平方数之和

【点击跳转】平方数之和——力扣

解题思路

我们可以知道,取值范围在 \([0, \sqrt{target}]\)

  1. 双指针法

这个简单,只要左右指针寻找就好,左指针初始值是 \(0\) ,右指针初始值是 \(\lfloor\sqrt{c}\rfloor\)

  • \(l^2 + r^2 \lt c\) 时,说明太小了, \(l\) 往右移;
  • \(l^2 + r^2 \gt c\) 时,说明太大了, \(r\) 往左移;
  • \(l^2 + r^2 = c\) 时,说明找到目标;
  1. 枚举a+二分法

毕竟这是二分中的题,还是用二分法吧。

由取值范围可知,对于每一个 \(a\),目标值就是 \(target = c - a^2\)

问题就变成了:在 \([0, \sqrt{target}]\) 的范围内二分查找,看是否存在一个整数 \(b\),使得 \(b^2 == target\)

实现

public class Solution {
    public bool JudgeSquareSum(int c) {
        // 两个未知数欸,怎么办呢
        // pow(x, 2) + pow(y, 2) = c
        // 这咋搞啊
        int big = Sqrt(c);
        for(int a = 0; a <= big; a++){
            long target = c - a * a;

            if(BinarySearch(0, target, target)){
                return true;
            }
        }
        return false;
    }

    private bool BinarySearch(long l, long r, long target){
        while(l < r){
            long mid = l + ((r - l) >> 1);
            if(mid*mid < target){
                l = mid+1;
            }else{
                r = mid;
            }
        }
        return l*l == target ? true : false;
    }

    private int Sqrt(int x){
        // 二分法
        if(x < 2) return x;
        int l = 1, r = x / 2;
        while(l < r){
            int mid = l + ((r - l + 1) >> 1);
            if(mid <= x / mid){
                // 说明mid 小了或者就是答案
                l = mid;
            }else{
                r = mid-1;
            }
        }
        return l;
    }
}
public class Solution {
    public bool JudgeSquareSum(int c) {
        // 两个未知数欸,怎么办呢
        // pow(x, 2) + pow(y, 2) = c
        // 这咋搞啊
        long l = 0, r = (long)Math.Sqrt(c);
        while(l <= r){
            long cur = l * l + r * r;
            if(cur < c){
                l++;
            }else if(cur > c){
                r--;
            }else{
                return true;
            }
        }
        return false;
    }
}
function judgeSquareSum(c: number): boolean {
    let l = 0, r = Sqrt(c);
    while(l <= r){
        const sum = l*l+r*r;
        if(sum < c){
            l++;
        }else if(sum > c){
            r--;
        }else{
            return true;
        }
    }
    return false;
};

function Sqrt(num: number):number{
    if(num < 2) return num;

    let l = 1, r = num >> 1;
    while(l < r){
        const mid = l + ((r - l + 1) >> 1);
        if(mid <= num / mid){
            l = mid;
        }else{
            r = mid-1;
        }
    }
    return r;
}
class Solution {
public:
    bool judgeSquareSum(int c) {
        long l = 0, r = sqrt(c);
        while(l <= r){
            long sum = l*l+r*r;
            if(sum == c){
                return true;
            }else if(sum < c){
                l++;
            }else{
                r--;
            }
        }
        return false;
    }
};

复杂度分析

  • 时间复杂度
    • 双指针:\(O(\sqrt{c})\),双指针最多走 \(\sqrt{c}\) 步。
    • 二分法:\(O(\sqrt{c} \log c)\)
  • 空间复杂度
    • 双指针:\(O(1)\)
    • 二分法:\(O(1)\)

拓展

原来开方也是可以用二分法来解出噢。

int Sqrt(int x){
    if(x < 2) return x;
    int l = 1, r = x / 2;
    while(l < r){
        int mid = l + ((r - l + 1) >> 1);
        if(mid <= x / mid){
            // 说明mid 小了或者就是答案
            l = mid;
        }else{
            r = mid-1;
        }
    }
    return l;
}

4. 搜索旋转排序数组

【点击跳转】搜索旋转排序数组——力扣

解题思路

旋转的数组是部分有序的,可以使用二分查找。

一开始我在想一边使用二分查找找原数组结尾,一边找target,但这个思路不太对。

可以假设仅一边有序,

  • 那么当左侧有序时,看target是否在左侧(判断方式根据递增特性),假如是,右侧进行收缩范围,否则左侧进行收缩范围;
  • 当右侧有序时,看target是否在右侧(判断方式根据递增特性),假如是,左侧进行收缩范围,否则右侧进行收缩范围。

实现

public class Solution {
    public int Search(int[] nums, int target) {
        int n = nums.Length;
        int l = 0, r = n-1;
        while(l <= r){
            int mid = l + ((r - l) >> 1);
            if(nums[mid] == target) return mid;

            // 判断左半是否有序
            if(nums[l] <= nums[mid]){
                if(nums[l] <= target && target < nums[mid]){
                    // 如果target在左侧有序范围(递增)
                    // 去左侧
                    r = mid-1;
                }else{
                    // 去右侧
                    l = mid+1;
                }
            }else{
                // 否则右侧有序
                if(nums[r] >= target && target > nums[mid]){
                    // 如果target在右侧有序范围
                    // 去右侧
                    l = mid+1;
                }else{
                    // 去左侧
                    r = mid-1;
                }
            }
        }
        return -1;
    }
}
function search(nums: number[], target: number): number {
    const n = nums.length;
    let l = 0, r = n-1;
    while(l <= r){
        const mid = l + ((r - l) >> 1);
        if(nums[mid] == target) return mid;

        if(nums[l] <= nums[mid]){
            if(nums[l] <= target && target < nums[mid]){
                r = mid-1;
            }else{
                l = mid+1;
            }
        }else{
            if(nums[r] >= target && target > nums[mid]){
                l = mid+1;
            }else{
                r = mid-1;
            }
        }
    }
    return -1;
};
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n = (int)nums.size();
        if (!n) {
            return -1;
        }
        if (n == 1) {
            return nums[0] == target ? 0 : -1;
        }
        int l = 0, r = n - 1;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] == target) return mid;
            if (nums[l] <= nums[mid]) {
                //如果最左小于中间,等于在有序空间
                if (nums[l] <= target && target < nums[mid]) {
                    //假如最左小于目标,且中间也大于目标,说明要缩小右侧,在最左到中间的区间
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            } else {
                //在无序空间(左边部分大于右边部分)
                if (nums[r] >= target && target > nums[mid]) {
                    //假如最右侧大于目标,中间也小于目标,说明就在右侧升序区间
                    l = mid + 1;
                } else {
                    r = mid - 1;
                }
            }
        }
        return -1;

    }
};

复杂度分析

  • 时间复杂度\(O(\log N)\)
  • 空间复杂度\(O(1)\)

拓展

  • 查找边界:查找第一个/最后一个大于等于 target 的元素位置(左/右边界)——此时要在每次满足条件时时刷新res=mid

  • 二分答案:在单调的解空间中猜答案(如:求满足条件的最小值/最大值)。


小知识:高中数学就学习了二分查找的思路,笔者完全没印象!
image


注:本文为个人学习与刷题笔记,部分文本结构与排版格式由 AI 辅助整理。

posted @ 2026-07-21 21:31  SEHOD  阅读(21)  评论(0)    收藏  举报