C++ 浅谈之 AVL 树和红黑树
HELLO,各位博友好,我是阿呆 🙈🙈🙈
这里是 C++ 浅谈系列,收录在专栏 C++ 语言中 😜😜😜
本系列阿呆将记录一些 C++ 语言重要的语法特性 🏃🏃🏃
OK,兄弟们,废话不多直接开冲 🌞🌞🌞
一 🏠 概述
AVL 树
上文提到对于二叉搜索树有单边树的缺陷,那么对于 STL 关联容器 map、set 等底层结构对其进行了平衡处理,采用平衡树实现
AVL 树,当向二叉搜索树插入新结点后,保证每个结点左右子树高度差绝对值不超过 1 ,降低树高度,减少平均搜索长度
概念
AVL 树是空树或具有如下性质的二叉搜索树
① 左右子树都是 AVL 树
② 左右子树高度之差 ( 简称平衡因子 : 右子树高 - 左子树高 ) 绝对值不超过 1
一棵二叉搜索树高度平衡,它就是 AVL 树。如果它有 n 个结点,其高度可保持在 O(log2N),搜索时间复杂度 O(log2N)
AVL 树节点定义
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;//定义成三叉链的形式
int _bf;//balance factor平衡因子
pair<K, V> _kv;//用pair同时存K和V两个数据
AVLTreeNode(const pair<K,V>& kv)//节点构造函数
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)//平衡因子初始给0
,_kv(kv)
{}
};
AVL 树插入
AVL 树是在二叉搜索树基础上引入平衡因子,插入过程如下
① 按照二叉搜索树方式找到空位置,插入新节点
② 插入新节点后,需要调整节点的平衡因子
③ 插入元素后可能导致 AVL 树左右子树高度不符合条件,需要旋转
AVL 树旋转
AVL树的旋转分为多种,具体举出一种如下图所示(右单旋)
Parent 平衡因子为 2 或 -2 ,分以下情况
① 平衡因子为 2,说明右子树高,需要往左边压高度,设 Parent 右子树根为 SubR
当 SubR 平衡因子为 1 时,执行左单旋
当 SubR 平衡因子为 -1 时,执行右左双旋
② 平衡因子为 -2,说明左子树高,需要往右边压高度,设 Parent 左子树根为 SubL
当 SubL 平衡因子为 -1 时,执行右单旋
当 SubL 平衡因子为 1 时,执行左右双旋
旋转完成后,原 Parent 为根子树个高度降低,已经平衡(无需往上更新,直接退出循环)
AVL 树模拟实现
#pragma once
#include<iostream>
#include<assert.h>
#include<string>
using namespace std;
template<class K,class V>
struct AVLTreeNode
{
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;//定义成三叉链的形式
int _bf;//balance factor平衡因子
pair<K, V> _kv;//用pair同时存K和V两个数据
AVLTreeNode(const pair<K,V>& kv)//节点构造函数
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_bf(0)//平衡因子初始给0
,_kv(kv)
{}
};
template<class K,class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
AVLTree()
:_root(nullptr)
{}
//拷贝构造和赋值拷贝也需要自己实现
AVLTree(const AVLTree<K,V>& kv)
{
_root = Copy(kv._root);
}
AVLTree<K, V>& operator=(AVLTree<K,V> kv)
{
swap(_root, kv._root);
return *this;
}
~AVLTree()
{
Destroy(_root);
_root = nullptr;
}
Node* Copy(Node* root)
{
if (root == nullptr)
return nullptr;
Node* newroot = new Node(root->_key);//建立新节点
newroot->_left = Copy(root->_left);//新节点的左右节点再去转换成子问题
newroot->_right = Copy(root->_right);
return newroot;//最后返回新节点
}
void Destroy(Node* root)
{
//利用后序遍历去释放节点
if (root == nullptr)
{
return;
}
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
V& operator[](const K& key)//重载operator[]
{
//operator[]的原则是:
//如果插入成功返回插入都value的引用
//如果插入失败则返回V类型默认缺省值
pair<Node*, bool> ret = Insert(make_pair(key, V()));//V采用传匿名对象的方式
return ret.first->_kv.second;
}
Node* Find(const pair<K, V>& kv)//查找函数
{
Node* cur = _root;
while (cur)
{
if (kv.first > cur->_kv.first)
{
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
cur = cur->_left;
}
else
{
return cur;
}
}
return nullptr;
}
void RotateR(Node* parent)//右单旋
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR != nullptr)//注意:这里一定要判断不为空的,因为下面可能会出现空指针的解引用
{
subLR->_parent = parent;
}
subL->_right = parent;
Node* parentParent = parent->_parent;//一定要在改变链接关系之前把这个指针存下来
parent->_parent = subL;
//if (parentParent == nullptr)或者采用这个条件也是可以的
if(parent==_root)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
//这里注意:parent还有父母时,链接之前需要注意判断到底是右孩子还是左孩子
if (parentParent->_left == parent)
parentParent->_left = subL;
else
parentParent->_right = subL;
subL->_parent = parentParent;//最后还要把父指针关系链接上
}
parent->_bf = subL->_bf = 0;//最后右单旋完成后平衡因子都要修改成0
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//先把subR的左孩子赋值给parent的右节点
parent->_right = subRL;
if (subRL != nullptr)//注意一定要判断是否为空的情况
{
subRL->_parent = parent;//然后链接parent指针
}
//然后subR的左节点链接上parent
subR->_left = parent;
Node* parentParent = parent->_parent;//提前记录
parent->_parent = subR;
//if (parentParent == nullptr)
if (parent == _root)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
parentParent->_left = subR;
else
parentParent->_right = subR;
subR->_parent = parentParent;
}
parent->_bf = subR->_bf = 0;
}
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;//注意:需要提前存subRL的平衡因子,因为旋转可能引起改变
//subRL的平衡因子是双旋的关键节点
RotateR(parent->_right);//先进行右旋,并注意旋转点为父节点的右节点
RotateL(parent);//再进行左旋,此时旋转点为父节点
if (bf == 0)
{
parent->_bf = 0;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == 1)
{
parent->_bf = -1;
subR->_bf = 0;
subRL->_bf = 0;
}
else if (bf == -1)
{
parent->_bf = 0;
subR->_bf = 1;
subRL->_bf = 0;
}//注意这里处理完成过后sunRL的平衡因子一定都是等于0的
else
{
assert(false);
}
}
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);//先进行左旋,并注意旋转点为父节点的左节点
RotateR(parent);//再进行右旋,此时旋转点为父节点
if (bf == 0)
{
parent->_bf = 0;
subL->_bf = 0;
subLR->_bf = 0;
}
else 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;
}//注意这里处理完成过后sunRL的平衡因子一定都是等于0的
else
{
assert(false);
}
}
pair<Node*,bool> Insert(const pair<K, V>& kv)
{
if (_root == nullptr)//根节点为空时先new一个新节点
{
_root = new Node(kv);
return make_pair(_root, true);
}
Node* cur = _root;
Node* parent = nullptr;
//先利用while循环去找cur的空位
while (cur)
{
if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else if (kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return make_pair(cur, false);
}
}
//将cur插入到相应位置
cur = new Node(kv);
Node* newnode = cur;//用一个newnode记录一下新节点用以返回
if (kv.first > parent->_kv.first)
{
parent->_right = cur;//注意三叉链的链接逻辑顺序,等号左右方向不能反,先把cur链接到父节点的右边
cur->_parent = parent;//然后再去把父指针知道父节点
}
else
{
parent->_left = cur;
cur->_parent = parent;
}
//进行旋转调整
//while(cur!=_root)
while (parent)
{
//1.进入循环先对平衡因子进行调整
if (cur == parent->_right)
{
parent->_bf++;
}
else
{
parent->_bf--;
}
//分三种情况向上走
if (parent->_bf == 0)//平衡因子等于0不需要调整
{
//为什么不需调整
//因为等于0的话,说明底层子树高度不平衡,添加进入新元素后平衡了,只要平衡了高度并没发生变化,不会影响上面的父节点
break;
}
else if (parent->_bf == -1 || parent->_bf == 1)
{
//平衡因子等于-1,说明插入新节点后子树的高度不平衡了,需要继续往上迭代查看父节点是否还满足平衡节点
cur = parent;
parent = parent->_parent;
}
else if (parent->_bf == 2 || parent->_bf == -2)
{
if (parent->_bf == -2)//父节点等于-2,说明左边高,触发右旋的情况
{
if (cur->_bf == -1)//cur节点等于-1,说明在cur的左边更高,触发右单旋的情况
{
RotateR(parent);
}
else//cur等于-1,说明在cur的右边更高,触发左右双旋
{
RotateLR(parent);
}
}
else//父节点等于1,说明右边更高,触发左旋的情况
{
if (cur->_bf == 1)//cur节点等于1时,说明在cur的右边更高,触发右单旋的情况
{
RotateL(parent);
}
else//cur等于-1,说明在cur的左边更高,触发右左双旋
{
RotateRL(parent);
}
}
//思考:为什么上面在传参数的时候,都是传parent的节点呢?这样的好处是什么呢
break;//调整完成后break退出循环
//这里为什么调整完成过后就可以退出,通过旋转调整平衡因子后,parent节点的平衡因子都为0了,调整过后不需要再向上继续查找了
}
else
{
assert(false);
}
}
return make_pair(newnode,true);
}
void _Inorder(Node* root)//中序遍历打印每个节点
{
if (root == nullptr)
return;
_Inorder(root->_left);
cout << root->_kv.first << ":" << root->_kv.second << endl;
_Inorder(root->_right);
}
void Inorder()
{
_Inorder(_root);
cout << endl;
}
//验证是否为平衡二叉树
//1.左子树高度与右子树高度差必须小于1
int _Height(Node* root)//求树的高度函数
{
if (root == nullptr)
{
return 0;
}
int leftHeight = _Height(root->_left);//递归去子问题求解
int rightHeight = _Height(root->_right);
return rightHeight > leftHeight ? rightHeight + 1 : leftHeight + 1;
}
bool _IsBalance(Node* root)
{
if (root == nullptr)
{
return true;
}
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
// 2.检查一下每颗树的平衡因子是否正确
if (rightHeight - leftHeight != root->_bf)
{
cout << "平衡因子异常:" << root->_kv.first << endl;
return false;
}
return abs(rightHeight - leftHeight) < 2
&& _IsBalance(root->_left)
&& _IsBalance(root->_right);//分别递归到各自的左右子树再去检查
}
bool IsAVLTree()
{
return _IsBalance(_root);
}
private:
Node* _root;
};
二 🏠 核心
红黑树
红黑树的概念
一种二叉搜索树,在每个结点上增加一个存储位表示结点颜色,Red 或 Black。确保没有一条路径会比其他路径长 2 倍,因而接近平衡
红黑树性质
红黑树的定义
- 每个结点不是红就是黑
- 根节点是黑色
- 一个节点是红,则两个孩子是黑 ( 没有连续红节点 )
- 每条路径上包含相同数量黑节点
- 每个叶子结点都是黑 ( 此处指的是空结点,即 NIL 节点 )
为什么红黑树能保证最长路径中节点个数不会超过最短路径的两倍 ?
假设黑节点数量 N 个,最短路径为 :logN,最长路径为 :2 * logN . 所以最长路径节点数不会超过最短路径的两倍
红黑树节点定义
enum Colour
{
RED,
BLACK
};//节点的颜色
template<class K, class V>
struct RBTreeNode
{
RBTreeNode<K, V>* _left;//节点左孩子
RBTreeNode<K, V>* _right;//节点右孩子
RBTreeNode<K, V>* _parent;//节点的父亲
pair<K, V> _kv;//节点中存放的键值对
Colour _col;//节点的颜色
RBTreeNode(const pair<K,V> kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_col(RED)
{}
};
为什么节点默认颜色是红色 ?
因为默认颜色黑色,将破坏定义四,其它每条路径都会因该路径增加一个黑节点而不满足红黑树性质,这是一种全局的破坏;默认颜色红色,会破坏定义三,但只会影响当前路径,并不会对其它路径造成影响
红黑树插入操作
可分为两步
按照二叉搜索树规则插入新节点
检测新节点插入后,红黑树性质是否造到破坏
如果父节点是黑色(未违反红黑树任何性质),则不需要调整;
父节点为红色时,违反了定义三不能有连在一起的红色节点
此时对红黑树有多种情况,如下图为其中一种讨论
红黑树模拟实现
#pragma once
#include<iostream>
using namespace std;
enum Colour
{
RED,
BLACK
};//节点的颜色
template<class K,class V>
struct RBTreeNode
{
RBTreeNode<K,V>* _left;//节点左孩子
RBTreeNode<K,V>* _right;//节点右孩子
RBTreeNode<K,V>* _parent;//节点的父亲
pair<K,V> _kv;//节点中存放的T类型的数据
Colour _col;//节点的颜色
RBTreeNode(const pair<K,V> kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_col(RED)
{}
};
template<class K,class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
RBTree()
:_root(nullptr)
{}
//拷贝构造和赋值拷贝也需要自行实现这里不做赘述
~RBTree()
{
Destory(_root);
_root = nullptr;
}
void Destory(Node* root)
{
if (root == nullptr)
{
return;
}
//利用后序遍历释放节点
Destory(root->_left);
Destory(root->_right);
delete root;
}
void RotateR(Node* parent)//右单旋
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR != nullptr)//注意:这里一定要判断不为空的,因为下面可能会出现空指针的解引用
{
subLR->_parent = parent;
}
subL->_right = parent;
Node* parentParent = parent->_parent;//一定要在改变链接关系之前把这个指针存下来
parent->_parent = subL;
//if (parentParent == nullptr)或者采用这个条件也是可以的
if (parent == _root)
{
_root = subL;
_root->_parent = nullptr;
}
else
{
//这里注意:parent还有父母时,链接之前需要注意判断到底是右孩子还是左孩子
if (parentParent->_left == parent)
parentParent->_left = subL;
else
parentParent->_right = subL;
subL->_parent = parentParent;//最后还要把父指针关系链接上
}
}
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
//先把subR的左孩子赋值给parent的右节点
parent->_right = subRL;
if (subRL != nullptr)//注意一定要判断是否为空的情况
{
subRL->_parent = parent;//然后链接parent指针
}
//然后subR的左节点链接上parent
subR->_left = parent;
Node* parentParent = parent->_parent;//提前记录
parent->_parent = subR;
//if (parentParent == nullptr)
if (parent == _root)
{
_root = subR;
_root->_parent = nullptr;
}
else
{
if (parentParent->_left == parent)
parentParent->_left = subR;
else
parentParent->_right = subR;
subR->_parent = parentParent;
}
}
pair<Node*, bool> Insert(const pair<K, V> kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;//根节点给黑色
return make_pair(_root, true);
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)//循环去找空位
{
if (kv.first > cur->_kv.first)
{
parent = cur;
cur = cur->_right;
}
else if(kv.first < cur->_kv.first)
{
parent = cur;
cur = cur->_left;
}
else
{
return make_pair(cur, false);
}
}
Node* newnode = new Node(kv);
newnode->_col = RED;//链接上新节点
if (kv.first > parent->_kv.first)
{
parent->_right = newnode;
newnode->_parent = parent;
}
else
{
parent->_left = newnode;
newnode->_parent = parent;
}
cur = newnode;
//父节点存在且父节点为红色时需要继续处理
while (parent && parent->_col == RED)
{
Node* grandfather = parent->_parent;
//关键看叔叔的脸色
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
if (uncle && uncle->_col == RED)
{
//具体变色的情况需要画图分析:
grandfather->_col = RED;
parent->_col = uncle->_col = BLACK;
//注意需要继续向上处理,容易忘记
cur = grandfather;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
RotateR(grandfather);//右单旋
parent->_col = BLACK;
grandfather->_col = RED;
}
else
{
RotateL(parent);//先以父节点为旋转点左旋
RotateR(grandfather);//再以g为旋转点右旋
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 // 情况2:+ 情况3:
{
if (cur == parent->_right)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else // cur == parent->_left
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break;
}
}
}
_root->_col = BLACK;
return make_pair(newnode, true);
}
void _InOrder(Node* root)//中序遍历递归打印
{
if (root == nullptr)
{
return;
}
_InOrder(root->_left);
cout << root->_kv.first << ":"<<root->_kv.second<<endl;
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
cout << endl;
}
bool _CheckBlance(Node* root, int blackNum, int count)//balckNum相当于一个标尺,count就是用来记录每条路径的黑节点数目
{
if (root == nullptr)//如果root走到空节点
{
if (count != blackNum)//count不等于最左路径的黑色节点数
{
cout << "黑色节点的数量不相等" << endl;
return false;//返回假
}
return true;//否则返回真
}
if (root->_col == RED && root->_parent->_col == RED)
{
cout << "存在连续的红色节点" << endl;
return false;
}
if (root->_col == BLACK)
{
count++;
}
return _CheckBlance(root->_left, blackNum, count)
&& _CheckBlance(root->_right, blackNum, count);//再递归到各子树的子问题
}
bool CheckBlance()
{
if (_root == nullptr)
{
return true;
}
if (_root->_col == RED)
{
cout << "根节点是红色的" << endl;
return false;
}
// 找最左路径做黑色节点数量参考值
int blackNum = 0;
Node* left = _root;
while (left)
{
if (left->_col == BLACK)
{
blackNum++;
}
left = left->_left;
}
int count = 0;
return _CheckBlance(_root, blackNum, count);
}
private:
Node* _root;
};
红黑树与 AVL 树比较
都是高效平衡二叉树,增删改查时间复杂度都是 O( log2N) ,红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径 2 倍,降低了插入和旋转次数,所以在经常进行增删结构中性能比 AVL 树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多
三 🏠 结语
身处于这个浮躁的社会,却有耐心看到这里,你一定是个很厉害的人吧 👍👍👍
各位博友觉得文章有帮助的话,别忘了点赞 + 关注哦,你们的鼓励就是我最大的动力
博主还会不断更新更优质的内容,加油吧!技术人! 💪💪💪