五大算法之二分搜索
概述
二分搜索是常见的搜索算法,能够将有序数组搜索的线性复杂度降低到对数级别。搜索过程每次取搜索区间内的中间元素,如果等于目标元素则直接返回结果;如果大于或小于目标元素,则将搜索区间缩短到对应的一半元素范围,继续搜索,直至搜索区间为空。当然二分搜索不限于找目标值,寻找左侧边界、寻找右侧边界也是常见的搜索场景。
时间复杂度:O(logN),搜索区间逐次减半。
空间复杂度:O(1),只占用常数量级空间。
核心思想
二分搜索思路比较简单,正是因为简单,大部分人会忽略细节的理解,比如,是left = mid + 1 还是 right = mid - 1,是 while(left < right) 还是 while(left <= right) ,这些都是经常让人困惑的点。只有理解二分搜索的核心思想,才能在一些细节上不含糊。二分搜索核心要素有以下几点:
-
搜索区间
搜索区间指的是每次搜索时的元素区间段,是[left, right] 还是 [left, right),这个是在搜索之初就需要确定好的。前闭后闭则循环条件为 left <= right;前闭后开则循环条件为 left < right。
-
目标边界
目标边界指的是搜索目标在有序数组中的边界条件,通常我们会考虑搜索目标是左边界还是右边界,比如,升序重复数组中查找目标值第一个位置、最后一个位置。
右边界也可以变成左边界,比如,升序重复数组中查找目标值最后一个位置,可以变成大于目标值的第一个位置,结果再减一。
更重要的是,根据边界选择判断条件,无论是左边界还是右边界,都是将搜索区间分成两段,如何区分是二分搜索的关键。
算法框架
既然右边界搜索可以转化为左边界搜索,单一元素查找也可以转为左边界搜索,那接下来就左边界搜索给出算法的通用框架,基本能够涵盖大部分二分搜索场景。
int findLeftBorder(vector<int>& nums) { int left = 0, right = nums.size(); while (left < right) { int mid = left + (right - left) / 2; if (mid满足右分区条件) { right = mid; } else { left = mid + 1; } } return left < nums.size() ? left : -1; }
例题分析
例题1(LeetCode - 34)
如题,典型的求目标值左右边界,我们的思路是都转成左边界问题。那么搜索区间我们设置为左闭右开(可以按习惯来,框架给的是左闭右开),则循环条件为 left < right。边界判断条件对应左右边界有所不同,左边界求解划分的右区间判断条件为 nums[i] >= target;右边界求解划分的右区间判断条件为 nums[i] >= target + 1,得到右分区左边界再减1得到最终目标右边界。
将相关逻辑填入框架,代码如下:
class Solution { public: vector<int> searchRange(vector<int>& nums, int target) { vector<int> res = {-1, -1}; int left = findLeftBorder(nums, target); int right = findLeftBorder(nums, target + 1) - 1; if (left < nums.size() && nums[left] == target) { res[0] = left; res[1] = right; } return res; } int findLeftBorder(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; } };
例题2(LeetCode - 153)
如题,同样我们还是转成左边界问题,那么以目标值为界,我们需要将数组分为左右两段,判断条件如何选择呢?如下图所示,我们可以发现右分区所有元素都小于等于最右的元素,当然图中右分区所有元素也都小于最左元素。那是否可以将nums[i] < nums[0] 作为有分区条件呢?不要忽略了一种情况,n次旋转后数组排序不变,那么此时右分区为整个数组,nums[i] < nums[0] 条件不成立,而nums[i] < nums[n - 1]恒成立。
将相关逻辑填入框架,代码如下:
class Solution { public: int findMin(vector<int>& nums) { int left = 0, right = nums.size(); int base = nums[nums.size() - 1]; while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] <= base) { right = mid; } else { left = mid + 1; } } return nums[left]; } };