算法通关村 —— 透彻理解二叉树中序遍历的应用

目录

算法通关村 —— 透彻理解二叉树中序遍历的应用

一、 有序数组转为二叉搜索树

二、二叉搜索树中的插入操作

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


算法通关村 —— 透彻理解二叉树中序遍历的应用

前面我们学习了很多有关二叉树中序遍历的经典算法,今天,让我们一起加大难度,挑战一下与其相关的应用题目,使得对二叉树中序遍历的了解更进一步。

一、 有序数组转为二叉搜索树

给定一个升序整数数组nums,将其转换为一棵高度平衡二叉树。

高度平衡二叉树:一棵满足每个节点的左右两个子树的高度差绝对值不超过1的二叉树。

如下所例:

输入:nums = [-10, -3, 0, 5, 9]
输出:[0,-3,9,-10,null,5]/[0,-10,5,null,-3,null,9]

如果只要求构造二叉搜索树,我们可以以升序序列中的任意一个元素作为根节点,以元素左边的升序序列构建左子树,右边构建右子树,这样便得到了一棵二叉搜索树。但由于本题要求高度平衡,所以我们就要尽量让两边子树节点数相同,所以我们可以选择升序序列的中间元素作为根节点,然后后面的构建子树其实就是二分查找的过程了,比根节点小便成为其左节点,否则成为其右节点。

具体实现代码如下:

class Solution {
    public TreeNode sortedArrayToBST(int[] nums) {
        // 递归将数组转换为二叉搜索树
        return helper(nums, 0, nums.length - 1);
    }

    public TreeNode helper(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 = helper(nums,left,mid-1);
        root.right = helper(nums,mid+1,right);
        return root;
    }
}

二、二叉搜索树中的插入操作

给定二叉搜索树的根节点和要插入树种的值,将值插入二叉搜索树,返回插入后的二叉搜索树根节点,要求仍保持为二叉搜索树。输入数据保证,新值和原始二叉搜索树中的任意节点值都不同。

那其实思路和上一道题也是一样的嘛,同样是中序遍历,如果比当前节点值小,就递归对该节点的左节点再次执行插入函数,如果比当前节点值大,就递归对该节点的右节点再次执行插入操作就可以了完成了。思路非常简单,具体实现代码如下:

class Solution {
    public static TreeNode insertIntoBST(TreeNode root, int val) {
        // 若根节点为空,则插入节点为树的根节点
        if(root == null) return new TreeNode(val);  
        // 若该值与某节点值相等,无法插入,直接返回根节点
        if(val == root.val) return root;
        // 如果比根节点值大,则递归对其右节点再次执行插入函数
        else if(val > root.val){
            if(root.right == null) root.right = new TreeNode(val);
            else insertIntoBST(root.right, val);
        }
        // 否则,对其左节点再次执行插入函数
        else{
            if(root.left == null) root.left = new TreeNode(val);
            else  insertIntoBST(root.left, val);
        }
        return root;
    }
}

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

这道题难度就稍微有点大了,做好心理准备哦。

给定两个大小分别为m和n的正序数组nums1和nums2,找出并返回两个数组的中位数,要求算法的时间复杂度应该为O(log(m+n))。如下所例:

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

这道题如果单单是解决问题,其实是很容易的,难的是如何把时间复杂度降到O(log(m+n))?时间复杂度里面要求有log的,通常都要考虑二分、快排或者堆三个方面。对于有序序列,那我们就考虑能否使用二分来解决吧。

那决定使用二分啦,核心问题是我们要基于什么规则每次判断将数据砍掉一半呢?而且本题为两个序列,所以我们要解决的核心问题是如何从两个序列中分别砍半,图示如下 k=(m+n)/2:

根据中位数的定义我们可知,当m+n为奇数,中位数是有序数组的第(m+n)/2个元素,为偶数,中位数是第(m+n)/2个元素和第(m+n)/2 + 1个元素的平均值。因此,这道题就可以转化成寻找两个有序数组的第k小的数,k为(m+n)/2或者(m+n)/2 + 1。

那下面就是我们要解决的重点了,如何砍掉一半的数据呢?

假如有两个有序数组nums1和nums2,要找到第k个元素,我们先比较nums1[k/2-1]和nums2[k/2-1]。比较完大小后有以下几种情况:

⚪ 如果nums1[k/2-1] < nums2[k/2-1],则比nums1[k/2-1]小的数最多只有nums1的前k/2 - 1个数和nums2的前k/2 - 1个,因此比其小的数最多只有k-2个,因此nums1[k/2 - 1]不可能是第k个数,那么便可以排除掉nums1[0]到nums[k/2 - 1]的数,也就是一次砍掉一半。

⚪ 如果nums1[k/2-1] > nums2[k/2-1],便可以排除掉nums2[0]到nums2[k/2 - 1]的数。

⚪ 如果nums1[k/2-1] =  nums2[k/2-1], 则归入第一种情况处理。

所以我们每次都能缩小一半范围,那最后我们就可以以log的时间复杂度找到我们要的中位数啦!

在此之前,我们还需要处理以下几个特殊情况

⚪ 如果nums1[k/2 - 1] 或者 nums2[k/2 - 1]越界,那我们我们可以选取其对应数组的最后一个元素。在这种情况下,我们必须根据排除的个数减少k的值,而不能直接将k减去k/2。

⚪ 如果k=1,我们只需要返回两个数组首元素的最小值就可以了。

具体实现代码如下:

class Solution {
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int len1 = nums1.length, len2 = nums2.length;
        int len = len1 + len2;
        // 如果长度为奇数,则中位数为最中间的树,否则为中间两数的平均值
        if (len % 2 == 1){
            int mid = len/2; // 取到最中间数的索引
            double median = getmedian(nums1, nums2, mid + 1); // 递归排除不存在中位数的区间,直到找到中位数
            return median;
        }else { 
            int mid1 = len / 2 - 1, mid2 = len / 2; // 取到中间两数的索引
            double median = (getmedian(nums1,nums2,mid1+1) + getmedian(nums1,nums2,mid2+1))/2.0;
            return median;
        }
    }
    // 找到两数组中第k小的数,不断排除不存在中位数的区间直到找到中位数
    public int getmedian(int[] nums1, int[] nums2, int k){
        int len1 = nums1.length, len2 = nums2.length;
        int index1 = 0, index2 = 0;
        int kthval = 0;
        while(true){
            // 处理边界情况:若数组索引越界,则选取对应数组中的最后一个元素
            if (index1 == len1) return nums2[index2 + k - 1];
            if (index2 == len2) return nums1[index1 + k - 1];
            // 若k为1,返回两数组首元素的最小值
            if (k == 1) return Math.min(nums1[index1],nums2[index2]);
            
            int mid = k/2;
            int newindex1 = Math.min(index1 + mid, len1) - 1;
            int newindex2 = Math.min(index2 + mid, len2) - 1;
            int tmp1 = nums1[newindex1], tmp2 = nums2[newindex2];
            // 若nums1[k/2 - 1]<=nums2[k/2 - 1],则nums1[0]~nums1[k/2-1]不可能有第k个数,全部排除
            if(tmp1 <= tmp2){
                k -= (newindex1 - index1 + 1);
                index1 = newindex1 + 1;
            } else {
            // 若nums1[k/2 - 1]>nums2[k/2 - 1],则nums2[0]~nums2[k/2-1]不可能有第k个数,全部排除
                k -= (newindex2 - index2 + 1);
                index2 = newindex2 + 1;
            }
        }   
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值