跳到主要内容 二分查找算法详解与经典例题解析 | 极客日志
C++ 算法
二分查找算法详解与经典例题解析 系统讲解了二分查找算法的核心原理及多种变体应用。涵盖基础二分查找、查找元素首尾位置、搜索插入位置、计算平方根、寻找山峰数组峰顶、寻找峰值、旋转排序数组最小值以及缺失数字问题。通过对比暴力解法与二分优化方案,分析了时间复杂度为 O(log n) 的实现细节,包括指针移动逻辑、边界条件处理及防止溢出的技巧。适合希望深入理解二分查找应用场景的开发者阅读。
人间失格 发布于 2026/3/28 更新于 2026/4/17 6 浏览1. 二分查找
题目链接: 704. 二分查找
题目描述:
给定一个升序排列的整数数组 nums,和一个目标值 target。如果 target 在数组中存在,返回其下标;否则,返回 -1。
示例 1:
输入:nums = [-1,0,3,5,9,12], target = 9
输出:4
解释:9 出现在 nums 中,并且下标为 4。
示例 2:
输入:nums = [-1,0,3,5,9,12], target = 2
输出:-1
解释:2 不存在 nums 中,因此返回 -1。
提示:
你可以假设数组中的所有元素是互不相同的。
数组 nums 的长度范围为 [1, 10000]。
数组 nums 的每个元素都在 [-9999, 9999] 之间。
解题思路
方法一:暴力遍历
从前往后遍历数组,遇到与 target 相等的数,返回下标。
class Solution {
public :
int search (vector<int >& nums, int target) {
for (int i = 0 ; i < nums.size (); ++i) {
if (nums[i] == target) return i;
}
return -1 ;
}
};
但是这种方法时间复杂度是 O(n) ,且没有利用到数组的有序性,略不足。
方法二:二分查找
利用数组的有序性,先从数组的中间位置的数开始比较,根据比较结果将数组的查找范围缩小。
具体过程如下:
初始化 left 和 right 指针分别指向数组的开头和末尾。
计算中间位置 。( 计算过程中结果可能会溢出,不采用。)
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
mid = left + (right - left) / 2
mid = (left + right) / 2
比较中间元素与目标值:
若 nums[mid] == target,返回 mid。
若 nums[mid] > target,根据有序性,mid 右边部分的数肯定比 target 大,舍去右边区间,target 应该在数组的左半部分,更新 right = mid - 1;
若 nums[mid] < target,mid 左边部分的数肯定比 target 小,舍去左边区间,target 应该在数组的右半部分,更新 left = mid + 1。
当 left > right 时,结束循环,target 不在数组内,返回 -1。 class Solution {
public :
int search (vector<int >& nums, int target) {
int left = 0 , right = nums.size () - 1 ;
while (left <= right) {
int mid = (right - left) / 2 + left;
if (target > nums[mid]) left = mid + 1 ;
else if (target < nums[mid]) right = mid - 1 ;
else return mid;
}
return -1 ;
}
};
时间复杂度 :每次比较都会将查找范围缩小一半,时间复杂度为 O(log n)。
空间复杂度 :O(1)
2. 在排序数组中查找元素的第一个和最后一个位置 给定一个按非递减顺序排列的整数数组 nums,和一个目标值 target,请找出给定目标值在数组中的开始位置 和结束位置 。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3, 4]
解释:目标值 8 在数组中的起始位置为下标 3,结束位置为下标 4。
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1, -1]
解释:目标值 6 不存在于数组中,因此返回 [-1, -1]。
输入:nums = [], target = 0
输出:[-1, -1]
解释:空数组中没有任何目标值,因此返回 [-1, -1]。
0 <= nums.length <= 10^5。
-10^9 <= nums[i] <= 10^9。
nums 是一个非递减数组。
-10^9 <= target <= 10^9。
题目要求实现时间复杂度为 O(log n) 的算法去找到目标值在数组中的开始位置 和结束位置 ,我们容易就想到了二分查找 解决问题。
所以我们只需要利用两次二分查找找到目标值区间在数组中的左端点和右端点即可。
定义指针 left 和 right 分别指向数组的开头与末尾,计算数组中间位置 mid = left + (right - left) / 2。
若 nums[mid] < target,说明目标值在右边,舍弃左边区间,更新 left = mid + 1。
若 nums[mid] >= target,说明目标值在 mid 位置或者 mid 的左边,舍弃右边区间,更新 right = mid。
当 left == right 时,结束循环。
检查 nums[left] 是否与目标值相等 ,若不相等,说明目标值不在数组内,返回 {-1, -1};若相等,说明找到目标值区间的左端点,用变量 LeftPoint 记录左端点,继续查找其右端点。
查找右端点:
定义指针 left 和 right 分别指向数组的开头与末尾,计算数组中间位置 mid = left + (right - left + 1) / 2。
若 nums[mid] > target,说明目标值在左边,舍弃右边区间,更新 right = mid - 1。
若 nums[mid] <= target,说明目标值在 mid 位置或者 mid 的右边,舍弃左边区间,更新 left = mid。
当 left == right 时,结束循环。
循环条件:left < right,如果是取等号,会有死循环的风险 。
以查找左端点为例,当循环进行到 left == right 时,如果 nums[mid] < target 时,更新 left = mid + 1,可以结束循环,但是如果 nums[mid] >= target 时,更新 right = mid,left、right、mid 将会一直处于同一位置,程序就会一直循环;查找右区间也是同理。
计算中间下标的不同,查找左区间:mid = left + (right - left) / 2,查找右区间:mid = left + (right - left + 1) / 2。
这两种计算下标的方式只有在数组元素是偶数个时才会有所不同 ,前者得到的中间偏左的位置,后者得到的是中间偏右的位置 。如果错误使用这两种计算中间下标的方式,程序也是可能会死循环的。还是以查找左端点为例,如果使用的查找右端点的方式,当 left 和 right 相邻时,计算得到的 mid 就会等于 right,如果 nums[mid] >= target,更新 right = mid,程序就会陷入死循环。
class Solution {
public :
vector<int > searchRange (vector<int >& nums, int target) {
if (nums.size () == 0 ) return {-1 , -1 };
int left = 0 , right = nums.size () - 1 ;
int mid = 0 ;
while (left < right) {
mid = left + (right - left) / 2 ;
if (nums[mid] < target) left = mid + 1 ;
else right = mid;
}
int Leftpoint = 0 ;
if (nums[left] != target) return {-1 , -1 };
else Leftpoint = left;
right = nums.size () - 1 ;
while (left < right) {
mid = left + (right - left + 1 ) / 2 ;
if (nums[mid] > target) right = mid - 1 ;
else left = mid;
}
return {Leftpoint, right};
}
};
时间复杂度 :O(log n)
空间复杂度 :O(1)
3. 搜索插入位置 给定一个排序数组 nums 和一个目标值 target,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你必须设计并实现时间复杂度为 O(log n) 的算法。
输入:nums = [1,3,5,6], target = 5
输出:2
输入:nums = [1,3,5,6], target = 2
输出:1
输入:nums = [1,3,5,6], target = 7
输出:4
1 <= nums.length <= 10^4
10^4 <= nums[i] <= 10^4
nums 为 无重复元素的升序排列数组
10^4 <= target <= 10^4
根据题目要求,我们应该要使用二分查找算法来解决问题。
分析题目,我们得出题目要求的是查找大于等于 target 的区间的左端点,因为当 target 在数组中不存在时,第一个大于 target 的元素下标就是插入位置(示例 2)。
定义指针 left 和 right 分别指向数组的开头与末尾。
计算数组中间位置 mid = left + (right - left) / 2。
若 nums[mid] < target,说明目标位置在右边,舍弃左边区间,更新 left = mid + 1。
若 nums[mid] >= target,说明目标位置在 mid 位置或者 mid 的左边,舍弃右边区间,更新 right = mid。
当 left == right 时,结束循环。
边界情况的处理:
如果数组的元素全都小于 target 的(示例 3),那么插入位置应该数组最后一个元素的下一个位置(下标为数组元素大小的位置)。
返回结果 left。
class Solution {
public :
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) right = mid;
else left = mid + 1 ;
}
if (nums[left] < target) return left + 1 ;
return left;
}
};
时间复杂度 :O(log n)
空间复杂度 :O(1)
4. x 的平方根 给定一个非负整数 x,计算并返回 x 的算术平方根。由于返回类型是整数,结果只保留整数部分,小数部分将被舍去。
**注意:**不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5。
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842...,由于返回类型是整数,小数部分将被舍去。
遍历 0 到 x 的平方根 i,若 i * i == x,返回 i;若 i * i > x,返回 i - 1。
class Solution {
public :
int mySqrt (int x) {
long long i = 0 ;
for (i = 0 ; i <= x; i++) {
if (i * i == x) return i;
if (i * i > x) return i - 1 ;
}
return -1 ;
}
};
时间复杂度是 O(sqrt(x)),不是最优解法。
由于 0 到 x 的平方 i * i,是有序的,具有二段性,因此我们可以使用二分查找算法解决问题。
进一步分析题目,其实要查找的是 i 的平方小于等于 x 区间的右端点 。
定义 left 指向 1,right 指向 x。
计算中间位置 mid = left + (right - left + 1) / 2(查找右端点)。
若 mid * mid <= x,说明平方根在 mid 位置或者 mid 的右边,舍弃左边区间,更新 left = mid。
若 mid * mid > x,说明平方根在 mid 的左边区间,舍弃右边区间,更新 right = mid - 1。
当 left == right 时,结束循环,返回结果 left。
细节问题:mid 的类型应该是 long long,因为两数相乘可能超过整型最大值。
class Solution {
public :
int mySqrt (int x) {
if (x == 0 ) return 0 ;
int left = 1 , right = x;
while (left < right) {
long long mid = left + (right - left + 1 ) / 2 ;
if (mid * mid <= x) left = mid;
else right = mid - 1 ;
}
return left;
}
};
时间复杂度 :O(log n)
空间复杂度 :O(1)
5. 山峰数组的峰顶
arr.length >= 3
存在索引 i(0 < i < arr.length - 1),使得:
arr[0] < arr[1] < ... < arr[i-1] < arr[i]
arr[i] > arr[i+1] > ... > arr[arr.length - 1]
给定一个由整数组成的山脉数组 arr,返回其峰顶索引 i,即满足条件 arr[i] 是整个数组中最大的元素,并且左右两边的数据呈现先升后降的趋势。
输入:arr = [24,69,100,99,79,78,67,36,26,19]
输出:2
3 <= arr.length <= 10^4
0 <= arr[i] <= 10^6
题目保证 arr 是山脉数组。
遍历数组的每一个元素,找到一个大于两边相邻元素的数,返回其下标即可。
class Solution {
public :
int peakIndexInMountainArray (vector<int >& arr) {
int n = arr.size ();
for (int i = 1 ; i < n - 1 ; i++) {
if (arr[i] > arr[i - 1 ] && arr[i] > arr[i + 1 ]) {
return i;
}
}
return -1 ;
}
};
由题目可以知道,一是山峰数组是没有重复元素 的;二是山峰数组是具有二段性 的,整个山峰数组只有上升趋势 和下降趋势 ,所以我们可以使用二分查找 算法来解决问题;
进一步分析题目,题目要求的峰顶索引其实就是上升趋势区间的右端点或者下降趋势区间的左端点。
初始化 left 和 right 分别指向山峰数组的开头与末尾。
计算山峰数组中间位置 mid = left + (right - left + 1) / 2。
若 arr[mid] > arr[mid - 1],说明 mid 处于上升趋势,峰顶元素在 mid 位置或者 mid 的右边,舍弃左边区间,更新 left = mid。
若 arr[mid] < arr[mid - 1],说明 mid 处于下降趋势,峰顶元素在 mid 的左边,舍弃右边区间,更新 right = mid - 1。
当 left == right 时,结束循环。
返回峰顶索引 left。
class Solution {
public :
int peakIndexInMountainArray (vector<int >& arr) {
int left = 1 , right = arr.size () - 2 ;
while (left < right) {
int mid = left + (right - left + 1 ) / 2 ;
if (arr[mid] > arr[mid - 1 ]) left = mid;
else right = mid - 1 ;
}
return left;
}
};
class Solution {
public :
int peakIndexInMountainArray (vector<int >& arr) {
int left = 1 , right = arr.size () - 2 ;
while (left < right) {
int mid = left + (right - left) / 2 ;
if (arr[mid] > arr[mid + 1 ]) right = mid;
else left = mid + 1 ;
}
return left;
}
};
时间复杂度 :O(log n)
空间复杂度 :O(1)
6. 寻找峰值 峰值元素是指其值严格大于左右相邻元素的元素。给定一个整数数组 nums,找到任意一个峰值元素并返回其索引。你可以假设 nums[-1] = -∞ 和 nums[n] = -∞,即数组两端可以视为负无穷。
你必须实现时间复杂度为 O(log n) 的算法来解决此问题。
输入:nums = [1,2,3,1]
输出:2
解释:3 是峰值元素,返回其索引 2。
输入:nums = [1,2,1,3,5,6,4]
输出:1 或 5
解释:函数可以返回索引 1(峰值元素为 2),或返回索引 5(峰值元素为 6)。
1 <= nums.length <= 1000
-2^31 <= nums[i] <= 2^31 - 1
数组中的所有元素 nums[i] 均不同。
初始化 left 和 right 分别指向数组的开头与末尾。
计算数组中间位置 mid = left + (right - left ) / 2。
若 nums[mid] > nums[mid + 1],说明在下降趋势,峰值一定存在于 mid 位置或者 mid 左侧,mid 右侧可能存在峰值 ,因此舍弃右边区间,更新 right = mid。
若 nums[mid] < nums[mid + 1],说明在上升降趋势,峰值一定存在于 mid 右侧 ,舍弃左边区间,更新 left = mid + 1。
当 left == right 时,结束循环。
返回峰值索引 left。
class Solution {
public :
int findPeakElement (vector<int >& nums) {
int left = 0 , right = nums.size () - 1 ;
while (left < right) {
int mid = left + (right - left) / 2 ;
if (nums[mid] > nums[mid + 1 ]) right = mid;
else left = mid + 1 ;
}
return left;
}
};
class Solution {
public :
int findPeakElement (vector<int >& nums) {
int left = 0 , right = nums.size () - 1 ;
while (left < right) {
int mid = left + (right - left + 1 ) / 2 ;
if (nums[mid] > nums[mid - 1 ]) left = mid;
else right = mid - 1 ;
}
return left;
}
};
时间复杂度 :O(log n)
空间复杂度 :O(1)
7. 搜索旋转排序数组中的最小值 给定一个按升序排列的整数数组 nums,数组中的值互不相同 。数组在传递给函数之前,在某个未知的下标 k(0 <= k < nums.length)上进行了旋转 ,使得数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]] 的形式。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。
输入:nums = [4,5,6,7,0,1,2]
输出:0
输入:nums = [3,4,5,1,2]
输出:1
输入:nums = [11,13,15,17]
输出:11
n == nums.length
1 <= n <= 5000
-5000 <= nums[i] <= 5000
数组中的所有整数互不相同
数组已按升序排列,并进行了旋转
分析题目,我们不难得出旋转数组是具有二段性的,整个旋转数组可以根据数组的末尾元素 last 的比较划分出两个区间。
根据二段性,查找最小值其实就是找小于等于 last 元素区间的左端点 ,我们就可以得出具体二分算法的步骤:
初始化 left 和 right 分别指向数组的开头与末尾。
计算数组中间位置 mid = left + (right - left ) / 2。
若 nums[mid] > last,说明 mid 处于大于 last 的区间,最小值在 mid 的右边,舍弃 mid 左边区间,更新 left = mid + 1。
若 nums[mid] <= last,说明 mid 处于小于等于 last 的区间,最小值在 mid 位置或者 mid 的左边,舍弃 mid 右边区间,更新 right = mid。
当 left == right 时,结束循环,返回最小值 left。
其实这道题也是可以以数组的起始元素作为基准值来划分区间 ,由此推出与上述不同的另一种二分算法 ,但是该二分算法没有办法处理旋转数组完全升序或者完全降序的情况 ,而上述的二分算法是可以处理两种旋转数组的特殊情况的。
class Solution {
public :
int findMin (vector<int >& nums) {
int left = 0 , right = nums.size () - 1 ;
int last = nums[right];
while (left < right) {
int mid = left + (right - left) / 2 ;
if (nums[mid] > last) left = mid + 1 ;
else right = mid;
}
return nums[left];
}
};
时间复杂度 :O(log n)
空间复杂度 :O(1)
8. 0~n-1 中缺失的数字 一个长度为 n-1 的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围 0~n-1 之内。在范围 0~n-1 内的 n 个数字中有且只有一个数字不在该数组中,请找出这个数字。
输入:[0,1,2,3,4,5,6,7,9]
输出:8
注意到 records 数组的大小是 10000,因为缺失了一个数字,所以我们直接开一大小为 10001 的数组作为哈希表,将 record 数组的元素放进哈希表,再遍历哈希表来查找缺失的元素 。
class Solution {
public :
int takeAttendance (vector<int >& records) {
int n = records.size ();
int hash[10001 ] = {0 };
for (auto e : records) hash[e]++;
for (int i = 0 ; i <= n; ++i) {
if (hash[i] == 0 ) return i;
}
return -1 ;
}
};
直接遍历数组,找到数组元素与其下标不相等的元素 ,其下标就是缺失的元素 。
class Solution {
public :
int takeAttendance (vector<int >& records) {
for (int index = 0 ; index < records.size (); index++) {
if (records[index] != index) return index;
}
return records.size ();
}
};
在不缺失任何一个数的情况,将 0 到 n - 1 全部异或,再将得到的数与 records 的元素异或,根据异或的性质就可以得到缺失的数。
class Solution {
public :
int takeAttendance (vector<int >& records) {
int LastElement = records.size ();
int target = 0 ;
for (int i = 0 ; i <= LastElement; ++i) target ^= i;
for (auto e : records) target ^= e;
return target;
}
};
在不缺失数的情况下,计算 0 到 n - 1 的和,然后将去缺失元素数组的元素,最后得到缺失的值。
class Solution {
public :
int takeAttendance (vector<int >& records) {
int LastElement = records.size ();
int sum = LastElement * (LastElement + 1 ) / 2 ;
for (auto e : records) sum -= e;
return sum;
}
};
分析题目,给定缺失元素的数组是具有二段性 的,可以根据数组元素与其下标是否相等划分出两个区间:数组元素与其下标相等的区间和数组元素与其下标不相等的区间 ,题目要求的缺失的数其实就是数组元素与其下标不相等的区间的左端点 ,我们就可以用二分查找来解决问题。
初始化 left 和 right 分别指向数组的开头与末尾。
计算数组中间位置 mid = left + (right - left ) / 2。
若 nums[mid] == mid,说明 mid 处于相等区间,缺失值在 mid 右边,舍弃左边区间,更新 left = mid + 1。
若 nums[mid] != mid,说明 mid 处于不相等区间,缺失值在 mid 位置或者 mid 左边,舍弃右边区间,更新 right = mid。
当 left == right 时,结束循环。
特殊情况的处理,当缺失了最后一个数字时,整个数组的元素都与其下标相等 ,结束循环 left 指向数组的最后一个位置,此时要返回(缺失的元素) left + 1,其他情况返回 left 即可。
class Solution {
public :
int takeAttendance (vector<int >& records) {
int left = 0 , right = records.size () - 1 ;
while (left < right) {
int mid = left + (right - left) / 2 ;
if (records[mid] == mid) left = mid + 1 ;
else right = mid;
}
return records[left] == left ? left + 1 : left;
}
};
时间复杂度 :O(log n)
空间复杂度 :O(1)