关于二叉树的一些有趣问题

本文探讨了LeetCode中关于二叉树的题目,包括扁平化嵌套列表迭代器的实现,以及如何优化以节省内存。同时讲解了二叉树最近公共祖先的求解方法,采用后序遍历策略。最后介绍了完全二叉树节点个数的计算,利用二叉树的特性降低时间复杂度。
摘要由CSDN通过智能技术生成

关于二叉树的一些有趣问题

leetcode341扁平化嵌套列表迭代器

不难发现:

class NestedInteger {
    Integer val;
    List<NestedInteger> list;
}

/* 基本的 N 叉树节点 */
class TreeNode {
    int val;
    TreeNode[] children;
}

所以其实输入的就是一个类似于n叉树的结构。所要求的结果就是叶子结点的遍历结果。那么现在这个数据结构里面有两种形式,一种是整数型,另外一种是列表型。我们需要找到所有的整数型并存储输出。代码实际上就是实现n叉树的遍历。

代码实现

class NestedIterator implements Iterator<Integer> {

    private Iterator<Integer> it;

    public NestedIterator(List<NestedInteger> nestedList) {
        // 存放将 nestedList 打平的结果
        List<Integer> result = new LinkedList<>();
        for (NestedInteger node : nestedList) {
            // 以每个节点为根遍历
            traverse(node, result);
        }
        // 得到 result 列表的迭代器
        this.it = result.iterator();
    }

    public Integer next() {
        return it.next();
    }

    public boolean hasNext() {
        return it.hasNext();
    }    

    // 遍历以 root 为根的多叉树,将叶子节点的值加入 result 列表
    private void traverse(NestedInteger root, List<Integer> result) {
        if (root.isInteger()) {
            // 到达叶子节点
            result.add(root.getInteger());
            return;
        }
        // 遍历框架
        for (NestedInteger child : root.getList()) {
            traverse(child, result);
        }
    }
}

代码优化

上面的思路中,一次性算出了所有叶子节点的值,全部装到result列表,也就是内存中,nexthasNext方法只是在对result列表做迭代。如果输入的规模非常大,构造函数中的计算就会很慢,而且很占用内存。为了满足迭代器的求值为惰性的,可以调用hasNext时,如果nestedList的第一个元素是列表类型,则不断展开这个元素,直到第一个元素是整数类型。由于调用next方法之前一定会调用hasNext方法,这就可以保证每次调用next方法的时候第一个元素是整数型,直接返回并删除第一个元素即可。

public class NestedIterator implements Iterator<Integer> {
    private LinkedList<NestedInteger> list;

    public NestedIterator(List<NestedInteger> nestedList) {
        // 不直接用 nestedList 的引用,是因为不能确定它的底层实现
        // 必须保证是 LinkedList,否则下面的 addFirst 会很低效
        list = new LinkedList<>(nestedList);
    }

    public Integer next() {
        // hasNext 方法保证了第一个元素一定是整数类型
        return list.remove(0).getInteger();
    }

    public boolean hasNext() {
        // 循环拆分列表元素,直到列表第一个元素是整数类型
        while (!list.isEmpty() && !list.get(0).isInteger()) {
            // 当列表开头第一个元素是列表类型时,进入循环
            List<NestedInteger> first = list.remove(0).getList();
            // 将第一个列表打平并按顺序添加到开头
            for (int i = first.size() - 1; i >= 0; i--) {
                list.addFirst(first.get(i));
            }
        }
        return !list.isEmpty();
    }
}

这道题对于迭代器设计模式和n叉树的数据结构有很好的启发性,所以建议掌握并熟练应用。特此记录~

leetcode236二叉树的最近公共祖先

这个问题其实在git中的分支合并有所应用,在这里就不详细展开,专注问题本身。

只要是二叉树问题,就离不开三大遍历方式,所以可以先将整体框架搭建出来。

TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);
}

然后通过已经定义的函数思考三个问题。

1、这个函数是干嘛的?

2、这个函数参数中的变量是什么的是什么?

3、得到函数的递归结果,你应该干什么?

解答:

1、输入pq节点,并且它们都在以root为根的树中,函数返回的即使是pq的最近公共祖先节点

2、函数参数中的变量是root,因为根据框架,lowestCommonAncestor(root)会递归调用root.leftroot.right;至于pq,我们要求它俩的公共祖先,它俩肯定不会变化的。

3、得到函数结果之后,我们就需要开始分情况讨论了。(base case)如果root节点为空,自然返回null,如果root本身就是p或者q,比如说root就是p节点吧,如果q存在于以root为根的树中,显然root就是最近公共祖先。

接着可以补全刚开始写的整体框架了。

TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    // 两种情况的 base case
    if (root == null) return null;
    if (root == p || root == q) return root;

    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);
}

然后加入普适性后序遍历的代码即可完成题目,至于为什么是后续遍历也非常好想。好比两个子节点先走完再走根节点,自然第一时间的时候相遇就是最近公共祖先。

TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
    // base case
    if (root == null) return null;
    if (root == p || root == q) return root;

    TreeNode left = lowestCommonAncestor(root.left, p, q);
    TreeNode right = lowestCommonAncestor(root.right, p, q);
    // 情况 1
    if (left != null && right != null) {
        return root;
    }
    // 情况 2
    if (left == null && right == null) {
        return null;
    }
    // 情况 3
    return left == null ? right : left;
}

情况 1,如果pq都在以root为根的树中,那么leftright一定分别是pq(从 base case 看出来的)。

情况 2,如果pq都不在以root为根的树中,直接返回null

情况 3,如果pq只有一个存在于root为根的树中,函数返回该节点。

这里三种情况在想函数定义时实际上就应该想得比较全面了,加深理解,注意掌握。

leetcode222完全二叉树的节点个数

遍历实现

class Solution {
    public int countNodes(TreeNode root) {
        // base case
        if (root == null) return 0;
        int left = countNodes(root.left);
        int right = countNodes(root.right);
        return 1 + left + right;
    }
}

拓展:满二叉树

public int countNodes(TreeNode root) {
    int h = 0;
    // 计算树的高度
    while (root != null) {
        root = root.left;
        h++;
    }
    // 节点总数就是 2^h - 1
    return (int)Math.pow(2, h) - 1;
}

代码改进

由于完全二叉树是介于满二叉树和普通二叉树之间的一种,所以可以结合两种节点计算方式来简化时间复杂度。

public int countNodes(TreeNode root) {
    TreeNode l = root, r = root;
    // 记录左、右子树的高度
    int hl = 0, hr = 0;
    while (l != null) {
        l = l.left;
        hl++;
    }
    while (r != null) {
        r = r.right;
        hr++;
    }
    // 如果左右子树的高度相同,则是一棵满二叉树,注意前面求右子树是一直取的最右边的节点,所以才可以判断为满二叉树
    // 不然单是根据左右子树高度相同是无法得出是满二叉树的结论的
    if (hl == hr) {
        return (int)Math.pow(2, hl) - 1;
    }
    // 如果左右高度不同,则按照普通二叉树的逻辑计算
    return 1 + countNodes(root.left) + countNodes(root.right);
}

复杂度分析

注意:一棵完全二叉树的两棵子树,至少有一棵是满二叉树。所以虽然有两个递归,即左右子树,但是有一个是通过if直接返回的,不会进入递归。所以算法的递归深度就是树的高度 O(logN),每次递归所花费的时间就是 while 循环,需要 O(logN),所以总体的时间复杂度是 O(logN*logN)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值