如何确定二叉树是否平衡?

117

离开那些校园已经有一段时间了。我在医院找到了一个IT专家的工作。现在正试图转为做一些实际的编程。我现在正在处理二叉树,我想知道确定树是否平衡的最佳方法是什么。

我考虑的方案如下:

public boolean isBalanced(Node root){
    if(root==null){
        return true;  //tree is empty
    }
    else{
        int lh = root.left.height();
        int rh = root.right.height();
        if(lh - rh > 1 || rh - lh > 1){
            return false;
        }
    }
    return true;
}

这是一个好的实现方法吗?还是我漏掉了什么?


如果您想查看Donal Fellows的带有图形的ASCII二叉树,请访问:http://i.imgur.com/97C27Ek.png - user7643681
1
好的回答,帮助我进入美国。(开玩笑) - Henry
28个回答

170

在寻找其他东西时偶然发现了这个旧问题。我注意到你从未得到一个完整的答案。

解决这个问题的方法是先为你想要编写的函数编写规范。

规范:如果一个二叉树是"高度平衡的",那么它必须满足以下条件:(1) 空树,或者(2) 左右子树都是"高度平衡的"并且左子树的高度与右子树之差不超过1。

现在你有了规范,编写代码就很容易了。只需按照规范编写即可:

IsHeightBalanced(tree)
    return (tree is empty) or 
           (IsHeightBalanced(tree.left) and
            IsHeightBalanced(tree.right) and
            abs(Height(tree.left) - Height(tree.right)) <= 1)
将其转化为所选编程语言应该很容易。
奖励练习:这个简单的代码草图在计算高度时遍历了树太多次。你能让它更有效率吗?
超级奖励练习:假设树是极度不平衡的。比如,一边有一百万个节点,而另一边只有三个节点。是否存在这样的情况,使得该算法会出现堆栈溢出?你能修复实现方式,使其永远不会在给定极度不平衡的树时出现堆栈溢出吗?
更新:Donald Fellows在他的答案中指出,有不同的“平衡”定义可以选择。例如,一个人可以采用更严格的“高度平衡”定义,并要求到最近的空子节点的路径长度与到最远的空子节点的路径之间的差距不超过1。我的定义比那个宽松,因此允许更多的树。
一个人也可以比我的定义更宽松;一个人可以说,平衡树是指每个分支到达空树的最大路径长度相差不超过两个、三个或其他常数。或者最大路径长度是最小路径长度的某个分数,比如一半或四分之一。
通常不重要。任何平衡树算法的重点是确保您不会陷入一侧有一百万个节点,而另一侧只有三个节点的情况。Donald的定义在理论上很好,但在实践中,找到满足那种严格性的平衡树算法是很痛苦的。性能节省通常不能证明实现成本。您会花费大量时间进行不必要的树形重排,以达到一定程度的平衡,在实践中几乎没有任何区别。如果有时需要经过四十个分支才能到达一百万个节点不完美平衡树的最远叶子,而在完美平衡树中只需要二十个,那又有什么关系呢?关键是它从来没有达到一百万。从最坏情况的一百万下降到最坏情况的四十通常已经足够了;你不必一直走到最优情况。

1
以下是针对“练习题”的答案... - Potatoswatter
回答了下面的奖励练习。 - Brian
SDK下面的答案似乎是正确的,只需要进行2次树遍历,因此时间复杂度为O(n)。除非我漏掉了什么,这不是至少解决了你的第一个奖励问题吗?当然,您也可以使用动态规划和您的解决方案来缓存中间高度。 - Aly
理论上,我仍然会支持唐纳尔·费洛斯的定义。 - Dhruv Gairola
好的,我认为Lucky的答案满足了奖励和超级奖励的所有要求——它是迭代的(使用队列),而不是递归的——因此堆栈不会溢出,并且我们将所有内存问题移动到堆中。此外,它按层移动,因此如果树向一侧严重不平衡,它将很快找到它。 - Maverick Meerkat

