【发布时间】:2015-07-01 22:47:26
【问题描述】:
我想计算Complete Binary tree 中的节点数,但我能想到的只是遍历整个树。这将是一个 O(n) 算法,其中 n 是树中的节点数。实现这一目标的最有效算法是什么?
【问题讨论】:
-
表示树的数据结构是什么?
标签: algorithm recursion tree binary-tree
我想计算Complete Binary tree 中的节点数,但我能想到的只是遍历整个树。这将是一个 O(n) 算法,其中 n 是树中的节点数。实现这一目标的最有效算法是什么?
【问题讨论】:
标签: algorithm recursion tree binary-tree
假设我们首先沿着树的左右刺走以确定它们的高度。我们要么发现它们是相同的,在这种情况下最后一行是满的,要么我们会发现它们是不同的。如果高度返回相同(例如高度为 h),那么我们知道有 2h - 1 个节点,我们就完成了。 (参考下图)
否则,高度必须分别为 h+1 和 h。我们知道,那么至少有 2h - 1 个节点,加上树底层的节点数。那么,问题是如何解决这个问题。一种方法是在最后一层中找到最右边的节点。如果您知道该节点在哪个索引处,则您确切知道最后一层中有多少个节点,因此您可以将其添加到 2h - 1 中,您就完成了。
如果你有一棵左高为 h+1 的完整二叉树,那么有 1 到 2 个h - 1 个可能的节点可能位于最后一层。那么问题是如何尽可能有效地确定这一点。
幸运的是,由于我们知道最后一层中的节点是从左到右填充的,所以我们可以使用二分搜索来尝试找出最后一层中最后填充的节点在哪里。本质上,我们猜测它可能在哪里的索引,从树的根走到叶子应该在的位置,然后要么在那里找到一个节点(所以我们知道底层最右边的节点要么是那个节点,要么就是到右边)或者我们没有(所以我们知道底层中最右边的节点必须纯粹是在当前位置的右边)。我们可以通过使用数字 k 中的位来引导向下搜索,向下走到底层的第 k 个节点应该在的位置:我们从根开始,如果 k 的第一位为 0,则向左走,如果 k 的第一位为 0,则向右走。 k 的第一位为 1,然后以相应的方式使用剩余的位沿树向下走。我们这样做的总次数是 O(h),每个探测需要时间 O(h),所以这里完成的总工作是 O(h2)。由于h是树的高度,我们知道h = O(log n),所以这个算法需要O(log2 n)时间来完成。
我不确定是否可以改进此算法。不过,我可以得到任何正确算法的 Ω(log n) 下限。我要争辩说,任何在所有情况下始终正确的算法都必须检查树最后一行中最右边的叶节点。要了解原因,假设有一棵树 T 算法不这样做。假设算法在底行中检查的最右边的节点是 x,底行中 actual 最右边的节点是 y,并且算法检测到的底行中最左边的缺失节点是 z。我们知道 x 必须在 y 的左侧(因为该算法没有检查底行中最左侧的节点)并且 y 必须在 z 的左侧(因为 y 存在而 z 不存在,所以 z 必须比 y 更靠右)。如果您考虑一下此时算法的“知识”是什么,则该算法不知道是否存在纯粹在 x 右侧或纯粹在 z 左侧的节点。因此,如果我们给它一个修改过的树 T',我们删除了 y,算法不会注意到任何变化,并且在 T 和 T' 上具有完全相同的执行路径。但是,由于 T 和 T' 具有不同数量的节点,因此算法必须至少在其中一个上是错误的。检查这个节点至少需要 Ω(log n) 的时间,因为走下树需要时间。
简而言之,使用上述 O(log2 n) 时间算法,你可以做得比 O(n) 更好,而且你可能会做得更好,尽管我'我不完全确定如何或是否可能。我怀疑这不是因为我怀疑二进制搜索是检查底行的最佳方法,并且即使考虑到它们共享节点后,到达您要探测的节点的路径长度也是 Θ (log2 n),但我不确定如何证明。
希望这会有所帮助!
【讨论】:
public int leftHeight(TreeNode root){
int h=0;
while(root!=null){
root=root.left;
h++;
}
return h;
}
public int rightHeight(TreeNode root){
int h=0;
while(root!=null){
root=root.right;
h++;
}
return h;
}
public int countNodes(TreeNode root) {
if(root==null)
return 0;
int lh=leftHeight(root);
int rh=rightHeight(root);
if(lh==rh)
return (1<<lh)-1;
return countNodes(root.left)+countNodes(root.right)+1;
}
在每次递归调用中,我们需要沿着完整二叉树的左右边界遍历,计算左右高度。如果它们相等,则树充满 2^h-1 个节点。否则我们在左子树和右子树上递归。第一次调用来自根(级别=0),需要 O(h) 时间来获得左右高度。我们递归直到我们得到一个完整的二叉树的子树。在最坏的情况下,我们可能会去直到叶子节点。所以复杂度将是 (h + (h-1) +(h-2) + ... + 0)= (h(h+1)/2)= O(h^2)。空间复杂度也是大小调用栈,O(h)。 注意:对于完全二叉树 h=log(n)。
【讨论】:
如果二叉树绝对完整(与 Wikipedia 文章中定义的“几乎完整”或“几乎完整”相反),您应该简单地沿着树的一个分支下降到叶子。这将是 O(logn)。然后将两个的幂相加到这个深度。所以 2^0 + 2^1... + 2^d
【讨论】:
C# 示例可能会帮助其他人。这类似于上面templatetypedef 很好解释的时间复杂度
public int GetLeftHeight(TreeNode treeNode)
{
int heightCnt = 0;
while (treeNode != null)
{
heightCnt++;
treeNode = treeNode.LeftNode;
}
return heightCnt;
}
public int CountNodes(TreeNode treeNode)
{
int heightIndx = GetLeftHeight(treeNode);
int nodeCnt = 0;
while (treeNode != null)
{
int rightHeight = GetLeftHeight(treeNode.RightNode);
nodeCnt += (int)Math.Pow(2, rightHeight); //(1 << rh);
treeNode = (rightHeight == heightIndx - 1) ? treeNode.RightNode : treeNode.LeftNode;
heightIndx--;
}
return nodeCnt;
}
【讨论】: