二叉排序树
1. 定义
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),其递归定义如下:
- 一棵空树
- 或者是有以下性质的二叉树
- 左子树不空,则左子树上所有结点的值均小于它的根节点的值
- 右子树不空,则右子树上所有结点的值均大于它根节点的值
- 左右子树也是二叉排序树
例如,下图这棵树就是一棵二叉排序树:
容易看出,它的中序遍历 3,12,24,37,45,53,61,78,90,100 就是一个排序好的序列;
而它的查找过程如下:
当二叉树不空时,将值与根的关键字比较
- 若相等,则查找成功;
- 若关键字大,则递归查找左子树;
- 托关键字小,则递归查找右子树;
2.基本操作
2.1 声明
// 二叉链表的定义
typedef struct node_t{
int data;
node_t* left, *right;
}Bnode,*Btree;
2.2 查找
Btree SearchBST(Btree T, int key) {
if (key == T->data || T == NULL) return T;
else if (T->data > key) SearchBST(T->left, key);
else SearchBST(T->right, key);
}
2.3 插入
二叉排序树是一种动态树表,其不是一次构成生成的,
而是在查找过程中,当树中不存在关键字等于给定值的结点的时候,再进行动态插入。
而且,新插入的结点一定是一个叶子结点(因为中间结点表示存在相等),并且是查找不成功时查找树上访问的最后一个结点的左孩子或右孩子。
我们修改2.2的查找算法就能得到如下插入算法:
int SearchBST(Btree T, int key, Btree f, Btree& p) {
/* f是当前查找结点的父结点,p是当前查找结点
* 若查找成功,则返回f和true
* 若查找失败,则返回父节点和false
*/
if (!T) {
p = f;
return 0; //查找失败
}
if (T->data == key) {
p = T; return 1;
}
else if (T->data > key) SearchBST(T->left, key, T, p);
else SearchBST(T->right, key, T, p);
}
int Inseart(Btree& T, int key) {
Bnode* p;
if (!SearchBST(T, key, NULL, p)) { // 查找失败
Bnode* tmp = new Bnode;
tmp->data = key; tmp->left = tmp->right = NULL;
if (!p) T = tmp; // 新的根结点
else if (key < p->data) p->left = tmp;
else p->right = tmp;
return 1;
}
else return 0; // 已经存在
}
例如对于一个无序序列**{45,24,53,45,12,24,90}**,我们有如下的构造过程:
而正如前面所说,中序遍历二叉排序树可得一个关键字有序的序列(去重)
。而且,其插入元素不需要移动记录,只需要改变一些指针,即具有二分查找和链表的优点。
2.4 删除
二叉排序树的删除即表明删除有序序列中的一个记录,同时保持二叉排序树的特性。
即我们时刻关注删除掉记录后,中序遍历序列仍然是一个有序序列,分为以下三种情况,
假设待删除结点为p
,其左子树为
P
L
P_L
PL ,右子树为
P
R
P_R
PR ,其父结点为f
:
- 若 p 为叶子结点,则直接删去 p, 并修改父节点的指针;
- 若 p 的左子树或者右子树为空,则删除左子树或者右子树后,将另一棵子树指向父节点的左子树即可;
- 若左右子树均不为空,则我们有如下两种方案:
- 直接令 p 的左子树为 f 的左子树,而 p 的右子树需要接到 p 的中序序列的直接前驱上,即p左子树的最右结点,即下图的
s
; - 或者令p的值为其之前前驱
s
的值,然后删去s
,删去s后,s
的前驱指向其父亲结点,也就是下图的 S L S_L SL需要指向 q。
而我们改变指针的重要依据就是要保证中序遍历的先后顺序不变。
- 直接令 p 的左子树为 f 的左子树,而 p 的右子树需要接到 p 的中序序列的直接前驱上,即p左子树的最右结点,即下图的
// -- 排序二叉树的删除 --
int Delete(Btree& p) {
// p 为叶子结点或者只有一棵子树
if (!(p->right)) { // 只有左子树
Bnode* q = p;
p = p->left; // 删除原有p
delete(q); // 释放内存
}
else if (!(p->left)) { // 只有右子树
Bnode* q = p;
p = p->right;
delete(q);
}
else { // 左右子树都存在
Bnode* q = p;
Bnode* s; // 假定为p的直接前驱
s = p->left;
while (s->right) {
q = s; s = s->right;
}
p->data = s->data; // 将值变成其直接前驱
if (p != q) q->right = s->left;
else p->left = s->left; // 此时,不需要修改P的右子树
delete(s);
}
return 1;
}
int DeleteBST(Btree& T, int key){
if (!T) return 0;
else {
if (T->data == key) return Delete(T);
else if (T->data > key) return DeleteBST(T->left, key);
else return DeleteBST(T->right, key);
}
}
2.5 中序遍历
void Inorder(Btree T) {
// 中序遍历
if (T == NULL)return;
Inorder(T->left);
printf("%d ", T->data);
Inorder(T->right);
}
测试
int main() {
int n = 7;
int num[] = { 45,24,53,45,12,24,90 };
Btree T = NULL;
// 建立二叉树
for (int i = 0; i < n; i++) Inseart(T, num[i]);
printf("建立完成,中序遍历:");
Inorder(T); printf("\n");
DeleteBST(T, 53);
printf("删除53,中序遍历:");
Inorder(T); printf("\n");
DeleteBST(T, 1000);
printf("删除1000,中序遍历:");
Inorder(T); printf("\n");
system("pause");
return 0;
}
3.复杂度
在二叉搜索树上查找给定关键值的结点,等价于走过一条从根结点到该结点路径的过程, 比较的次数等于该结点所在的层次数。因此,与给定值比较的关键字个数不超过树的深度。
最坏的时候,二叉树退化为单支树,树的深度为
n
n
n;最好的情况,二叉树类似于折半查找树,树的深度在
l
o
g
n
logn
logn。如下图,两棵二叉搜索树的序列的值相同,但是树的深度却不相同。
所以二叉搜索树的复杂度在
O
(
n
)
O(n)
O(n) 到
O
(
l
o
g
n
)
O(logn)
O(logn),而要取得比较好的速度,需要建立的二叉搜索树的深度尽可能的小,即二叉树比较的宽和集中,也就是左右子树的结点数尽可能地接近,最常见的就是建立一棵平衡二叉树(AVL)]。
平衡二叉树
1.定义
平衡二叉树(Balanced Binary Tree),又称AVL树,其递归定义如下:
- 要么是一棵空树
- 要么是有如下性质的二叉树:左右子树均是AVL树,且左右子树的深度之差的绝对值不超过1
若定义二叉树结点的 平衡因子(Balance Factor) 为该结点左子树的深度减去右子树的深度,而AVL树上所有结点的平衡因子为-1,0,1。即有平衡因子超过2的二叉树都不是AVL树,例如下图
在上一节中我们有谈论到,希望建立的二叉搜索树的深度尽可能地小,最好是一棵平衡二叉树,而如何建立一棵二叉平衡搜索树呢?
2.基本操作
2.1 平衡操作
为使得建立的二叉搜索树都是AVL树,我我们看下面这个例子。
例如有一个关键字序列 {13,24,37,90,53} ,其建立二叉平衡搜索树的过程如下:
- 图(d)中,当插入
37
后,有一结点的平衡因子变为-2,需要对子树进行调整,调整的关键是 保持二叉排序树的中序遍历中关键字的先后顺序不变,使得子树的深度减小。所以,对于这种连续的右右插入
,我们进行向左逆时针的旋转
,变成图(e) - 图(f),中连续的
右左插入
后不平衡,根据结点中序遍历的先后顺序,我们先进行向右顺时针旋转
变成图(g),类似于图(d),然后进行向左逆时针旋转
变成图(b)
所以,我们得到以下启发:
- 连续的插入,例如
右右
、左左
、右左
、左右
可能使得二叉树不平衡 - 调整子树,主要先调整最先不平衡的子树,子树根节点的BF为2或-2,调整的方向要满足使得中序遍历先后顺序不变
因此,调整规律主要有以下4种情况(假设离插入点最近,且平衡因子绝对值超过1的结点为a
):
- 单向右旋处理:在
a
结点的左子树根节点的左子树插入结点,a
的BF变为2,则进行一次向右顺时针的旋转
- 单向左旋处理:在
a
结点的右子树根结点的右子树插入结点,使的a
的BF变为-2,则进行一次向左逆时针旋转
- 先左后右旋处理:在
a
结点的左子树根节点的右子树插入结点,a
的BF变为2,则进行先左后右旋转
- 先右后左旋处理:在
a
结点的右子树根节点的左子树插入结点,a
的BF变为-2,则进行先右后左旋转
同时,我们主要到以新结点B
或C
为根节点的子树的BF和插入前的A
结点一样,所以,我们只需要对最小不平衡子树 进行旋转处理,其所有的祖先结点也会恢复平衡,因为它们本来就是平衡的。
2.2 插入操作
在平衡二叉树BBST上插入一个新的元素e的递归算法如下:
(1) 若BBST为空树,则元素e结点作为BBST的新结点,树的深度加1;
(2) 若e的关键字和BBST的根结点的关键字相等,则不进行插入;
(3) 若e的关键字小于BBST的根节点的关键字,而且在BBST的左子树中不存在和e相同的结点,则将e插入在BBST的左子树上,并当左子树的深度增加
时(+1),判断是否需要进行旋转:
- 若BBST的根节点因子为-1,则将根节点的因子增加到0,BBST深度不变;
- 若BBST的根节点因子为0,则将根节点的因子增加到1,BBST的深度增加1;
- 若BBST的根节点因子为1:
- 若BBST左子树根节点的平衡因子为1,则需要进行
单向右旋处理
,并且处理之后,将根节点和其右子树根节点的因子改为0,BBST的深度不变; - 若BBST左子树根节点的平衡因子为-1,则需要进行
先左后右旋转
处理,处理完成后根节点和左右子树的平衡因子需要根据原来结点C的因子来决定,即下图中 C L C_L CL 和 C R C_R CR的高度差。树的深度不变。
(4) 若e的关键字大于BBST根节点的关键字,而且在BBST得右子树中不存在和e相同的结点,则将e插入在BBST的右子树上,并且当右子树的深度增加
时(+1),判断是否需要进行旋转
- 若BBST左子树根节点的平衡因子为1,则需要进行
- 若BBST的根节点因子为1,则将根节点的因子增加到0,树的深度不变;
- 若BBST的根节点因子为0,则将根节点的因子变为-1,BBST的深度增加1;
- 若BBST的根节点因子为-1:
- 若BBST的右子树根节点为-1,则需要进行
单项左旋转
处理,处理完成后,将根节点和其左子树根节点的因子改为0,树的深度不变; - 若BBST的右子树根节点为1,则需要进行
先右后左旋转
处理,处理完成后根节点和左右子树的平衡因子需要根据原来BBST中结点C的因子来决定,即下图中 C L C_L CL 和 C R C_R CR 的高度差。树的深度不变。
- 若BBST的右子树根节点为-1,则需要进行
2.3 实现
#define LH 1 // 左高
#define EH 0 // 等高
#define RH -1 // 右高
// 结点定义
typedef struct node_t {
int bf; // 平衡因子
int data;
struct node_t* lchild, *rchild; // 左右子树
}BSTNode,*BSTree;
// -- 左旋右旋定义 --
void R_Roate(BSTree& p) { // 右旋
BSTNode* lc = p->lchild;
p->lchild = lc->rchild;
lc->rchild = p;
p = lc;
}
void L_Roate(BSTree& p) { // 左旋
BSTNode* rc = p->rchild;
p->rchild = rc->lchild;
rc->lchild = p;
p = rc;
}
// -- 左右平衡操作 --
void LeftBalanced(BSTree& T) {
// 以T结点为根的二叉树做左平衡处理
// 根据T的左子树根节点的因子为1,-1分为两种情况
BSTNode* lc = T->lchild;
switch (lc->bf)
{
case LH:
//lc->bf = T->bf = EH; // 先修改平衡因子
R_Roate(T);
T->bf = T->rchild->bf = EH; // 后修改平衡因子时,结点已经发生改变
break;
case RH:
BSTNode* rd = lc->rchild;
if (rd->bf == LH) { // 左高
T->bf = RH;
lc->bf = EH;
}
else if (rd->bf == RH) { // 右高
lc->bf = LH;
T->bf = EH;
}
else if (rd->bf == EH) { // 好像是不可能的
lc->bf = T->bf = EH;
}
rd->bf = EH; // 新的根节点
L_Roate(T->lchild); // 对T的左子树作左旋处理
R_Roate(T); // 对T作右旋处理
break;
} // switch (lc->bf)
}// LeftBalanced
void RightBalanced(BSTree& T) { // 右旋,即使左平衡的镜像情况
// 以T结点为根的二叉树做左平衡处理
// 根据T的右子树根节点的因子为1,-1分为两种情况
BSTNode* rd = T->rchild;
switch (rd->bf)
{
case RH:
//T->bf = rd->bf = EH; // 先修改平衡因子
L_Roate(T);
T->bf = T->lchild->bf = EH; // 如果后修改平衡因子,则结点已经发生改变
break;
case LH:
BSTNode* lc = rd->lchild;
if (lc->bf == LH) {
T->bf = EH;
rd->bf = RH;
}
else if (lc->bf == RH) {
T->bf = LH;
rd->bf = EH;
}
else if (lc->bf == EH) {
T->bf = rd->bf = EH;
}
lc->bf = EH;
R_Roate(T->rchild);
L_Roate(T);
break;
}// switch (rd->bf)
}// RightBalanced
int InsertAVL(BSTree& T, int e, bool& taller) {
/*
* 若平衡二叉树中不存在元素e,则插入一个新的元素并返回1,否则返回0
* 若插入后,失去平衡,则作平衡处理
* 判断是否失去平衡的必要条件是子树有没有长高,即布尔变量taller
*/
if (!T) {
T = (BSTNode*)malloc(sizeof(BSTNode));
T->data = e;
T->lchild = T->rchild = NULL;
T->bf = EH; taller = true;
return 1;
}
if (T->data == e) {
taller = false; return 0;
}
if (T->data > e) { // 需要插到左子树
if (!InsertAVL(T->lchild, e, taller)) return 0; // 未插入
if (taller) {// 插入后判断是否长高
switch (T->bf) {
case LH:
LeftBalanced(T); taller = false;
break;
case RH:
T->bf = EH; taller = false; break;
case EH:
T->bf = LH; taller = true; break;
}// switch (T->bf)
}// if
}
else { // 插到右子树
if (!InsertAVL(T->rchild, e, taller)) return 0; // 已存在
if (taller) {
switch (T->bf) {
case LH:
T->bf = EH; taller = false; break;
case RH:
RightBalanced(T); taller = false;
break;
case EH:
T->bf = RH; taller = true; break;
}// switch (T->bf)
}// if
}// else
return 1;
}// InsertALV
需要注意:
1.修改平衡因子在旋转前后均可以进行,但是对应的结点就不一样
2.传参方式用引用传值,便于实参的修改
测试
// 中序遍历
void Inorder(BSTree T) {
if (!T) return;
Inorder(T->lchild);
printf("%d ", T->data);
Inorder(T->rchild);
}
int main() {
BSTree T = NULL;
int n;
bool taller = false;
int num[] = { 13,24,37,90,53 ,24,53,100};
n = sizeof(num) / sizeof(num[0]);
for (int i = 0; i < n; i++) InsertAVL(T, num[i], taller);
cout << "树根" << T->data << endl;
Inorder(T);
system("pause");
return 0;
}
3.复杂度
在平衡树上进行查找时,比较次数不超过树的深度,而含有n个关键字的平衡二叉树的最大深度大约在
l
o
g
φ
(
5
(
n
+
1
)
)
−
2
log_\varphi (\sqrt{5}(n+1))-2
logφ(5(n+1))−2, 其中
φ
=
1
+
5
2
\varphi = \frac{1+\sqrt{5}}{2}
φ=21+5。
所以,在平衡二叉树上进行查找的时间复杂度在
O
(
l
o
g
n
)
O(logn)
O(logn)。
4.模板代码(C++)
稍微简洁一点的模板代码,不存储结点的平衡因子,但是每次插入后需要计算插入结点root
的左右子树的高度,然后比较其差值是否大于1,来判断是否进行旋转。因此,需要一个查找二叉树高度的函数getheight
,代码简洁一点,但是需要多花一点查找的时间,换来的是不用存储平衡因子和调整平衡因子。
#include<iostream>
#include<algorithm>
using namespace std;
// 结点定义
typedef struct node_t {
int data;
struct node_t* left, *right;
}node,*tree;
// -- 旋转操作 --
void Left_Roate(tree& T) { // 左旋
node* rd = T->right;
T->right = rd->left;
rd->left = T;
T = rd;
}
void Right_Roate(tree& T) { // 右旋
node* lc = T->left;
T->left = lc->right;
lc->right = T;
T = lc;
}
void LeftRight_Roate(tree& T) { // 先左后右旋转
Left_Roate(T->left);
Right_Roate(T);
}
void RightLeft_Roate(tree& T) { // 先右后左旋转
Right_Roate(T->right);
Left_Roate(T);
}
int getHeight(tree T) { // 获得二叉树的高度
if (T == NULL) return 0;
return max(getHeight(T->left), getHeight(T->right)) + 1;
}
int InsertAVL(tree& T, int e) { // 插入结点e
// 成功返回1,失败返回0
if (!T) {
T = (node*)malloc(sizeof(node));
T->left = T->right = NULL;
T->data = e;
}
if (T->data == e) return 0; // 已经存在
if (T->data > e) {
if (InsertAVL(T->left, e)) { // 插入左子树成功
if (getHeight(T->left) - getHeight(T->right) == 2) {
e < T->left->data ? Right_Roate(T) : LeftRight_Roate(T);
// 判断是左左插入,还是右右插入
}
}
}// 插入左子树
else {
if (InsertAVL(T->right, e)) {
if (getHeight(T->right) - getHeight(T->left) == 2) {
e > T->right->data ? Left_Roate(T) : RightLeft_Roate(T);
}
}
}
return 1;
}
例题收录
参考资料
《数据结构 C语言描述》, 严蔚敏著。