目录
问题
给定一个完全二叉树,统计其中的节点数,要求时间复杂度低于O(n)
分析
统计一个二叉树的节点最简单的方法当然是遍历一次,但是这样的时间复杂度是严格的O(n),不满足要求。要更快,自然要在完全二叉树的性质上下功夫。
完全二叉树的最后一层,节点一定是从左向右紧密排列的。由此可以想到什么?完全二叉树里一定有满的子树!而满二叉树的节点个数可以由树高直接得出,是2^h-1。所以通过判断这个完全二叉树中作为满二叉树的子树,就可以在小于O(n)的时间里得出节点数目。
从根节点出发,沿着每个节点的左孩子行进的到底的所有节点组成的路径(称为最左路)。对于一个完全二叉树的根节点,有下面两种可能:最左路达到底层,最左路不到底层。
① 正如前面所说的,完全二叉树的最后一层必然从左到右紧邻排列,所以如果到达完全二叉树的最底层,则说明左子树的最后一层没有null值,左子树是满二叉树,节点数为2^h-1,加上根节点,共2^h。现在,这个二叉树只剩下右子树没有被处理,问题转化为求右子树的节点数。
② 右子树的最左路没有到最底层,那么左子树不一定是满二叉树,但是右子树所有的叶节点一定都在第depth-1层,右子树一定是满二叉树,节点数为2^(h-1)-1,加上根节点,共2^(h-1)。这时候整个二叉树还剩下左子树没有被处理,问题转化为求左子树的节点数。(图里当时写错了,应该是不一定满,懒得改了哈哈哈哈哈哈哈哈哈)
完全二叉树的子树一定是完全二叉树,所以每次问题被转化为更小规模,目标依然是一个完全二叉树,前面的规律依然适用,递归可以简明地解决问题。
代码
我们需要一个函数来统计从一个节点出发的最左路的深度,这个简单,输入一个节点和它的当前深度,在到达null之前递增深度,最后把深度-1再返回。
public int DeepestLevel(Node x, int h) {
while(x != null) {
h++;
x = x.left;
}
return h - 1;
}
递归函数要传入当前二叉树的根节点root、这个头节点所在的层数level、整棵树的高度h。
(当然h也可以作为全局变量引入,习惯问题。还是避免使用全局变量吧,按层遍历,多一个int开销也不大)
public int countNodes(Node root, int level, int h) {
if(level == h) {//叶节点
return 1;
}
if(DeepstLevel(root) == h) {//左子树满
return(1 << (h - level) + countNodes(root.right, level + 1, h));//h-level为子树树高
}else {//左子树不满
return(1 << (h - level - 1) + countNodes(root.left, level + 1, h))
}
}
主函数就简单了。
public int count(Node root) {
if(root == null) {//空树当然是0个节点
return 0;
}
return countNodes(root, 1, DeepestLevel(root, 1));
}
复杂度
在每一层上,算法只会向一个方向发展,递归算法的时间复杂度是O(logn)。DeepestLevel()的时间复杂度也是O(logn)。整个算法的复杂度是O((logn)^2)。由洛必达法则,n趋于无穷大时,(logn)^2 / n 趋于无穷小,算法复杂度低于O(n)。