30
平衡是一种非常微妙的属性;你认为你知道它是什么,但很容易出错。特别是,即使Eric Lippert的(好的)答案也有偏差。那是因为高度的概念是不够的。你需要有一棵树的最小和最大高度的概念(其中最小高度是从根到叶子节点的最少步数,而最大高度是......好吧,你明白了)。在此基础上,我们可以定义平衡为:

任何分支的最大高度不超过任何分支的最小高度

(这实际上意味着分支本身也是平衡的;你可以选择相同的分支作为最大和最小值。)

您只需要执行一个简单的树遍历来验证此属性,并跟踪当前深度。第一次回溯时,这会给您一个基准深度。之后每次回溯时,您将新深度与基准深度进行比较:

  • 如果它等于基准深度,则继续
  • 如果相差超过一,则树不平衡
  • 如果相差一,则现在您知道平衡的范围,并且所有后续深度(当您即将回溯时)必须是第一个或第二个值。

代码如下:

class Tree {
    Tree left, right;
    static interface Observer {
        public void before();
        public void after();
        public boolean end();
    }
    static boolean traverse(Tree t, Observer o) {
        if (t == null) {
            return o.end();
        } else {
            o.before();
            try {
                if (traverse(left, o))
                    return traverse(right, o);
                return false;
            } finally {
                o.after();
            }
        }
    }
    boolean balanced() {
        final Integer[] heights = new Integer[2];
        return traverse(this, new Observer() {
            int h;
            public void before() { h++; }
            public void after() { h--; }
            public boolean end() {
                if (heights[0] == null) {
                    heights[0] = h;
                } else if (Math.abs(heights[0] - h) > 1) {
                    return false;
                } else if (heights[0] != h) {
                    if (heights[1] == null) {
                        heights[1] = h;
                    } else if (heights[1] != h) {
                        return false;
                    }
                }
                return true;
            }
        });
    }
}

我想你可以不使用观察者模式来完成这个任务,但是我发现使用观察者模式更容易理解。


[编辑]: 为什么不能只考虑每一侧的高度呢?考虑下面这棵树:

        /\
       /  \
      /    \
     /      \_____
    /\      /     \_
   /  \    /      / \
  /\   C  /\     /   \
 /  \    /  \   /\   /\
A    B  D    E F  G H  J

好的,有点混乱,但根的两侧是平衡的: C 的深度为2, A, B, D, E 的深度为3,F, G, H, J 的深度为4。左侧分支的高度为2 (记住,随着遍历分支,高度会减少),右侧分支的高度为3。然而,整个树并不平衡,因为 CF 之间的高度差为2。您需要一个极小值-极大值规范(虽然实际算法可以更简单,因为只应该允许两个高度)。


啊,说得好。你可以有一棵树,h(LL)=4,h(LR)=3,h(RL)=3,h(RR)=2。因此,h(L)=4和h(R)=3,所以它看起来平衡到了早期的算法,但最大/最小深度为4/2,这并不是平衡的。这可能需要用图片来更清晰地表达。 - Tim
1
这就是我刚刚添加的内容(带有世界上最糟糕的ASCII图形树)。 - Donal Fellows
@DonalFellows:你提到左分支的高度为2。但是左分支包括根节点和叶子A在内共有4个节点。在这种情况下,高度将为3,对吗? - brain storm
“一棵树,其中任何分支的最大高度不超过任何分支的最小高度加一。” 这个定义是不正确的。一个例子是- A--(右分支)---B---(右分支)---C在这里,最大深度和最小深度都等于2,但它不是平衡二叉树。 - Yash Verma

23

这只是确定树的顶层是否平衡。也就是说,你可以有一个树,在远左和远右分别有两个长分支,中间什么都没有,但这将返回true。在返回true之前,您需要递归检查root.leftroot.right以查看它们是否内部平衡。


然而,如果代码有一个最大和最小高度的方法,如果它在全局上是平衡的,那么它在局部上也会平衡。 - Ari

22

额外练习响应。简单的解决方案。显然,在实际实现中,人们可能会将其包装起来或采取其他措施,以避免要求用户在其响应中包含高度。

IsHeightBalanced(tree, out height)
    if (tree is empty)
        height = 0
        return true
    balance = IsHeightBalanced(tree.left, out heightleft) and IsHeightBalanced(tree.right, out heightright)
    height = max(heightleft, heightright)+1
    return balance and abs(heightleft - heightright) <= 1     

