zoukankan      html  css  js  c++  java
  • 二分查找总结

    最近刷leetcode,做了挺多道二分查找的题目,感觉二分查找并没有想象中的简单,一些变形题还是有一定难度的,所以这里做一个总结,方便自己以后复习。

    首先,二分查找一般有两种写法

    左闭右闭写法

    int search(vector<int> &nums, int target){
        int left = 0, right = nums.size() - 1, mid; //注意,right的初始值是nums.size()-1,而不是nums.size()
        while (left <= right){ //注意,是≤,不是<
            mid = left + ((right - left) >> 1); //注意,写成 mid = (left+right)/2 有可能发生溢出,除法换成移位操作可以提升计算速度,移位运算记得加括号,因为优先级比加号低
            if (nums[mid] == target){ //找到了
                return mid;
            }
            else if (nums[mid] < target){ //更新左边界
                left = mid + 1;
            }
            else{ //更新右边界
                right = mid - 1;
            }
        }
    
        return -1; //注意,如果执行到这里来了,说明没有找到目标元素,返回-1,因为如果找得到目标元素,则一定会进入循环里面的 if(nums[mid] == target)
    }
    

    左闭右开写法

    int search(vector<int> &nums, int target){
        int left = 0, right = nums.size(), mid; //注意,right的初始值是nums.size(),而不是nums.size()-1
        while (left < right){ //注意,是<,不是≤
            mid = left + ((right - left) >> 1); //同上
            if (nums[mid] == target){ //找到了
                return mid;
            }
            else if (nums[mid] < target){ //更新左边界
                left = mid + 1;
            }
            else{ //更新右边界,注意,这种情况下不是 mid-1
                right = mid;
            }
        }
    
        return -1;
    }
    

    以上两种写法在大部分情况下是等价的,不过有些题目却只能用其中一种写法来书写,这个还是得依据具体的题目去分析。

    但是,从以上两种写法,我们发现,在二分查找题目中,有这几点是需要我们注意的:

    • right的初始值,是选择 nums.size(),还是选择 nums.size()-1
    • 循环条件,是 left < right,还是 left <= right
    • right如何更新,是 right = mid,还是 right = mid - 1
    • 可能还有一点,就是有些时候循环最后还要返回一个值,是返回 left,还是返回 mid

    所以,其实你去仔细思考的话,二分查找还是不那么容易想明白的。

    下面我就记录一下几道自己在leetcode上做到的二分查找题,个人感觉有一定的代表性。




    34. Find First and Last Position of Element in Sorted Array

    这道题我在 网上 看到这么一种写法:
    先找第一个大于等于target的数,记这个位置为low;然后再找第一个大于等于target+1的数,记这个位置为high。那么target的范围就是{low, high-1}。
    于是我们先得写一个子函数,就是找到第一个大于等于target的数的位置

    class Solution {
    public:
        vector<int> searchRange(vector<int>& nums, int target) {
            int start = firstGreaterEqual(nums, target);
            if (start == nums.size() || nums[start] != target) return {-1, -1};
            return {start, firstGreaterEqual(nums, target + 1) - 1};
        }
    
        int firstGreaterEqual(vector<int>& nums, int target) {
            int left = 0, right = nums.size();
            while (left < right) {
                int mid = left + (right - left) / 2;
                if (nums[mid] < target) left = mid + 1;
                else right = mid;
            }
            return right;
        }
    };
    

    个人觉得不是很好想出来,而且你会发现,firstGreaterEqual这个函数一些变量的更新和之前标准的二分查找有点不一样了。

    于是我写了一个更好理解的代码

    class Solution {
    public:
        vector<int> searchRange(vector<int>& nums, int target) {
            int start_idx = find(nums, target, 0);
            if(start_idx == -1) return {-1, -1};
            else return {start_idx, find(nums, target, 1)};
        }
        
        int find(vector<int>& nums, int target, int tag){
            int left = 0, right = nums.size()-1, mid;
            while(left <= right){
                mid = (left + right)/2;
                if(target == nums[mid]){
                    if(tag == 0){//表示找最左边的
                        if(mid > 0 && nums[mid-1] == target) right = mid-1;
                        else return mid;
                    }
                    else{//表示找最右边的
                        if(mid < nums.size()-1 && nums[mid+1] == target) left = mid + 1;
                        else return mid;
                    }
                }
                else if(target < nums[mid]) right = mid-1;
                else left = mid + 1;
            }
    
            return -1;
        }
    };
    

    其他部分跟标准的二分查找一样,就是当我们找到target的时候,不能停下来,我们需要判断是否需要接着往左边找或者往右边找,
    当我们需要查找最左边的哪个target时,我们需要判断当前这个target是否已经是最左边的了,如果不是,那么接着往左边找,同理,找最右边的也是这个逻辑。

    33. Search in Rotated Sorted Array

    读过题目之后,我们发现,Rotated Sorted Array 其实是长下面这个样子的:

    这个图的意思就是数组右半段跑到左边去了,需要注意的是,这个数组是不包含重复数字的,所以 旋转过后左半段的最左边数字 一定大于 右半段的最右边的数字

    所以接下来我们的代码这么写

    class Solution {
    public:
        int search(vector<int>& nums, int target) {
            int left = 0, right = nums.size() - 1;
            while (left <= right) {
                int mid = (left + right)/ 2;
                if (nums[mid] == target) return mid;
                if (nums[mid] <= nums[right]) {
                    if (target > nums[mid] && target <= nums[right]) left = mid + 1;
                    else right = mid - 1;
                }
                else {
                    if (target >= nums[left] && target < nums[mid]) right = mid - 1;
                    else left = mid + 1;
                }
            }
    
            return -1;
        }
    };
    

    注意,当我们发现 target != nums[mid]的时候,接下来该怎么办呢?这个地方估计是很多人卡到的地方,
    主要问题就是我们不知道现在mid是处于左边半段还是右边半段,那么如何确定我们所在的位置呢?就是通过和nums[right]比较,如果小于或者等于nums[right],说明mid在右半段,否则在左半段。

    现在问题又变成了,我们知道了mid在左边还是右边,接下来如何更新left或者right的值呢?

    其实我们发现,无论是到了左半段还是右半段,我们都存在一个接下来往左还是往右继续搜索的问题。所以里面还有一个ifelse判断。

    81. Search in Rotated Sorted Array II

    这个题和上一个题唯一的区别就是 数组中允许出现重复数字,别小看这一个区别,这可能给我们接下来的搜索带来不小的问题。

    现在数组变成了下面这个样子

    就是一开始的时候,nums[left]的值是有可能等于nums[right]的,当然,中间也可能出现这种情况

    那么这怎么解决呢,既然nums[left]可能等于nums[right],那么我们就从nums[left]下手。

    先看一下完整代码:

    class Solution {
    public:
        bool search(vector<int>& nums, int target) {
            int left = 0, right = nums.size()-1, mid;
            while(left <= right){
                mid = (left+right)/2;
                if(target == nums[mid]) return true;
                if(nums[left] == nums[mid]){
                    //无法判断[left...mid]和[mid...right]两个区间哪个是有序的
                    left++;
                }
                else if(nums[mid] <= nums[right]){//[mid...right]区间是递增的
                    if(target > nums[mid] && target <= nums[right]) left = mid+1;
                    else right = mid-1;
                }
                else{//[left...mid]区间是递增的
                    if(target >= nums[left] && target < nums[mid]) right = mid-1;
                    else left = mid+1;
                }
            }
            
            return false;
        }
    };
    

    首先,如果target==nums[mid]的时候我们自然返回true就行,这跟最原始的二分查找是一样的。

    然后,我们发现target != nums[mid]的时候,就去比较nums[mid]和nums[left]的值,注意,nums[left]是可能等于nums[right]的。

    • 如果nums[left] == nums[mid],那么我们并不知道mid是在左半段,还是在右半段,也就无法判断[left...mid]和[mid...right]两个区间哪个是有序的,这种情况下,我们就让left++,因为target此时是不等于nums[left]的,自然可以跳过它

    • 如果nums[mid] <= nums[right],那么分两种情况

      • 一种是刚好等于nums[right],说明这时候nums[left]是不等于nums[left]的,否则就会进上一个判断条件,这说明,mid此时就是在右半段
      • 还有一种就是nums[mid] < nums[right],这时候自然也说明mid是在右半段的,因为左半段的数字都大于等于nums[right]
        于是这时候就回到了上一题的判断逻辑,就是在右半段去看一下接下来往左查询还是往右查询
    • 如果nums[mid] > nums[right],那么自然是去左半段找了,接下来的过程还是分为往左还是往右继续搜索


    注意下面这两道题,同样是基于 Rotated Sorted Array 结构上的查找,虽然可以用二分查找做,但是用分治法明显更简单

    153. Find Minimum in Rotated Sorted Array

    这道题规定了数组的元素不重复
    代码如下:

    class Solution {
    public:
        int findMin(vector<int>& nums) {
            return helper(nums, 0, nums.size()-1);
        }
        
        int helper(vector<int>& nums, int start, int end){
            if(nums[start] <= nums[end]) return nums[start];
            int mid = start + ((end-start)>>1);
            return min(helper(nums, start, mid), helper(nums, mid+1, end));
        }
    };
    

    你会发现这个函数及其简单,试想,加入我们数组没被翻转过,那么你如何快速查找数组的最小值?

    那很简单啊,就是第一个值呀,因为数组已经是按照升序排列的。
    那怎么判断一个序列是不是没有翻转过呢?或者说,怎么知道数组的某一个部分是升序的?
    很简单,假设这个子片段开始下标是start,结束下标是end,那么加入nums[start] < nums[end],说明这个子序列是有序的,其中最小值就是nums[start]。

    但是,我们在递归过程中,可能出现 start等于end 的情况,只包含一个元素的序列也是有序的,
    所以当nums[start] <= nums[end]时,说明这个子序列[start...end]是有序的,直接返回其中的最小值nums[start]
    当然这有一个前提,就是数组中不包含重复的数字,如果包含,当nums[start]==nums[end]的时候,[start...end]是不一定有序的。例如[3,1,3]

    如果这段子序列无序,则说明最小值可能存在于[left...mid],也可能存在于[mid+1...end],于是我们递归计算两边的最小值,去两者中更小的值作为本次调用的最小值。
    注意,有些人递归调用可能写成下面这个样子:
    min(helper(nums, start, mid-1), helper(nums, mid, end));
    这样是会出现错误的,可以自己去试试。

    154. Find Minimum in Rotated Sorted Array II

    这道题允许数组的元素重复

    这道题和上一道题的唯一区别就在于,当我们发现 nums[start]==nums[end] 的时候,不能直接判断[start...end]之间是否是有序的,也就无法判断其中的最小值是nums[start]。
    例如出现[3,1,3]的时候,最小值并不是3,而是1

    那么怎么办呢?很好办,只需要在这里改造一下上一道题的代码。

    class Solution {
    public:
        int findMin(vector<int>& nums) {
            return helper(nums, 0, nums.size() - 1);
        }
    
        int helper(vector<int>& nums, int start, int end) {
            if (start == end) return nums[start];
            else {
                if (nums[start] < nums[end]) return nums[start];
                else {
                    int mid = start + ((end - start) >> 1);
                    return min(helper(nums, start, mid), helper(nums, mid+1, end));
                }
            }
        }
    };
    

    当我们nums[start] < nums[end]的时候,这一段是肯定有序的,其他情况我们都拆成两段递归。

    只有0和1的世界是简单的
  • 相关阅读:
    ICONS-图标库
    图形资源
    vue项目中,如果修改了组件名称,vscode编辑器会在引入修改组件的名字处提示红色波浪线 The file is in the program because:Imported via xxx Root file specified for compilation .
    接口在dev环境报跨域问题(has been blocked by CORS policy:Response to preflight request doesn't pass access control check:No 'Access-Control-Allow-Origin' header ispresent on the requested resource.),qa环境正常
    阿里云occ的图片文件URL用浏览器直接打开无法访问,提示This XML file does noe appear to have any style information associated with it. The document tree is shown below.
    vue 项目使用element ui 中tree组件 check-strictly 用法(父子不互相关联的反显情况)
    高德地图进行线路规划绘制标记点操作(vue)
    vue中实现拖拽调整顺序功能
    2021-01-22 浏览器相关知识
    2021-01-22 js 相关知识点
  • 原文地址:https://www.cnblogs.com/nullxjx/p/14529117.html
Copyright © 2011-2022 走看看