二分查找学习笔记

一、核心概念

二分查找是一种高效的查找算法,核心思想是分治 + 区间缩减:将有序数据的查找区间不断折半,每次舍弃不符合条件的一半,仅在剩余区间继续查找,时间复杂度为 O (log n),远优于暴力遍历的 O (n),尤其适用于大数据量(如 n>10⁸)场景。

适用前提 :查找对象必须是有序数列(升序或降序),无序数据无法通过二分法缩小查找范围。

二、基础实现:数组中查找目标值的起止位置

(一)核心工具:双指针 + 中间指针

  • 左指针(l):指向查找区间左边界
  • 右指针(r):指向查找区间右边界
  • 中间指针(mid):通过计算划分区间,有两种核心写法:
    1. mid = (l + r) / 2:偏向左侧的中间位置(偶数长度数组时取左半部分末尾)
    2. mid = (l + r + 1) / 2:偏向右侧的中间位置(偶数长度数组时取右半部分开头)

(二)查找起始位置(第一个≥目标值的元素)

1. 实现逻辑
  • 区间划分:左半区(<target)、右半区(≥target)
  • 指针移动规则:
    • 若 a mid ≥ target:目标在左半区,r = mid(保留 mid 所在的右半区左边界)
    • 若 a mid < target:目标在右半区,l = mid + 1(舍弃 mid,直接跳转至下一位)
  • 关键细节:循环条件为 l < r(避免 l=r 时进入死循环),循环结束后需验证结果是否为目标值。
2. 示例代码(C 语言)
复制代码
#include <stdio.h>

// 查找第一个大于等于target的元素下标,不存在则返回-1
int findFirstPosition(int nums[], int numsSize, int target) {
    int l = 0, r = numsSize - 1;
    // 循环条件:左指针小于右指针
    while (l < r) {
        // 偏向左侧的mid写法,适配起始位置查找
        int mid = (l + r) / 2;
        if (nums[mid] >= target) {
            // 目标在左半区,收缩右边界
            r = mid;
        } else {
            // 目标在右半区,收缩左边界
            l = mid + 1;
        }
    }
    // 循环结束后l==r,验证是否找到目标值
    if (nums[l] == target) {
        return l;
    } else {
        return -1;
    }
}

int main() {
    // 测试用例:有序数组
    int nums[] = {1, 3, 5, 5, 5, 7, 9};
    int numsSize = sizeof(nums) / sizeof(nums[0]);
    int target = 5;
    int pos = findFirstPosition(nums, numsSize, target);
    if (pos != -1) {
        printf("目标值%d的第一个位置:%d\n", target, pos); // 输出:2
    } else {
        printf("未找到目标值\n");
    }
    return 0;
}

(三)查找结束位置(最后一个≤目标值的元素)

1. 实现逻辑
  • 区间划分:左半区(≤target)、右半区(>target)
  • 指针移动规则:
    • 若 a mid > target:目标在左半区,r = mid - 1(舍弃 mid,直接跳转至上一位)
    • 若 a mid ≤ target:目标在右半区,l = mid(保留 mid 所在的左半区右边界)
  • 关键细节:mid 必须用 (l + r + 1)/2,否则会陷入死循环。
2. 示例代码
复制代码
#include <stdio.h>

// 查找最后一个小于等于target的元素下标,不存在则返回-1
int findLastPosition(int nums[], int numsSize, int target) {
    int l = 0, r = numsSize - 1;
    while (l < r) {
        // 偏向右侧的mid写法,适配结束位置查找
        int mid = (l + r + 1) / 2;
        if (nums[mid] > target) {
            // 目标在左半区,收缩右边界
            r = mid - 1;
        } else {
            // 目标在右半区,收缩左边界
            l = mid;
        }
    }
    if (nums[l] == target) {
        return l;
    } else {
        return -1;
    }
}

int main() {
    int nums[] = {1, 3, 5, 5, 5, 7, 9};
    int numsSize = sizeof(nums) / sizeof(nums[0]);
    int target = 5;
    int pos = findLastPosition(nums, numsSize, target);
    if (pos != -1) {
        printf("目标值%d的最后一个位置:%d\n", target, pos); // 输出:4
    } else {
        printf("未找到目标值\n");
    }
    return 0;
}

三、C++ 内置二分函数

C++ STL 提供了lower_boundupper_bound函数,可直接实现二分查找,无需手动写循环,大幅提升编码效率。

(一)核心函数说明

