二分法的历史:
二分法的应用领域:
二分法在计算机科学中的地位:
二分法的变种:
二分法的优势:
二分法的局限性:
何时使用二分法:
二分法的常见优化:
mid = (left + right) >> 1mid = left + (right - left) / 2// 在有序数组中查找目标值,返回索引,若不存在则返回-1
int binarySearch(vector<int>& nums, int target) {
int left = 0;
int right = nums.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; // 目标在左半部分
}
}
return -1; // 目标值不存在
}
// 查找第一个等于目标值的元素,返回索引,若不存在则返回-1
int findFirstEqual(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
result = mid; // 记录当前找到的位置
right = mid - 1; // 继续向左查找
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
// 查找最后一个等于目标值的元素,返回索引,若不存在则返回-1
int findLastEqual(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int result = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
result = mid; // 记录当前找到的位置
left = mid + 1; // 继续向右查找
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
// 查找第一个大于等于目标值的元素,返回索引,若不存在则返回数组长度
int findFirstGreaterOrEqual(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int result = nums.size(); // 默认为数组长度,表示所有元素都小于目标值
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
result = mid; // 记录当前找到的位置
right = mid - 1; // 继续向左查找
} else {
left = mid + 1;
}
}
return result;
}
// 查找最后一个小于等于目标值的元素,返回索引,若不存在则返回-1
int findLastLessOrEqual(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int result = -1; // 默认为-1,表示所有元素都大于目标值
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
result = mid; // 记录当前找到的位置
left = mid + 1; // 继续向右查找
} else {
right = mid - 1;
}
}
return result;
}
// 二分查找的递归实现
int binarySearchRecursive(vector<int>& nums, int target, int left, int right) {
// 基本情况:区间为空
if (left > right) {
return -1;
}
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid; // 找到目标值
} else if (nums[mid] < target) {
// 在右半部分递归查找
return binarySearchRecursive(nums, target, mid + 1, right);
} else {
// 在左半部分递归查找
return binarySearchRecursive(nums, target, left, mid - 1);
}
}
// 包装函数,提供更简洁的接口
int binarySearch(vector<int>& nums, int target) {
return binarySearchRecursive(nums, target, 0, nums.size() - 1);
}
// 浮点数二分查找,用于求解方程 f(x) = 0
double binarySearchFloat(double left, double right, double epsilon) {
while (right - left > epsilon) {
double mid = left + (right - left) / 2;
if (f(mid) == 0) {
return mid; // 找到精确解
} else if (f(mid) * f(left) < 0) {
// f(mid)和f(left)异号,解在[left, mid]区间
right = mid;
} else {
// f(mid)和f(left)同号,解在[mid, right]区间
left = mid;
}
}
// 返回近似解
return left; // 或者返回right,此时两者非常接近
}
// 示例函数f(x),求解x^2 - a = 0(即求a的平方根)
double f(double x, double a) {
return x * x - a;
}
// 求a的平方根
double sqrt(double a, double epsilon = 1e-9) {
if (a < 0) return -1; // 负数没有实数平方根
if (a == 0) return 0;
double left = 0;
double right = max(1.0, a); // 确保右边界足够大
while (right - left > epsilon) {
double mid = left + (right - left) / 2;
if (mid * mid <= a) {
left = mid;
} else {
right = mid;
}
}
return left;
}
二分法的常见错误与注意事项
- 整数溢出:计算mid时,如果left和right都很大,
(left + right) / 2可能会溢出。应使用left + (right - left) / 2。- 死循环:如果更新条件不当,可能导致死循环。例如,当
left = mid时,如果mid计算为(left + right) / 2,可能会导致left不变,陷入死循环。此时应使用mid = left + (right - left + 1) / 2(向上取整)。- 边界条件:注意循环条件是
left <= right还是left < right,以及退出循环后left和right的含义。- 区间定义:明确区间是闭区间
[left, right]还是左闭右开区间[left, right),并保持一致的更新策略。- 浮点数精度:处理浮点数时,使用epsilon控制精度,而不是固定的迭代次数。
对于vector,可以使用lower_bound()和upper_bound()函数。
lower_bound(),v.lower_bound(x)-v.begin(),x是搜索范围的下界v.upper_bound(x),v.upper_bound(x)-v.begin(),x是搜索范围的上界v.low_bound(x)且等于xv.upper_bound(x)-1且等于xv.upper_bound(x)-1v.lower_bound(x)-1v.upper_bound(x)-v.lower_bound(x)问题描述:
给定一个按非递减顺序排列的整数数组nums和一个目标值target,请你在数组中找出目标值的位置,如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
分析:
// 搜索插入位置
int searchInsert(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
int result = nums.size(); // 默认插入到末尾
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
result = mid; // 找到一个大于等于target的位置
right = mid - 1; // 继续向左查找
} else {
left = mid + 1;
}
}
return result;
}
代码解析:
result为数组长度,表示如果所有元素都小于目标值,则插入到末尾复杂度分析:
问题描述:
给定一个非负整数
分析:
// 计算整数平方根
int mySqrt(int x) {
if (x <= 1) return x;
int left = 1;
int right = x / 2;
int result = 0;
while (left <= right) {
int mid = left + (right - left) / 2;
// 使用除法避免溢出
if (mid <= x / mid) {
result = mid; // 记录当前找到的值
left = mid + 1; // 继续向右查找
} else {
right = mid - 1;
}
}
return result;
}
代码解析:
mid <= x / mid代替乘法mid * mid <= x,避免整数溢出复杂度分析:
注意:对于浮点数平方根,需要使用浮点数二分查找,并控制精度。
问题描述:
给定一个经过旋转的排序数组nums和一个目标值target,如果目标值存在于数组中,返回其索引,否则返回-1。旋转排序数组是指将一个排序数组在某个未知的位置进行了旋转,如[0,1,2,4,5,6,7]可能变为[4,5,6,7,0,1,2]。
分析:
// 在旋转排序数组中搜索目标值
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid; // 找到目标值
}
// 判断mid在哪个有序子数组中
if (nums[left] <= nums[mid]) {
// 左半部分有序
if (nums[left] <= target && target < nums[mid]) {
// 目标在左半部分
right = mid - 1;
} else {
// 目标在右半部分
left = mid + 1;
}
} else {
</div>
<div class=rdiv>
**代码解析**:
- 首先判断中间元素是否为目标值
- 通过比较`nums[left]`和`nums[mid]`确定哪个子数组有序
- 如果左半部分有序,判断目标值是否在左半部分的范围内
- 如果右半部分有序,判断目标值是否在右半部分的范围内
- 根据判断结果缩小搜索范围
**复杂度分析**:
- 时间复杂度:O(log n)
- 空间复杂度:O(1)
</div>
### 4.4 实例分析:猜数字游戏
<!-- _class: fixedtitleA -->
**问题描述**:
猜数字游戏的规则如下:
- 系统选择一个1到n之间的整数
- 你需要通过询问系统"这个数字是否小于等于x"来猜测这个数字
- 目标是用最少的次数猜出这个数字
**分析**:
- 这是二分查找的一个典型应用
- 每次猜测都可以排除一半的可能性
- 最优策略是每次都猜测当前范围的中间数
### 4.4 实例分析:猜数字游戏(代码示例)
<!-- _class: fixedtitleA cols-2-->
<div class=ldiv>
```cpp
// 猜数字游戏
int guessNumber(int n) {
int left = 1;
int right = n;
while (left <= right) {
int mid = left + (right - left) / 2;
int result = guess(mid); // API调用
if (result == 0) {
return mid; // 猜对了
} else if (result < 0) {
right = mid - 1; // 猜大了
} else {
left = mid + 1; // 猜小了
}
}
return -1; // 不会执行到这里
}
代码解析:
guess(int num)是系统提供的API
复杂度分析:
[POJ 1064] Cable master
[HDU 2141] 今年暑假不AC
[CSP-J 2019] 数列
[USACO 2017 Feb Silver] Why Did the Cow Cross the Road II
[NOIP 2014] 生活大爆炸版石头剪刀布
[CSES 1620] Factory Machines
[CSP-S 2020] 期末考试
[POJ 3579] Median
[HDU 3853] LOOPS
[NOIP 2015] 运输计划
[USACO 2016 Dec Platinum] Team Building
[IOI 2016] Paint
二分法的核心要点:
基本思想:
关键技巧:
常见应用:
实践建议:
编码前:
编码时:
调试时:
二分法的扩展:
三分查找:
分数二分:
二分图匹配:
并行二分:
实际应用中的注意事项:
在使用二分法之前,先考虑:
优化建议:
记住:

演讲者备注不会在演示模式中显示给观众,仅供演讲者参考。 每个关键页面都添加了备注,帮助演讲者更好地讲解内容。
封面页: - 欢迎学生,简要介绍本次课程的主题和目标 - 强调二分法是算法设计中的基础技巧,也是竞赛和面试中的常见题型 - 可以简单提及本课程将从基础概念到实际应用,全面讲解二分法的思想和技巧
目录页: - 简要介绍本次讲座的结构和内容安排 - 可以提及讲座的逻辑流程:从基础概念到具体应用,再到实践建议 - 强调我们将通过多个例子来深入理解二分法的应用 - 提醒学生关注每个部分之间的联系,以及如何将理论应用到实践中
转场页: - 这是一个简单的转场页,用于引入二分法概述部分 - 可以简单提及二分法是算法设计中的重要策略,有着广泛的应用
二分法概述: - 介绍二分法的基本概念和核心思想 - 强调二分法的高效性和应用场景 - 简要提及二分法的历史和发展
二分法的历史与应用: - 介绍二分法的历史背景 - 讨论二分法在不同领域的应用 - 强调二分法的重要性和普遍性
转场页: - 引入二分法的特点部分 - 可以简单提及理解这些特点有助于更好地应用二分法
二分法的特点: - 详细介绍二分法的主要特点 - 强调这些特点如何帮助识别和解决二分法问题 - 解释二分法的优势和局限性
二分法的优势与局限性: - 分析二分法的优势和局限性 - 讨论何时应该选择二分法 - 提供一些优化二分法的思路
二分法的适用场景: - 详细介绍二分法适用的各种场景 - 提供一些实际应用的例子 - 讨论如何识别可以使用二分法的问题
转场页: - 引入二分法的模板代码部分 - 可以简单提及掌握这些模板有助于快速实现二分法算法
二分法的基本模板: - 提供二分法的基本代码模板 - 解释模板中的关键部分 - 强调循环不变量的重要性
查找第一个等于目标值的元素: - 提供查找第一个等于目标值的元素的代码模板 - 解释与基本模板的区别 - 强调处理重复元素的技巧
查找最后一个等于目标值的元素: - 提供查找最后一个等于目标值的元素的代码模板 - 解释与查找第一个等于目标值的元素的区别 - 强调边界条件的处理
查找第一个大于等于目标值的元素: - 提供查找第一个大于等于目标值的元素的代码模板 - 解释与前面模板的区别 - 强调这种模板的应用场景
查找最后一个小于等于目标值的元素: - 提供查找最后一个小于等于目标值的元素的代码模板 - 解释与前面模板的区别 - 强调这种模板的应用场景
二分法的递归实现: - 提供二分法的递归实现代码 - 比较递归实现和迭代实现的优缺点 - 讨论何时选择递归实现
浮点数二分查找: - 提供浮点数二分查找的代码模板 - 解释浮点数二分查找的特点和注意事项 - 讨论精度控制的方法
二分法的常见错误与注意事项: - 列举二分法实现中的常见错误 - 提供避免这些错误的方法 - 强调细节的重要性
转场页: - 引入实例分析部分 - 可以简单提及我们将通过几个经典例子来深入理解二分法
二分查找基础应用: - 详细介绍二分查找的基础应用 - 通过具体例子说明二分查找的实现和应用 - 强调二分查找的效率和适用条件
二分查找基础应用的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度
求平方根: - 详细介绍如何使用二分法求平方根 - 解释整数平方根和浮点数平方根的区别 - 强调精度控制的重要性
求平方根的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度
旋转排序数组中的搜索: - 详细介绍如何在旋转排序数组中使用二分查找 - 解释旋转排序数组的特点和处理方法 - 强调二分查找在复杂场景中的应用
旋转排序数组中的搜索的代码实现: - 提供完整的代码实现 - 解释代码中的关键部分 - 分析算法的时间和空间复杂度