LeetCode 222. Count Complete Tree Nodes(完全二叉树节点数统计)

52 篇文章 1 订阅
51 篇文章 0 订阅

本人原创,如有错误恳请指正,谢谢!

原题网址https://leetcode.com/problems/count-complete-tree-nodes/

Given a complete binary tree, count the number of nodes.

Definition of a complete binary tree from Wikipedia:
In a complete binary tree every level, except possibly the last, is completely filled, and all nodes in the last level are as far left as possible. It can have between 1 and 2hnodes inclusive at the last level h.

方法一:对二叉树进行遍历

最直接的方法是对二叉树进行遍历,然后逐个节点进行统计,时间复杂度是O(n),n为节点数量。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public int countNodes(TreeNode root) {
        return root == null ? 0 : 1 + countNodes(root.left) + countNodes(root.right);
    }
}
方法一在LeetCode上面会Time Limited Exceeded,有没有更好的方法呢?其实只是把它当作一颗普通的二叉树来处理了,因此它能够统计任意二叉树的节点数,但并没有利用到完全二叉树的特性。


方法二:对完全二叉子树进行合并统计

先来看看完全二叉树有哪些形态:



可以看出,形态1是完美的二叉树(perfect binary tree),即所有中间节点均有两个子节点,而所有叶子节点都在同一深度上。如何判断一颗完全二叉树是否完美二叉树呢?只要检查根节点的最左后代节点和最右后代节点是否在同一深度上即可。



而对于形态2和3怎么办呢?可以分别分析根节点的左子树和右子树,由完全二叉树的特点可以推出,它的左子树和右子树中,至少有一颗是完美二叉树,而另一颗则是完全二叉树。这样,我们就可以应用递归的方法,将一棵树从根节点一下拆分为左子树和右子树进行统计即可。





这样写出来的代码如下:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public int countNodes(TreeNode root) {
        if (root == null) return 0;
        int leftmost = 0;
        TreeNode left = root;
        while (left != null) {
            leftmost ++;
            left = left.left;
        }
        int rightmost = 0;
        TreeNode right = root;
        while (right != null) {
            rightmost ++;
            right = right.right;
        }
        if (leftmost == rightmost) return (int)Math.pow(2, leftmost)-1;
        return 1 + countNodes(root.left) + countNodes(root.right);
    }
}
假设最坏情况下总有一颗子树不是完美二叉树,这样从根节点开始,检查左右子树深度需要2h时间(h为深度),而对于根节点下面的一颗子树,检查深度需要2(h-1)时间,因此总体的时间复杂度为O(2h+2(h-1)+2(h-2)+...+2*1)=O(h^2)。

但是提交发现还是Time Limit Exceeded,这是为什么呢?原来是Math.pow函数坑爹啊,把它改为移位运算,立马accept:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public int countNodes(TreeNode root) {
        if (root == null) return 0;
        int leftmost = 0;
        TreeNode left = root;
        while (left != null) {
            leftmost ++;
            left = left.left;
        }
        int rightmost = 0;
        TreeNode right = root;
        while (right != null) {
            rightmost ++;
            right = right.right;
        }
        if (leftmost == rightmost) return (1<<leftmost)-1;
        return 1 + countNodes(root.left) + countNodes(root.right);
    }
}

但成绩并不理想:



还有什么提升的地方吗?分析上面程序发现,递归统计子树的时候,leftmost和rightmost被重复统计多次,这是没有必要的,这就有点动态规划的思想了,有子问题重叠,例如左子树的leftmost值应等于根节点leftmost-1,右子树同理,所以按照动态规划思想改进一下,得到了方法三。


方法三:动态规划思想的完全二叉树递归统计

考虑到统计最左节点深度和最有节点深度有子问题重叠,所以可以避免此部分重复计算:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    private int countLeftmost(TreeNode root) {
        int leftmost = 0;
        TreeNode left = root;
        while (left != null) {
            leftmost ++;
            left = left.left;
        }
        return leftmost;
    }
    private int countRightmost(TreeNode root) {
        int rightmost = 0;
        TreeNode right = root;
        while (right != null) {
            rightmost ++;
            right = right.right;
        }
        return rightmost;
    }
    
    private int countWithLeftmost(TreeNode root, int leftmost) {
        if (root == null) return 0;
        int rightmost = countRightmost(root);
        if (leftmost == rightmost) return (1<<leftmost)-1;
        return 1 + countWithLeftmost(root.left, leftmost-1) + countWithRightmost(root.right, rightmost-1);
    }
    private int countWithRightmost(TreeNode root, int rightmost) {
        if (root == null) return 0;
        int leftmost = countLeftmost(root);
        if (leftmost == rightmost) return (1<<leftmost)-1;
        return 1 + countWithLeftmost(root.left, leftmost-1) + countWithRightmost(root.right, rightmost-1);
    }
    public int countNodes(TreeNode root) {
        if (root == null) return 0;
        int leftmost = countLeftmost(root);
        int rightmost = countRightmost(root);
        if (leftmost == rightmost) return (1<<leftmost)-1;
        return 1 + countWithLeftmost(root.left, leftmost-1) + countWithRightmost(root.right, rightmost-1);
    }
}
成绩提高不少:


