五大算法之二分搜索

概述

  二分搜索是常见的搜索算法,能够将有序数组搜索的线性复杂度降低到对数级别。搜索过程每次取搜索区间内的中间元素,如果等于目标元素则直接返回结果;如果大于或小于目标元素,则将搜索区间缩短到对应的一半元素范围,继续搜索,直至搜索区间为空。当然二分搜索不限于找目标值,寻找左侧边界、寻找右侧边界也是常见的搜索场景。

时间复杂度: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];
    }
};

 


 

posted @ 2022-07-28 00:13  BobPong  阅读(103)  评论(0编辑  收藏  举报