目录
前言
一、AVL树
1、AVL树概念
2、AVL树节点的定义
3、AVL树的插入
4、AVL树的旋转
5、AVL树的删除
二、红黑树
1、红黑树的概念
2、红黑树节点的定义
3、红黑树的插入操作
三、红黑树与AVL树比较
哈喽,小伙伴们大家好。之前我们介绍了二叉搜索树用于搜索数据,但是二叉搜索树具有一些缺陷,比如在大多数节点的子节点都只有一个时,那搜索二叉树就会近似成一条线,搜索的时间复杂度就会从O(logN)退化成O(N)。针对这个问题,一些人对二叉搜索树进行了升级改造,也就是我们今天要学习的AVL树与红黑树。
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查
找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis(AVL树正是由这两位数学家的名字命名的)在1962年发明了一种解决上述问题的方法:当向一棵搜索二叉树中插入新节点时,要通过调整,使每个节点的左右子树高度差的绝对值不超过1,这样就能使二叉树接近满二叉树,进而提高搜素效率。
一棵AVL树具有以下性质:
我们定义AVL树的节点时,不但有左右指针,为了方便还增加了父节点的指针。其中bf代表平衡因子。
template
struct AVLTreeNode
{
AVLTreeNode(const pair& kv)
:left(nullptr)
,right(nullptr)
,parent(nullptr)
,bf(0)
,_kv(kv)
{}
AVLTreeNode* left;
AVLTreeNode* right;
AVLTreeNode* parent;
int bf; //banlance factor 平衡因子
pair _kv;
};
AVL树就是在二叉搜索树的基础上引入了平衡因子,因为AVL树也可以看作是二叉搜索树。AVL树的插入分为两步:
调正平衡因子:
按照二叉搜索树的方法插入后,需要调整平衡因子。平衡因子的计算方法为右子树的高度减左子树的高度。插入节点后一定会影响该节点父节点的平衡因子,可能会影响祖先节点的平衡因子,如果该节点在父节点的左侧插入,则父节点的平衡因子减1,如果在右侧,父节点的平衡因子加1,计算出父节点的平衡因子后分为以下三种情况:
//调整平衡因子
while (cur != _root)
{
if (parent->_left = cur)
{
parent->_bf--;
}
else
{
parent->_bf++;
}
if (parent->_bf == 0)
{
//调整完毕
break;
}
else if (parent->_bf == 1||parent->_bf==-1)
{
//继续向上调整
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
//旋转
}
else
{
//在插入之前二叉树已经不平衡了
return false;
}
}
如果在一棵原本是平衡的AVL树中插入一个新节点,可能造成不平衡,此时必须调整树的结构,
使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:
(1)左左插入:右单旋
新节点插在较高左子树的左侧时要使用右旋转。
在旋转后要同时满足搜索二叉树和平衡因子不大于一的性质,以图中为例,我们要把父节点右侧向下压,看作左侧节点的右子树,然后把左侧节点原本的右子树变成父节点的左子树。旋转完成后更新平衡因子即可。在这个过程中可能遇到两种特殊情况:
旋转完毕后要更新平衡因子,图中只有60和30两个节点的平衡因子发生了变化,且都变为了0。
代码如下:
void rotateR(node* parent)
{
node* subL = parent->_left;
node* subLR = subL->_right;
//保存父节点的父节点
node* parentparent = parent->_parent;
//判断左节点的右节点是否为空
if (subLR)
{
subLR->_parent = parent;
}
parent->_left = subLR;
subL->_right = parent;
parent->_parent = subL;
//判断parent是否为根节点
if (parent == _root)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parentparent->_left == parent)
{
parentparent->_left = subL;
}
else
{
parentparent->_right = subL;
}
subL->_parent = parentparent;
}
parent->_bf = subL->_bf = 0;
}
(2)右右插入:左单旋
新节点插在较高右子树的右侧时要使用左旋转。
实现及情况与右单旋相似。
(3)左右插入:先左单旋再右单旋(左右双旋)
新节点插在较高左子树的右侧,先对30进行左单旋,再对90进行右单旋,旋转完后再考虑平衡因子的更新。
平衡因子的调整可以根据插入节点位置的不同分为三种情况,我们要再插入后提前记录好subLR平衡因子的值,来方便判断是哪种情况:
旋转后60的平衡因子为0,30和90的平衡因子根据以上三种情况的不同做出调整。
代码如下:
void rotateLR(node* parent)
{
node* subL = parent->_left;
node* subLR = subL->_right;
//提前把平衡因子存起来
int bf = subLR->_bf;
rotateL(subL);
rotateR(parent);
//判断情况调整平衡因子
if (bf == 1)
{
parent->_bf = 0;
subL->_bf = -1;
subLR->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 1;
subL->_bf = 0;
subLR->_bf = 0;
}
else if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else
{
assert(false);
}
}
(4)右左插入:先右单旋再左单旋(右左双旋)
新节点插在了较高右子树的左侧,使用右左双旋,具体实现方法参考左右双旋。
AVL树的删除可以参考二叉搜素树的删除,删除完毕后更新平衡因子,再根据平衡因子的值做出调成。
AVL树的性能:
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即$log_2 (N)$。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
概念:红黑树也是一种二叉搜索树,在二叉搜索树的基础上,给每个节点分配了一个颜色:红色或黑色。根据颜色的不同,对红黑树的排列进行限制,保证红黑树任何一条由根到叶子节点的路径不会比其它路径长出两倍,进而达到近似平衡。
性质: 一棵红黑树需要具备以下性质,换句话说只有满足了以下性质,才能保证最长路径不超过最短路径的两倍。
思考一下,为什么具备了以上性质,就可以保证最长路径不超过最短路径的两倍?
根据性质(4),任意节点到叶节点的几条路径中黑色节点的个数必须相等,假设一棵红黑树中只有黑色节点,那这棵树必然是满二叉树。再根据性质(3),两个红色节点不能相邻,那么红色节点只能插入到两个黑色节点之间。现在存在一个黑色满二叉树,假设高度为N,我们向里面随机插入一些红色节点。以根节点为例,最短路径一定是全为黑色节点的那条路径,长度为N,而最长路径一定是红黑相间的那条路径,长度为2N,其它路径的长度都在N和2N之间,不会超过最短路径的两倍。
在定义节点时候,我们一般默认把节点定义成红色。因为定义成黑色的话在插入过程中又可能打破性质(4),而定义成红色在插入时有可能打破了性质(3),显然打破性质(3)比打破性质(4)更加容易调整。
enum Color
{
RED,
BLACK
};
template
class RBTreeNode
{
public:
RBTreeNode(pair& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_col(RED)
,_kv(kv)
{}
private:
RBTreeNode* _left;
RBTreeNode* _right;
RBTreeNode* _parent;
Color _col;
pair _kv;
};
插入操作分为两步:
调整过程:
约定:cur为当前节点,p为父节点,g为祖父节点,u为uncle节点。且cur默认为红,既然需要调整,那p肯定也为宏,g一定为黑。所以情况的不同关键取决于叔叔节点。
情况一:uncle存在且为红
解决方法:将p和u改为黑,将g改为红,然后把g改成cur,继续向上调整。要注意,这里绝对不能仅仅简单的把p改成黑,这样会使各路径上的黑色节点不相等,违反性质(4),所以必须要对g和u做出调整。
情况二:uncle不存在或为黑,cur与p在同一侧
解决办法:根据p和cur在左侧还是右侧,以p为轴,进行右旋或左旋。然后p、g变色--p变黑,g变红。
情况二:uncle不存在或为黑,cur与p在两侧
解决办法:p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反,p为g的右孩子,cur为p的左孩子,则针对p做右单旋转。则转换成了情况2。按照情况2旋转后进行颜色改变。
我们也可以尝试一下手撕红黑树的感觉,方便以后和同学吹牛,下面模拟实现红黑树的插入操作,代码如下:
pair insert(pair kv)
{
//插入操作
if (nullptr == _root)
{
_root = new node(kv);
_root->_col = BLACK;
return make_pair(_root, true);
}
node* cur = _root;
node* parent = cur;
while (cur)
{
parent = cur;
if (kv.second < cur->_kv.second)
{
cur = cur->_left;
}
else if (kv.second > cur->_kv.second)
{
cur = cur->_right;
}
else
{
return make_pair(cur, false);
}
}
node* newnode = new node(kv);
if (kv.second < parent->_kv.second)
{
parent->_left = newnode;
newnode->_parent = parent;
}
else
{
parent->_right = newnode;
newnode->_parent = parent;
}
cur = newnode;
//开始调整,如果父亲存在,并且父亲颜色为红,则需要调整。
//并且在这种情况下,祖父是一定存在的,因为根节点为黑色,父亲一定不是根节点。
while (parent&&parent->_col==RED)
{
node* grandfather = parent->_parent;
//分情况讨论,看看叔叔在左还是在右
if (grandfather->_left == parent)
{
node* uncle = grandfather->_right;
//情况1,叔叔存在并且为红色
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
//情况2加3,叔叔不存在或者叔叔存在且为黑
else
{
//parent和cur在同一侧
if (parent->_left == cur)
{
rotateR(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
//parent和cur在两侧
else
{
rotateL(parent);
rotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
else
{
node* uncle = grandfather->_left;
if (uncle && uncle->_col == RED)
{
uncle->_col = parent->_col = BLACK;
grandfather->_col = RED;
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)
{
rotateL(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
else
{
rotateR(parent);
rotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return make_pair(newnode,true);
}
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(logN),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。map和set的底层都是使用红黑树实现的。
总结
以上就是本文的全部内容,通过对AVL树和红黑树的学习,我们可以对map和set容器有更加深刻的认识。今天的内容就到这里啦,如果觉得博主写的不错的话可以点赞支持一下,江湖路远,来日方长,我们下次见~