查找算法详解:二分查找

🔍 查找算法详解:二分查找

二分查找是最高效的查找算法之一,时间复杂度仅为O(log n),但前提是数据必须有序。

一、二分查找概述

1.1 什么是二分查找?

二分查找(Binary Search) ,也称为折半查找,是一种在有序数组中查找特定元素的搜索算法。

核心思想:每次将搜索区间减半,通过比较中间元素与目标值,决定继续在左半部分还是右半部分查找。

1.2 二分查找的优势

算法 时间复杂度 空间复杂度 适用条件
线性查找 O(n) O(1) 无序数组
二分查找 O(log n) O(1) 有序数组
哈希查找 O(1) O(n) 需要额外空间

💡 O(log n) 有多快?

  • 100万个元素,最多只需要20次比较
  • 10亿个元素,最多只需要30次比较

二、二分查找框架

2.1 基本模板

c 复制代码
int binarySearch(int nums[], int n, int target) {
    int left = 0;
    int right = n - 1;  // 注意
    
    while (...) {
        int mid = (left + right) / 2;
        if (nums[mid] == target) {
            // ...
        } else if (nums[mid] < target) {
            left = ...
        } else if (nums[mid] > target) {
            right = ...
        }
    }
    return ...;
}

🎯 分析技巧 :不要出现 else,而是要把所有情况用 else if 写清楚,这样可以展现所有的细节。

2.2 mid 计算技巧

c 复制代码
// 方式1:可能溢出
int mid = (left + right) / 2;

// 方式2:防止溢出(推荐)
int mid = left + (right - left) / 2;

// 方式3:位运算(更高效)
int mid = left + ((right - left) >> 1);

📌 为什么方式2更好?

当 left 和 right 都很大时,left + right 可能溢出。使用 left + (right - left) / 2 可以避免这个问题。

三、基本二分搜索

3.1 寻找一个数

场景:在有序数组中搜索一个数,如果存在,返回其索引,否则返回-1。

c 复制代码
int binarySearch(int nums[], int n, int target) {
    int left = 0;
    int right = n - 1;  // 注意:两端都闭
    
    while (left <= right) {  // 注意:<=
        int mid = left + (right - left) / 2;
        
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            left = mid + 1;  // 注意:mid已经搜索过
        } else if (nums[mid] > target) {
            right = mid - 1;  // 注意:mid已经搜索过
        }
    }
    return -1;
}

3.2 while条件分析

问题1:为什么 while 循环的条件是 <=,而不是 <

因为初始化 right 的赋值是 n - 1,即最后一个元素的索引,而不是 n

两种初始化方式的区别

初始化 含义 搜索区间
right = n - 1 两端都闭 [left, right]
right = n 左闭右开 [left, right)

终止条件分析

复制代码
while (left <= right) 的终止条件是 left = right + 1
搜索区间形式:[right+1, right],例如 [3, 2]
这时搜索区间为空,没有数字既大于3又小于2,while循环正确终止

while (left < right) 的终止条件是 left = right
搜索区间形式:[right, right],例如 [2, 2]
这时搜索区间非空,还有一个数字2,但while循环终止了
这意味着区间 [2, 2] 被遗漏,索引2没有被搜索!

3.3 left/right更新分析

问题2:为什么 left = mid + 1right = mid - 1

因为本算法的搜索区间是两端都闭区间 [left, right],当我们发现索引 mid 不是要找的 target 时,需要确定下一步的搜索区间。

复制代码
如果 nums[mid] < target:
  target 在 [mid+1, right] 中
  所以 left = mid + 1

如果 nums[mid] > target:
  target 在 [left, mid-1] 中
  所以 right = mid - 1

📌 注意mid 已经搜索过,应该从搜索区间中剔除!

3.4 算法的局限性

问题3:此算法有什么缺陷?

比如说给你有序数组 nums = [1, 2, 2, 2, 3]target = 2,此算法返回的索引是 2,没错。

但是如果:

  • 我想得到 target 的左侧边界,即索引 1
  • 或者我想得到 target 的右侧边界,即索引 3

这样的需求,此算法是无法处理的。

四、寻找左侧边界

4.1 算法实现

c 复制代码
int left_bound(int nums[], int n, int target) {
    if (n == 0) return -1;
    
    int left = 0;
    int right = n;  // 注意:左闭右开
    
    while (left < right) {  // 注意:<
        int mid = left + (right - left) / 2;
        
        if (nums[mid] == target) {
            right = mid;  // 收缩右边界
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;  // 注意:不是 mid - 1
        }
    }
    return left;
}

4.2 细节分析

细节1:为什么 while (left < right) 而不是 <=

因为初始化 right = n 而不是 n - 1,因此每次循环的搜索区间是 [left, right) 左闭右开。

while (left < right) 终止的条件是 left = right,此时的搜索区间是 [left, left) 恰巧为空,所以可以正确终止。

细节2:为什么 nums[mid] == target 时,right = mid

我们要找的是左侧边界,当找到 target 时,不要立即返回,而是收缩右边界,继续在左半部分查找。

复制代码
nums = [1, 2, 2, 2, 3]
target = 2

第一次:mid = 2,nums[mid] = 2
找到target,但不返回,right = mid = 2
继续在 [0, 2) 中查找

第二次:mid = 1,nums[mid] = 2
找到target,right = mid = 1
继续在 [0, 1) 中查找

第三次:left = right = 1,循环结束
返回 left = 1,即左侧边界

细节3:为什么 nums[mid] > target 时,right = mid 而不是 mid - 1

因为搜索区间是 [left, right) 左闭右开,mid - 1 可能会遗漏元素。

4.3 返回值分析

