算法学习笔记 平衡二叉树 AVL树

AVL树是最先发明的自平衡二叉查找树, 其增删查时间复杂度都是 O(logn), 是一种相当高效的数据结构。当面对需要频繁查找又经常增删这种情景时,AVL树就非常的适用。[ 博客地址:http://blog.csdn.net/thisinnocence ]

AVL树定义

AVL树诞生于 1962 年,由 G.M. Adelson-Velsky 和 E.M. Landis 发明。AVL树首先是一种二叉查找树。二叉查找树是这么定义的,为空或具有以下性质:

  • 若它的左子树不空,则左子树上所有的点的值均小于其根节点;
  • 若它的右子树不空,则右子树上所有的点的值均大于其根节点;
  • 它的左右子树分别为二叉查找树;

AVL 树是具有以下性质的二叉查找树:

  • 左子树和右子树的高度差(平衡因子)不能超过 1;
  • 左子树和右子树都是 AVL树;

AVL树与红黑树对比

提到平衡二叉树的常用算法就不得不提红黑树,红黑树诞生于1972年,由 Rudolf Bayer 发明。比 AVL 树晚 10 年,由于其随机数据插入性能更好,获得了更为广泛的应用。如
Java 的 TreeMap和C++ STL 的map,linux 内核中的用户态地址空间管理等都采用了红黑树算法。当然 AVL树也是很有应用价值的,比如
32 位的 Windows 系统用户态地址空间管理就采用了AVL树, 毕竟 AVL的搜索性能还是很具优势的。

对比之前,再介绍一下红黑树的定义。在《算法导论》红黑树定义如下,红黑树是指一棵满足下述性质的二叉查找树:(1) 每个结点或者为黑色或者为红色; (2) 根结点为黑色; (3) 每个叶结点(实际上就是NULL指针)都是黑色的; (4) 如果一个结点是红色的,那么它的两个子节点都是黑色的(也就是说,不能有两个相邻的红色结点); (5)对于每个结点,从该结点到其所有子孙叶结点的路径中所包含的黑色结点数量必须相同;

AVL与红黑树不同之处:

  • 红黑树对于数值随机插入性能更好,这种场景实际更常见,故应用更广泛;
  • AVL 树对于顺序数据插入更有优势;
  • 红黑树部分平衡,降低了旋转的要求;
  • AVL 树高度平衡,查询操作更具优势;
  • AVL 树代码更为简单易实现;

小实验(C实现)

对于二叉树的相关算法,一定要有递归的思想,分析问题时,要会合理分析整体与局部。AVL树实现的核心在于插入数据后,如何维护其平衡。对于数据的插入,肯定通过递归来找到对应的叶节点,将对应数据插入。然后插入后,就是通过对应的旋转操作来维护其平衡。这种旋转有四种情形:

  1. LL 型,节点的左孩子的左子树上插入一个新的节点,单右旋;
  2. LR 型, 节点的左孩子的右子树上插入一个新的节点,双旋,先左旋后右旋;
  3. RR 型,节点的右孩子的右子树上插入一个新的节点,单左旋;
  4. RL 型,节点的右孩子的左子树上插入一个新的节点,双旋,先右旋后左旋;

代码:

#include <stdlib.h>
#include <stdio.h>

typedef int bool;
#define true 1
#define false 0

typedef int dataType;
typedef struct BiTNode {
	dataType data;
	int balance;  // 平衡因子 = 左子树深度 - 右子树深度,平衡二叉树取:-1, 0, 1
	struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

/* 右插入,左旋 */
void leftRotate(BiTree *T) {
	BiTree rightTree = (*T)->rchild;
	(*T)->rchild = rightTree->lchild;
	rightTree->lchild = (*T);
	(*T) = rightTree;
}

/* 左插入,右旋 */
void rightRotate(BiTree *T) {
	BiTree leftTree = (*T)->lchild;
	(*T)->lchild = leftTree->rchild;
	leftTree->rchild = (*T);
	(*T) = leftTree;
}

/* 左子树高,左平衡 */
void leftBalance(BiTree *T) {
	BiTree leftTree = (*T)->lchild;
	BiTree leftTreeRight = leftTree->rchild;
	switch (leftTree->balance) {
	case 1:  // 左子树平衡因子为 1, (插入左孩子左子树), 单右旋
		(*T)->balance = 0;
		leftTree->balance = 0;
		rightRotate(T);
		break;
	case -1: //左子树平衡因子为 -1, (插入左孩子右子树), 双旋
		switch (leftTreeRight->balance) {
		case 1:
			(*T)->balance = -1;
			leftTree->balance = 0;
			break;
		case 0:
			(*T)->balance = 0;
			leftTree->balance = 0;
			break;
		case -1:
			(*T)->balance = 0;
			leftTree->balance = 1;
			break;
		}
		leftTreeRight->balance = 0;
		leftRotate(&((*T)->lchild));
		rightRotate(T);
	}
}

/* 右子树高,右平衡 */
void rightBalance(BiTree *T) {
	BiTree rightTree = (*T)->rchild;
	BiTree rightTreeLeft = rightTree->lchild;
	switch (rightTree->balance) {
	case -1:   //右子树平衡因子为 -1, (插入右孩子右子树), 单左旋
		(*T)->balance = 0;
		rightTree->balance = 0;
		leftRotate(T);
		break;
	case 1:  //右子树平衡因子为 1, (插入右孩子左子树), 双旋
		switch (rightTreeLeft->balance) {
		case -1:
			(*T)->balance = 1;
			rightTree->balance = 0;
			break;
		case 0:
			(*T)->balance = 0;
			rightTree->balance = 0;
			break;
		case 1:
			(*T)->balance = 0;
			rightTree->balance = -1;
			break;
		}
		rightTreeLeft->balance = 0;
		rightRotate(&((*T)->rchild));
		leftRotate(T);
	}
}

/* AVL 树插入, 先定位, 再插入, 然后维持自平衡*/
bool insertAVL(BiTree *T, int elem, bool *taller) {
	if (*T == NULL) {
		*T = (BiTree) malloc(sizeof(BiTNode));
		(*T)->data = elem;
		(*T)->balance = 0;
		(*T)->lchild = NULL;
		(*T)->rchild = NULL;
		*taller = true;
		return true;
	}
	if (elem == (*T)->data) {
		*taller = false;
		return false;
	}
	// 如果插入元素小于根节点,递推搜索左子树,插入后维持-左平衡
	if (elem < (*T)->data) {
		if (!insertAVL(&((*T)->lchild), elem, taller))
			return false;
		if (*taller) {
			switch ((*T)->balance) {
			case 1:
				leftBalance(T);
				*taller = false;
				break;
			case 0:
				(*T)->balance = 1;
				*taller = true;
				break;
			case -1:
				(*T)->balance = 0;
				*taller = false;
				break;

			}
		}
	}
	// 如果插入元素大于根节点,递推搜索右子树, 插入后维持-右平衡
	if (elem > (*T)->data) {
		if (!insertAVL(&((*T)->rchild), elem, taller))
			return false;
		if (*taller) {
			switch ((*T)->balance) {
			case 1:
				(*T)->balance = 0;
				*taller = false;
				break;
			case 0:
				(*T)->balance = -1;
				*taller = true;
				break;
			case -1:
				rightBalance(T);
				*taller = false;
				break;
			}
		}
	}
	return true;
}

void inOrderTraverse(BiTree T) {
	if (T == NULL)	return;
	inOrderTraverse(T->lchild);
	printf("%d ", T->data);
	inOrderTraverse(T->rchild);
}

void preOrderTraverse(BiTree T) {
	if (T == NULL)	return;
	printf("%d ", T->data);
	preOrderTraverse(T->lchild);
	preOrderTraverse(T->rchild);
}

int main() {
	int a[10] = {3, 2, 1, 4, 5, 6, 7, 0, 9, 8};
	int i;
	bool taller;
	BiTree T = NULL;
	for (i = 0; i < 10; i++) {
		insertAVL(&T, a[i], &taller);
	}
	printf("inOrderTraverse:  ");
	inOrderTraverse(T);
	printf("\npreOrderTraverse: ");
	preOrderTraverse(T);
	return 0;
}

/* 运行结果如下,由中序和前序遍历,可还原二叉树,验证是否准确
inOrderTraverse:  0 1 2 3 4 5 6 7 8 9
preOrderTraverse: 4 2 1 0 3 6 5 8 7 9
*/
时间: 2024-10-16 20:50:59

算法学习笔记 平衡二叉树 AVL树的相关文章

【算法学习笔记】40.树状数组 动态规划 SJTU OJ 1289 扑克牌分组

Description cxt的扑克牌越来越先进了,这回牌面的点数还可以是负数, 这回cxt准备给扑克牌分组,他打算将所有的牌分成若干个堆,每堆的牌面总和和都要大于零.由于扑克牌是按顺序排列的,所以一堆牌在原牌堆里面必须是连续的.请帮助cxt计算一下,存在多少种不同的分牌的方案.由于答案可能很大,只要输出答案除以1,000,000,009的余数即可. Input Format 第一行:单个整数:N,1 ≤ N ≤ 10^6 第二行到N + 1行:在第i + 1行有一个整数:Ai, 表示第i张牌牌

java数据结构与算法之平衡二叉树(AVL树)的设计与实现

[版权申明]未经博主同意,不允许转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/53892797 出自[zejian的博客] 关联文章: java数据结构与算法之顺序表与链表设计与实现分析 java数据结构与算法之双链表设计与实现 java数据结构与算法之改良顺序表与双链表类似ArrayList和LinkedList(带Iterator迭代器与fast-fail机制) java数据结构与算法之栈(Stack)设

图解平衡二叉树,AVL树(一)

图解平衡二叉树,AVL树(一) 学习过了二叉查找树,想必大家有遇到一个问题.例如,将一个数组{1,2,3,4}依次插入树的时候,形成了图1的情况.有建立树与没建立树对于数据的增删查改已经没有了任何帮助,反而增添了维护的成本.而只有建立的树如图2,才能够最大地体现二叉树的优点. 在上述的例子中,图2就是一棵平衡二叉树.科学家们提出平衡二叉树,就是为了让树的查找性能得到最大的体现(至少我是这样理解的,欢迎批评改正).下面进入今天的正题,平衡二叉树. AVL的定义 平衡二叉查找树:简称平衡二叉树.由前

带花树算法学习笔记

带花树算法学习笔记 难得yyb写了一个这么正式的标题 Q:为啥要学带花树这种东西啊? A:因为我太菜了,要多学点东西才能不被吊打 Q:为啥要学带花树这种东西啊? A:因为我做自己的专题做不动了,只能先去"预习"ppl的专题了 Q:为啥要学带花树这种东西啊? A:因为可以用来做题啊,比如某WC题目 先推荐一个很皮很皮的带花树讲解: 戳这里嗷 QaQ 言归正传 带花树的算法用来解决一般图的最大匹配问题 说起来,是不是想起来网络流里面的最小路径覆盖? 或者二分图的最大匹配的问题? 的确,带花

算法学习笔记 递归之 快速幂、斐波那契矩阵加速

递归的定义 原文地址为:http://blog.csdn.net/thisinnocence 递归和迭代是编程中最为常用的基本技巧,而且递归常常比迭代更为简洁和强大.它的定义就是:直接或间接调用自身.经典问题有:幂运算.阶乘.组合数.斐波那契数列.汉诺塔等.其算法思想: 原问题可分解子问题(必要条件): 原与分解后的子问题相似(递归方程): 分解次数有限(子问题有穷): 最终问题可直接解决(递归边界): 对于递归的应用与优化,直接递归时要预估时空复杂度,以免出现用时过长或者栈溢出.优化递归就是以

EM算法学习笔记2:深入理解

文章<EM算法学习笔记1:简介>中介绍了EM算法的主要思路和流程,我们知道EM算法通过迭代的方法,最后得到最大似然问题的一个局部最优解.本文介绍标准EM算法背后的原理. 我们有样本集X,隐变量Z,模型参数θ,注意他们3个都是向量,要求解的log似然函数是lnp(X|θ),而这个log似然函数难以求解,我们假设隐变量Z已知,发现lnp(X,Z|θ) 的最大似然容易求解. 有一天,人们发现引入任意一个关于隐变量的分布q(Z),对于这个log似然函数,存在这样一个分解: lnp(X|θ)=L(q,θ

算法学习笔记 KMP算法之 next 数组详解

最近回顾了下字符串匹配 KMP 算法,相对于朴素匹配算法,KMP算法核心改进就在于:待匹配串指针 i 不发生回溯,模式串指针 j 跳转到 next[j],即变为了 j = next[j]. 由此时间复杂度由朴素匹配的 O(m*n) 降到了 O(m+n), 其中模式串长度 m, 待匹配文本串长 n. 其中,比较难理解的地方就是 next 数组的求法.next 数组的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀,也可看作有限状态自动机的状态,而且从自动机的角度反而更容易推导一些. "前

算法学习笔记 最短路

图论中一个经典问题就是求最短路,最为基础和最为经典的算法莫过于 Dijkstra 和 Floyd 算法,一个是贪心算法,一个是动态规划,这也是算法中的两大经典代表.用一个简单图在纸上一步一步演算,也是很好理解的,理解透自己多默写几次即可记住,机试时主要的工作往往就是快速构造邻接矩阵了. 对于平时的练习,一个很厉害的 ACMer  @BenLin_BLY 说:"刷水题可以加快我们编程的速度,做经典则可以让我们触类旁通,初期如果遇见很多编不出,不妨就写伪代码,理思路,在纸上进行整体分析和一步步的演算

[算法学习笔记]直接插入排序笔记

直接插入排序概念: 带排元素放在elem[0...n-1]中,初始化时,elem[0]自成1个有序区,无序区为elem[1...n-1],从i=1起,到i=n-1,依次将elem[i]插入有序区[0...n-1]中 直接插入排序算法步骤: 1.在当前有序区域R[1,i-1]中查找R[i]的正确插入位置K(1<=K<=i-1) 2.将R[K,i-1]中的记录均向后移动 3.移动后腾出K位置,插入R[i] (最坏)时间复杂度:O(n^2) 空间复杂度:O(1) /// <summary>