二分查找

二分查找

二分法(Binary Search)是一种高效的有序数据集查找算法,核心思想是 “通过不断将查找范围减半,快速缩小目标位置”,其时间复杂度为 O(log n)(n 为数据集长度),远优于线性查找的 O (n)。但需注意:二分法的前提是数据集已按升序或降序排列(通常默认升序)。

使用场景特点

  1. 搜索空间有序:无论是数组元素、函数输入、解的范围,其 “排序性” 是二分的基础;
  2. 判断可缩小空间:能通过一次简单判断(如 “中间值与目标的大小”“解是否可行”)将搜索空间直接减半。

实际应用问题场景

  1. 有序数据结构中的 “查找问题”
    • 判断有序数组中是否存在某个目标值,若存在则返回索引
    • 定位目标值的第一个出现位置最后一个出现位置(如去重、统计元素个数)。
    • 查找目标值的前驱(小于目标的最大元素)、后继(大于目标的最小元素)
  2. “单调函数” 的求解问题
    • 求解 “函数零点”(方程求解)
  3. “有序解空间” 的资源优化问题
    • 比如查找与一个数相等的另一个数的基数

算法原理

二分法的本质是 “利用有序性排除无效范围”,具体逻辑如下:

  1. 确定范围:初始查找范围是整个有序数据集(左边界 left 指向第一个元素,右边界 right 指向最后一个元素)。
  2. 计算中点:找到当前范围的中间位置 mid,取 mid 处的元素与目标值 target 比较。
  3. 缩小范围
    • mid 处元素 等于 target:找到目标,返回 mid(或记录结果)。
    • mid 处元素 小于 target:目标在 mid右侧(因数组有序),将左边界 left 调整为 mid + 1(排除左半部分)。
    • mid 处元素 大于 target:目标在 mid左侧,将右边界 right 调整为 mid - 1(排除右半部分)。
  4. 循环终止:重复步骤 2-3,直到 left > right(说明范围为空,目标不存在)。

实现步骤

步骤 1:初始化边界指针

定义两个变量 leftright,分别表示当前查找范围的 “左边界” 和 “右边界”:

步骤 2:循环缩小查找范围(核心步骤)

循环条件:left <= right(当 left > right 时,范围为空,终止循环)。

  1. 计算中点索引 mid
  2. 比较 arr[mid]target

步骤 3:处理查找失败

若循环结束(left > right)仍未返回,说明目标值不在数组中,返回 -1(或其他约定的 “不存在” 标记)

通用模版

模版1

BinarySearch(int arr[], int target) {
	// 初始化边界指针
	int left = 0, right = arr.size() - 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;
		}

	}
	// 循环结束仍未找到,返回-1(表示不存在)
	return -1;
}

模板 2:二分法(找左边界,含重复元素)

适用于有序数组中查找 “第一个≥目标值” 的元素索引(如找第一个大于等于 target 的位置,常用于插入排序定位、重复元素中找首次出现的位置)。

// 函数功能:在升序数组nums中,找第一个≥target的元素索引(左边界)
// 参数:nums-升序有序数组,target-待查找目标值
int binarySearch_LeftBound(const vector<int>& nums, int target) {
    // 1. 初始化:查找范围覆盖整个数组
    int left = 0;
    int right = nums.size() - 1;
    int result = nums.size();  // 初始化为数组长度(表示未找到时返回插入位置)

    // 2. 核心循环:缩小范围,定位左边界
    while (left <= right) {
        int mid = left + (right - left) / 2;

        if (nums[mid] >= target) {
            // 找到可能的左边界,继续向左缩小范围(找更靠左的≥target的元素)
            result = mid;          // 暂存当前mid为候选结果
            right = mid - 1;       // 右边界左移,探索左半区
        } else {
            // 当前mid值小于target,目标在右半区
            left = mid + 1;
        }
    }

    // 3. 结果处理:返回最终找到的左边界(或插入位置)
    return result;
}

模板 3:二分法(找右边界,含重复元素)

适用于有序数组中查找 “最后一个≤目标值” 的元素索引(如找最后一个小于等于 target 的位置,常用于重复元素中找末次出现的位置)。

// 函数功能:在升序数组nums中,找最后一个≤target的元素索引(右边界)
// 参数:nums-升序有序数组,target-待查找目标值
int binarySearch_RightBound(const vector<int>& nums, int target) {
    // 1. 初始化:查找范围覆盖整个数组
    int left = 0;
    int right = nums.size() - 1;
    int result = -1;  // 初始化为-1(表示未找到时返回无效索引)

    // 2. 核心循环:缩小范围,定位右边界
    while (left <= right) {
        int mid = left + (right - left) / 2;

        if (nums[mid] <= target) {
            // 找到可能的右边界,继续向右缩小范围(找更靠右的≤target的元素)
            result = mid;          // 暂存当前mid为候选结果
            left = mid + 1;        // 左边界右移,探索右半区
        } else {
            // 当前mid值大于target,目标在左半区
            right = mid - 1;
        }
    }

    // 3. 结果处理:返回最终找到的右边界(或-1表示无符合元素)
    return result;
}

不同情况的不同之处

  1. 找左边界(找第一个大于等于 target 的位置) :>= 找右边界(找第一个小于等于target的位置):<= ,找哪一边的边界,就向哪一边缩小范围

中位数选取的核心原则

mid 的计算方式(向下 / 向上取整)由边界收缩方向决定:

  • 当收缩方向为 向左right = mid - 1)时,用向下取整(默认方式),确保范围有效缩小。

mid = left + (right - left) / 2

  • 当收缩方向为 向右left = mid + 1)时,若 leftright 可能相邻(差 1),需用向上取整,避免 mid 始终等于 left 导致的死循环。

left + (right - left + 1) / 2

本质上,所有调整都是为了保证:每次迭代后,搜索范围的大小必须严格减小,最终能在有限步内结束循环。

若都考虑使用 int mid = left + (right - left) / 2; 则需要像

// 找到可能的右边界,继续向右缩小范围(找更靠右的≤target的元素) result = mid; // 暂存当前mid为候选结果 left = mid + 1; // 左边界右移,探索右半区

用result保存答案,用left = mid + 1 来防止陷入循环

实数二分

实数二分

while((left-right)>eps)
{
    int ans;
    mid = left + (right - left) / 2;
    if (check(mid))
    {
      right = mid;
    }   
    else left = mid;
}


posted @ 2025-09-08 16:45  yaohaha  阅读(115)  评论(0)    收藏  举报
返回顶端