算法通关村——透彻理解二叉搜索树的应用

本节我们介绍两道有挑战的问题,一道是关于二叉搜索树的,一道是从两个数组中寻找中位数的(也与二分搜索相关)。

有序数组转为二叉搜索树

LeetCode108 给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。

示例1:
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案。

在这里插入图片描述
理论上我们要构造二叉搜索树,可以以升序序列中的任意一个元素作为根节点,以该元素左边的升序序列构造左子树,以该元素右边的升序序列构造右子树即可。但是本题要求该搜索树高度平衡,因此我们每次递归都取升序序列的中间节点为根节点,这本质上就是二分查找的过程:

public static TreeNode sortedArrayToBST(int[] nums) {
        return dfs(nums, 0, nums.length - 1);
    }

    private static TreeNode dfs(int[] nums, int left, int right) {
        TreeNode root = null;
        if (nums == null || nums.length == 0)
            return null;
        if (left <= right) {
            int mid = left + ((right - left) >> 1);
            root = new TreeNode(nums[mid]);
            root.left = dfs(nums, left, mid - 1);
            root.right = dfs(nums, mid + 1, right);
        }
        return root;
    }

寻找两个正序数组的中位数

上面一题只是开胃菜,下面这题就加大难度了。LeetCode4.给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数 。算法的时间复杂度应该为 O(log (m+n)) 。

示例1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000 解释:合并数组 = [1,2,3] ,中位数 2
示例2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000 解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5

对于该题目,我们最直观的思路有两种:

  • 使用归并排序合并这两个有序数组,得到一个大的有序数组,大的有序数组的中间元素的位置,即为中位数。该方法时间复杂度为O(m + n)空间复杂度为O(m + n).
  • 另一种方式是,我们可以不用合并两个数组,只要找到中位数的位置即可。我们先获取两数组的总元素的个数 len,然后获取中位数的位置,然后维护两个指针,初始化分别指向两个数组的下标00位置,每次将较小值后移一位(如果已到达数组末尾,则只需要移动另一个数组的指针),直到到达中位数的位置。该方法可以将空间复杂度降为O(1),但是时间复杂度仍然是O(m+n).

要把时间复杂度降至O(log(m+n)),那我们大概率就要考虑二分、快排或者堆三个方面。而对于有序序列,通常要考虑是否能够使用二分来实现
对于二分法,我们的核心问题就是依据什么规则将数据砍成一半。而本题是两个序列,所以我们的核心问题是如何从两个序列中分别砍半。图示如下:
在这里插入图片描述
根据中位数的定义,当m+n 是奇数时,中位数是两个有序数组中的第(m+n)/2 个元素,当m+n 是偶数时,中位数是两个有序数组中的第(m+n)/2 个元素和第(m+n)/2+1 个元素的平均值。因此,这道题可以转化成寻找两个有序数组中的第 k 小的数,其中 k 为 (m+n)/2 或 (m+n)/2+1。
假设两个有序数组分别是LA 和LB。要找到第 k 个元素,我们可以比较LA[k/2−1] 和LB[k/2−1]。由于LA[k/2−1] 和LB[k/2−1] 的前面分别有LA[0…k/2−2] 和 LB[0…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]。也就是一次砍掉一半。
  • 如果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,我们只要返回两个数组首元素的最小值即可。
    实现代码(代码有点长,简单注释了一下):
 public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int len1 = nums1.length,len2 = nums2.length;
        //获取两个数组的总长度
        int totalLength = len1 + len2;
        //若元素总数为奇数
        if ( totalLength % 2 == 1){
        	//获取其中位数下标
            int midIndex = totalLength/2;
            //k取midIndex + 1的原因是,中位数的位数k为其下标数加一
            double median = getKthElement(nums1,nums2,midIndex + 1);
            return median;
        } else {
         //若元素总数为偶数
         	//获取其目标中位数下标
            int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
            double median = (getKthElement(nums1,nums2,midIndex1 + 1) + getKthElement(nums1,nums2,midIndex2 + 1)) * 0.5;
            return median;
        }
    }

    public static int getKthElement(int[] nums1, int[] nums2, int k) {
        int len1 = nums1.length, len2 = nums2.length;
        int index1 = 0, index2 = 0;
        while(true){
        	//当下标数index等于数组长度len,则该数组下标越界,则表示该数组所有元素已被砍去。
        	//直接获取另一数组剩余部分的k处即为所求。
            if (index1 == len1){
                return nums2[index2 + k - 1];
            }
            if (index2 == len2){
                return nums1[index1 + k - 1];
            }
            if (k == 1){
            //当k = 1时,两数组剩余部分的第一位的最小值为所求。
                return Math.min(nums1[index1], nums2[index2]);
            }
            int half = k/2;
            int newIndex1 = Math.min(index1 + half, len1) - 1;
            int newIndex2 = Math.min(index2 + half, len2) - 1;
            //对比两数组LA[k/2−1] 和LB[k/2−1]部分。
            //当LA[k/2−1]>=LB[k/2−1]时,则砍去LA[k/2-1]前面部分
            if (nums1[newIndex1] <= nums2[newIndex2]){
            	//k值则减去本次该数组砍去部分的元素个数
                k -= (newIndex1 - index1 + 1);
                //由于砍去LA[k/2-1]前面部分,下标取到newINdex1的下一位
                index1 = newIndex1 + 1;
            } else {
            //当LA[k/2−1]<LB[k/2−1]时,则砍去LB[k/2-1]前面部分
                k -= (newIndex2 - index2 + 1);//同上
                index2 = newIndex2 + 1;
            }
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值