函数 功能(升序数组) 参数格式 返回值
lower_bound 查找第一个≥target 的元素 (数组首地址,数组尾地址 + 1, target) 对应元素的迭代器
upper_bound 查找第一个 > target 的元素 (数组首地址,数组尾地址 + 1, target) 对应元素的迭代器

(二)升序数组示例代码

复制代码
#include <iostream>
#include <algorithm> // 必须包含头文件
#include <vector>
using namespace std;

int main() {
    vector<int> nums = {1, 3, 5, 5, 5, 7, 9};
    int target = 5;

    // 1. lower_bound:查找第一个≥5的元素
    // auto需C++11及以上版本,等价于vector<int>::iterator
    auto it1 = lower_bound(nums.begin(), nums.end(), target);
    if (it1 != nums.end()) {
        cout << "第一个≥" << target << "的元素:" << *it1 
             << ",下标:" << it1 - nums.begin() << endl; // 输出:5,下标2
    }

    // 2. upper_bound:查找第一个>5的元素
    auto it2 = upper_bound(nums.begin(), nums.end(), target);
    if (it2 != nums.end()) {
        cout << "第一个>" << target << "的元素:" << *it2 
             << ",下标:" << it2 - nums.begin() << endl; // 输出:7,下标5
    }

    // 3. 特殊情况:查找不存在的元素(如10)
    auto it3 = upper_bound(nums.begin(), nums.end(), 10);
    if (it3 == nums.end()) {
        cout << "未找到大于10的元素" << endl;
    }

    return 0;
}

(三)降序数组示例代码

降序数组使用内置函数时,需额外传入greater<int>()作为比较规则,函数功能会反转:

  • lower_bound:查找第一个≤target 的元素

  • upper_bound:查找第一个 < target 的元素

    #include
    #include
    #include
    using namespace std;

    int main() {
    // 降序数组
    vector nums = {9, 7, 5, 5, 5, 3, 1};
    int target = 5;

    复制代码
      // 查找第一个≤5的元素(降序)
      auto it1 = lower_bound(nums.begin(), nums.end(), target, greater<int>());
      cout << "第一个≤" << target << "的元素:" << *it1 
           << ",下标:" << it1 - nums.begin() << endl; // 输出:5,下标2
    
      // 查找第一个<5的元素(降序)
      auto it2 = upper_bound(nums.begin(), nums.end(), target, greater<int>());
      cout << "第一个<" << target << "的元素:" << *it2 
           << ",下标:" << it2 - nums.begin() << endl; // 输出:3,下标5
    
      return 0;

    }

(四)编译器要求

  • 使用auto关键字接收迭代器时,需将编译器版本切换为C++11 及以上(如 Dev-C++ 在 "工具 - 编译选项" 中设置,VS 默认支持)。

四、注意事项

  1. 数据有序性:若输入数据无序,C 语言中需手动实现排序函数(如冒泡、快速排序),C++ 可调用sort(nums.begin(), nums.end()),排序后再使用二分查找。
  2. 边界处理:C 语言中需注意数组越界问题(如数组为空、numsSize=0 时需提前判断),目标值不存在、目标值在数组两端等场景需额外验证。
  3. 效率对比:C 语言手动实现二分查找更贴近底层原理,适合理解逻辑;C++ 内置函数代码简洁、不易出错,适合实战开发。

五、总结

关键点回顾

  1. 二分查找的核心是有序区间 + 折半缩减 ,C 语言手动实现时,找起始位置用mid=(l+r)/2,找结束位置用mid=(l+r+1)/2,避免死循环;
  2. C++ 内置lower_bound/upper_bound函数可快速实现二分查找,升序 / 降序数组的参数和功能需区分,auto关键字需 C++11 支持;
  3. 所有二分查找实现后,都需验证结果有效性(如 C 语言判断返回值是否为 - 1,C++ 判断迭代器是否等于end()),避免数组越界。
相关推荐
Jerry1 小时前
LeetCode 28. 找出字符串中第一个匹配项的下标
算法
Jerry3 小时前
LeetCode 459. 重复的子字符串
算法
哥不想学算法6 小时前
【C++】字符串字面量拼接
开发语言·c++
海石6 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石6 小时前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
Jerry8 小时前
LeetCode 151. 反转字符串中的单词
算法
gugucoding9 小时前
31. 【C语言】堆栈与队列的实现
c语言·开发语言·数据结构·链表
ChaoZiLL10 小时前
我的数据结构3——链表(link list)
数据结构·链表
a11177610 小时前
LM 算法迭代过程动画演示(SLAM)
算法
头茬韭菜11 小时前
Context 的生死抉择:四层压缩、截断算法与 Session Memory
算法·ai