• 算法通过村第九关-二分(中序遍历)黄金笔记|二叉搜索树



    前言


    提示:有时候,我感觉自己一辈子活在两个闹钟之间,早上的第一次闹钟,以及5分钟之后的第二次闹钟。 --奥利弗·萨克斯《意识的河流》

    每个专题都有简单题,有难的题目。这里就介绍两道有挑战的题,一道是关于二叉搜索树的,一道是从两个数组中寻找中位数的。

    1. 有序数组转二叉搜索树

    参考题目介绍:108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode)

    在这里插入图片描述
    在这里插入图片描述
    理论上如果要构造二叉搜索树,可以以升序序列中的任意一个元素作为根节点,以该元素左边的升序序列构建左子树,以该元素的右边的升序序列构建右子树,这样得到的树就是一棵二叉搜索树。本体要求高度平衡,一次我们需要选择升序序列的中间元素作为根节点,其本质就是二分查找的过程。

    话不多说,看代码😎:

    	/**
         * 升序数组转二叉搜索树
         * @param nums
         * @return
         */
        public static TreeNode sortedArrayToBST(int[] nums) {
            return dfs(nums,0,nums.length - 1);
        }
    
        private static TreeNode dfs(int[] nums, int left, int right) {
            if (left > right){
                return null;
            }
            // 处理节点 总是选择中间位置左边的数字作为根节点
            int mid = left + ((right - left) >> 1);
            TreeNode root = new TreeNode(nums[mid]);
            root.left = dfs(nums,left,mid - 1);
            root.right = dfs(nums,mid + 1,right);
            return root;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    除了通过数组构造,是否可以通过一个个插入的方式来实现呢?当然可以。那如果从中间删除一个元素呢?

    推荐一下题目:⭐⭐⭐⭐⭐

    701. 二叉搜索树中的插入操作 - 力扣(LeetCode)

    450. 删除二叉搜索树中的节点 - 力扣(LeetCode)

    2. 寻找连个正序数组的中位数

    参考题目介绍:4. 寻找两个正序数组的中位数 - 力扣(LeetCode)

    在这里插入图片描述
    在这里插入图片描述
    对于本题,最值观的思路有一下两种:

    • 使用并归的方式,合并两个有序数组,得到一个大的有序数组。大的有序数组中间的元素,即使中位数。这种方式的时间复杂度为O(m + n),空间复杂度为O(m + n)
    • 直接找到中位数。由于两个数组的长度已知,因此中位数对应的两个数组的下标之和也是已知的。维护两个指针,初始分别指向两个数组的下标0,0的位置,每次将指针指向较小的指针后移一位,如果指针已经达到数组末尾,只需要移动另一个数组指针,直到到达中位数的位置。这样的方式可以将空间复杂度降低至O(1),但是时间复杂度仍是O(m + n).

    如何把时间复杂度降低至O(log(m+n))呢?如果对时间复杂度的要求是log,通常都要考虑二分,快排或者堆三个方面。而对于有序的序列,通常要优先考虑使用二分来解决。

    如果要使用二分,核心问题是基于什么规则将数据砍掉一半。而本题是两个序列,所以我们的核心问题是如何从两个序列中分别砍半,图示k = (m + n) >> 1;

    在这里插入图片描述
    根据中位数的定义,当 m + n是奇数时,中间的两个序列数组的第(m + n) >> 1个元素,当 m + n是偶数时,中间是两个有序数组中的第(m + n) >> 1个元素和第((m + n) >> 1)+ 1的平均值。因此,这道题可以转换为寻找两个有序数组中的第k小的数,其中k为(m + n) >> 1或者((m + n) >> 1)+ 1

    假设两个有序数组分别是LA和LB。要找到第k个元素我饿们可以比较LA[k / 2 - 1]和LB[k / 2 -1]。由于LA[k / 2 - 1]和LB[k / 2 -1]的前面分别是LA[k / 2 - 2]和LB[k / 2 -2],即k / 2 - 1个元素,对于LA[k / 2 - 1]和LB[k / 2 -1]中的最小值,最多只会有[k / 2 - 1] + [k / 2 -1] <= k - 2个元素比它小,那么它不就是我们要的第k小的数了。

    因此我们可以总结一下:

    • 如果LA[k / 2 - 1] < LB[k / 2 -1],则比LA[k / 2 - 1]小的最多有LA的前面k / 2 - 1个数和LB前面k / 2 - 1个数;即比LA[k / 2 - 1]小的数最多只有k - 2个,因此LA[k / 2 - 1]不可能是第k个数,所以LA[0]到LA[k / 2 - 1]也不可能是第k个数,可以全部舍弃掉
    • 如果LA[k / 2 - 1] > LB[k / 2 -1],则可以推理排除LB[0]到LB[k / 2 - 1]也不可能是第k个数,可以全部舍弃掉
    • 如果LA[k / 2 - 1] > LB[k / 2 -1],则可以归入第一种情况处理。
      在这里插入图片描述
      可以看到,比较LA[k / 2 - 1] > LB[k / 2 -1]之后,可以排除k / 2个不可能是第k小的数,查找范围缩小了一半。同时我们将排除后的数组上继续进行二分查找的话,并且根据我们排除的个数,减少k的值,这是因为我们排除的数都不大于第k小的数。

    注意一下边界处理:

    • 如果LA[k / 2 - 1] 或者LB[k / 2 -1]越界,那么我们可以以选取对应数组中的最后一个元素。这种情况下,我们必须根据排除数的个数减少k的值,而不是直接将k减去k / 2.
    • 如果k = 1 我们只需要返回两数组的首元素的最小值即可。

    分析了这么多,怎么写这个代码呢?🤔

    	/**
         * 两个有序数组找中间值
         * @param nums1
         * @param nums2
         * @return
         */
        public double findMedianSortedArrays(int[] nums1, int[] nums2) {
            int length1 = nums1.length,length2 = nums2.length;
            int totalLength = length1 + length2;
            if (totalLength % 2 == 1){
                int midIndex = totalLength >> 1;
                double median = getKthElement(nums1,nums2,midIndex + 1);
                return median;
            }else{
                int midIndex1 = (totalLength >> 1) - 1;
                int midIndex2 = (totalLength >> 1);
                double median = (getKthElement(nums1,nums2,midIndex1 + 1) + getKthElement(nums1,nums2,midIndex2 + 1)) / 2.0;
                return median;
            }
        }
    
        private double getKthElement(int[] nums1, int[] nums2, int k) {
            int length1 = nums1.length,length2 = nums2.length;
            int index1 = 0, index2 = 0;
            int kthElement = 0;
    
            while(true){
                // 边界问题
                if (index1 == length1){
                    return nums2[index2 + k - 1];
                }
                if(index2 == length2){
                    return nums1[index1 + k - 1];
                }
                if (k == 1){
                    return Math.min(nums1[index1],nums2[index2]);
                }
                // 正常情况
                int half = k >> 1;
                int newIndex1 = Math.min(index1 + half,length1) - 1;
                int newIndex2 = Math.min(index2 + half,length2) - 1;
                int pivot1 = nums1[newIndex1],pivot2 = nums2[newIndex2];
                if(pivot1 <= pivot2){
                    k -= (newIndex1 - index1 + 1);
                    index1 = newIndex1 + 1;
                }else{
                    k -= (newIndex2 - index2 + 1);
                    index2 = newIndex2 + 1;
                }
            }
        }
    
    • 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
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    总结

    提示:二分进阶;二分搜索;中序遍历;递归算法;分治思想

  • 相关阅读:
    spring5.0 源码解析 registerDisposableBeanIfNecessary12
    关于Linux搭建DedeCMS说明
    制作一个企业网站——html华为官网购物商城项目的设计与实现
    3D人物建模用哪个软件入门比较快?
    CSS:text-decoration文本装饰
    CRM系统开发
    【经验分享】如何构建openGauss开发编译提交一体化环境
    MySQL:温备份和恢复-mysqldump (4)
    Turtlebot3-burger入门教程#foxy版#-网络设置
    基于Redis手工实现分布式锁
  • 原文地址:https://blog.csdn.net/weixin_46585492/article/details/133271092