【数组和二分查找】
一、数组核心知识点解构
1. 数组的内存存储模型
数组是连续内存空间上的相同类型数据集合,其物理结构决定了访问和操作特性:
-
随机访问:通过下标直接定位元素,时间复杂度 (O(1))。
int arr[5] = {1,2,3,4,5}; int value = arr[3]; // 直接访问第4个元素(下标3),内存地址计算:base_address + 3 * sizeof(int)
-
插入/删除操作:需移动后续元素,时间复杂度 (O(n))。
原数组:[1,2,3,4,5] → 删除下标2的元素 操作后:[1,2,4,5,_] → 元素4、5前移,空缺位置由垃圾值填充(逻辑删除)
-
二维数组的内存布局:
- C++:按行优先存储,二维数组
arr[m][n]
可视为m
个连续的一维数组。int arr[2][3] = {{0,1,2}, {3,4,5}}; // 内存布局:0 1 2 3 4 5(行与行无缝衔接)
- Java:二维数组是数组的数组,每行首地址不连续。
int[][] arr = new int[2][3]; // arr[0] 和 arr[1] 是两个独立的一维数组引用
- C++:按行优先存储,二维数组
2. 数组与动态数组对比
特性 | 原生数组 | 动态数组(如C++ vector) |
---|---|---|
内存分配 | 静态分配(编译时确定大小) | 动态分配(运行时可扩容) |
大小调整 | 不可变 | 支持resize()、push_back() |
内存连续性 | 完全连续 | 扩容时可能重新分配内存 |
性能 | 略高(无额外开销) | 略低(需维护容量信息) |
二、二分查找算法深度解析
1. 算法本质:分治思想
二分查找通过每次将搜索区间减半,将时间复杂度优化至 (O(\log n))。其核心在于有序性和区间定义的不变性。
2. 循环不变量规则详解
区间定义决定了代码的边界处理方式,常见两种模式:
-
左闭右闭区间
[left, right]
- 循环条件:
left <= right
(区间包含两端点,left==right
时仍有效)。 - 边界收缩:若
nums[middle] > target
,更新right = middle - 1
(排除middle
)。
- 循环条件:
-
左闭右开区间
[left, right)
- 循环条件:
left < right
(区间不含right
,left==right
时为空集)。 - 边界收缩:若
nums[middle] > target
,更新right = middle
(保留middle
在下次搜索区间外)。
- 循环条件:
三、二分查找核心代码实现与对比
版本一:左闭右闭区间 [left, right]
// C++ 实现(ACM模式)
#include <iostream>
#include <vector>
using namespace std;int binarySearch(vector<int>& nums, int target) {int left = 0; // 左边界初始化为数组第一个元素下标int right = nums.size() - 1; // 右边界初始化为数组最后一个元素下标(闭区间)while (left <= right) { // 循环条件:区间内至少有一个元素int middle = left + (right - left) / 2; // 防止整数溢出,等价于 (left + right) / 2if (nums[middle] == target) {return middle; // 找到目标值,返回下标} else if (nums[middle] > target) {right = middle - 1; // 目标在左半区,排除middle,右边界收缩到middle-1} else {left = middle + 1; // 目标在右半区,排除middle,左边界扩张到middle+1}}return -1; // 未找到目标值
}int main() {int n, target;cin >> n >> target; // 输入数组长度和目标值vector<int> nums(n);for (int i = 0; i < n; i++) {cin >> nums[i]; // 输入数组元素}cout << binarySearch(nums, target) << endl;return 0;
}
版本二:左闭右开区间 [left, right)
// C++ 实现(ACM模式)
int binarySearch(vector<int>& nums, int target) {int left = 0; // 左边界初始化为数组第一个元素下标int right = nums.size(); // 右边界初始化为数组长度(开区间,不包含此位置)while (left < right) { // 循环条件:区间内至少有一个元素int middle = left + (right - left) / 2; // 防止整数溢出if (nums[middle] == target) {return middle; // 找到目标值,返回下标} else if (nums[middle] > target) {right = middle; // 目标在左半区,右边界收缩到middle(不含middle)} else {left = middle + 1; // 目标在右半区,左边界扩张到middle+1}}return -1; // 未找到目标值
}
Python实现对比
# 左闭右闭区间
def binary_search_closed(nums, target):left, right = 0, len(nums) - 1while left <= right:mid = left + (right - left) // 2 # 整数除法防止溢出if nums[mid] == target:return midelif nums[mid] > target:right = mid - 1else:left = mid + 1return -1# 左闭右开区间
def binary_search_open(nums, target):left, right = 0, len(nums)while left < right:mid = left + (right - left) // 2if nums[mid] == target:return midelif nums[mid] > target:right = midelse:left = mid + 1return -1
四、二分查找经典题型详解
1. 704. 二分查找(基础题)
- 题意:在有序数组中查找目标值,返回下标或-1。
- 思路:直接二分,两种区间均可。
- 关键点:
- 数组有序且无重复元素,找到即返回。
- 若使用左闭右开区间,注意右边界初始化为
nums.size()
。
2. 35. 搜索插入位置
- 题意:在有序数组中查找目标值,若存在返回下标;否则返回插入位置。
- 思路:
- 二分查找过程中,若找到目标值,直接返回。
- 若未找到,最终
left
即为插入位置(因为循环结束时,left
左侧所有元素均小于目标值)。
- 代码实现:
int searchInsert(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] == target) {return mid; // 找到目标值,返回下标} else if (nums[mid] > target) {right = mid - 1; // 目标在左半区} else {left = mid + 1; // 目标在右半区}}return left; // 未找到时,left即为插入位置 }
- 图解:
数组:[1,3,5,6],目标值4 初始:left=0, right=3 第一次循环:mid=1, nums[1]=3 < 4 → left=2, right=3 第二次循环:mid=2, nums[2]=5 > 4 → left=2, right=1 循环结束:left=2,插入位置为2(即5的位置)
3. 34. 在排序数组中查找元素的第一个和最后一个位置
- 题意:在有序数组中查找目标值的左右边界,若不存在返回
[-1, -1]
。 - 思路:
- 找左边界:调整二分条件,当
nums[mid] >= target
时收缩右边界,最终left
为左边界。 - 找右边界:当
nums[mid] <= target
时收缩左边界,最终right
为右边界的下一个位置,减1得右边界。
- 找左边界:调整二分条件,当
- 代码实现:
vector<int> searchRange(vector<int>& nums, int target) {int left = findLeft(nums, target);int right = findRight(nums, target);if (left <= right && nums[left] == target) { // 验证存在性return {left, right};}return {-1, -1}; }int findLeft(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] >= target) { // 关键条件:>=时收缩右边界right = mid - 1;} else {left = mid + 1;}}return left; // 返回左边界 }int findRight(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] <= target) { // 关键条件:<=时收缩左边界left = mid + 1;} else {right = mid - 1;}}return right; // 返回右边界 }
- 关键点:
- 左边界函数中,即使找到目标值,仍继续向左收缩。
- 右边界函数中,即使找到目标值,仍继续向右收缩。
- 最终需验证
left <= right
且nums[left] == target
,确保目标值存在。
4. 69. x 的平方根
- 题意:计算非负整数
x
的平方根,结果向下取整。 - 思路:
- 二分查找范围为
[0, x]
,判断中点mid
是否满足mid * mid <= x
。 - 注意整数溢出,使用
long
类型存储平方值。
- 二分查找范围为
- 代码实现:
int mySqrt(int x) {if (x == 0) return 0; // 特殊处理0int left = 1, right = x;int res = 0; // 记录可能的解while (left <= right) {long mid = left + (right - left) / 2; // 防止溢出long square = mid * mid;if (square == x) {return mid; // 精确解} else if (square < x) {res = mid; // 记录当前可能的解left = mid + 1; // 尝试更大的值} else {right = mid - 1; // 尝试更小的值}}return res; // 返回最大的满足条件的值 }
- 优化:
- 对于较大的
x
,可将右边界初始化为x/2
(当x >= 2
时,平方根不超过x/2
)。
- 对于较大的
5. 367. 有效的完全平方数
- 题意:判断正整数
x
是否为完全平方数。 - 思路:
- 同69题,二分查找平方根,若找到精确解则返回
true
。
- 同69题,二分查找平方根,若找到精确解则返回
- 代码实现:
bool isPerfectSquare(int x) {if (x == 0) return true;int left = 1, right = x;while (left <= right) {long mid = left + (right - left) / 2;long square = mid * mid;if (square == x) {return true; // 找到精确解} else if (square < x) {left = mid + 1;} else {right = mid - 1;}}return false; // 未找到精确解 }
五、二分查找进阶技巧与拓展
1. 二分查找的数学原理
二分查找每次将搜索区间减半,时间复杂度为 (O(\log n)),推导如下:
- 第1次迭代:区间长度 (n)
- 第2次迭代:区间长度 (n/2)
- 第k次迭代:区间长度 (n/2^k)
- 当区间长度为1时停止:(n/2^k = 1 \Rightarrow k = \log_2 n)
2. 二分查找的变种题型
- 查找第一个大于等于目标值的元素:
int lower_bound(vector<int>& nums, int target) {int left = 0, right = nums.size();while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] >= target) {right = mid;} else {left = mid + 1;}}return left; }
- 查找第一个大于目标值的元素:
int upper_bound(vector<int>& nums, int target) {int left = 0, right = nums.size();while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] > target) {right = mid;} else {left = mid + 1;}}return left; }
3. 二分查找的应用场景扩展
- 最小值最大化问题:如LeetCode 410分割数组的最大值。
- 最大值最小化问题:如LeetCode 875爱吃香蕉的珂珂。
- 旋转排序数组:如LeetCode 33搜索旋转排序数组。
六、C++与Python库函数对比
1. C++标准库中的二分查找
lower_bound
:返回首个不小于目标值的元素迭代器。vector<int> nums = {1,3,5,7}; auto it = lower_bound(nums.begin(), nums.end(), 4); // it指向5,下标为distance(nums.begin(), it)
upper_bound
:返回首个大于目标值的元素迭代器。auto it = upper_bound(nums.begin(), nums.end(), 5); // it指向7,下标为2
binary_search
:判断元素是否存在。bool exists = binary_search(nums.begin(), nums.end(), 5); // 存在返回true,否则false
2. Python中的二分查找
bisect
模块:import bisect nums = [1, 3, 5, 7]# 查找左边界 left = bisect.bisect_left(nums, 4) # 返回2# 查找右边界的下一个位置 right = bisect.bisect_right(nums, 5) # 返回3# 插入元素保持有序 bisect.insort(nums, 6) # nums变为[1,3,5,6,7]
七、二分查找通用模板(终极版)
模板一:左闭右闭区间(找确定值)
int binarySearch(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] == target) {return mid; // 找到目标值} else if (nums[mid] > target) {right = mid - 1; // 左半区} else {left = mid + 1; // 右半区}}return -1; // 未找到
}
模板二:左闭右开区间(找边界)
// 找左边界:第一个大于等于target的位置
int lowerBound(vector<int>& nums, int target) {int left = 0, right = nums.size();while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] >= target) {right = mid; // 收缩右边界} else {left = mid + 1; // 扩张左边界}}return left; // 返回左边界
}// 找右边界:第一个大于target的位置减1
int upperBound(vector<int>& nums, int target) {int left = 0, right = nums.size();while (left < right) {int mid = left + (right - left) / 2;if (nums[mid] > target) {right = mid; // 收缩右边界} else {left = mid + 1; // 扩张左边界}}return left - 1; // 返回右边界
}
八、常见错误与避坑指南
-
整数溢出:
- 错误写法:
mid = (left + right) / 2
- 正确写法:
mid = left + (right - left) / 2
- 错误写法:
-
循环条件与区间定义不一致:
- 左闭右闭区间:循环条件应为
left <= right
。 - 左闭右开区间:循环条件应为
left < right
。
- 左闭右闭区间:循环条件应为
-
边界处理错误:
- 找左边界时,若
nums[mid] == target
,应继续向左收缩。 - 找右边界时,若
nums[mid] == target
,应继续向右收缩。
- 找左边界时,若
-
数组越界:
- 当使用左闭右开区间时,右边界初始化为
nums.size()
,但访问元素时需确保下标不越界。
- 当使用左闭右开区间时,右边界初始化为
九、总结与记忆要点
-
数组特性:
- 内存连续,支持随机访问,插入/删除需移动元素。
- 二维数组在C++中连续存储,Java中为数组的数组。
-
二分条件:
- 数组有序,时间复杂度 (O(\log n))。
-
区间定义:
- 左闭右闭:
[left, right]
,循环left <= right
,边界收缩right = mid - 1
。 - 左闭右开:
[left, right)
,循环left < right
,边界收缩right = mid
。
- 左闭右闭:
-
解题步骤:
- 明确题目要求(找值、左边界、右边界)。
- 选择区间定义,确定循环条件。
- 处理中点比较逻辑,更新边界。
- 验证结果是否符合题意。
-
变种题型:
- 基础查找、插入位置、左右边界、平方根、完全平方数、旋转数组等。
-
库函数:
- C++:
lower_bound
、upper_bound
、binary_search
。 - Python:
bisect_left
、bisect_right
。
- C++:
通过以上系统化解析,可彻底掌握二分查找的核心思想与实现细节,应对各类面试题时游刃有余。建议结合LeetCode题目反复练习,形成肌肉记忆!
第二份:
二分查找经典题型详解(ACM模式)
1. 704. 二分查找(基础题)
题意:在升序数组中查找目标值,存在返回下标,否则返回-1。
思路:使用二分查找,分两种区间定义实现。
C++实现(左闭右闭区间 [left, right]
)
#include <iostream>
#include <vector>
using namespace std;// 二分查找:左闭右闭区间
int binarySearch(vector<int>& nums, int target) {int left = 0; // 左边界初始化为数组首个元素下标(闭区间)int right = nums.size() - 1; // 右边界初始化为数组末元素下标(闭区间)while (left <= right) { // 区间内至少有一个元素(left==right时仍有效)int mid = left + (right - left) / 2; // 防溢出计算中点,等价于(left+right)/2if (nums[mid] == target) {return mid; // 找到目标值,直接返回下标} else if (nums[mid] > target) {right = mid - 1; // 目标在左半区,收缩右边界(排除mid)} else {left = mid + 1; // 目标在右半区,扩张左边界(排除mid)}}return -1; // 未找到目标值
}int main() {int n, target;cin >> n >> target; // 输入数组长度和目标值vector<int> nums(n);for (int i = 0; i < n; i++) {cin >> nums[i]; // 输入有序数组元素}cout << binarySearch(nums, target) << endl;return 0;
}
Python实现(左闭右开区间 [left, right)
)
def binary_search(nums, target):left = 0 # 左边界初始化为数组首个元素下标(闭区间)right = len(nums) # 右边界初始化为数组长度(开区间,不包含此位置)while left < right: # 区间内至少有一个元素(left==right时区间为空)mid = left + (right - left) // 2 # 防溢出计算中点if nums[mid] == target:return mid # 找到目标值,直接返回下标elif nums[mid] > target:right = mid # 目标在左半区,收缩右边界(不含mid)else:left = mid + 1 # 目标在右半区,扩张左边界(含mid+1)return -1 # 未找到目标值# ACM输入处理
import sys
data = list(map(int, sys.stdin.read().split()))
n = data[0]
target = data[1]
nums = data[2: 2+n]
print(binary_search(nums, target))
关键点:
- 区间定义:左闭右闭用
<=
,左闭右开用<
。 - 中点计算:避免
left+right
溢出,用left + (right-left)/2
。
2. 35. 搜索插入位置
题意:在升序数组中查找目标值,存在返回下标;否则返回插入位置。
思路:二分查找,未找到时left
为插入位置(左闭右闭区间)。
C++实现(左闭右闭区间)
#include <vector>
using namespace std;int searchInsert(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) {right = mid - 1; // 目标在左半区,收缩右边界} else {left = mid + 1; // 目标在右半区,扩张左边界}}// 未找到时,left左侧元素均小于target,故left为插入位置return left;
}// ACM主函数(同704题,修改函数调用即可)
Python实现(左闭右闭区间)
def search_insert(nums, target):left, right = 0, len(nums) - 1 # 闭区间初始化while left <= right:mid = left + (right - left) // 2if nums[mid] == target:return midelif nums[mid] > target:right = mid - 1else:left = mid + 1return left # 插入位置为left# ACM输入处理(同704题Python版)
关键点:
- 循环结束后,
left
是首个不小于target
的元素下标,即插入位置。
3. 34. 查找元素的第一个和最后一个位置
题意:在升序数组中找目标值的左右边界,不存在返回[-1, -1]
。
思路:
- 找左边界:调整条件为
nums[mid] >= target
,循环结束后left
为左边界。 - 找右边界:调整条件为
nums[mid] <= target
,循环结束后right
为右边界的下一个位置,减1得右边界。
C++实现
#include <vector>
using namespace std;// 找左边界:首个>=target的位置
int findLeft(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1; // 闭区间初始化while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] >= target) { // 关键条件:>=时向左收缩right = mid - 1;} else {left = mid + 1;}}return left; // 返回左边界(可能越界,需验证)
}// 找右边界:首个<=target的位置的下一个位置减1
int findRight(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1; // 闭区间初始化while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] <= target) { // 关键条件:<=时向右收缩left = mid + 1;} else {right = mid - 1;}}return right; // 返回右边界(可能越界,需验证)
}vector<int> searchRange(vector<int>& nums, int target) {int left = findLeft(nums, target);int right = findRight(nums, target);// 验证左边界是否越界且目标值存在if (left < nums.size() && nums[left] == target) {return {left, right};}return {-1, -1};
}// ACM主函数(输入数组和target,输出左右边界)
Python实现
def find_left(nums, target):left, right = 0, len(nums) - 1 # 闭区间初始化while left <= right:mid = left + (right - left) // 2if nums[mid] >= target: # 向左收缩条件right = mid - 1else:left = mid + 1return leftdef find_right(nums, target):left, right = 0, len(nums) - 1 # 闭区间初始化while left <= right:mid = left + (right - left) // 2if nums[mid] <= target: # 向右收缩条件left = mid + 1else:right = mid - 1return rightdef search_range(nums, target):left = find_left(nums, target)right = find_right(nums, target)if left <= len(nums)-1 and nums[left] == target:return [left, right]else:return [-1, -1]# ACM输入处理
关键点:
- 左边界函数中,即使
nums[mid] == target
,仍继续向左收缩以找到第一个出现的位置。 - 右边界函数中,需验证
left
是否越界,避免访问无效下标。
4. 69. x 的平方根
题意:计算非负整数x
的平方根,向下取整。
思路:二分查找范围[1, x]
,判断mid*mid <= x
,用long
防溢出。
C++实现
#include <iostream>
using namespace std;int mySqrt(int x) {if (x == 0) return 0; // 特判x=0int left = 1, right = x; // 查找范围[1, x]int res = 0; // 记录最大可行解while (left <= right) { // 闭区间循环long mid = left + (right - left) / 2; // 防溢出long square = mid * mid;if (square == x) {return mid; // 精确解} else if (square < x) {res = mid; // 记录当前解,尝试更大值left = mid + 1;} else {right = mid - 1; // 尝试更小值}}return res; // 返回最大下界
}// ACM主函数(输入x,输出平方根)
int main() {int x;cin >> x;cout << mySqrt(x) << endl;return 0;
}
Python实现
def my_sqrt(x):if x == 0:return 0left, right = 1, x # 查找范围[1, x]res = 0while left <= right:mid = left + (right - left) // 2square = mid * midif square == x:return midelif square < x:res = mid # 记录当前解left = mid + 1else:right = mid - 1return res# ACM输入处理
x = int(input())
print(my_sqrt(x))
关键点:
- 用
long
存储mid*mid
避免整数溢出(C++中int
相乘可能溢出)。 - 循环结束后
res
是最大的满足mid*mid <= x
的值。
5. 367. 有效的完全平方数
题意:判断正整数x
是否为完全平方数。
思路:同69题,找到平方根后验证平方是否等于x
。
C++实现
#include <iostream>
using namespace std;bool isPerfectSquare(int x) {if (x == 0) return true; // 特判x=0int left = 1, right = x; // 查找范围[1, x]while (left <= right) {long mid = left + (right - left) / 2; // 防溢出long square = mid * mid;if (square == x) {return true; // 找到精确解} else if (square < x) {left = mid + 1; // 尝试更大值} else {right = mid - 1; // 尝试更小值}}return false; // 未找到精确解
}// ACM主函数(输入x,输出是否为完全平方数)
int main() {int x;cin >> x;cout << (isPerfectSquare(x) ? "true" : "false") << endl;return 0;
}
Python实现
def is_perfect_square(x):if x == 0:return Trueleft, right = 1, xwhile left <= right:mid = left + (right - left) // 2square = mid * midif square == x:return Trueelif square < x:left = mid + 1else:right = mid - 1return False# ACM输入处理
x = int(input())
print(is_perfect_square(x))
二分查找规律抽象
-
适用条件:
- 数组有序(升序/降序,需明确比较方向)。
- 需快速定位目标值或边界(时间复杂度 (O(\log n)))。
-
区间定义决定代码结构:
区间类型 循环条件 边界收缩逻辑(以升序数组为例) 左闭右闭 [l,r]
l <= r
r = mid-1
(当nums[mid] > target
)左闭右开 [l,r)
l < r
r = mid
(当nums[mid] > target
) -
边界处理通用逻辑:
- 找左边界:调整条件为
>= target
,循环结束后left
为首个不小于target
的位置。 - 找右边界:调整条件为
<= target
,循环结束后right
为首个大于target
的位置减1。
- 找左边界:调整条件为
-
防溢出技巧:
- 中点计算用
mid = left + (right - left) / 2
代替(left+right)/2
。 - 涉及平方运算时用
long
类型(C++)或大整数(Python)。
- 中点计算用
快速记忆要点
-
二分三步曲:
- 定义区间(闭区间/开区间)。
- 计算中点,比较中点值与目标值。
- 根据区间定义更新边界。
-
关键变量含义:
left
:当前搜索区间的左边界(包含)。right
:当前搜索区间的右边界(闭区间包含,开区间不包含)。mid
:中点,用于分割区间。
-
题型变种处理:
- 插入位置:循环结束后
left
即为结果。 - 左右边界:用
>=
和<=
调整条件,验证结果有效性。 - 数值计算:注意类型溢出,用二分查找可行解区间。
- 插入位置:循环结束后
通过“区间定义→条件判断→边界收缩”的固定逻辑,可应对各类二分查找问题。
二分查找拓展题型详解(ACM模式)
1. LeetCode 410. 分割数组的最大值(最小值最大化问题)
题意:将一个非负整数数组分割成m
个子数组,求所有分割方式中子数组最大值的最小值。
思路:
- 二分查找:目标是找到最小的最大值
mid
,范围为[max(nums), sum(nums)]
。 - 验证函数:判断是否可以将数组分割成不超过
m
个子数组,且每个子数组的和不超过mid
。
C++实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;// 验证是否可以在m个子数组内,每个子数组和<=mid
bool check(vector<int>& nums, int m, int mid) {int cnt = 1; // 子数组数量,至少1个long sum = 0; // 防止溢出,用longfor (int num : nums) {if (num > mid) return false; // 单个元素超过mid,直接失败if (sum + num > mid) { // 当前子数组和超过mid,分割cnt++;sum = num;} else {sum += num;}}return cnt <= m; // 子数组数量不超过m则可行
}int splitArray(vector<int>& nums, int m) {long left = *max_element(nums.begin(), nums.end()); // 左边界:数组最大值long right = accumulate(nums.begin(), nums.end(), 0L); // 右边界:数组总和long res = right; // 初始化为最大值while (left <= right) { // 左闭右闭区间long mid = left + (right - left) / 2;if (check(nums, m, mid)) { // 可行,尝试更小值res = mid;right = mid - 1;} else { // 不可行,需要更大值left = mid + 1;}}return res;
}int main() {int n, m;cin >> n >> m;vector<int> nums(n);for (int i = 0; i < n; i++) cin >> nums[i];cout << splitArray(nums, m) << endl;return 0;
}
Python实现
def check(nums, m, mid):cnt = 1total = 0for num in nums:if num > mid:return Falseif total + num > mid:cnt += 1total = numelse:total += numreturn cnt <= mdef split_array(nums, m):left = max(nums)right = sum(nums)res = rightwhile left <= right: # 闭区间mid = left + (right - left) // 2if check(nums, m, mid):res = midright = mid - 1else:left = mid + 1return res# ACM输入处理
import sys
data = list(map(int, sys.stdin.read().split()))
n = data[0]
m = data[1]
nums = data[2: 2+n]
print(split_array(nums, m))
关键点:
- 二分范围:最小值至少是数组中的最大值(单个元素无法分割),最大值是数组总和(不分割)。
- 验证逻辑:贪心分割,尽可能让每个子数组的和接近
mid
,统计分割次数是否达标。
2. LeetCode 875. 爱吃香蕉的珂珂(最大值最小化问题)
题意:珂珂吃香蕉,每小时最多吃k
根,求吃完所有香蕉的最小速度k
,使得总时间不超过h
小时。
思路:
- 二分查找:
k
的范围是[1, max(nums)]
,验证k
是否可行。 - 计算时间:每堆香蕉需
ceil(pile/k)
小时,可用(pile + k - 1) // k
计算。
C++实现
#include <iostream>
#include <vector>
using namespace std;// 验证速度k是否能在h小时内吃完
bool canEat(vector<int>& piles, int h, int k) {long time = 0; // 防止溢出for (int pile : piles) {time += (pile + k - 1) / k; // 向上取整if (time > h) return false; // 超时}return true;
}int minEatingSpeed(vector<int>& piles, int h) {int left = 1; // 最小速度1int right = *max_element(piles.begin(), piles.end()); // 最大速度为最大堆int res = right;while (left <= right) { // 闭区间int mid = left + (right - left) / 2;if (canEat(piles, h, mid)) { // 可行,尝试更小速度res = mid;right = mid - 1;} else { // 不可行,需要更大速度left = mid + 1;}}return res;
}int main() {int n, h;cin >> n >> h;vector<int> piles(n);for (int i = 0; i < n; i++) cin >> piles[i];cout << minEatingSpeed(piles, h) << endl;return 0;
}
Python实现
def can_eat(piles, h, k):time = 0for pile in piles:time += (pile + k - 1) // k # 向上取整if time > h:return Falsereturn Truedef min_eating_speed(piles, h):left = 1right = max(piles)res = rightwhile left <= right:mid = left + (right - left) // 2if can_eat(piles, h, mid):res = midright = mid - 1else:left = mid + 1return res# ACM输入处理
import sys
data = list(map(int, sys.stdin.read().split()))
n = data[0]
h = data[1]
piles = data[2: 2+n]
print(min_eating_speed(piles, h))
关键点:
- 向上取整技巧:
(a + b - 1) // b
等价于ceil(a/b)
。 - 二分方向:可行解向左收缩,不可行解向右扩张,寻找最小可行
k
。
3. LeetCode 33. 搜索旋转排序数组(旋转数组搜索)
题意:升序数组在某个点旋转后(如[0,1,2,4,5,6,7]
→[4,5,6,7,0,1,2]
),搜索目标值,存在返回下标,否则返回-1。
思路:
- 二分查找:判断中点所在的有序区间,确定目标值在左半区还是右半区。
- 判断有序区间:
- 若
nums[mid] >= nums[left]
,左半区有序。 - 否则,右半区有序。
- 若
C++实现
#include <iostream>
#include <vector>
using namespace std;int search(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1; // 闭区间while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] == target) return mid; // 找到目标值// 判断左半区是否有序if (nums[mid] >= nums[left]) {// 左半区有序,判断target是否在左半区if (target >= nums[left] && target < nums[mid]) {right = mid - 1; // 目标在左半区} else {left = mid + 1; // 目标在右半区}} else {// 右半区有序,判断target是否在右半区if (target > nums[mid] && target <= nums[right]) {left = mid + 1; // 目标在右半区} else {right = mid - 1; // 目标在左半区}}}return -1;
}int main() {int n, target;cin >> n >> target;vector<int> nums(n);for (int i = 0; i < n; i++) cin >> nums[i];cout << search(nums, target) << endl;return 0;
}
Python实现
def search(nums, target):left, right = 0, len(nums) - 1 # 闭区间while left <= right:mid = left + (right - left) // 2if nums[mid] == target:return mid# 判断左半区是否有序if nums[mid] >= nums[left]:if nums[left] <= target < nums[mid]:right = mid - 1else:left = mid + 1else:# 右半区有序if nums[mid] < target <= nums[right]:left = mid + 1else:right = mid - 1return -1# ACM输入处理
import sys
data = list(map(int, sys.stdin.read().split()))
n = data[0]
target = data[1]
nums = data[2: 2+n]
print(search(nums, target))
关键点:
- 旋转数组性质:必定存在一个有序子数组,通过比较中点和左端点判断哪侧有序。
- 边界条件:处理
target
等于端点的情况,确保区间判断正确。
4. LeetCode 153. 寻找旋转排序数组中的最小值(旋转数组找最小值)
题意:升序数组旋转后,寻找最小值(如[3,4,5,1,2]
的最小值为1)。
思路:
- 二分查找:比较中点和右端点,确定最小值在左半区或右半区。
- 若
nums[mid] > nums[right]
,最小值在右半区。 - 否则,最小值在左半区或就是
nums[mid]
。
- 若
C++实现
#include <iostream>
#include <vector>
using namespace std;int findMin(vector<int>& nums) {int left = 0, right = nums.size() - 1; // 闭区间while (left < right) { // 循环至left==rightint mid = left + (right - left) / 2;if (nums[mid] > nums[right]) {// 最小值在右半区(mid右侧)left = mid + 1;} else {// 最小值在左半区或mid位置right = mid;}}return nums[left]; // 返回最小值
}int main() {int n;cin >> n;vector<int> nums(n);for (int i = 0; i < n; i++) cin >> nums[i];cout << findMin(nums) << endl;return 0;
}
Python实现
def find_min(nums):left, right = 0, len(nums) - 1 # 闭区间while left < right: # 开区间思想,最终left==rightmid = left + (right - left) // 2if nums[mid] > nums[right]:left = mid + 1 # 最小值在右半区else:right = mid # 最小值在左半区或midreturn nums[left]# ACM输入处理
n = int(input())
nums = list(map(int, input().split()))
print(find_min(nums))
关键点:
- 循环条件:使用
left < right
,最终left
等于right
时即为最小值。 - 判断逻辑:若中点值大于右端点,说明旋转点在中点右侧,否则在左侧或中点本身。
二分查找规律再抽象
-
题型分类:
类型 典型题目 二分范围 验证逻辑 基础查找 704. 二分查找 [0, n-1]
直接比较元素值 边界查找 34. 左右边界 [0, n-1]
调整条件为 >=
或<=
数值计算 69. 平方根 [1, x]
判断平方值与x的大小关系 最小值最大化 410. 分割数组的最大值 [max(nums), sum(nums)]
贪心分割,统计子数组数量 最大值最小化 875. 爱吃香蕉的珂珂 [1, max(nums)]
计算时间是否达标 旋转数组搜索 33. 搜索旋转排序数组 [0, n-1]
判断有序区间,缩小搜索范围 旋转数组找最小值 153. 寻找旋转排序数组最小值 [0, n-1]
比较中点与右端点,确定最小值位置 -
通用步骤:
- 确定二分范围:根据题意确定左边界和右边界(如数值范围、下标范围)。
- 定义区间类型:选择闭区间
[left, right]
或开区间[left, right)
,统一循环条件。 - 编写验证函数:判断当前中点是否可行,或确定目标值所在区间。
- 更新边界:根据验证结果收缩左边界或右边界,直至找到解。
-
关键技巧:
- 防溢出:中点计算用
left + (right-left)/2
,涉及大数用long
(C++)或自动类型(Python)。 - 旋转数组处理:通过比较中点与端点,判断有序区间,避免直接找旋转点。
- 上下取整:最大值最小化问题中常用
(a + b - 1) // b
实现向上取整。
- 防溢出:中点计算用
快速记忆要点(终极版)
-
二分条件:
- 数组有序、旋转有序、或可转化为有序的极值问题(如最小值最大化)。
-
区间模板:
- 闭区间:
left <= right
,适用于找精确值或边界(如704、34)。 - 开区间:
left < right
,适用于逐步逼近解(如153)。
- 闭区间:
-
题型对应逻辑:
- 基础查找:直接比较,收缩边界。
- 边界查找:用
>=
或<=
调整条件,验证结果有效性。 - 极值问题:定义可行解区间,用贪心或数学方法验证可行性。
- 旋转数组:利用有序子数组性质,缩小搜索范围。
-
代码框架:
while (left <= right) {mid = left + (right - left) / 2;if (条件成立) {收缩左边界或记录解;} else {收缩右边界;} }
通过“题型分类→区间定义→验证逻辑”的三层思维,可快速套用二分查找解决各类问题。建议结合题目练习,重点掌握旋转数组和极值问题的变种处理!