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的世界是简单的
  • 相关阅读:
    Vue目录查询
    Vue框架学习(五)
    Vue框架学习(四)
    Vue框架学习(三)
    Vue框架学习(二)
    Vue框架学习(一)
    python中定义函数和参数的传递问题
    数据处理
    关于在程序中 文件新生成 在用os.system()程序对新生成的文件处理 举个栗子 如下:
    c++2008 并行配置文件和获取字典的所有key的方法
  • 原文地址:https://www.cnblogs.com/nullxjx/p/14529117.html
Copyright © 2011-2022 走看看