AVL树基础知识与代码实现

普通二叉排序树的缺点

之前讲过二叉排序树, 即某个树的任意一个节点,都满足左子树的值都小于该节点值,右子树的值都大于该节点的值。
同样的数字集合,按照不同的方式插入一个新的二叉排序树时,会形成不同的树结构。
例如按照:1, 2, 3, 4, 5 的顺序插入二叉排序树,则正常操作下二叉排序树会退化成一个只有右节点的链表。
链表结构对于查找,删除等操作都是不方便操作的,平均时间复杂度都是O(N)。这就是普通二叉排序树的缺点。

AVL树简介

来源与概念

AVL树是一种特殊的二叉排序树。
名称:AVL树
发明者:
G. M. Adelson-Velsky
E. M. Landis
年代:1962年(60年历史)
性质:对于任意一个节点,左子树和右子树的高度差绝对值都不大于1。
| H(left) - H(right) | <= 1
优点:
由于对每个节点的左右子树的树高做了限制,所以整棵树不会退化成一个链表。

AVL树-左旋操作介绍

抓住K1节点,向左旋转:K1成了K3的左子树,K3原来的左子树成了K1的右子树。
在这里插入图片描述
可以看出,如果左旋之前是二叉排序树,那么左旋操作以后仍然是二叉排序树。

AVL树-右旋操作介绍

右旋是左旋的逆操作,如下图抓住K1右旋:K1成了K2的右子树,K2原来的右子树成了K1的左子树。
在这里插入图片描述
可以看出,如果右旋之前是二叉排序树,那么右旋操作以后仍然是二叉排序树。

AVL树-失衡类型及调整方案

K1节点:插入与删除的过程中发现的第一个失衡的节点。
共分四种类型:LL型,LR型,RL型, RR型。
例如LL:K1节点往下看时第一次发现失衡,且左子树更高,左子树的左子树更高。注意K1往下的K2, K3等都没有发现失衡。
LR型为K1的左子树的右子树更高,依次类推。
在这里插入图片描述
当出现LL型失衡时,A和B两个节点一定A比B高。因为节点是一个一个插入的。其他失衡类型也类似,对应的两个孙子节点一定有一个高一个低。
可以看出,LL和RR类型本质可以归为一大类,LR和RL类型可以归为一大类。

AVL树-LL型失衡

抓着K1进行一个大右旋。
在这里插入图片描述
为什么右旋以后就会变成AVL树?
首先上面分析过,A节点的高度一定比B节点的高度多一个。
从右旋之前来看,K2, K3两个节点的高度分别为:
h K 2 = h A + 1 (1) h_{K2} = h_A + 1\tag{1} hK2=hA+1(1)
h K 3 = max ⁡ ( h C , h D ) + 1 (2) h_{K3} = \max(h_C, h_D) + 1\tag{2} hK3=max(hC,hD)+1(2)
又因为LL失衡,所以:
h K 2 = h K 3 + 2 (3) h_{K2} = h_{K3} + 2\tag{3} hK2=hK3+2(3)
将(1)式和(2)式分别带入(3)式,可得:
h A = max ⁡ ( h C , h D ) + 2 (4) h_A = \max(h_C, h_D) + 2\tag{4} hA=max(hC,hD)+2(4)

h A = h B + 1 = max ⁡ ( h C , h D ) + 2 (5) h_A = h_B + 1 = \max(h_C, h_D) + 2\tag{5} hA=hB+1=max(hC,hD)+2(5)
所以从A,B,C,D四个节点的关系看右旋后的树, 从K3节点向下看只有C和D两个子树,没失衡;
从K1节点向下看,根据(5)式, h B = max ⁡ ( h C , h D ) + 1 = h K 3 h_B = \max(h_C, h_D) + 1 = h_{K3} hB=max(hC,hD)+1=hK3, 所以也没有失衡;
从K2节点向下看, h A = h B + 1 = h K 1 h_{A} = h_{B} + 1 = h_{K1} hA=hB+1=hK1, 也没有失衡。
所以右旋后,无论从哪个节点看,都维持了AVL树的性质。

AVL树-LR型失衡

先抓住K2进行一个小左旋,将其变为LL型失衡,再抓住K1进行一个大右旋。
在这里插入图片描述
经过这样的调整后会变成一个巨平衡的树。证明如下:
首先看调整之前,K3的高度可以表示为:
h K 3 = max ⁡ ( h B , h C ) + 1 (6) h_{K3} = \max(h_B, h_C) + 1\tag{6} hK3=max(hB,hC)+1(6)
又因为之前说过,出现LR失衡时K3的高度一定比A高度高1个,即:
h K 3 = h A + 1 (7) h_{K3} = h_A + 1\tag{7} hK3=hA+1(7)
结合(6)和(7)可得:
h A = max ⁡ ( h B , h C ) (8) h_{A} = \max(h_B, h_C)\tag{8} hA=max(hB,hC)(8)
K2的高度:
h K 2 = h K 3 + 1 = h A + 2          ( K 3 比 A 高一个 ) = h D + 2          ( 从 K 1 向下看失衡了 ) (9) \begin{aligned} h_{K2} &= h_{K3} + 1 \\ &= h_A+2 \ \ \ \ \ \ \ \ (K3比A高一个) \\ &= h_D + 2 \ \ \ \ \ \ \ \ (从K1向下看失衡了) \end{aligned} \tag{9} hK2=hK3+1=hA+2        (K3A高一个)=hD+2        (K1向下看失衡了)(9)
所以可以得到
h A = h D (10) h_A = h_D\tag{10} hA=hD(10)
即A,B,C,D的高度关系为:
h A = h D = max ⁡ ( h B , h C ) (11) h_A = h_D = \max(h_B, h_C)\tag{11} hA=hD=max(hB,hC)(11)
所以用这个关系看调整后的每个节点,可以发现从K1, K2, K3向下看都是平衡的。

