目录
一、二叉搜索树的局限性
1.BST的局限性
二叉搜索树在多次插入和删除操作后,可能退化为链表。在这种情况下,所有操作的时间复杂度将从O(log n)劣化为O(n)。
例:
如该图所示,经过两次删除节点操作,这颗二叉搜索树便会退化为链表。
再例如:
在完美二叉树中插入两个节点后,树将严重倾斜,查找操作时间复杂度也会随之劣化。
2.解决思路
为克服BST的局限性,需要引入自平衡机制,主要解决方案包括:
方案 | 平衡标准 | 特点 |
---|---|---|
AVL树 | 严格平衡 | 查询效率最高,维护成本较高 |
红黑树 | 近似平衡 | 综合性能好,维护成本适中 |
二、平衡二叉树
1.平衡二叉树基础
平衡二叉树是一种特殊的二叉搜索树(BST),它能通过一定的规则或调整方式,保证树的高度始终保持在 O(log n) 级别,从而确保查找、插入、删除等操作的时间复杂度稳定在 O(log n),避免退化成链表(最坏情况 O(n))。
2. AVL树:严格平衡
(1)平衡条件
每个节点的左右子树高度差<=1。
节点平衡因子=左树高度-右树高度
|height(left)-height(right)|<=1
typedef struct TreeNode {
int val;
int height;//由于AVL树的相关操作需要获取节点高度,因此我们需要为节点类添加height变量
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
TreeNode *newTreeNode(int val) {
TreeNode *node;
node = (TreeNode *)malloc(sizeof(TreeNode));
node->val = val;
node->height = 0;
node->left = NULL;
node->right = NULL;
return node;
}
int balance(TreeNode *node) {
//空节点平衡因子为0
if (node == NULL) {
return 0;
}
return height(node->left) - height(node->right);
}
(2)AVL树的判断
例1:
该树就不是AVL平衡二叉树,对于3来讲,左边有2个节点,右边有0个节点,所以不平衡。
例2:
对于该树来讲,每一个节点左右子树高度的绝对值不超过1。
例3:
对于该树节点,7节点的左子树比右多两层,所以不是AVL树。
(3)节点高度
节点高度是指从该节点到它最远叶节点的距离,即所经过的“边”的数量。叶节点高度为0,而空节点的高度为-1。以下代码用于获取和更新节点高度:
//获取节点高度
int height(TreeNode *node) {
if (node != NULL) {
return node->height;
}
//空节点高度为-1,叶节点为0
return -1;
}
//更新节点高度
void update(TreeNode *node) {
int lh = height(node->left);
int rh = height(node->right);
//节点高度等于最高子树高度+1
if (lh > rh) {
node->height = lh + 1;
} else {
node->height = rh + 1;
}
}
(4)旋转操作
①LL旋转
插入1后,不平衡的发现者是3,麻烦节点1在发现者左子树的左边,因而叫LL插入,需要LL旋转。
注:“不平衡的发现者” 是指从插入点向上回溯时,遇到的第一个不平衡节点。
TreeNode *right(TreeNode *node) {
TreeNode *child, *grandChild;
child = node->left;
grandChild = child->right;
//以child为原点将node向右旋转
child->right = node;
node->left = grandChild;
update(node);
update(child);
//返回旋转后子树的根节点
return child;
}
②LR旋转
插入3后,不平衡的发现者是5,麻烦节点3在发现者左子树的右边,因而叫LR插入,需要LR旋转。
TreeNode *leftright(TreeNode *node) {
// 获取节点 node 的平衡因子
int bf = balance(node);
// 左偏树
if (bf > 1 && balance(node->left) < 0) {
//先左旋后右旋
node->left = left(node->left);
return right(node);
}
}
return node;
}
③RR旋转
插入3后,不平衡的发现者是1,麻烦节点3在发现者右子树的右边,因而叫RR插入,需要RR旋转。
TreeNode *left(TreeNode *node) {
TreeNode *child, *grandChild;
child = node->right;
grandChild = child->left;
//以child为原点,将node向左旋转
child->left = node;
node->right = grandChild;
update(node);
update(child);
//返回旋转后子树的根节点
return child;
}
④RL旋转
插入4后,不平衡的发现者是2,麻烦节点4在发现者右子树的左边,因而叫RL插入,需要RL旋转。
TreeNode *rightleft(TreeNode *node) {
int bf=balance(node);
//右偏树
if (bf < -1 && balance(node->right) > 0) {
// 先右旋后左旋
node->right = right(node->right);
return left(node);
}
return node;
}
(5)基本操作
①处理所有不平衡情况
//执行旋转操作,使该子树重新恢复平衡
TreeNode *turn(TreeNode *node) {
int bf = balance(node);
//左偏树
if (bf > 1) {
if (balance(node->left) >= 0) {
//右旋
return right(node);
} else {
//先左旋后右旋
node->left = left(node->left);
return right(node);
}
}
//右偏树
if (bf < -1) {
if (balance(node->right) <= 0) {
//左旋
return left(node);
} else {
//先右旋后左旋
node->right=right(node->right);
return left(node);
}
}
return node;
}
②插入节点
AVL树的节点插入操作和二叉搜索树在主体上类似。唯一的区别在于,在AVL树中插入节点后,从该节点到根节点的路径上可能会出现一系列不平衡衡节点。因此,我们需要从这个节点开始,自下向上执行旋转操作,使所有不平衡节点恢复平衡。
//插入
void insert(AVLTree *tree, int val) {
tree->root = inserthelp(tree->root, val);
}
//递归插入节点
TreeNode *inserthelp(TreeNode *node, int val) {
if (node == NULL) {
return newTreeNode(val);
}
//查找插入位置并插入节点
if (val < node->val) {
node->left = inserthelp(node->left, val);
} else if (val > node->val) {
node->right = inserthelp(node->right, val);
} else {
//重复节点不插入,直接返回
return node;
}
update(node);
//执行旋转操作,使该子树重新恢复平衡
int bf=balance(node);
if (bf > 1) {
if (balance(node->left) >= 0) { // LL型
node = right(node);
} else { // LR型
node = leftright(node);
}
}
if (bf < -1) {
if (balance(node->right) <= 0) { // RR型
node = left(node);
} else { // RL型
node = rightleft(node);
}
}
//返回子树的根节点
return node;
}
③删除节点
在二叉搜索树的删除节点的基础上,需要从下至上执行旋转操作,使所有失衡节点恢复平衡。
//删除节点
void delete(AVLTree *tree, int val) {
tree->root = deletehelp(tree->root, val);
}
//递归删除节点
TreeNode *deletehelp(TreeNode *node, int val) {
TreeNode *child, *grandChild;
if (node == NULL) {
return NULL;
}
//查找节点并删除
if (val < node->val) {
node->left = deletehelp(node->left, val);
} else if (val > node->val) {
node->right = deletehelp(node->right, val);
} else {
if (node->left == NULL || node->right == NULL) {
child = node->left;
if (node->right != NULL) {
child = node->right;
}
//子节点数量=0,直接删除node并返回
if (child == NULL) {
return NULL;
} else {
//子节点数量=1,直接删除node
node = child;
}
} else {
//子节点数量=2,则将中序遍历的下个节点删除,并用该节点替换当前节点
TreeNode *temp = node->right;
while (temp->left != NULL) {
temp = temp->left;
}
int Val = temp->val;
node->right = deletehelp(node->right, temp->val);
node->val = Val;
}
}
update(node);
//执行旋转操作,使该子树重新恢复平衡
node = turn(node);
// 返回子树的根节点
return node;
}
3.红黑树:近似平衡
(1).红黑树的定义
红黑树是一种自平衡二叉搜索树,通过在节点中增加颜色和约束规则确保树始终保持近似平衡,从而保证基本操作的时间复杂度为O(log n)。(任一节点左右子树的高度相差不超过两倍)
#include <stdio.h>
#include <stdlib.h>
//红黑树结点颜色定义,采用枚举类型
typedef enum { RED, BLACK } Color;
//红黑树结点结构
typedef struct RBNode {
int data;//数据
Color color;//颜色
struct RBNode *left, *right, *parent;//左右孩子和父节点指针
} RBNode;
// 创建新结点
RBNode* createNode(int data) {
RBNode* newNode = (RBNode*)malloc(sizeof(RBNode));
newNode->data = data;
newNode->color = RED; //新插入的结点默认为红色
newNode->left = newNode->right = newNode->parent = NULL;
return newNode;
}
(2).红黑树的性质
①.左根右
前提:二叉搜索树(左<根<右)
②.根叶黑
根和叶子节点(NULL)都是黑色
③.不红红
不存在连续的两个红色节点
④.黑路同
任一节点到叶所有路径路径黑节点数量相同
(3).插入
插入节点默认为红色节点。
如果插入后性质被破坏,则根据下面三种情况做调整:
①破坏了根叶黑
插入节点是根节点 --> 直接变黑
②破坏了不红红
-
插入结点的叔叔是红色 --> 叔父爷变色,爷爷变插入结点,如果为根节点,则变黑。
-
插入结点的叔叔是黑色 --> (LL<RR<LR<RL)旋转,然后变色
1.LL型(右旋)
2.RR型(左旋)
3.LR型:左旋左孩子,然后右旋
4.RL型:右旋右孩子,然后左旋
代码的具体实现如下:
//左旋
void left(RBNode **root, RBNode *x) {
if (x == NULL || x->right == NULL) return;
RBNode *y = x->right;
x->right = y->left;
if (y->left != NULL) {
y->left->parent = x;
}
y->parent = x->parent;
if (x->parent == NULL) {
*root = y;
} else if (x == x->parent->left) {
x->parent->left = y;
} else {
x->parent->right = y;
}
y->left = x;
x->parent = y;
}
//右旋
void right(RBNode **root, RBNode *y) {
if (y == NULL || y->left == NULL) return;
RBNode *x = y->left;
y->left = x->right;
if (x->right != NULL) {
x->right->parent = y;
}
x->parent = y->parent;
if (y->parent == NULL) {
*root = x;
} else if (y == y->parent->left) {
y->parent->left = x;
} else {
y->parent->right = x;
}
x->right = y;
y->parent = x;
}
//插入修改树,使其满足红黑树的特性
void Inserthelp(RBNode **root, RBNode *z) {
while (z != *root && z->parent->color == RED) {
if (z->parent == z->parent->parent->left) {
RBNode *y = z->parent->parent->right; // 叔叔结点
//叔叔是红色
if (y != NULL && y->color == RED) {
z->parent->color = BLACK;
y->color = BLACK;
z->parent->parent->color = RED;
z = z->parent->parent;
} else {
//叔叔是黑色,且z是右孩子
if (z == z->parent->right) {
z = z->parent;
left(root, z);
}
//叔叔是黑色,且z是左孩子
z->parent->color = BLACK;
z->parent->parent->color = RED;
right(root, z->parent->parent);
}
} else {
// 对称的情况
RBNode *y = z->parent->parent->left; // 叔叔结点
// 情况1:叔叔是红色
if (y != NULL && y->color == RED) {
z->parent->color = BLACK;
y->color = BLACK;
z->parent->parent->color = RED;
z = z->parent->parent;
} else {
// 情况2:叔叔是黑色,且z是左孩子
if (z == z->parent->left) {
z = z->parent;
right(root, z);
}
// 情况3:叔叔是黑色,且z是右孩子
z->parent->color = BLACK;
z->parent->parent->color = RED;
left(root, z->parent->parent);
}
}
}
(*root)->color = BLACK; // 根结点始终为黑色
}
// 插入结点
void insert(RBNode **root, int data) {
RBNode *z = createNode(data);
RBNode *y = NULL;
RBNode *x = *root;
while (x != NULL) {
y = x;
if (z->data < x->data) {
x = x->left;
} else {
x = x->right;
}
}
z->parent = y;
if (y == NULL) {
*root = z; // 树为空
} else if (z->data < y->data) {
y->left = z;
} else {
y->right = z;
}
Inserthelp(root, z);
}
三、小结
红黑树和 AVL 树都是自平衡二叉搜索树,适用场景因需求而异。红黑树平衡条件较宽松,插入和删除时旋转操作少,插入效率高,但树高可能较高,影响查询性能。AVL 树平衡条件严格,左右子树高度差不超 1,树高较低,查询性能佳,不过插入和删除时需更多旋转,效率较低。
若应用场景以插入和删除操作为主,对查询性能要求不是特别苛刻,那么红黑树是更合适的选择;若应用场景主要是进行频繁的查询操作,插入和删除操作相对较少,AVL 树则能更好地满足需求。