如果树高超过几百层,你会得到一个堆栈溢出异常。虽然你已经有效率地完成了它,但是它无法处理中等或大型数据集。 - Eric Leschinski
你刚才想出来的是伪代码还是真实的编程语言?(我的意思是“out height”变量符号) - kap
@kap:这是伪代码,但out语法取自C#。基本上,它意味着参数从被调用函数传递到调用者(与标准参数相反,标准参数从调用者传递到被调用函数或ref参数从调用者传递到被调用函数并返回)。这有效地允许函数返回多个值。 - Brian
很想知道 heightleftheightright 是从哪里来的。 - Jac Frall
@JacFrall:IsHeightBalanced 的第二个参数是一个 out 参数。例如,IsHeightBalanced(tree.left, heightleft) 并没有传递 heightleft 的值,而是计算了 heightleft。我已经编辑了我的答案,使这种使用 out 参数更加明确。 - Brian

22

后序遍历解法,仅遍历一次树。时间复杂度为O(n),空间复杂度为O(1),比自顶向下的解法更好。我给你提供一个Java版本的实现。

public static <T> boolean isBalanced(TreeNode<T> root){
    return checkBalance(root) != -1;
}

private static <T> int checkBalance(TreeNode<T> node){
    if(node == null) return 0;
    int left = checkBalance(node.getLeft());

    if(left == -1) return -1;

    int right = checkBalance(node.getRight());

    if(right == -1) return -1;

    if(Math.abs(left - right) > 1){
        return -1;
    }else{
        return 1 + Math.max(left, right);
    }
}

4
好的解决方案,但空间复杂度应为O(H),其中H是树的高度。这是因为递归需要使用堆栈分配空间。 - legrass
left == -1 是什么意思?在什么情况下会出现这种情况?我们是否可以假设递归调用意味着如果左子树的所有子树都不平衡,则 left == -1 为真? - Adrienne
left == 1 表示左子树不平衡,那么整棵树就不平衡了。我们无需再检查右子树,可以直接返回 -1 - tning
时间复杂度为O(n),因为您必须遍历所有元素。如果您有x个节点,并且检查平衡需要y时间;如果您有2x个节点,则检查平衡需要2y时间。这些都正确吗? - Jack
这里有带图解的详细说明: https://algorithms.tutorialhorizon.com/find-whether-if-a-given-binary-tree-is-balanced/ - Shir

15

平衡二叉树的定义如下:

对于每个节点,其两棵子树的高度差不超过1。

因此,空二叉树一定是平衡的。
对于非空二叉树,当且仅当:

  1. 其左子树是平衡的;
  2. 其右子树是平衡的;
  3. 左右子树的高度差不超过1。

考虑以下树:

    A
     \ 
      B
     / \
    C   D
作为左子树为空,右子树高度平衡的A树不满足第三个条件,即左子树的高度为0,右子树的高度为2,因此它并不是高度平衡的。
即使左右子树的高度相等,下图所示的树也未必是高度平衡的。现有代码将返回 true。
       A
     /  \ 
    B    C
   /      \
  D        G
 /          \
E            H

因此,定义中的every一词非常重要。

这将起作用:

int height(treeNodePtr root) {
        return (!root) ? 0: 1 + MAX(height(root->left),height(root->right));
}

bool isHeightBalanced(treeNodePtr root) {
        return (root == NULL) ||
                (isHeightBalanced(root->left) &&
                isHeightBalanced(root->right) &&
                abs(height(root->left) - height(root->right)) <=1);
}

Ideone链接


所以这个答案对我帮助很大。然而,我发现免费的[MIT算法导论课程]似乎与条件3相矛盾。第4页展示了一棵红黑树,其中左分支的高度为2,右分支的高度为4。你能给我一些澄清吗?也许我没有理解子树的定义。[1]:http://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-046j-introduction-to-algorithms-sma-5503-fall-2005/video-lectures/lecture-10-red-black-trees-rotations-insertions-deletions/lec10.pdf - i8abug
区别似乎来自于课程笔记中的这个定义。从任何节点x到后代叶子的所有简单路径具有相同数量的黑色节点= black-height(x) - i8abug
仅仅是跟进一下,我找到了一个定义,将你回答中的第三点改为“每个叶子离根节点的距离都不超过任何其他叶子离根节点的距离”。这似乎满足了两种情况。这里是一些随机课件的链接。 - i8abug

