美文网首页程序员
Leetcode 总结 -- Talking Recursive

Leetcode 总结 -- Talking Recursive

作者: kolibreath | 来源:发表于2020-07-21 16:00 被阅读0次

    Talking Recursively Part II :

    Complete Binary Tree & Binary Search Tree

    这篇文章相较于Part I 而言会通过题目展示完全二叉树和二叉搜索树的性质。

    题目列表

    完全二叉树

    二叉查找树

    完全二叉树和满二叉树

    满二叉树

    首先先介绍一下满二叉树的概念


    满二叉树

    在图中可以看到,满二叉树除了叶子节点之外,其余的所有节点都有两个子节点。

    完全二叉树

    一棵深度为h的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。

    完全二叉树
    完全二叉树则其中有一部分是满二叉树,并且最后一层的节点需要连续集中在左侧。

    很显然满二叉树是完全二叉树的一个特例。

    小根堆

    完全二叉树的性质

    如果将一个完全二叉树按照层次遍历的顺序标记每个节点,我们不难发现,每一个父节点的值小于两个子节点的值。

    如果将这个完全二叉树按照层次遍历的方式进行序列化,可以得到一个数组,对于这个数组中的下标为i的个体,他的值(用value(i))表示,是小于value(2i)和value(2i+1)的。

    所以对于一个这样标记过的完全二叉树,对于节点值为i的元素进行(i/2)向下取整就可以找到父节点的值。

    二叉树寻路

    利用这个性质可以完成二叉树寻路这道题。
    本文的解法来源来自于这篇题解,二叉树寻路,下面我结合这篇原文的思想说的更清楚一点。

    二叉树

    对于一个编号完全的满二叉树(就像上面的图一样),将偶数行进行翻转,就成了下面的二叉树(下面的图)。

    对于数组反转的策略,我们通常使用数组两侧对调位置(轴对称翻转)的策略来实现:

    对调对应位置的元素
    所以很明显,原本在满二叉树i的父节点x会被进行轴对称翻转到另外一侧。但是尽管做了翻转,上图中对应颜色的两个数字加起来的值是相等的(因为是按照顺序编号),所以这个公式应该成立:
    递推公式

    但是Java中没有提供直接的log2函数,这个地方可以使用换底公式。将公式写成代码如图:

    class Solution {
            public List<Integer> pathInZigZagTree(int label) {
                LinkedList<Integer> result= new LinkedList<>();
                int N = (int) (Math.log(label) / Math.log(2)) + 1;
                while(label > 1){
                    result.add(label);
                    N--;
                    label = (int) (3 * Math.pow(2, N - 1) - label/ 2- 1);
                }
                result.add(1);
                Collections.reverse(result);
                return result;
            }
        }
    
    

    完全二叉树的结点个数

    • 完全二叉树的节点个数
      使用上一节讲过的『要素分析法』:
    • 子问题: 对于子树root的节点个数等于左子树的节点个数+右子树的节点个数
    • 递归出口:对于root == null时 return 0
    • 返回值: 表示节点root的节点数量
    class Solution {
            public int countNodes(TreeNode root) {
                if(root == null) return 0;
                int left = countNodes(root.left);
                int right = countNodes(root.right);
                return left + right + 1;
            }
        }
    

    我也就写到了上面这里,真正好的思路的题解来自这里
    这样的代码对于任何一棵树root都是可行的,完全没有使用到完全二叉树的性质。
    因为完全二叉树是将节点从左向右放置的(最后一行),因此可以满足如图的性质:

    两种情况
    1. 当左子树的深度和右子树相等时:
      说明左子树一定是满二叉树,右子树是否是完全二叉树不知道。
    2. 当左子树的深度和右子树不相等时:
      左子树不一定是满二叉树,但是右子树一定是满二叉树。

    对于求深度,这里也不需要使用递归的策略,因为是满二叉树,所以先填充左侧,通过一直看左子树的深度就知道整棵树的深度了,代码如下:

     class Solution {
            private int depth(TreeNode root){
                int depth = 0;
                TreeNode node = root;
                while(node != null) {
                    node = node.left;
                    depth++;
                }
                return depth;
            }
            public int countNodes(TreeNode root) {
                if(root == null) return 0;
                int left = depth(root.left);
                int right=  depth(root.right);
    
                if(left == right) {
                    //左子树数量 + 节点 + 右子树
                    return (1 << left )  + countNodes(root.right);
                }else{
                    //右子树数量 + 节点 + 左子树
                    return (1 << right)  + countNodes(root.left);
                }
            }
        }
    

    二叉查找树

    二叉查找树可以说是数据结构中非常优美和工整的一类了,它的定义是这样的:
    对于树root,他的左子树的所有节点全部小于root的value,右子树的所有节点值大于root的value。
    二叉查找树的英文名字是:Binary Search Tree,这个名字一看就有一种『二分查找』的意思,确实,二叉查找树的主要功能就是提供一个非常方便的二分搜索。

    二叉搜索树的基本性质

    二叉搜索树中的搜索

    先通过一个题快速了解二叉搜索树:

    • 二叉搜索树中的搜索

    • 返回值: 如果存在这个节点,返回这个节点对应的Treenode

    • 子问题:对于节点root,如果root的value等于给定值,返回root,如果小于给定值,在树的右边继续搜索,如果大于给定值,在树的左边继续搜索。

    • 递归出口:当root为空时返回null,表示这个树root不存在给定值对应的节点。
      很显然,这个题是一个遍历结构的问题,可以直接使用遍历模板:

    class Solution {
            //带有返回值的递归搜索
            public TreeNode searchBST(TreeNode root, int val) {
                if(root == null || root.val == val) return root;
                return root.val < val ? searchBST(root.right,val) :searchBST(root.left, val);
            }
        }
    

    合法二叉搜索树

    这个题本身实际上是对二叉树的定义的一个巩固:

    • 验证二叉搜索树
      二叉搜索树的定义在更深的层面上揭示了一棵树root的左右子树要满足一个数量区间的约束关系,这道题思路比较简单,直接上代码:
    public class 验证二叉搜索树 {
        class Solution {
            public boolean isValidBST(TreeNode root) {
                return validate(root,Long.MIN_VALUE, Long.MAX_VALUE);
            }
    
            public boolean validate(TreeNode node, long  min, long max){
                if(node == null) return true;
                if(node.val <= min || node.val >= max) return false;
                return validate(node.left, min, node.val) && validate(node.right,node.val,max);
            }
        }
    }
    

    二叉搜索树的构建

    二叉搜索树还有一个非常好的性质,二叉搜索树的中序遍历是有序的。下面看看两个和有序数列相关的二叉搜索树问题:

    将有序链表转换为二叉搜索树

    这个问题在最开始讲链表的总结的时候讲过,当时只是当做一个快慢指针的应用讲的,这次我们从递归以及构建二叉平衡搜索树的角度来看看这道题。

    之前说过,二叉平衡搜索树的定义是这样的:对于树root,其左子树和右子树的节点绝对值差不超过2(可以等于0、1)并且需要是一个二叉搜索树。 因此我们需要从链表的中间节点下手来构建这棵二叉平衡搜索树:

    • 返回值: 需要返回最终构成的节点
    • 子问题:将mid的左边作为左子树,将mid的右边作为右子树
    • 递归出口:当链表head为空,或者mid等于head时(此时说明链表已经被分解成了惟一的节点,可以作为一一颗子树了)


    代码如下:

    class Solution {
    
            public ListNode findMiddleElement(ListNode node){
                ListNode prev = null;
                ListNode slow = node;
                ListNode fast = node;
                while(fast != null && fast.next != null){
                    prev = slow;
                    slow = slow.next;
                    fast = fast.next.next;
                }
                if(prev != null) prev.next = null;
                return slow;
            }
    
            public TreeNode sortedListToBST(ListNode head) {
                if(head == null) return null;
                ListNode mid = findMiddleElement(head);
    
                if(mid == head) return new TreeNode(mid.val);
    
                TreeNode midTreeNode = new TreeNode(mid.val);
                midTreeNode.left = sortedListToBST(head);
                midTreeNode.right = sortedListToBST(mid.next);
                return midTreeNode;
            }
        }
    

    二叉树的修剪

    修剪二叉搜索树

    • 修剪二叉搜索树
      对于这道题,可能一上来就回考虑到删除是否要包含根节点的问题,如果两者不分开考虑的话很容易掉进分析的误区,这里不妨将是否包含根节点分解成两个子问题来看:
      分解成两个子问题
    1. 删除不包含根节点

    假设删除不包含根节点,则删除时:

    • 返回值:返回删除完成之后的子树
    • 子问题:节点root的value 小于删除下界,说明待删除都聚集在root的右边,继续删除右子树;root的value大于上界说明待删除都聚集在root的左边。
    • 递归出口:这个明显有一个遍历树的特征,当root为空时,返回null

    这样的三个步骤是否也适用于包含根节点的情况呢?很显然,确实!
    对于图二的情况,只要左移到左子树,之后返回以3为根节点的子树就可以了:

    class Solution {
            public TreeNode trimBST(TreeNode root, int L, int R) {
                if(root == null) return null;
                if(root.val < L) return trimBST(root.right, L, R);
                if(root.val > R) return trimBST(root.left,  L, R);
                root.left = trimBST(root.left, L, R);
                root.right= trimBST(root.right,L, R);
                return root;
            }
        }
    

    这道题和我们之前写过的带有返回值的递归函数有点点不同,以往写的递归函数,如斐波拉契数列,求树的深度,尾递归调用了两个递归函数,求他们之间的一个数量关系,所以整体来看,这样的递归函数的返回值并不是自身计算的结果,下面用一个形象的图展示:


    对比图

    所以在最后验证的时候,不需要有过多的顾虑。

    删除二叉搜索树中的节点

    • 删除二叉搜索树中的节点
      二叉搜索树还有一个非常重要的性质:二叉搜索树的中序遍历是一个有序的序列。节点node 在中序遍历之后的序列中的前面一个节点称为它的前驱节点,后面的节点称为它的后继节点。
      前驱和后继

    如果需要删除二叉搜索树的节点的话,需要保证删除完成的中序遍历也是有序的。那也就是说,可以使用这个被删除的节点的前驱和后继顶上。

    这道题对于不同种类的节点的删除操作是不一样的,我们不妨通过枚举法枚举所有的情况


    1. 叶子节点,直接删除
    2. 只有一个孩子:
      2.1 只有左孩子:


      只有左孩子

      可以使用前驱代替。
      为什么不使用后继节点呢?因为后继节点大于当前的值,一定在序列的后面,根据中序遍历来说,这个值在该节点的右子树或者父节点或者,该节点不存在右子树,找父节点也不方便。
      2.2 只有右孩子:


      只有右孩子
      同理,需要使用后继代替。
    3. 有两个孩子:
      那就随便使用前驱还是后继了。

    如何找前驱和后继

    对于节点root,他的前驱应该小于root的value,但是需要是小于中的最大的,所以这个节点应该在root的左子树的最右边;同理可以找到后继节点。
    代码如下:

    class Solution {
                private int forward(TreeNode node){
                    node = node.left;
                    while(node.right!= null) node = node.right;
                    return node.val;
                }
    
                private int backward(TreeNode node){
                    node = node.right;
                    while(node.left != null) node = node.left;
                    return node.val;
                }
    
                public TreeNode deleteNode(TreeNode node, int key) {
                    //需要
                    //为什么需要: 是中序遍历的出口
                    if(node == null) return null;
                    if(key > node.val) node.right = deleteNode(node.right, key);
                    else if(key < node.val) node.left = deleteNode(node.left, key);
                    else{
                        if(node.left == null && node.right == null) node = null;
                        else if(node.right == null){
                            node.val = forward(node);
                            node.left = deleteNode(node.left, node.val);
                        }else{
                            node.val = backward(node);
                            node.right = deleteNode(node.right, node.val);
                        }
                    }
                    return node;
                }
            }
    

    相关文章

      网友评论

        本文标题:Leetcode 总结 -- Talking Recursive

        本文链接:https://www.haomeiwen.com/subject/vdfvahtx.html