二分查找可能是很多人学习的第一类算法。在之前的学习中,没有系统总结这类算法,导致我在刷 LeetCode 时常常陷入边界判断不清的窘境。在这篇文章里,我总结了二分查找三个模版,并且表明边界判断,同时附上例题,希望对读者有所帮助。
模板 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;
}
题目描述
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O ( l o g n ) O(log n) O(logn) 级别。
算法思路
一般看到时间复杂度级别为 O ( l o g n ) O(log n) O(logn) 的这一要求,基本就是二分查找算法。
这道题是二分搜索的一个提升版。旋转后,左半序列或右半序列是升序的,如果想要套用模板 I,需要不断缩小左右边界,使得目标处于从左边界到右边界的升序子序列中。
代码实现
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 是二分查找的高级模板。与模板 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;
}
题目描述
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
你可以假设数组中不存在重复元素。
算法思路
本题与 LeetCode 33 题类似,旋转后只有左半序列或右半序列是升序的,所以最小元素到右边界一定是升序的。基于此,我们可以确定判断条件,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 是二分查找的另一种独特形式。 它用于搜索需要访问当前索引及其在数组中的直接左右邻居索引的元素或条件。
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;
}
题目描述
给定一个排序好的数组,两个整数 k 和 x,从数组中找到最靠近 x(两数之差最小)的 k 个数。返回的结果必须要是按升序排好的。如果有两个数与 x 的差值一样,优先选择数值较小的那个数。
说明:
算法思路
这道题之所以适合模板 III,是因为:
算法在最初设置好初始值后,要判断 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