8

可以通过层序遍历来检查二叉树是否平衡:

private boolean isLeaf(TreeNode root) {
    if (root.left == null && root.right == null)
        return true;
    return false;
}

private boolean isBalanced(TreeNode root) {
    if (root == null)
        return true;
    Vector<TreeNode> queue = new Vector<TreeNode>();
    int level = 1, minLevel = Integer.MAX_VALUE, maxLevel = Integer.MIN_VALUE;
    queue.add(root);
    while (!queue.isEmpty()) {
        int elementCount = queue.size();
        while (elementCount > 0) {
            TreeNode node = queue.remove(0);
            if (isLeaf(node)) {
                if (minLevel > level)
                    minLevel = level;
                if (maxLevel < level)
                    maxLevel = level;
            } else {
                if (node.left != null)
                    queue.add(node.left);
                if (node.right != null)
                    queue.add(node.right);
            }
            elementCount--;
        }
        if (abs(maxLevel - minLevel) > 1) {
            return false;
        }
        level++;
    }

    return true;
}

2
非常出色的答案。我认为它符合 Eric 在奖金和超级奖金方面发布的所有要求。它是迭代的(使用队列),而不是递归的,因此调用堆栈不会溢出,并且我们将所有内存问题转移到堆上。它甚至不需要遍历整个树。它逐层移动,因此如果树明显向一侧不平衡,它会很快找到(最快?比大多数递归算法更快,尽管您可以实现后序遍历迭代算法,它将更快地找到最后一层的不平衡,但在第一层上表现较差)。所以 +1 :-) - Maverick Meerkat
它对于一些情况不起作用,例如:1->2->3 请查看https://leetcode.com/problems/balanced-binary-tree/ - Davide Pugliese

7

这个算法比实际情况复杂得多。

算法如下:

  1. 令A = 最高级节点的深度
  2. 令B = 最低级节点的深度

  3. 如果abs(A-B) <= 1,则树是平衡的


3
两个问题,它的效率还不如可能达到的那么高,你需要对整棵树进行两次遍历。对于左侧只有一个节点而右侧有成千上万节点的树来说,当你可以在3次检查之后停止时,你不必要地遍历整棵树。 - Eric Leschinski

5
根据不同的结构,平衡的含义也有所不同。例如,B树不能有节点距离根节点太深或太浅,所有数据都在距离根节点的固定深度处,但如果叶子节点和倒数第二个节点之间的分布不均匀,则可能会失衡。跳表完全没有平衡的概念,而是依靠概率来实现良好的性能。斐波那契树有意失衡,推迟重新平衡以换取卓越的渐进性能,尽管偶尔需要更长时间的更新。AVL树和红黑树在每个节点上附加元数据,以达到深度平衡不变式。
这些结构及更多其他结构都存在于大多数常见编程系统的标准库中(除了Python,愤怒!)。实现一两个结构是良好的编程实践,但为生产环境自己编写可能不是一个好主意,除非您的问题有某些特殊的性能需求,无法通过任何现成的集合来满足。

4
注意1:任何子树的高度只计算一次。
注意2:如果左子树不平衡,则会跳过右子树的计算,右子树可能包含数百万个元素。
// return height of tree rooted at "tn" if, and only if, it is a balanced subtree
// else return -1
int maxHeight( TreeNode const * tn ) {
    if( tn ) {
        int const lh = maxHeight( tn->left );
        if( lh == -1 ) return -1;
        int const rh = maxHeight( tn->right );
        if( rh == -1 ) return -1;
        if( abs( lh - rh ) > 1 ) return -1;
        return 1 + max( lh, rh );
    }
    return 0;
}

bool isBalanced( TreeNode const * root ) {
    // Unless the maxHeight is -1, the subtree under "root" is balanced
    return maxHeight( root ) != -1;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接