二叉排序树及平衡二叉树
二叉排序树
定义:
二叉排序树或是一棵空树,或者是一棵满足以下条件的二叉树:
- 如果二叉排序的左子树非空,则其左子树所有顶点的值均小于该二叉排序树的根节点;其左子树也是一颗二叉排序树。
- 如果二叉排序的右子树非空,则其右子树的所有顶点的值均大于该二叉排序树的根节点;其右子树也是一颗二叉排序树。
如图所示:
由上图可以发现,图b中的6比根节点5要大,不满足二叉排序树的条件,因此不是一颗二叉排序树。
二叉排序树可以用于查找算法的优化中,除去哈希表等之外,其他查找算法最快的时间复杂度基本都是O(lgn),使用二叉排序进行查找在某些情况下也可以到达这个时间复杂度,下面说明二叉排序树进行查找的过程。
已有一颗二叉排序树如上图的a所示,需要查找数字7(记为x)是否在该二叉树中:
- 将x与根节点比较,如果相等则该数组在该二叉树中,查找成功;
- 如果x 小于根节点的值,则查找左子树,继续比较;
- 如果x 大于根节点的值,则查找右子树,继续比较,7 > 5,因此往右子树方向查。
- 重复上述过程,如果查到叶子节点仍然没有查找到该值,则可以将该数据添加到该二叉排序树中作为该叶子节点的孩子节点,究竟是左孩子还是右孩子要满足二叉排序树的要求,大于叶子节点的值插入右孩子中。小于叶子节点的值,插入左孩子中。
代码实现如下:
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
typedef struct _node{
int key;
struct _node *lchild, *rchild;
}*Tree, Node;
Node* binarySearch(Tree &root, int key) {
if (root == NULL) { //为空插入该二叉排序树中
root = (Node *)malloc(sizeof(Node));
root->key = key;
root->lchild = NULL;
root->rchild = NULL;
return NULL;
}
Node *temp = root;
if (temp->key == key) {
return temp;
} else if (temp->key < key) { //查找的数值比较大,遍历右子树
return binarySearch(temp->rchild, key);
} else { //查找的数值比较小,遍历左子树
return binarySearch(temp->lchild, key);
}
}
/**
* 中序遍历二叉树
*/
void dfs(Tree root) {
if (root->lchild) {
dfs(root->lchild);
}
printf("%d\t", root->key);
if (root->rchild) {
dfs(root->rchild);
}
}
int main() {
Tree root = (Node*)malloc(sizeof(Node));
root->lchild = NULL;
root->rchild = NULL;
cin >> root->key;
int key;
for (int i = 1; i < 5; i++) {
cin >> key;
binarySearch(root, key); //这里并没有接收返回值
}
dfs(root);
return 0;
}
注:上述代码是使用二叉排序树的思想创建了一棵二叉排序树,并没有显示的进行查找操作,如果对该二叉排序树使用中序遍历的方式,那么将会得到一个非递减的有序序列。
测试用例
输入:5
4 6 3 2
输出:2 3 4 5 6
性能分析
现在来计算二叉排序树的平均查找长度(ASL),举例说明:
如上图所示,不同形状的二叉排序树的查找性能有很大的不一样,最优的为O(lgn),最差的为O(n),而在随机的情况下,有证明给出二叉排序树的查找性能为O(lgn)量级的,但在同时有证明给出,有46.5%的概率(数据从《数据结构》这边书上得来)需要让二叉排序树“平衡化”才能得到这个量级的时间复杂度。使之“平衡化”的算法有很多中,其中一种比较简单理解的就是平衡二叉树。
平衡二叉树
平衡二叉树,即AVL树,所谓平衡就是指该平衡二叉树的根节点的左右子树之间的深度差值不超过1,可以使得二叉树在满足二叉排序树条件的情况下尽量充满(尽量接近完全二叉树)。
相关概念:
平衡因子
该节点左子树与右子树的高度差。可取值为 -1 , 0 , 1来表示
-1:表示右子树的高度比左子树的高度大1
0:表示左右子树的高度相等
1:表示左子树的高度比右子树的高度大1
当平衡因子的绝对值大于1时,该二叉树失去平衡,此时要使用一些操作进行调整,使得该二叉树中节点的平衡因子的绝对值都不大于1。结果可以调整该二叉树的高度保持在O(lgn)级别,这样其查找时候的时间复杂度也在O(lgn)级别;
当一棵二叉树中插入一个节点时失去平衡,这个时候必须重新调整该平衡二叉树,使之恢复平衡,这个过程称之为:平衡旋转。一般可以分为LL平衡旋转,RR平衡旋转,LR平衡旋转,LR平衡旋转。(L指的是是左子树,R指的是右子树,例如LL表示该节点的只有左孩子,左孩子又只有左孩子)
- LL平衡旋转
即以插入的节点的父亲节点为旋转轴,顺旋转其祖先节点。
2.RR平衡旋转
即以插入的节点的父亲节点为旋转轴,逆旋转其祖先节点。
3.LR平衡旋转
即以插入节点为旋转轴,既要逆时针旋转其父亲节点,又要顺时针旋转其祖先节点。
- RL平衡旋转
即以插入节点为旋转轴,既要顺时针旋转其父亲节点,又要逆时针旋转其祖先节点。
代码实现如下
typedef struct _bstNode{
int data; //数据域
int bf; //balance factor 平衡因子 0:高度差一致,1:左高,-1:右高
struct _bstNode *lchild, *rchild;
}BSTNode, *BSTree;
/**
* 右旋操作
* 将根节点root旋转到其左子树的右孩子节点上
* 如果左子树的右孩子有内容,则要将其作为根节点的左子树,因为其值是要比根节点的值要小的
*/
void R_rotate(BSTree &root) {
BSTNode *temp = root->lchild; //根节点的左子树
root->lchild = temp->rchild; //左孩子的右子树挂在根节点上,便于下面操作时不使树断开
temp->rchild = root; //右旋操作,根节点作为左子树的有孩子
root = temp; //重新设置根 -- 注意这里的参数是引用传递
}
/**
* 左旋操作
* 将根节点root旋转到其右子树的左孩子节点上
* 如果右子树的左孩子有值,则要将其添加到根节点的右子树上,因为其值要比根节点的值要大的
*/
void L_rotate(BSTree &root) {
BSTNode *temp = root->rchild; //根节点的右子树
root->rchild = temp->lchild; //右孩子的左子树挂在根节点上,下面操作会使树断开
temp->lchild = root;
root = temp;
}
/**
* 左平衡 -- 用在LR平衡旋转和LL平衡旋转中
*/
void leftBalance(BSTree &T) {
BSTNode *lchild = T->lchild;
switch(lchild->bf) {
case 1: //LL平衡旋转
lchild->bf = 0;
t->bf = 0;
R_rotate(T); //对根节点T右旋即可
break;
case -1: //LR平衡旋转
BSTNode *L_rchild = lchild->rchild;
switch(L_rchild->bf) {
case 1:
T->bf = -1;
lchild->bf = 0;
break;
case 0:
T->bf = lchild->bf = 0;
break;
case -1:
T->bf = 0;
lchild->bf = 1;
break;
}
L_rchild->bf = 0;
L_rotate(T->lchild); //先对根节点的左孩子左旋
R_rotate(T); //再对根节点右旋 -- 可以按照上图过程来看
break;
}
}
/**
* 右平衡操作 -- 用在RL平衡旋转和RR平衡旋转中
*/
void rightBalance(BSTree &T) {
BSTNode * rchild = T->rchild;
switch(rchild->bf) {
case -1: //RR平衡旋转
rchild->bf = 0;
T->bf = 0;
L_rotate(T); //对根节点进行左旋即可
break;
case 1: //RL平衡旋转
BSTNode *R_lchild = rchild->lchild;
switch(R_lchild->bf) {
case 0:
T->bf = rchild->bf = 0;
break;
case 1:
T->bf = 0;
rchild->bf = -1;
break;
case -1:
rchild->bf = 0;
T->bf = 1;
break;
}
R_lchild->bf = 0;
R_rotate(T->rchild); //先对根节点的T右孩子右旋
L_rotate(T); //再对根节点进行左旋 -- 按照上图来看
break;
}
}
上面只给出旋转时修改节点的操作来帮助理解其过程,毕竟在使用的时候一般都不会手动实现这类平衡算法,而是常常使用别人封装好的例如java中的集合框架,C++中是STL。