一种简单的平衡树-AVL树
AVL 树
AVL树(Adelson-Velskii 和 Laandis)树是带有平衡条件(balance condition)的二叉查找树。这个平衡条件必须要容易保持,而且他保证树的深度必须是 O(log N)。最简单的想法是要求左右子树具有相同的高度。
另一种平衡条件是要求每个节点都必须有相同高度的左子树和右子树。如果空子树的高度定义为 -1,那么只有具有 2k-1 个节点的理想平衡树(perfectly balanced tree)满足这个条件。因此,虽然这种平衡条件保证了树的深度小,但是太严格难以使用。
AVL 树是其每个节点的左子树和右子树的高度最多差 1 的二叉查找树(空树的高度定义为 -1)。
下图是一棵具有最少节点(143)和高度为 9 的 AVL 树。这棵树的左子树是高度为 7 且大小最小的 AVL 树,右子树是高度为 8 且大小最小的 AVL 树。他告诉我们,在高度为 h 的 AVL 树种,最少节点数由
- S(h) = S(h - 1) + S(h - 2) + 1 给出
对于 h = 0,S(h) = 1;h = 1, S(h) = 2。函数 S(h) 与斐波那契数列密切相关。
因此,出去可能的插入外(我们将假设懒惰删除),所有的树操作都可以以时间 O(log N) 执行。当执行插入操作时,我们需要更新通向根节点路径上的那些节点的所有平衡信息,而插入操作隐含的困难的原因在于,插入一个节点可能破坏 AVL 树的特性。如果发生这种情况,那么就要在考虑这一步插入完成之前回复平衡的性质。[br]事实上,我们可以通过旋转来对树进行简单的修正来做到。
- 在插入以后,只有那些从插入点到根节点的路径上的节点的平衡可能被改变,因为只有这些节点的子树可能发生变化。当我们沿着这条路径上行到根并更新平衡信息时,可以发现一个节点,它的新平衡破坏了 AVL 条件。我们将支出如何在第一个这样的节点(即最深的节点)重新平衡这棵树,并证明这一重新平衡保证整个树满足 AVL 性质。
我们把必须重新平衡的节点叫做 α。由于任意节点最多只有两个儿子,因此出现高度不平衡就需要 α 点的两颗子树的高度差 2。这可能出现在一下四中情况中:
对 α 的左儿子的左子树进行一次插入;
对 α 的左儿子的右子树进行一次插入;
对 α 的右儿子的左子树进行一次插入;
对 α 的右儿子的右子树进行一次插入。
情形 1 和 4 是关于 α 点的镜像对称,而 2 和 3 是关于 α 点的镜像对称。
第一种情况是插入发生在 "外边" 的情况(即左-左的情况或右-右的情况),该情况通过对树的一次单旋转(single rotation)而完成调整。第二种情况是插入发生在 "内部" 的情况(即左-右的情况或右-左的情况),该情况通过稍微复杂一些的双旋转(double rotation)实现。
情形可能如下所示:
/ * 考虑对如下的树中插入数字 2 * * 5 * / \ * 4 6 * / \ * 3 ? * * 4 * / \ * 3 5 * / / \ * 2 ? 6 * * ? 代表可能存在也可能不存在,不会影响结果.并且,在这里,我们可以将 ? 当做 5 的左节点,也可以将 * ? 当做 3 的左节点 * @param k2 * @return */
- k2不满足 AVL 平衡性质,因为他的左子树比右子树深 2 层(图中间的虚线表示树的各层)。该图所描述的情况是情形 1 的一种可能的情况,在插入之前 k2满足 AVL 性质,但是在插入之后,子树 X 长出一层,这使得他比子树 Z 深出 2 层。Y 不可能与新 X 在同一水平上,因为那样 k2 在插入以前就已经失去平衡了;Y 也不可能与 Z 在同一层上,因为那样 k1 就会是在通向根路径上破坏 AVL 平衡条件的第一个节点。
现在我们观察这棵树,我们会得到一些结论:
kX 1 Y 2 Z,因为现在节点 X 的深度为 2,所以节点 X 必须为节点 1 的一个单独的节点;[br]又因为 kY 2 Z。所以可以使用 k2 作为根节点,kY 和 kZ 作为 k2 的左节点和右节点生成新的 AVL 树。[br]最后,以这棵新生成的 AVL 树作为 k1 的右节点。
我们沿着插入的节点上行,找到第一个特殊的节点,这个特殊的节点破坏了 AVL 树的平衡;[br]如果是情况 1,那么我们假设连接插入数据的节点和特殊节点的节点为 k1,这个特殊的节点为 k2。那么,我们现在只需要用 k1 当做新的根节点,左节点不变,同时将 k1 的右节点当做 k2 的左节点,并将 k2 作为 k1 的新的右节点即可。[br]如果是情况 4,那么我们这个特殊的节点为 k1,连接插入数据的节点和特殊节点的节点为 k2。那么,我们现在可以使用 k2 作为新的根节点,右节点不变,同时将 k2 的左节点当做 k1 的右节点,最后将 k1 作为 k2 的新的左节点即可。
双旋转
/ * 对于情形 2 的一次双旋转 * * k3 * / \ * k1 D * / \ * A k2 * * 插入节点 B 或 C,B 和 C 在插入任意一个的时候就已经破坏了 Avl 树的平衡条件 * * k3 * / \ * k1 D * / \ * A k2 * / \ * B C * * k3.left = rotateWithRightChild(k3.left),以 k3.left 即 k1 为根节点进行一次右旋转 * * k3 * / \ * k2 D * / \ * k1 C * / \ * A B * * rotateWithLeftChild(k3) * * k2 * / \ * k1 k3 * / \ / \ * A B C D * * @param k3 * @return */
我们注意到,在上面的图中,k1 2 3,这迫使 k1 是 k2 的左儿子,k3 是 k2 的右儿子。于是,最后我们得到的结果就很明显了。
需要旋转的三个节点是:上行找到的第一个破坏 AVL 树的节点,新的节点插入后的父节点,连接第一个破坏 AVL 树的节点和新的节点的父节点。
假设这三个节点分别是 k1,k2,k3,则 k1 和 k3 是根据插入的数据是情况 2 或情况 3 下变化的,而 k2 永远都是那个被插入数据的节点。[br]在情况 2 下,k1 是连接特殊节点和被插入数据节点的节点,k3 是特殊节点;[br]在情况 3 下,k1 是特殊节点,k3 是连接特殊节点和被插入数据节点的节点;[br]在情况 2 和 3 下,我们都是使用 k2 作为新的根节点,k1 作为左儿子,k3 作为右儿子;[br]随后,我们将 k2 原来的左子节点作为 k1 的右子节点,k2 的原来的右子节点作为 k3 的左子节点。
不论是单旋转还是双旋转,我们都需要将得到的新的 AVL 子树添加到原来的树中。
AVL 树的插入方法的实现
为了将项是 X 的一个新节点插入到一棵 AVL 树 T 种去,我们递归的将 X 插入到 T 相应的子树(称为 TLR)中。如果 TLR 的高度不变,那么插入完成。否则,如果在 T 中出现高度不平衡,则根据 X 以及 T 和 TLR 中的项左适当的单旋转或双旋转,更新这些高度(并解决好与树的其余部分的链接)从而完成插入。
package com.mosby.common.structure;
/
AVL 树
*/
public class AvlTree > {
private static class AvlNode >{
AvlNode(T element){
this(element, null, null);
}
AvlNode(T element, AvlNode left, AvlNode right){
this.element = element;
this.left = left;
this.right = right;
this.height = 0;
}
T element;
AvlNode left;
AvlNode right;
int height;
@Override
public String toString(){
return element.toString();
}
}
public AvlTree(){
root = null;
}
public void insert(T x){
root = insert(x, root);
}public void remove(T x){
System.out.println("Sorry, remove unimplemented");
}public T findMin(){
return elementAt(findMin(root));
}
public T findMax(){
return elementAt(findMax(root));
}
public T find(T x){
return elementAt(find(x, root));
}
public void makeEmpty(){
root = null;
}public boolean isEmpty(){
return root == null;
}
private T elementAt(AvlNode t){
return t == null ? null : t.element;
}
private AvlNode findMin(AvlNode t){
if(t == null){
return t;
}
while(t.left != null){
t = t.left;
}
return t;
}
private AvlNode findMax(AvlNode t){
if(t == null){
return t;
}
while(t.right != null){
t = t.right;
}
return t;
}
private AvlNode find(T x, AvlNode t){
while(t != null){
if(x.compareTo(t.element) 0){
t = t.right;
}else{
return t;
}
}
return null;
}
/- 插入方法
- @param x
- @param t
- @return
*/
private AvlNode insert(T x, AvlNode t){
if(t == null){
return new AvlNode(x);
}else if(x.compareTo(t.element) 0){
t.right = insert(x, t.right);
if(height(t.right) - height(t.left) == 2){
if(x.compareTo(t.right.element) > 0){
t = rotateWithRightChild(t);
}else{
t = doubleWithRightChild(t);
}
}
}else{
//Duplicate; do nothing
}
t.height = max(height(t.left), height(t.right)) + 1;
return t;
}
/
- 插入相关操作
*/
private static > int height(AvlNode t){
return t == null ? -1 : t.height;
}
private static int max(int lhs, int rhs){
return lhs > rhs ? lhs : rhs;
}
/ - 对于情形 1 的一次旋转
- 5
- / \
- 4 6
- / \
- 3 ?
- 4
- / \
- 3 5
- / / \
- 2 ? 6
- ? 代表可能存在也可能不存在,不会影响结果
- @param k2
- @return
*/
private static > AvlNode rotateWithLeftChild(AvlNode k2){
//k2 是节点 5,k1 是节点 4
AvlNode k1 = k2.left;
k2.left = k1.right;
k1.right = k2;
/- 我们可以看到,节点中有三个节点的高度改变了:3,4,5.
- 而我们在最后插入时的函数栈如下所示:
- 2, null
- 2, 3
- 2, 4
- 2, 5
*/
k2.height = max(height(k2.left), height(k2.right)) + 1;
k1.height = max(height(k1.left), height(k1.right)) + 1;
return k1;
}
/
- 对于情形 4 的一次旋转
- 5
- / \
- 4 6
- / \
- ? 7
- 6
- / \
- 5 7
- / \ \
- 4 ? 8
- @param k1
- @return
*/
private static> AvlNode rotateWithRightChild(AvlNode k1){
//k1 是节点 5,k2 是节点 6
AvlNode k2 = k1.right;
k1.right = k2.left;
k2.left = k1;
/- 节点中有 3 个节点的高度改变了:5,6,7
- 在最后插入时的函数栈如下所示:
- 8, null
- 8, 7
- 8, 6
- 8, 5
*/
k1.height = max(height(k1.left), height(k1.right)) + 1;
k2.height = max(height(k2.left), height(k2.right)) + 1;
return k2;
}
/
- 对于情形 2 的一次双旋转
- k3
- / \
- k1 D
- / \
- A k2
- 插入节点 B 或 C,B 和 C 在插入任意一个的时候就已经破坏了 Avl 树的平衡条件
- k3
- / \
- k1 D
- / \
- A k2
- / \
- B C
- k3.left = rotateWithRightChild(k3.left),以 k3.left 即 k1 为根节点进行一次右旋转
- k3
- / \
- k2 D
- / \
- k1 C
- / \
- A B
- rotateWithLeftChild(k3)
- k2
- / \
- k1 k3
- / \ / \
- A B C D
- @param k3
- @return
*/
private static > AvlNode doubleWithLeftChild(AvlNode k3){
/- k1 是节点 4,k2 是节点 5,k3 是节点 7
- 我们看到,在第一次旋转中,有几个节点的高度改变了:k2、k3
- 在插入的时候,函数栈应该如下所示:
- B|C null
- B|C k2
- B|C k1
- B|C k3
*/
k3.left = rotateWithRightChild(k3.left);
return rotateWithLeftChild(k3);
}
private static > AvlNode doubleWithRightChild(AvlNode k1){
k1.right = rotateWithLeftChild(k1.right);
return rotateWithRightChild(k1);
}
private AvlNode root;
public static void main(String[] args) {
AvlTree avl = new AvlTree();
avl.insert(5);
avl.insert(4);
avl.insert(3);
avl.insert(2);
avl.insert(1);
avl.insert(0);
}
}
注意几点:
所谓的 rotateLeft 和 rotateRight 是指旋转左树还是右树,而不是指向左旋转还是向右旋转。例如 rotateLeft(t) 就是将 t.left 和 t 调换位置;
我们在递归中已经修改了 height,为什么在旋转的时候还要修改树的高度呢?[br]以左旋转为例子:
* 2, null
* ------
* 2, 3
* ------
* 2, 4
* ------
* 2, 5
在这个函数栈中,4 相对于 5 先出栈,并且在递归的求 4 的高度的时候需要用到 5 的高度,而 5 在进行旋转之后它的高度就已经改变了,所以我们必须在旋转后立即改变 5 的高度。
而 3 虽然它的高度改变了,但是 3 在栈中的位置决定了它在 null 出栈后即运行,且它仅仅会用到新插入的节点的高度,而新插入的节点的高度显然是为 0 的,所以它不需要做特别的处理,等出栈的时候计算它的高度就可以了。
关键字:数据结构和算法
版权声明
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处。如若内容有涉嫌抄袭侵权/违法违规/事实不符,请点击 举报 进行投诉反馈!