# 二叉排序树
二叉排序树,又称二叉查找树 (BST),一颗二叉树或者是空二叉树,或者具有如下性质的二叉树:
左子树上所有结点的关键字均小于根节点的关键字(左子树结点值 <根结点值 < 右子树结点值 -> 进行中序遍历,可以得到一个递增的有序序列)
右子树上所有结点的关键字均大于根节点的关键字
左子树和右子树又各是一棵二叉排序树。
//二叉排序树的节点
typedef struct BSTNode{
int key;
struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;
# 查找
若树非空,目标值与根节点的值比较:若相等,则查找成功;若小于根节点,则在左子树上查找,否则在右子树上查找。
查找成功,返回结点指针;查找失败返回 NULL
//在二叉排序树中查找为key的结点
BSTNode *BST_Search(BSTree T,int key){
while(T!=NULL && key!=T->key){ //若树空或等于根结点,则结束循环
if(key<T->key) //小于,则在左子树查找
T=T->lchild;
else //大于,则在右子树上查找
T=T->rchild;
}
return T;
}
//在二叉排序树中查找值为key的节点(递归实现)
BSTNode *BSTSearch(BSTree T,int key){
if(T==NULL)
return NULL; //查找失败
if(key==T->key)
return T; //查找成功
else if(key<T->key)
return BSTSearch(T->lchild,key); //在左子树中找
else
return BSTSearch(T->rchild,key); //在右子树中找
}
# 插入
若原二叉树为空,则直接插入结点;否则,若关键字 k 小于根节点值,则插入到左子树,若关键字 k 大于根结点值,则插入到右子树
//在二叉排序树插入关键字为k的新节点(递归实现)
int BST_Insert(BSTree &T,int k){
if(T==NULL){ //原数为空,新插入的结点为根结点
T=(BSTree)malloc(sizeof(BSTNode));
T->key=k;
T->lchild=T->rchild=NULL;
return 1; //返回1,插入成功
}
else if(k==T->key) //树中存在相同关键字的节点,插入失败
return 0;
else if(k<T->key)
return BST_Insert(T->lchild,k);
else
return BST_Insert(T->rchild,k);
}
# 构造
//按照str[]中的关键字序列建立二叉排序树
void Creat_BST(BSTree &T,int str[],int n){
T=NULL; //初始时,T为空
int i=0;
while(i<n){ //依次将每个关键字插入到二叉排序树中
BST_insert(T,str[i]);
i++;
}
}
# 删除
先搜索到目标结点:
- 若被删除的结点 z 是叶节点,则直接删除,不会破坏二叉排序树的性质
- 若结点 z 只有一颗左子树或右子树,则让 z 的子树称为 z 父节点的子树,替代 z 的位置
- 若结点 z 由左、右两棵子树,则令 z 的直接后继 (或直接前驱) 替代 z,然后从二叉排序树中删去这个直接后继 (或直接前驱),这样就转换成了第一或第二种情况
# 查找效率分析
若树高 h,找到最下层的一个结点需要对比 h 次
最好情况:n 个结点的二叉树最小高度为,平均查找长度为
最坏情况:每个节点只有一个分支,树高 h = 节点数 n,平均查找长度 = O (n)
# 平衡二叉树
平衡二叉树,简称平衡树 (AVL 树)—— 树上任一节点的左子树和右子的树的深度之差不超过 1
结点的平衡因子 = 左子树高 - 右子树高,平衡二叉树结点的平衡因子的值只可能是 - 1,0,1
只要有任一结点的平衡因子绝对值大于 1,就不是平衡二叉树
//平衡二叉树结点
typedef struct AVLNode{
int key; //数据域
int balance; //平衡因子
struct AVLNode *lchild,*rchild;
}AVLNode,*AVLTree;
# 插入
查找路径上的所有结点都有可能受到影响
从插入点往回找到第一个不平衡结点,调整以该结点为根的子树,每次调整对象都是 "最小不平衡子树"
在插入操作中,只要将最小不平衡子树调整平衡,则其他祖先结点都会恢复平衡
# 调整最小不平衡子树
# LL:在 A 的左孩子的左子树插入导致不平衡
右单旋转:(假设最小不平衡子树的根结点为 A,左孩子为 B)
由于结点 A 的左孩子 (L) 的左子树 (L) 上插入了新结点,A 的平衡因子由 1 增至 2,导致以 A 为根的子树失去平衡,需要一次向右的旋转操作。将 A 的左孩子 B 向右上旋转代替 A 称为根结点,将 A 结点向右下旋转称为 B 的右子树的根节点,而 B 的原右子树作为 A 结点的左子树
# RR:在 A 的右孩子的右子树插入导致不平衡
左单旋转:(假设最小不平衡子树的根结点为 A,右孩子为 B)
由于结点 A 的右孩子 (R) 的右子树 (R) 上插入了新结点,A 的平衡因子由 - 1 减至 - 2,导致以 A 为根的子树失去平衡,需要一次向左的旋转操作。将 A 的右孩子 B 向左上旋转代替 A 称为根结点,将 A 结点向左下旋转称为 B 的左子树的根节点,而 B 的原左子树作为 A 结点的右子树
右旋:实现 f 向右下旋转,p 向右上旋转:其中 f 是父亲,p 为左孩子,gf 为 f 的父亲
f->lchild=p->rchild;
p->rchild=f;
gf->lchild/rchild=p;
左旋:实现 f 向左下旋转,p 向左上旋转:其中 f 是父亲,p 为左孩子,gf 为 f 的父亲
f->rchild=p->lchild;
p->lchild=f;
gf->lchild/rchild=p;
# LR:在 A 的左孩子的右子树插入导致不平衡
先左后右双旋转:(假设最小不平衡子树的根结点为 A,左孩子为 B,左孩子的右孩子为 C)
由于结点 A 的左孩子 (L) 的右子树 (R) 上插入了新结点,A 的平衡因子由 1 增至 2,导致以 A 为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。先将 A 结点的左孩子 B 的右子树的根结点 C 向左上旋转提升至 B 结点的位置,然后再把该 C 结点向右上旋转提升到 A 结点的位置
注:左旋和右旋与上方一样
# RL:在 A 的右孩子的左子树插入导致不平衡
先右后左双旋转:(假设最小不平衡子树的根结点为 A,右孩子为 B,右孩子的左孩子为 C)
由于结点 A 的右孩子 (R) 的左子树 (L) 上插入了新结点,A 的平衡因子由 - 1 减至 - 2,导致以 A 为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。先将 A 结点的右孩子 B 的左子树的根结点 C 向右上旋转提升至 B 结点的位置,然后再把该 C 结点向左上旋转提升到 A 结点的位置
注:左旋和右旋与上方一样
# 查找效率分析
若树高为 h,则最坏情况下,查找一个关键字最多需要对比 h 次,即查找操作的时间复杂度不可能超过 O (h)
假设以 表示深度为 h 的平衡二叉树中含有最少的结点数。则有,并且有
可以证明含有 n 个结点的平衡二叉树的最大深度为,平衡二叉树的平均查找长度为
# 删除
平衡二叉树的删除操作:
- 删除结点后,要保持二叉排序树的特性不变 (左 < 中 < 右)
- 若删除结点导致不平衡,则需要调整平衡
平衡二叉树的删除操作具体步骤:
- 删除结点 (方法同二叉排序树)
- 若删除的结点是叶子结点,直接删
- 若删除的结点有一个子树,用子树顶替删除位置
- 若删除的结点有两棵子树,用前驱 (或后继) 结点顶替,并转换为对前驱 (或后继) 结点的删除
- 一路向上找到最小不平衡子树
- 找到最小不平衡子树下,高度最高的儿子和孙子
- 根据孙子的位置,调整平衡 (LL/RR/LR/RL)
- 孙子在 LL:儿子右单旋
- 孙子在 RR:儿子左单旋
- 孙子在 LR:孙子先左旋,再右旋
- 孙子在 RL:孙子先右旋,再左旋
- 如果不平衡向上传导,则继续第二步
- 对最小不平衡子树的旋转可能导致树变矮,从而导致上层祖先不平衡 (不平衡的向上传递)
平衡二叉树删除操作时间复杂度 =
# 红黑树
平衡二叉树 AVL:插入 / 删除很容易破坏 "平衡" 特性,需要频繁调整树的形态
红黑树 RBT:插入 / 删除很多时候不会破坏 "红黑" 特性,无需频繁调整树的形态。即使需要调整,一般可以在常数级时间内完成
平衡二叉树:以查为主、很少插入 / 删除的场景
红黑树:适用于频繁插入、删除的场景,实用性更强
红黑树是二叉排序树 —— 左子树结点值 < 根结点值 < 右子树结点值
- 每个结点或是红色,或是黑色的
- 根节点是黑色的
- 叶结点 (外部结点、NULL 结点、失败结点) 均是黑色的
- 不存在两个相邻的红结点 (即红结点的父节点和孩子结点均是黑色的)
- 对每个结点,从该结点到任一叶结点的简单路径上,所含黑结点的数目相同
struct RBNode { //红黑树结点定义
int key; //关键字的值
RBNode* parent; //父节点指针
RBNode* lchild; //左孩子指针
RBNode* rchild; //右孩子指针
int color; //结点颜色,如:可用0/1表示黑/红
}
结点的黑高 bh—— 从某结点出发 (不含该结点) 到达任一空叶结点的路径上黑结点总数
性质:
- 从根结点到叶结点的最长路径不大于最短路径的 2 倍
- 有 n 个内部结点的红黑树高度
- 红黑树查找操作时间复杂度 =
查找
与 BST、AVL 相同,从根出发,左小右大,若查找到一个空叶结点,则查找失败
# 插入
- 先查找,确定插入位置 (原理同二叉排序树),插入新结点
- 新结点是根 —— 染为黑色
- 新结点非根 —— 染为红色
- 若插入新结点后依然满足红黑树定义,则插入结束
- 若插入新结点后不满足红黑树定义,需要调整,使其重新满足红黑树定义
- 如何调整:看新结点叔叔的颜色
- 若叔叔为黑色:旋转 + 染色
- LL 型:右单旋,父换爷 + 染色
- RR 型:左单旋,父换爷 + 染色
- LR 型:左、右双旋,儿换爷 + 染色
- RL 型:右、左双旋,儿换爷 + 染色
- 若叔叔为红色:染色 + 变新
- 叔父爷染色,爷变为新结点
结点的黑高 bh—— 从某结点出发 (不含该结点) 到达任一叶结点的路径上黑结点总数
若根结点黑高为 h,内部结点数 (关键字) 最少有 个
# 删除
红黑树删除操作的时间复杂度 =
在红黑树中删除结点的处理方式和二叉排序树的删除一样
按第二步删除结点后,可能破坏红黑树特性,此时需要调整结点颜色、位置,使其再次满足红黑树特性