小练习:
按照如下顺序插入数字,画出对应的AVL树。
1 : [ 5, 9, 8, 3, 2, 4, 1, 7 ]
2:[ 1, 2, 3, 4, 5 ]
答案略。
RR类型和LL类型类似,进行一个大左旋即可;
RL类型和LR类型类似,先将右子树进行一个小右旋,再将失衡的当前节点进行一个大左旋即可。

AVL树的代码演示

#include <iostream>
#include <vector>
#include <cstdio>
using namespace std;

//虚拟空节点,为避免后续各种为空的判断,引入之
#define NIL (&Node::__NIL)

struct Node {
	Node(int key = 0, int h = 0, Node *left = NIL, Node *right = NIL)
		:key(key), h(h), left(left), right(right) {}
	int key, h;
	Node *left, *right;
	static Node __NIL; //虚拟空节点静态变量
};
Node Node::__NIL;  //虚拟空节点变量声明

Node *getNewNode(int key) {
	return new Node(key, 1);
}

//调整树的高度
void update_height(Node *root) {
	root->h = max(root->left->h, root->right->h) + 1;
	return;
}

//左旋操作
Node *left_rotate(Node *root) {
	Node *temp = root->right;
	root->right = temp->left;
	temp->left = root;
	update_height(root);
	update_height(temp);
	return temp;
}

//右旋操作
Node *right_rotate(Node *root) {
	Node *temp = root->left;
	root->left = temp->right;
	temp->right = root;
	update_height(root);
	update_height(temp);
	return temp;
}

//二叉树的高度调整
Node *maintain(Node *root) {
	if (abs(root->left->h - root->right->h) < 2) return root;
    if (root->left->h > root->right->h) {
		if (root->left->right->h > root->left->left->h) {
			root->left = left_rotate(root->left);
		}
		root = right_rotate(root);
	}else {
    if (root->right->left->h > root->right->right->h) {
			root->right = right_rotate(root->right);
		}
		root = left_rotate(root);
	}
	return root;
}

//AVL 树的插入操作
Node *insert(Node *root, int key) {
	if (root == NIL) return getNewNode(key);
	if (key == root->key) return root;
	else if (key < root->key) root->left = insert(root->left, key);
	else if (key > root->key) root->right = insert(root->right, key);
	update_height(root);
	return maintain(root);
}

//AVL树寻找节点的前驱
Node *predecessor(Node *root) {
	Node *temp = root->left;
	while (temp->right != NIL) temp = temp->right;
	return temp;
}

//AVL 树的删除操作
Node *erase(Node *root, int key) {
	if (root == NIL) return root;
	if (key < root->key) root->left = erase(root->left, key);
	else if (key > root->key) root->right = erase(root->right, key);
	else {
		if (root->left == NIL || root->right == NIL) {
			Node *temp = root->left == NIL ? root->right : root->left;
			delete root;
			return temp;
		}else {
			Node *temp = predecessor(root);
			root->key = temp->key;
			root->left = erase(root->left, temp->key);
		}
	}
	update_height(root);
	return maintain(root);
}

//删除一棵树
void clear(Node *root) {
	if (root == NIL) return ;
	clear(root->left);
	clear(root->right);
	cout << "delete : " << root->key << endl;
	delete root;
	return;
}

//打印节点信息
void print(Node *root) {
	printf("(%d[%d]) | %d %d\n", root->key, root->h, root->left->key, root->right->key);
}

//输出一棵树
void output(Node *root) {
	if (root == NIL) return;
	print(root);
	output(root->left);
	output(root->right);
	return ;
}

int main() {
	int op, val;
	Node *root = NIL;
	while (cin >> op >> val) {
		switch(op) {
			case 0: root = insert(root, val); break;
			case 1: root = erase(root, val); break;
		}
	    cout << endl << "===== AVL tree print ======" << endl;
	    output(root);
	    cout << endl << "===== tree print done ======" << endl;
	}
	clear(root);
	return 0;
}

总结:1. 引入虚拟空节点,减少了大量判断的操作,使得实际的调整代码较为简洁;2. 递归插入,递归删除;3. 合并某些重复的代码。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值