返回 left 的含义

  • 如果 target 存在:返回第一个等于 target 的索引
  • 如果 target 不存在:返回第一个大于 target 的索引

改进版本(处理target不存在的情况)

c 复制代码
int left_bound(int nums[], int n, int target) {
    if (n == 0) return -1;
    
    int left = 0;
    int right = n;
    
    while (left < right) {
        int mid = left + (right - left) / 2;
        
        if (nums[mid] == target) {
            right = mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;
        }
    }
    
    // 检查越界和是否找到
    if (left >= n || nums[left] != target) {
        return -1;
    }
    return left;
}

五、寻找右侧边界

5.1 算法实现

c 复制代码
int right_bound(int nums[], int n, int target) {
    if (n == 0) return -1;
    
    int left = 0;
    int right = n;  // 左闭右开
    
    while (left < right) {
        int mid = left + (right - left) / 2;
        
        if (nums[mid] == target) {
            left = mid + 1;  // 收缩左边界
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;
        }
    }
    
    // 检查越界和是否找到
    if (right - 1 < 0 || nums[right - 1] != target) {
        return -1;
    }
    return right - 1;  // 注意:返回 right - 1
}

5.2 细节分析

为什么返回 right - 1 而不是 right

因为当找到 target 时,我们执行 left = mid + 1,这意味着最终 left 会多走一步。所以返回时要减1。

复制代码
nums = [1, 2, 2, 2, 3]
target = 2

最终 left = right = 4
返回 right - 1 = 3,即右侧边界

六、实际应用场景

6.1 二分查找的应用

应用场景 说明
查找元素 基本二分查找
查找边界 左边界、右边界
查找插入位置 左边界变体
查找最接近的元素 变体应用
数值计算 求平方根、方程求根
旋转数组查找 变体应用

6.2 经典题目示例

例1:在排序数组中查找元素的第一个和最后一个位置
c 复制代码
// 返回 [left_bound, right_bound]
int* searchRange(int nums[], int n, int target, int* returnSize) {
    int* result = (int*)malloc(sizeof(int) * 2);
    *returnSize = 2;
    
    result[0] = left_bound(nums, n, target);
    result[1] = right_bound(nums, n, target);
    
    return result;
}
例2:x的平方根
c 复制代码
int mySqrt(int x) {
    if (x <= 1) return x;
    
    int left = 1, right = x;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (mid == x / mid) {
            return mid;
        } else if (mid < x / mid) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return right;
}

6.3 旋转数组查找

c 复制代码
int searchRotated(int nums[], int n, int target) {
    int left = 0, right = n - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (nums[mid] == target) {
            return mid;
        }
        
        // 左半部分有序
        if (nums[left] <= nums[mid]) {
            if (target >= nums[left] && target < nums[mid]) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        // 右半部分有序
        else {
            if (target > nums[mid] && target <= nums[right]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
    }
    return -1;
}

七、常见问题与技巧

7.1 二分查找的常见错误

错误类型 错误示例 正确做法
死循环 left = midright = mid left = mid + 1right = mid - 1
遗漏元素 while (left < right) 配合闭区间 根据区间选择正确的条件
越界访问 不检查返回值 检查 leftright 是否越界
溢出 (left + right) / 2 left + (right - left) / 2

7.2 二分查找的统一模板

c 复制代码
// 统一模板(闭区间)
int binarySearch(int nums[], int n, int target) {
    int left = 0, right = n - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (nums[mid] == target) {
            // 找到了,可以返回或继续收缩
            return mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    
    // 未找到,left 是插入位置
    return -1;  // 或 return left
}

7.3 调试技巧

打印中间结果

c 复制代码
while (left <= right) {
    int mid = left + (right - left) / 2;
    printf("left=%d, mid=%d, right=%d, nums[mid]=%d\n", 
           left, mid, right, nums[mid]);
    // ...
}

八、总结

二分查找类型 搜索区间 返回值 适用场景
基本二分 [left, right] 索引或-1 查找单个元素
左边界 [left, right) 左边界索引 查找第一个等于target的
右边界 [left, right) 右边界索引 查找最后一个等于target的

二分查找的关键点

  1. 明确搜索区间:闭区间还是左闭右开
  2. 正确更新边界mid 已搜索过,需要剔除
  3. 防止溢出 :使用 left + (right - left) / 2
  4. 处理边界情况:检查返回值是否越界

相关推荐
君义_noip4 小时前
信息学奥赛一本通 4164:【GESP2512七级】学习小组 | 洛谷 P14922 [GESP202512 七级] 学习小组
学习·算法·动态规划·gesp·信息学奥赛
MicroTech20254 小时前
微算法科技(NASDAQ :MLGO)面向区块链的系统的高效反量子晶格盲签名技术
科技·算法·区块链
Peregrine94 小时前
数据结构 -> 顺序表
数据结构
yuan199974 小时前
OpenCV ViBe 运动检测算法实现
人工智能·opencv·算法
动恰客流管家5 小时前
动恰3DV3客流统计方案:赋能智慧公厕精细化运营
数据结构·人工智能·3d
人工智能培训5 小时前
如何将高层任务分解为可执行的动作序列?
大数据·人工智能·算法·机器学习·知识图谱
罗湖老棍子5 小时前
Power Strings(信息学奥赛一本通- P1457)
算法·字符串·哈希
MIngYaaa5205 小时前
The 2025 Sichuan Provincial Collegiate Programming Contest 复盘
算法
网域小星球5 小时前
C 语言从 0 入门(二十一)|typedef 类型重定义:简化复杂类型,代码更清爽
c语言·算法·类型重定义·结构体简化·函数指针简化