二分查找总结——模板+例题
@二分查找总结——模板+例题
前言
二分查找可能是很多人学习的第一类算法。在之前的学习中,没有系统总结这类算法,导致我在刷 LeetCode 时常常陷入边界判断不清的窘境。在这篇文章里,我总结了二分查找三个模版,并且表明边界判断,同时附上例题,希望对读者有所帮助。
二分查找模板 I
模板 I 是最标准的二分查找模板,也是大家最常用的模板,用于查找可以通过访问数组中的单个索引来确定的元素或条件。
代码
int binarySearch(vector<int>& nums, int target) {
if (nums.empty()) return -1;
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) left = mid + 1;
else right = mid - 1;
}
return -1;
}
关键属性
- 最基础、最常用的二分查找模板
- 查找过程与元素两侧的无关,即不需要与周边元素进行比较或其他计算
- 不需要在剩余空间中查找是否还有符合条件的元素,因为在每一步中算法都在检查是否找到了元素
边界判断与语法
- 初始条件:left = 0, right = nums.size() - 1
- 终止条件:left > right
- 向左查找:right = mid - 1
- 向右查找:left = mid + 1
例题:LeetCode33-搜索旋转数组
题目描述
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 级别。
算法思路
一般看到时间复杂度级别为 的这一要求,基本就是二分查找算法。
这道题是二分搜索的一个提升版。旋转后,左半序列或右半序列是升序的,如果想要套用模板 I,需要不断缩小左右边界,使得目标处于从左边界到右边界的升序子序列中。
- 如果nums[mid]等于target,返回mid
- 如果左半序列为升序序列,意味着nums[left] <= nums[mid](这里的等号不是因为重复元素,而是包含当序列只有两个元素的情况)
- 如果目标值在左边界到中间索引之间,意味着nums[left] <= target < nums[mid],则向左搜索(这里的等号便于判断左边界);
- 否则,向右搜索,舍弃左边无用序列;
- 如果右半序列为升序序列
- 如果目标值在中间索引到右边界之间,意味着nums[mid] < target <= nums[right],则向右搜索;
- 否则,向左搜索,舍弃右边无用序列;
- 如果目标不在序列中,返回-1;
代码实现
class Solution {
public:
int search(vector<int>& nums, int target) {
if (nums.empty()) return -1;
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) return mid;
if (nums[left] <= nums[mid]) {
if (nums[left] <= target && target < nums[mid])
right = mid - 1;
else
left = mid + 1;
} else {
if (nums[mid] < target && target <= nums[right])
left = mid + 1;
else
right = mid - 1;
}
}
return -1;
}
};
二分查找模板 II
模板 II 是二分查找的高级模板。与模板 I 不同的是,它用于查找需要访问数组中当前索引及其直接右邻居索引的元素或条件。在实际使用中,需根据特定场景下设置特殊的初始化边界值,各位读者需灵活使用。
代码
int binarySearch(vector<int>& nums, int target) {
if (nums.empty()) return -1;
int left = 0, right = nums.size();
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;
}
if (left != nums.size() && nums[left] == target) return left;
return -1;
}
关键属性
- 一种实现二分查找的高级方法
- 查找需要访问元素的直接右邻居
- 使用元素右邻居来确定是否满足条件,并决定向左还是向右
- 保证查找空间在每一步至少有 2 个元素
- 需要在最后进行判断,当循环 / 递归结束时,需要判断剩下的 1 个元素是否符合条件
边界判断与语法
- 初始条件:left = 0, right = nums.size()
- 终止条件:left = right
- 向左查找:right = mid
- 向右查找:left = mid + 1
例题:LeetCode153-寻找旋转排序数组中的最小值
题目描述
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
你可以假设数组中不存在重复元素。
算法思路
本题与 LeetCode 33 题类似,旋转后只有左半序列或右半序列是升序的,所以最小元素到右边界一定是升序的。基于此,我们可以确定判断条件,nums[mid] < nums[right]。
- 如果最小元素在右半序列,意味着nums[mid] > nums[right],则向右搜索;
- 否则,向左搜索;
代码实现
class Solution {
public:
int findMin(vector<int>& nums) {
int left = 0, right = nums.size() - 1;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) left = mid + 1;
else right = mid;
}
return nums[left];
}
};
二分查找模板 III
模板 III 是二分查找的另一种独特形式。 它用于搜索需要访问当前索引及其在数组中的直接左右邻居索引的元素或条件。
代码
int binarySearch(vector<int>& nums, int target) {
if (nums.empty()) return -1;
int left = 0, right = nums.size() - 1;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) return mid;
else if (nums[mid] < target) left = mid;
else right = mid;
}
if (nums[left] == target) return left;
if (nums[right] == target) return right;
return -1;
}
关键属性
- 实现二分查找的另一种方法
- 查找时需要访问元素的直接左右邻居
- 使用元素的邻居来确定是向左查找还是向右查找
- 查找空间在每步中至少有 3 个元素
- 需要在最后进行判断,当循环 / 递归结束时,需要评估剩下的 2 个元素是否符合条件
边界判断与语法
- 初始条件:left = 0, right = nums.size() - 1
- 终止条件:left + 1 == right
- 向左查找:right = mid
- 向右查找:left = mid
例题:LeetCode658-找到 K 个最接近元素
题目描述
给定一个排序好的数组,两个整数 k 和 x,从数组中找到最靠近 x(两数之差最小)的 k 个数。返回的结果必须要是按升序排好的。如果有两个数与 x 的差值一样,优先选择数值较小的那个数。
说明:
- k 的值为正数,且总是小于给定排序数组的长度
- 数组不为空,且长度不超过
- 数组里的每个元素与 x 的绝对值不超过
算法思路
这道题之所以适合模板 III,是因为:
- 本题是寻找最接近的元素
- 寻找到元素后需要向左向右拓展
算法在最初设置好初始值后,要判断 x 是在左半序列还是右半序列
- 如果 x 位于左半序列,意味着x <= arr[mid],则向左查找
- 否则,向右查找
在确定最接近 x 的左右边界值后,同时向左向右扩展,将最接近 x 的值存储在双向队列中(双向队列可以避免后续排序)
- 如果右边界抵达数组最后一个元素或者左边界有效且左边界的值距 x 比右边界更近,则将左边界值插入双向队列头部
- 否则,将右边界值插入双向队列尾部
代码实现
class Solution {
public:
vector<int> findClosestElements(vector<int>& arr, int k, int x) {
if (arr.size() == 1) return arr;
int left = 0, right = arr.size() - 1;
while (left + 1 < right) {
int mid = left + (right - left) / 2;
if (x <= arr[mid]) right = mid;
else left = mid;
}
deque<int> res;
while (res.size() < k) {
if (right == arr.size() || (left >= 0 && x - arr[left] <= arr[right] - x)
res.push_front(arr[left--]);
else
res.push_back(arr[right++]);
}
return vector<int>(res.begin(), res.end());
}
};
参考资料
[1] LeetCode二分查找章节
@ 北京·海淀 2019.11.29