方法四:用二分法找到最底层的最右叶子

完全二叉树的最底层叶子都左靠,如何找到底层最右的叶子呢?思路就是应用二分法。


当我们从根节点出发一直向右向下到达最右叶子节点,那么这个最右叶子节点至少是完美二叉树的层数,这样我们就可以知道底层的最大叶子数。我们再从根节点出发,经过左子树记0,右子树记1,这样每个底层叶子都会有一个唯一的二进制编码了:

应用二分法查找的原理,根据二进制编码进行路径寻找,最终能够定位到代表二分法所指的中间节点的位置。

二分法查找的时间复杂度为O(log(2^h))=O(hlog2),但为了到达底层,中间需要经过最大h层节点,这样相当于O(h^2)的时间复杂度。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    private boolean find(TreeNode root, int depth, int target) {
        if (root == null) return false;
        int bit = 1 << (depth-1);
        TreeNode current = root;
        for(int i=0; i<depth; i++) {
            if ((bit & target) == 0) current = current.left;
            else current = current.right;
            if (current == null) return false;
            bit >>= 1;
        }
        return true;
    }
    public int countNodes(TreeNode root) {
        if (root == null) return 0;
        int completeDepths = 0;
        TreeNode right = root;
        while (right != null) {
            right = right.right;
            completeDepths ++;
        }
        int i = 0;
        int j = (1<<completeDepths)-1;
        while (i<=j) {
            int m=(i+j)/2;
            if (find(root, completeDepths, m)) {
                i=m+1;
            } else {
                j=m-1;
            }
        }
        return (int)Math.pow(2, completeDepths)+i-1;
    }
}
但成绩并不理想:



方法五:利用二叉树特点的二分法

有网友的方法,首先扫描根节点的最右后代节点,最右后代节点的深度就是完美二叉树的深度,如果这个深度之下还有叶子的话,我们也能够计算出来最大可能的叶子数量。同时二叉树具有天然的二分特性,例如寻找根节点的右子节点的最左后代节点,或者寻找根节点的左节点的最右后代节点,就可以找到底层叶子的中间节点!!!

对于根节点,如果在最底层能够找到它的右子节点的最左后代节点,说明根节点的左子树是完美二叉树,这个时候我们需要检查该最左后代节点的后面一段,而迭代的方法很巧妙,只需要深入一层到根节点的右子节点,继续寻找它的右子节点的最左后代节点即可,否则就深入一层到根节点的左子节点,继续寻找它的右子节点的最左后代节点,如此下去。



这个思路给我的最大启发,是二分法原来可以这样应用在二叉树上面,而二叉树天然有二分的特性!

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    private int leftmost(TreeNode root) {
        int count = 0;
        while (root != null) {
            count ++;
            root = root.left;
        }
        return count;
    }
    public int countNodes(TreeNode root) {
        if (root == null) return 0;
        int leftmost = leftmost(root.left);
        int count = 1;
        int middle = leftmost(root.right);
        if (leftmost == middle) {
            count += (1<<leftmost)-1;
            count += countNodes(root.right);
        } else {
            count += (1<<middle)-1;
            count += countNodes(root.left);
        }
        return count;
    }
}
算法复杂度是O(log(2^h)*h)=O(h^2)

成绩提升不少:


如果再结合动态规划思想,避免子问题重复计算:

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    private int leftmost(TreeNode root) {
        int count = 0;
        while (root != null) {
            count ++;
            root = root.left;
        }
        return count;
    }
    private int countLeft(TreeNode root, int leftmost) {
        if (root == null) return 0;
        int count = 1;
        leftmost --;
        int middle = leftmost(root.right);
        if (leftmost == middle) {
            count += (1<<leftmost)-1;
            count += countLeft(root.right, middle);
        } else {
            count += (1<<middle)-1;
            count += countLeft(root.left, leftmost);
        }
        return count;
    }
    public int countNodes(TreeNode root) {
        int leftmost = leftmost(root);
        return countLeft(root, leftmost);
    }
}
则成绩可以进一步提升:


感谢网友,参考文章:

http://www.programcreek.com/2014/06/leetcode-count-complete-tree-nodes-java/

http://bookshadow.com/weblog/2015/06/06/leetcode-count-complete-tree-nodes/

http://blog.csdn.net/xudli/article/details/46385011

http://www.cnblogs.com/easonliu/p/4556487.html

http://yucoding.blogspot.com/2015/10/leetcode-question-question-count.html

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值