• 算法通过村第九关-二分(中序遍历)白银笔记|二分搜索



    前言


    提示:我不想再听人说什么了,我想听听松树和风说了什么。 --巴呀呀《因思念而沉着》

    二分查找很经典,但是面试的时候一般不直接考察这个问题,而是考察其变形题目,我们看下。

    另外,二分查找和二叉搜索树有异曲同工之妙,具体我们就向下看吧。

    基于二分查找思想,可以扩展除很多算法问题,而且很多都是考察热门问题,这里我们找些经典问题看看

    二分查找在算法中应用也非常多,也是很多大厂所钟爱的考察类型,感兴趣的同学可以进一步学习研究一下:

    推荐下面这些题目⭐⭐⭐⭐⭐:

    LCR 172. 统计目标成绩的出现次数 - 力扣(LeetCode)

    LCR 173. 点名 - 力扣(LeetCode)

    34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

    875. 爱吃香蕉的珂珂 - 力扣(LeetCode)

    29. 两数相除 - 力扣(LeetCode)

    在前面我们发现很多题目使用前序,后序或者层次遍历都可以解决,但是几乎没有中序遍历的。这是因为中序和前序后序的特点不一样,例如中序可以和搜索树结合在一起,前序和后序就不可以。

    理解了二分搜索树之后,你会发现一个惊天密码,二分中序竟然是一个问题,真的像,这里我们就学习一下。

    1. 基于二分查找的拓展问题

    1.1 山脉数组的峰顶索引

    参考题目介绍:852. 山脉数组的峰顶索引 - 力扣(LeetCode)

    在这里插入图片描述
    在这里插入图片描述
    看着这个题目要求有点多,核心就是一个,在数组中找到每个位置开始i,从0到i是递增的,从i+ 1之后就是递减的的,找到这个i的位置就是结果了。

    所以根据这个问题,和前面的找最小值相关的过程一样。最简单的方式就是对数组遍历一次,当我们遍历到的下标满足:arr[i - 1] < arr[i] > arr[i + 1],那么此时i就是我们要的结果。

    其实也可以再简单一些,因为是从左开始找,那开始时必然满足arr[i - 1] < arr[i],所以我们只要找到第一个满足arr[i] > arr[i + 1]的位置就可以了。

    	/**
         * 一次遍历 i= 1
         * @param arr
         * @return
         */
        public static int peakIndexInMountainArray(int[] arr) {
           int n = arr.length;
           int res = -1;
           // 从1 开始就神
           for(int i = 1; i < n - 1; i++){
               if (arr[i] > arr[i+1]){
                   res = i;
                   break;
               }
           }
           return res;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这个题可以采用二分的思想吗? 当然可以

    对于二分的某个位置mid,mid可能有3种情况:

    • mid在上升阶段,满足arr[mid] > arr[mid - 1] && arr[mid] < arr[mid + 1];
    • mid在顶峰阶段,满足arr[mid] > arr[mid - 1] && arr[mid] > arr[mid + 1]
    • mid在下降阶段。满足arr[mid] < arr[mid - 1] && arr[mid] > arr[mid + 1]

    因此我们根据mid当前所在的位置,调整二分的左右指针,就可以很快的找到顶峰。

    代码如下:

        /**
         * 二分查找  捉拿边界问题
         * @param arr
         * @return
         */
    	public int peakIndexInMountainArray(int[] arr) {
            if (arr.length == 3){
                return 1;
            }
            // 二分的左右指针 边界
            int left = 1, right = arr.length - 2;
            while(left <= right){
                int mid = left + ((right - left) >> 1);
                if (arr[mid] > arr[mid - 1] && arr[mid] > arr[mid + 1]){
                    return mid;
                }
                if (arr[mid] < arr[mid - 1] && arr[mid] > arr[mid + 1]){
                    // 下降阶段
                    right = mid - 1;
                }
                if (arr[mid] > arr[mid - 1] && arr[mid] < arr[mid + 1]){
                    // 上升降阶段
                    left = mid + 1;
                }
            }
            return left;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    1.2 旋转数字的最小数字

    参考题目介绍:153. 寻找旋转排序数组中的最小值 - 力扣(LeetCode)

    在这里插入图片描述

    在这里插入图片描述
    读了这个题,你有没有和我一样不知道题目是说什么的?我们看看力扣的官方的图解吧:

    在这里插入图片描述
    其中横轴表示数组元素的下标,纵轴表示数组元素的值。途中编出了最小值的位置,是需要我们查找的目标值。

    我们考虑数组中的最后一个元素x:在最小值右侧的元素(不包括最后一个元素本身)它的值一定是严格小于x;

    而在最小值左侧的元素,它的值一定是严格大于x。因此我们可以根据这个特性,通过二分的方法找出最小值。

    在二分查找的第一步,判断边界:

    low 左边界 high 有边界 区间中点 pivot 也就是最小值在该区间内

    我们将中轴元素nums[pivot]与右边界元素nums[high]进行比较,有可能存在一下三种情况:

    • 第一种情况:nums[pivot] < nums[high]。如下图所示,说明nums[privot]是最小值右侧的元素,因此我们可以忽略二分查找区间的右半部分。

    在这里插入图片描述

    • 第二种情况:nums[pivot] > nums[high]。如下图所示,这个说明nums[pivot]是最小值的左侧元素,因此可以忽略二分查找区间的左半部分。

    在这里插入图片描述

    • 第三种情况: 由于数组不包含重复元素,并且只要当前的区间长度不为1,pivot就不会与high重合,而如果当前的区间长度为1,这说明我我们已经可以结束二分查找二零。因此不会存在nums[pivot] == nums[hight]的情况

    当二分结束的时候,我们就可以得到最小值所在的位置了。

    	/**
         * 旋转数字的最小数字
         * @param nums
         * @return
         */
        public static int findMin(int[] nums) {
            int low = 0;
            int high = nums.length - 1;
            while(low < high) {
                int pivot = low + ((high - low) >> 1);
                // 注意这里的变化
                if (nums[pivot] < nums[high]){
                    high = pivot;
                }else {
                    low = pivot + 1;
                }
            }
            return nums[low];
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这里你是否注意到high = pivot;而不是我们习惯上的high = pivot - 1 呢? 这里为了防止遗漏元素,例如[3,1,2],执行的时候nums[pivot] = 1,小于nums[high] = 2,此时如果high = pivot - 1,这里直接会变成0。所以对于边界问题情况,很难去解释清楚,最好的策略就是多写几种场景测试一下看看。这也是二分查找比较烦的情况,一般来说解释起来比较困难,也不容易理解清楚,所以写几个经典的例子试一下,面试的时候大部分case能过就能通过。

    我们这里也可以拓展一下,如果在上面的基础上存在重复元素会怎么样呢?感兴趣的同学可以研究一下这个题。

    推荐题目⭐⭐⭐⭐:

    154. 寻找旋转排序数组中的最小值 II - 力扣(LeetCode)

    1.3 寻找缺失数字

    参考地址:剑指offer面试题53题目二-0-n-1中缺失的数字_剑指offer 缺失的数字_执子手 吹散苍茫茫烟波的博客-CSDN博客

    在这里插入图片描述
    这道题很简单吧?说实话从头到尾遍历一边不久解决了吗?但是这么简单肯定不是面试需要的。这里要考察的点是什么呢?就是二分查找!!

    对于有序的也是可以采用二分查找,这里的关键点是在缺失的数字之前,必然有nums[i] = i,在缺失的数字之后,必然有nums[i] != i.

    因此,只需要二分找出第一个nums[i] != i,此时下标i就是答案了。如果数组中没有找到次下标,那么缺失的就是n。

    考虑下怎么写的?代码很简单💕:

    	 /**
         * 寻找缺失数
         * @param nums
         * @return
         */
        public static int solve(int[] nums) {
           int left = 0;
           int right = nums.length - 1;
           while(left <= right ){
               int mid = left + ((right - left) >> 1);
               if (nums[mid] == mid){
                   left = mid + 1;
               }else{
                   right = mid - 1;
               }
           }
            return left;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    1.4 优化求平方根

    参考题目介绍:69. x 的平方根 - 力扣(LeetCode)

    这个其实很简单,需要注意的地方要考虑下。

    /**
     * 二叉求平方根
     * @param x
     * @return
     */
    public static int sqrt (int x) {
      int left = 1;
      int right = x;
      int res = -1;
      while(left <= right){
          // 注意写法
          int mid = left + ((right - left)>>1);
          // 这个小技巧留一下
          if(x / mid >= mid ){
              res = mid;
              // 返回多加1
              left = mid + 1;
          }else if (x / mid < mid){
              right = mid - 1;
          }
      }
      return res;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    记住这种优化的思想,凡是在有序的区间内查找的场景,都可以用二分查找来优化速度。如果有序区间是变化的,那么每次都要针对这个变化区间进行二分。这种问题做多了可以体会一下。

    2. 中序与搜索树原理

    在前面我们看了很多前序、后序或者层次遍历解决的问题,很少遇到有中序遍历解决的。这是因为中序与前后序相比较有不一样的特征,例如中序可以和搜索树结合在一起,但是前序和后序就不行了。

    二叉搜索树是一个很简单的概念,但是想说清楚也不容易。简单来说就是如果一棵二叉树就是搜索树,按照中序遍历其序列正好是一个递增序列。比较规范的定义是:

    • 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值。
    • 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值。
    • 它的左右子树也分别是二叉排序树。

    下面是一个中序序列{3,6,9,10,14,16,19},一个{3,6,9,10},都是搜索树。
    在这里插入图片描述
    搜索树的题目虽然依然是递归,但是与前后遍历区别很大,主要是因为搜索树是有序的,就是可以根据条件决定某些递归就不必执行了,也就是所谓的“剪枝”。

    2.1 二叉搜索树中搜索特定值

    参考题目介绍:700. 二叉搜索树中的搜索 - 力扣(LeetCode)

    在这里插入图片描述
    在这里插入图片描述
    这道题看起来挺复杂的,但是实现起来却很简单,简单递归搞定:

    • 如果根节点为空 root == null 或者根节点的值等于搜索值 val == root.val,返回根节点。
    • 如果val < root.val ,进入到根节点的左子树 查找searchBST(root.left, val)。
    • 如果val > root.val ,进入到根节点的右子树 查找searchBST(root.right, val)。

    递归写法:

        /**
         * 递归方式实现
         *
         * @param root
         * @param val
         * @return
         */
    public static TreeNode searchBST(TreeNode root, int val) {
        // 校验参数
        if(root == null || root.val == val) {
            return  root;
        }
        return root.val > val ? searchBST(root.left,val) : searchBST(root.right,val);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    迭代写法:

    • 条件:根节点不空 root != null 且 根节点不是目标节点值 val != root.val

      • 如果val < root.val ,进入到根节点的左子树 查找root = root.left
      • 如果val > root.val ,进入到根节点的右子树 查找root = root.right
        /**
         * 迭代实现
         *
         * @param root
         * @param val
         * @return
         */
        public static TreeNode searchBST2(TreeNode root, int val) {
           while(root != null && root.val != val){
              root = root.val > val ? root.left : root.right;
           }
           return root;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2.2 验证二叉搜索树

    参考题目介绍:98. 验证二叉搜索树 - 力扣(LeetCode)

    在这里插入图片描述
    在这里插入图片描述
    根据这个题目给出的性质,我们可以进一步直到二叉树【中序遍历】得到的结构序列一定是递增序列,在中序遍历的时候,可以检查当前节点的值时候大于前一个中序遍历到的值即可。

    /**
     * 递归实现
     */
    static long pre = Long.MIN_VALUE;
    public static boolean isValidBST(TreeNode root) {
        if (root == null) {
            return true;
        }
        // 处理左子树   不满足条件就退出
        if (!isValidBST(root.left)){
            return false;
        }
        // 处理根 当前节点:如果当前节点小于等于中序遍历的前一个节点 说明不满足
        if (root.val <= pre){
            return false;
        }
        pre = root.val;
        // 处理右子树
        return isValidBST(root.right);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    考虑一下迭代怎么写呗🥰:

    /**
     * 迭代实现
     *
     * @param root
     * @return
     */
    public static boolean isValidBST2(TreeNode root) {
        // 校验参数
        if (root == null){
            return true;
        }
        int pre = Integer.MIN_VALUE;
        // 创建空间
        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        // 根节点入栈  不断的循环遍历
        while(!stack.isEmpty() && root != null){
            // 左
            while(root != null){
                stack.push(root);  // 这里没有offer
                root = root.left;
            }
            //中
            root = stack.pop();
            // 如果中序遍历中得到的节点值小于等于前一个节点值 pre 说明不是二叉搜索树
            if (root.val <= pre){
                return false;
            }
            pre = root.val;
            root =  root.right;
        }
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    这个题要是弄明白了,感兴趣的话可以继续研究。

    推荐题目⭐⭐⭐⭐⭐:

    530. 二叉搜索树的最小绝对差 - 力扣(LeetCode)


    总结

    提示:二分查找;中序遍历;算法小技巧;搜索树问题;平方根优化

  • 相关阅读:
    PostgreSQL 的窗口函数 OVER, WINDOW, PARTITION BY, RANGE
    GB28181控制、传输流程和协议接口之注册|注销和技术实现
    西安---高时空分辨率、高精度一体化预测技术之风、光、水能源自动化预测技术应用
    BIOTIN ALKYNE CAS:773888-45-2价格,供应商
    暮光壁纸(安卓)
    [Qt]QListView 重绘实例之三:滚动条覆盖的问题处理
    ArcGIS绘制地球
    竞赛 基于视觉的身份证识别系统
    软件架构设计 C/S与B/S架构的区别
    vue.js el-tooltip根据文字长度控制是否提示toolTip
  • 原文地址:https://blog.csdn.net/weixin_46585492/article/details/133224214