目录
2.2.4.2 创建一棵完全二叉树(空间连续,使用顺序结构)
二叉树上某结点平衡因子定义为:该结点的左子树的深度减去它的右子树的深度
一、树
相比哈希表的好处:可以一个节点一个节点加入
二、二叉树
树中每个节点最多两个孩子
A的左子树:以A的左孩子为根节点的子树
2.1 满二叉树
每一层都是满节点
对于一棵高度为k的满二叉树
(1) 总结点数 2^k-1
(2) 总叶子节点数 2^(k-1)
2.2 完全二叉树(CBT)
除了最后一层都是满的,且最后一层的节点都连续集中在最左边
2.2.1 性质
对于任意二叉树,总节点个数(s)=度为0的节点(n0)+度为1的节点(n1)+度为2的节点(n2)
同时,总节点个数(s)=根节点数(1)+叶子节点数(n0*0+n1*1+n2*2) 所以,n0=n2+1
(3) 度为0的节点永远比度为2的节点多1,n0=n2+1
若一棵完全二叉树,叶子节点为124,求总结点数最多有多少个?
叶子节点n0=124,n2=123,在一颗完全二叉树中度为1的节点只有1个或者0个。
所以答案为:124+123+1=248
(4) 对于一棵节点数为n的完全二叉树,有2^(k-1)-1<n<2^k-1,即k=floor(log2n)+1
2.2.2 线性存储结构
(5)将一棵完全二叉树,按照从上到下从左到右的顺序编号1~n,父节点(非叶子节点)为1~n/2
节点i的左孩子为2i,右孩子为2i+1
将一棵完全二叉树,按照从上到下从左到右的顺序编号0~n-1,父节点为0~n/2-1
节点i的左孩子为2i+1,右孩子为2i+2
2.2.3 链式存储结构
二叉链表
typedef struct node {
int nVlaue;
struct node* pLeft;
struct node* pRight;
}BinaryTree;
2.2.3 遍历
2.2.3.1 深度遍历:先父子
2.2.3.1.1 前序遍历:根左右
void PreOrderTraversal(BinaryTree* pTree) {
if (pTree) {
printf("%d ", pTree->nVlaue);
PreOrderTraversal(pTree->pLeft);
PreOrderTraversal(pTree->pRight);
}
}
循环遍历方式的优点:
1. 便于保存完整路径
2. 减少了函数调用的时间
void PreOrderTraversal1(BinaryTree* pTree) {
stack<BinaryTree*> s;
while (pTree || !s.empty()) {
while (pTree) {
printf("%d ", pTree->nVlaue);
s.push(pTree);
pTree = pTree->pLeft;
}
if (!s.empty()) {
pTree = s.top();
s.pop();
pTree = pTree->pRight;
}
}
printf("\n");
}
2.2.3.1.2 中序遍历:左根右
void InOrderTraversal(BinaryTree* pTree) {
if (pTree) {
InOrderTraversal(pTree->pLeft);
printf("%d ", pTree->nVlaue);
InOrderTraversal(pTree->pRight);
}
}
void InOrderTraversal1(BinaryTree* pTree) {
stack<BinaryTree*> s;
while (pTree || !s.empty()) {
while (pTree) {
s.push(pTree);
pTree = pTree->pLeft;
}
if (!s.empty()) {
pTree = s.top();
s.pop();
printf("%d ", pTree->nVlaue);
pTree = pTree->pRight;
}
}
printf("\n");
}
2.2.3.1.3 后序遍历:左右根
void PostOrderTraversal(BinaryTree* pTree) {
if (pTree) {
PostOrderTraversal(pTree->pLeft);
PostOrderTraversal(pTree->pRight);
printf("%d ", pTree->nVlaue);
}
}
可以看到,前序遍历和中序遍历的非递归算法很相似,原因在于都是先处理根后再寻找其右孩子。而当后序遍历时则会遇到一些问题:按照左右根顺序处理某节点时,并不知道此时栈顶节点是否输出(右子树是否完成),所以此时应当添加标记,记录上一个处理的节点。若此时栈顶节点右子树为空或为标记节点,即可以说明该节点的右子树已经处理完了。
void PostOrderTraversal1(BinaryTree* pTree) {
BinaryTree* last = NULL;
stack<BinaryTree*> s;
while (pTree || !s.empty()) {
while (pTree != NULL) {
s.push(pTree);
pTree = pTree->pLeft;
}
if (s.top()->pRight == NULL || s.top()->pRight == last) {
printf("%d ", s.top()->nVlaue);
last = s.top();
s.pop();
}
else {
pTree = s.top()->pRight;
}
}
printf("\n");
}
能否通过任意两种遍历还原一棵二叉树?
通过中序遍历将左右子树区分开,这样就能能通过前序或者后序还原二叉树。
步骤:先根据前序或后序找到根,再通过中序找到根的位置分开左右子树。
2.2.3.2 广度遍历:先兄弟(层序遍历)
void LevelOrderTraversal(BinaryTree* pTree) {
queue<BinaryTree*> q;
q.push(pTree);
while (!q.empty()) {
pTree = q.front();
q.pop();
printf("%d ", pTree->nVlaue);
if (pTree->pLeft)
q.push(pTree->pLeft);
if (pTree->pRight)
q.push(pTree->pRight);
}
}
※按层换行打印?(高度?每层最左?最右?每层平均值?Z字形,螺旋形打印?)
1.标记末尾节点
1.1 q.back
1.2 双标记
2.双队列
2.1 队列交替,队列空则换行
2.2 新队列记录节点层数。当输出元素与队首层数不同,换行
3.计数
3.1 q.size
输出计数器-1,当计数器归0换行,重新获取为q.size
3.2 双计数器
记录当前层和下一层
4.后序
后序中根和最右侧节点总在最后面
6.按前序遍历,+【】
1【2【4【】5【7【【】9【】】8【】】】3【【】6【】】】
【加一层,】减一层,根据括号关系确定节点深度,需要额外空间存储结果
7.入队特殊标记分割每一层
root ▲ left right ▲ll lr rl rr▲
出队特殊标记时,换行并把标记压入队尾
8.补空
1 23 45▲6 ▲▲78▲▲▲▲
子节点为空压入特殊字符,这样就可以根据2的次方换行
2.2.4 构造二叉树
2.2.4.1 前序序列反向构造二叉树
//前序序列反向构造二叉树
BinaryTree* PreCreate(int* a, int& cur) {
if (a[cur] != 0) {
BinaryTree* p = (BinaryTree*)malloc(sizeof(BinaryTree));
p->nVlaue = a[cur];
cur++;
p->pLeft = PreCreate(a, cur);
p->pRight = PreCreate(a, cur);
return p;
}
else {
cur++;
return NULL;
}
}
void PreCreate1(BinaryTree** tree) {
int num;
scanf_s("%d", &num);
if (num == 0) {
(*tree) = NULL;
return;
}
*tree = (BinaryTree*)malloc(sizeof(BinaryTree));
(*tree)->nVlaue = num;
PreCreate1(&(*tree)->pLeft);
PreCreate1(&(*tree)->pRight);
}
2.2.4.2 创建一棵完全二叉树(空间连续,使用顺序结构)
1.节点数组
2.结点赋值
3.左右关联
2.3 二叉搜索树/二叉查找树/排序二叉树(BST)
树中任意一个父节点的值,大于左子树,小于右子树。
查找效率与树的高度有关,理想情况下为O(logn)
核心需求:增删对树的影响越小越好
2.3.1 添加节点
1. 先将数据放入节点中
2. 看树的状态(空|非空)
3. 小于找左子树,大于找右子树;一般情况下BST要求树上的值唯一
typedef struct node{
int nVlaue;
node* pLeft;
node* pRight;
}BinaryTree;
void addNode(BinaryTree** pTree, int num) {
BinaryTree* pTmp = (BinaryTree*)malloc(sizeof(BinaryTree));
pTmp->nVlaue = num;
pTmp->pLeft = nullptr;
pTmp->pRight = nullptr;
if (*pTree == nullptr) {
*pTree = pTmp;
return;
}
BinaryTree* pNode = *pTree;
while (pNode) {
if (pNode->nVlaue == num) {
free(pTmp);
pTmp = nullptr;
return;
}
else if (pNode->nVlaue < num) {
if (pNode->pRight == nullptr) {
pNode->pRight = pTmp;
return;
}
else pNode = pNode->pRight;
}
else if (pNode->nVlaue > num) {
if (pNode->pLeft == nullptr) {
pNode->pLeft = pTmp;
return;
}
else pNode = pNode->pLeft;
}
}
}
BinaryTree* CreateBST(int* a,int len) {
if (a == NULL || len <= 0)
return NULL;
BinaryTree* pTree = nullptr;
for (int i = 0; i < len; i++) {
addNode(&pTree, a[i]);
}
return pTree;
}
2.3.2 删除节点
要保留原BST的查找效率,不能简单的将一棵子树加到另一棵子树后面
将值替换为右子树的最小值或左子树的最大值,然后删掉替换的叶子节点
1. 查找
2. 分析孩子情况(0,1,2)
3. 值替换,删除替换节点
void DelNode(BinaryTree** pTree, int num) {
//查找要删除的位置
BinaryTree* pDel = *pTree; //删除位置
BinaryTree* pFather = NULL; //父节点
while (pDel) {
if (pDel->nVlaue == num) {
break;
}
else if (pDel->nVlaue < num) {
pFather = pDel;
pDel = pDel->pRight;
}
else if (pDel->nVlaue > num) {
pFather = pDel;
pDel = pDel->pLeft;
}
}
//检测
if (pDel == NULL) {
printf("删除失败,%d节点不存在\n", num);
return;
}
//孩子情况
//度为2,查找替换删除节点(右子树的最小值或左子树的最大值)
BinaryTree* pMark = NULL; //标记原删除位置
if (pDel->pLeft != NULL && pDel->pRight != NULL) {
pMark = pDel;
//左子树最大值
pFather = pDel;
pDel = pDel->pLeft; //实际删除
while (pDel->pRight != NULL) {
pFather = pDel;
pDel = pDel->pRight;
}
//值替换
pMark->nVlaue = pDel->nVlaue;
}
//如果实际删除节点是根
if (pFather == NULL) {
*pTree = pDel->pLeft ? pDel->pLeft : pDel->pRight;
free(pDel);
pDel = NULL;
return;
}
//非根,将度为2的情况和度为1、0的情况统一处理
if (pDel == pFather->pLeft) {
pFather->pLeft = pDel->pLeft ? pDel->pLeft : pDel->pRight;
}
else {
pFather->pRight = pDel->pLeft ? pDel->pLeft : pDel->pRight;
}
free(pDel);
pDel = NULL;
}
2.3.3 将一棵BST转变为有序的双向链表
不申请新空间,在原树的基础上修改。中序遍历,将树节点拆下来放到双向链表中。
pLeft -> Pre、pRight -> Next,表头、表尾
中序遍历,原表尾下一个是栈顶节点,栈顶节点变成新的表尾
void LinkedList(BinaryTree* pTree, BinaryTree** pHead, BinaryTree** pTail) {
stack<BinaryTree*> s;
while (pTree || !s.empty()) {
while (pTree) {
s.push(pTree);
pTree = pTree->pLeft;
}
if (!s.empty()) {
pTree = s.top();
s.pop();
if (*pHead == NULL) {
*pHead = pTree;
*pTail = pTree;
}
else {
(*pTail)->pRight = pTree;
pTree->pLeft = *pTail;
*pTail = pTree;
}
pTree = pTree->pRight;
}
}
}
三、 平衡树
3.1 二叉平衡搜索树(AVL)
在BST的基础上,树中任意节点,左右子树深度差不超过1
3.1.1 平衡因子(BF)
二叉树上某结点平衡因子定义为:该结点的左子树的深度减去它的右子树的深度
3.1.2 旋转
当程序发现问题节点A后,对其进行旋转
enum colors { BLACK, RED };
typedef struct node {
int nVlaue;
node* pLeft;
node* pRight;
node* pFather;
int nColor;
}RBT;
RBT* pRoot;
3.1.2.1 LL型平衡旋转 右旋
由根A的左子树B的左子树Bl引起的不平衡,对根A进行右旋。以根A的左子树B为中心,将A向右侧折断(向右把A压下去)
1.B的右子树成为A的左子树
2. A成为B的右子树
3. B代替A成为新根
void LL(RBT* pTree) { //RightRotate右旋
if (pTree == NULL || pTree->pLeft == NULL)
return;
RBT* A = pTree;
RBT* B = pTree->pLeft;
RBT* FA = A->pFather;
//B的右子树变成A的左子树
A->pLeft = B->pRight;
if (B->pRight)
B->pRight->pFather = A;
//A成为B的右子树
B->pRight = A;
A->pFather = B;
//B代替A成为子树的新根
if (FA == NULL) //A为根
pRoot = B;
else if (FA->pLeft == A)
FA->pLeft = B;
else FA->pRight = B;
B->pFather = FA;
}
3.1.2.2 RR型平衡旋转 左旋
由根A的右子树B的右子树Br引起的不平衡,对根A进行左旋。以根A的右子树B为中心,将A向左侧折断(向左把A压下去)
1.B的左子树成为A的右子树
2. A成为B的左子树
3. B代替A成为新根
void RR(RBT* pTree) { //LeftRotate左旋
if (pTree == NULL || pTree->pLeft == NULL)
return;
RBT* A = pTree;
RBT* B = pTree->pRight;
RBT* FA = A->pFather;
//B的左子树成为A的右子树
A->pRight = B->pLeft;
if (B->pLeft)
B->pLeft->pFather = A;
//A成为B的左子树
B->pLeft = A;
A->pFather = B;
//B代替A成为子树的新根
if (FA == NULL) //A为根
pRoot = B;
else if (FA->pLeft == A)
FA->pLeft = B;
else FA->pRight = B;
B->pFather = FA;
}
左旋右旋互为还原操作
3.1.2.3 LR型平衡旋转 先左旋再右旋
对B进行左旋,再对A进行右旋
3.1.2.4 RL型平衡旋转 先右旋再左旋
对B进行右旋,再对A进行左旋
3.2 红黑树(RBT)
基于BST的带有标记的树,通过红黑标记使得两侧数据更加均衡
满足以下5条性质:
1. 红黑树中每个节点,不是红的(R)就是黑B的(B)
2. 根节点必须是黑色B的
3. 空节点被称为终端节点,被认为是黑色的(黑哨兵)
4. 不允许两个红节点为父子关系
5. 从任意节点向下(孩子方向)出发,他所能到达的各终端节点的路径上的黑节点数目相同
结论:
1. 不会有一条路径长度超过其他路径长度的两倍
2. 红黑树的增删查的复杂度都为O(logn)
3.2.1 红黑树的添加
新加节点初始颜色为红色(对树的影响小,只影响该条路径)
步骤:
(一)查找父亲节点
(二)树的情况
一、空树,新节点Z为root,变黑色B
二、非空树
(三)父节点的颜色
(1) 父节点为黑色B,新节点Z直接放入
(2) 父节点为红色R
(父节点要变黑,但是会导致新路径变长,所以爷爷节点要变红,对于叔节点进行讨论)
(四)叔叔节点的颜色
1. 叔叔节点是红色,父节点变黑色为B 叔节点变黑色B 爷节点变红色R
爷爷节点作为新的操作节点Z,重新讨论(注意爷爷节点为根的情况)
2.叔叔是黑色(此时叔叔路径变少,要将新路径节点转移给叔路径——旋转)
(五)父节点的方向
1> 父是爷节点左
a. 新节点Z是父节点的右,父为新操作节点Z,Z为旋转点,左旋 LR
b.新节点Z是父节点的左,父变黑B,爷变红R,爷为旋转点,右旋 LL
2> 父为爷节点右
a. 新节点是父节点的左,父为新操作节点Z,Z为旋转点,右旋 RL
b. 新节点是父节点的右,父变黑B,爷变红R,爷为旋转点,左旋 RR
RBT* Search(RBT* pTree, int num) {
if (pTree == NULL)
return NULL;
while (pTree) {
if (pTree->nVlaue > num) {
//左子树
if (pTree->pLeft == NULL)
return pTree;
pTree = pTree->pLeft;
}
else if (pTree->nVlaue < num) {
//右子树
if (pTree->pRight == NULL)
return pTree;
pTree = pTree->pRight;
}
else {
//数据错误
printf("Data error:%d\n", num);
exit(1);
}
}
}
RBT* GetUncle(RBT* pNode) {
if (pNode == pNode->pFather->pLeft)
return pNode->pFather->pRight;
else return pNode->pFather->pLeft;
}
void AddNode(RBT* pTree, int num) {
//查找
RBT* pNode = Search(pTree, num);
//节点申请
RBT* pTemp = (RBT*)malloc(sizeof(RBT));
pTemp->nVlaue = num;
pTemp->nColor = RED;
pTemp->pFather = pNode;
pTemp->pLeft = NULL;
pTemp->pRight = NULL;
//空树
if (pNode == NULL) {
pRoot = pTemp;
pRoot->nColor = BLACK;
return;
}
//非空树
//连接
if (pNode->nVlaue > num) {
//左侧
pNode->pLeft = pTemp;
}
else {
//右侧
pNode->pRight = pTemp;
}
//父亲黑色
if (pNode->nColor == BLACK) {
return;
}
//父亲红色
RBT* pGrandFather = NULL;
RBT* pUncle = NULL;
while (pNode->nColor == RED) {
pGrandFather = pNode->pFather;
pUncle = GetUncle(pNode);
//叔叔红色
if (pUncle != NULL && pUncle->nColor == RED) {
pUncle->nColor = BLACK;
pNode->nColor = BLACK;
pGrandFather->nColor = RED;
pTemp = pGrandFather;
pNode = pTemp->pFather;
//根
if (pNode == NULL) {
pRoot->nColor = BLACK;
break;
}
continue;
}
//叔叔黑色
if (pUncle == NULL || pUncle->nColor == BLACK) {
//父亲在爷爷的左边
if (pNode == pGrandFather->pLeft) {
//新节点在父亲右侧
if (pTemp == pNode->pRight) {
pTemp = pNode;
RR(pTemp);
pNode = pTemp->pFather;
}
//新节点是父亲的左侧
if (pTemp == pNode->pLeft) {
pNode->nColor = BLACK;
pGrandFather->nColor = RED;
LL(pGrandFather);
break;
}
}
//父亲是爷爷的右侧
if (pNode == pGrandFather->pRight) {
//新节点是父亲的左侧
if (pTemp == pNode->pLeft) {
pTemp = pNode;
LL(pTemp);
pNode = pTemp->pFather;
}
//新节点是父亲的右侧
if (pTemp == pNode->pRight) {
pNode->nColor = BLACK;
pGrandFather->nColor = RED;
RR(pGrandFather);
break;
}
}
}
}
}
void CreateRBT(int arr[], int len) {
if (arr == NULL || len <= 0)
return;
for (int i = 0; i < len; i++) {
AddNode(pRoot, arr[i]);
}
}
例:当加入新节点4时,父5为红(2)树8为红1.:
父5 叔8变黑,爷7变红,此时叔8路径少一个黑,需要传递
爷7为新操作节点,此时父2为红(2),叔14为黑2.:(LR)
父是爷左1> 新7是父的右a. :父2为新操作节点,以2为旋转,左旋
此时新的操作节点为2,新2是父的左b. :父7变黑,爷11变红,对爷11右旋
3.2.2 红黑树的删除
BST的删除?度为2节点替换删除左子树最大值或右子树最小值
步骤:
一、查找
二、被删除节点Z有几个孩子
度为2要转变(左子树最大值或右子树最小值),统一为1或0的情况
三、对被删除节点Z(真正的删除位置)
(一)被删除节点Z是根
(1) 没有孩子:直接删除
(2) 有一个孩子(一定是红色的):删除Z,孩子变成黑色,成为新根
(二)非根
(1) 红色:直接删除
(2) 黑色:
1> 有1子:孩子变成黑色,爷孙相连,删除Z
2> 无子:删除Z(此时没有节点能补充删除黑色导致的不平衡,先找兄弟借)
1. 兄弟节点是红色的:父变红,兄变黑,以父为旋转点,旋转(看兄在父哪个方向,R左旋,L右旋)
2. 兄弟节点是黑色的:(找侄子借)
a) 两个侄子全黑
1' 父红:父变黑,兄弟变红
2' 父黑:兄变红,父为新操作节点(先将子树调平,再对父节点处理),重新讨论——>非根,黑,无子 【注意父为根的情况】
b) 右侄子红,左侄子黑:
1' 兄是父左:兄变红,右侄子变黑,以兄弟为旋转点,左旋 LR < 变 /
2‘ 兄是父右:父亲颜色给兄弟,父变黑,右侄子变黑,以父为旋转点,左旋 RR \ 变 ^
c) 左侄子红,右侄子黑:
1' 兄是父左:父亲颜色给兄弟,父变黑,左侄子变黑,以父为旋转点,右旋 LL / 变 ^
2' 兄是父右:兄变红,左侄子变黑,以兄为旋转点,右旋 RL > 变 \
void DeleteRBTNode(RBT* pTree, int val) {
RBT* pDel = pTree; //实际删除节点
//查找
while (pDel) {
if (pDel->nVlaue == val)
break;
else if (pDel->nVlaue > val)
pDel = pDel->pLeft;
else if (pDel->nVlaue < val)
pDel = pDel->pRight;
}
//检测
if (pDel == NULL) {
printf("删除失败,节点%d不存在\n", val);
return;
}
//孩子情况,度为2转移
RBT* pMark = NULL; //标记原删除位置
if (pDel->pLeft != NULL && pDel->pRight != NULL) {
pMark = pDel;
//左子树最大值
pDel = pDel->pLeft; //实际删除
while (pDel->pRight != NULL) {
pDel = pDel->pRight;
}
//值覆盖
pMark->nVlaue = pDel->nVlaue;
}
RBT* pFa = pDel->pFather;
//如果实际删除节点是根
if (pFa == NULL) {
//无子
if (pDel->pLeft == NULL && pDel->pRight == NULL) {
free(pDel);
pDel = NULL;
pRoot = NULL;
return;
}
//有子
else {
pRoot = pDel->pLeft ? pDel->pLeft : pDel->pRight;
pRoot->nColor = BLACK;
pRoot->pFather = NULL;
free(pDel);
pDel = NULL;
return;
}
}
//非根,将度为2的情况和度为1、0的情况统一处理
if (pDel->nColor == RED) { //红色,直接删除
if (pDel == pFa->pLeft) {
pFa->pLeft = NULL;
}
else {
pFa->pRight = NULL;
}
free(pDel);
pDel = NULL;
return;
}
else if (pDel->nColor == BLACK) { //黑色
//有1子,孩子变黑,爷孙相连
if (pDel->pLeft || pDel->pRight) {
if (pDel == pFa->pLeft) {
pFa->pLeft = pDel->pLeft ? pDel->pLeft : pDel->pRight;
pFa->pLeft->nColor = BLACK;
pFa->pLeft->pFather = pFa;
}
else {
pFa->pRight = pDel->pLeft ? pDel->pLeft : pDel->pRight;
pFa->pRight->nColor = BLACK;
pFa->pRight->pFather = pFa;
}
free(pDel);
pDel = NULL;
return;
}
//无子删除pDel,再调平
RBT* pBrother = GetUncle(pDel);
if (pDel == pFa->pLeft) {
pFa->pLeft = NULL;
}
else {
pFa->pRight = NULL;
}
free(pDel);
pDel = NULL;
while(1){ //此时兄弟不可能为空
//兄弟是红色
if (pBrother->nColor == RED) {
pFa->nColor = RED;
pBrother->nColor = BLACK;
if (pBrother == pFa->pLeft) {
LL(pFa);
pBrother = pFa->pLeft;
continue;
}
else {
RR(pFa);
pBrother = pFa->pRight;
continue;
}
}
//兄弟是黑色
else if (pBrother->nColor == BLACK) {
if ((pBrother->pLeft == NULL || pBrother->pLeft->nColor == BLACK) &&
(pBrother->pRight == NULL || pBrother->pRight->nColor == BLACK)) { //侄子全黑
if (pFa->nColor == RED) { //父红
pFa->nColor = BLACK;
pBrother->nColor = RED;
break;
}
else if (pFa->nColor == BLACK) { //父黑
pBrother->nColor = RED;
pDel = pFa;
if (pFa->pFather==NULL) {
pRoot = pFa;
return;
}
pFa = pDel->pFather;
pBrother = GetUncle(pDel);
continue;
}
}
if (pBrother->pRight && pBrother->pRight->nColor == RED ) { //右红左黑
if (pBrother == pFa->pLeft) { //LR
pBrother->nColor = RED;
pBrother->pRight->nColor = BLACK;
RR(pBrother);
pBrother = pFa->pLeft;
continue;
}
else if (pBrother == pFa->pRight) { //RR
pBrother->nColor = pFa->nColor;
pFa->nColor = BLACK;
pBrother->pRight->nColor = BLACK;
RR(pFa);
break;
}
}
if (pBrother->pLeft && pBrother->pLeft->nColor == RED ) { //左红右黑
if (pBrother == pFa->pLeft) { //LL
pBrother->nColor = pFa->nColor;
pFa->nColor = BLACK;
pBrother->pLeft->nColor = BLACK;
LL(pFa);
break;
}
else if (pBrother == pFa->pRight) { //RL
pBrother->nColor = RED;
pBrother->pLeft->nColor = BLACK;
LL(pBrother);
pBrother = pFa->pRight;
continue;
}
}
}
}
}
}
例: 删1
父黑,兄红:23变红,35变黑,以23为旋转点,左旋
此时不平衡,需要借,侄子全黑,找父亲借,父黑
23变黑,28变红
删23:
兄黑,侄子全黑,父黑:(先把父子树调平,再去找别人借)兄44变红,父35为新操作节点
删70:
兄黑,右侄子红:父gei'xion兄60变红,右侄子65变黑,以兄为旋转点,左旋
兄黑,左侄子红:父颜色给兄,兄65变红,父68变黑,左侄子60变黑,以父68为旋转点,右旋
最多三次旋转
3.3 多路平衡搜索树(B/B+)
多应用于底层(数据库、磁盘),面对较大的、存放在外存储器上的文件的算法
一个节点具有多个数据域和多个指针,相比RBT要大的多
3.3.1 B-Tree(B树)
对于一棵M阶B-Tree,满足以下条件:
1、一个节点最多有M个指针(M叉)
2、每个节点内最多有M-1条记录(Key-Data)
3、根节点内记录最少为1
4、其他节点内记录数>=ceil(M/2)-1
5、每个节点内记录的索引值key,从左至右从小到大有序
6、每个记录的索引值,大于等于左子树,小于等于右子树
3.3.1.1 B-Tree的添加
裂变:中间记录上移至父亲层,左右记录分别成为其左右子树
新记录会添加到叶子节点中,再从下往上裂变
对于一棵M阶B-Tree,步骤:
(1)将记录放入叶子
(2)讨论节点内记录的个数
① 节点个数<=M-1,结束
② 节点个数>M-1,裂变,讨论父亲层记录个数
3.3.1.2 B-Tree的删除
对于一棵M阶B-Tree,步骤:
(1)查找,若为非叶子节点转换成叶子节点,删除对应的记录
(2)讨论节点内记录的个数
① >=ceil(M/2)-1,结束
② <ceil(M/2)-1,节点内记录不满足条件,需要借一条记录来平衡。看兄弟节点内记录个数
1> 兄弟节点记录数 > ceil(M/2)-1,兄弟记录上移,父亲记录下移至当前节点
2> 兄弟节点记录数 = ceil(M/2)-1,父亲记录下移,与当前节点和兄弟节点合并成一个新结点,讨论父亲节点内记录个数
3.3.2 B+-Tree(B+树)
对于一棵M阶B+-Tree,满足以下条件:
(1)结点分为两种:索引结点/内部结点(Key)、叶子结点(记录和指向兄弟的指针)
(2)一个结点最多有M叉
(3)一个结点内索引/记录个数<=M-1
(4)根结点既可以是叶子结点,也可以是索引结点
(5)根结点内索引/记录个数>=1
(6)其他结点内索引/记录个数>=ceil(M/2)-1
(7)每个结点内索引值,从左至右从小到大有序
(8)每个结点内索引值,大于等于左子树,小于等于右子树
(9)相邻的叶子结点之间,有指针从左向右指向相邻的叶子结点
3.3.2.1 B+-Tree的添加
叶子结点裂变:前M/2个结点作为左侧,剩余为右侧,第M/2+1复制一份到父亲层
索引结点裂变:与B-Tree一样
(1)当前记录放入叶子结点
(2)讨论当前结点内记录个数
① <=M-1,完成
② >M-1,前M/2个记录成为左节点,剩余记录成为右结点,第M/2+1个记录的索引复制一份至父亲层
③ 讨论父亲层索引个数
1> <=M-1,完成
2> >M-1 裂变,中间索引上移至父亲层,左侧索引为左子树,右侧索引为右子树。讨论父亲层索引个数
3.3.2.2 B+-Tree的删除
(1)找到记录,删除
(2)讨论叶子结点记录个数
① >=ceil(M/2)-1,完成
② <ceil(M/2)-1,看当前兄弟结点记录个数
1> >ceil(M/2)-1,兄弟移动一个记录至当前结点,更新父亲索引值
2> =ceil(M/2)-1,兄弟结点与当前结点合并,删除当前父亲索引。
讨论父亲结点索引个数
1)>=ceil(M/2)-1,完成
2)<ceil(M/2)-1,看兄弟结点索引个数
a. >ceil(M/2)-1,父亲索引下移至当前结点,兄弟结点上移一个至父亲层
b. =ceil(M/2)-1,父亲索引下移至当前结点,和兄弟结点合并成新节点。讨论父亲层索引个数
【数据库】mysql索引底层数据结构原理(为什么采用B+树而不采用链表、BST、AVL)_索引为什么用b+树不用列表链表-CSDN博客
对于一个N个节点的M阶B树,深度为logMN。搜索效率为1~logMN,B+树为logMN
B+树链表结构的应用:范围查找(B树需要跨层访问,占用很多无用的内存)
3.3.2.3 B-Tree和B+-Tree的区别
四、哈夫曼树
为了进行哈夫曼编码,实现无损压缩和恢复
这棵树的带权路径长度WPL:W1L1+W2L2+...+WnLn
WPL最小的被称为最优二叉树(哈夫曼树)
4.1 构建哈夫曼树
1、排序
2、拿两个min,构成新节点
3、放回序列
重复直到只剩一个
4.2 哈夫曼编码
左侧边放0右侧放1
无前缀码
五、字典树 TrieTree
是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串。
Trie树本质,利用字符串之间的公共前缀,将重复的前缀合并在一起。
功能:查找、计数、排序
结构体:①指针数组*[256](哈希的思想,按字典序分组)
②单词末尾标记(计数器)
步骤:
(1)创建根结点
(2)添加单词
1> 每个字符对应一个指针
结点为空:创建新节点
结点非空:转到相应的子节点
2> 末尾标记
#include <iostream>
using namespace std;
typedef struct node {
int cnt;
node* next[26];
string str;
node() {
cnt = 0;
for (int i = 0; i < 26; i++)
next[i] = nullptr;
}
}TrieTree;
void add(TrieTree* pTree, string word) {
for (int i = 0; i < word.length(); i++) {
char c = word[i];
if (pTree->next[c - 'a'] == nullptr)
pTree->next[c - 'a'] = new TrieTree;
pTree = pTree->next[c - 'a'];
}
pTree->cnt++;
pTree->str = word;
}
int search(TrieTree* pTree, string word) {
for (int i = 0; i < word.length(); i++) {
char c = word[i];
if (pTree->next[c - 'a'] == nullptr)
return false;
pTree = pTree->next[c - 'a'];
}
return pTree->cnt;
}
void Preorder(TrieTree* pTree) {
if (pTree == NULL)
return;
if (pTree->cnt != 0)
cout << pTree->str << endl;
for (int i = 0; i < 26; i++) {
Preorder(pTree->next[i]);
}
}
int main() {
TrieTree* tree = new TrieTree;
string str[] = { "hello","asda","aab","abuno","sdaad" };
for (int i = 0; i < 5; i++) {
add(tree, str[i]);
}
cout << search(tree, "asda") << endl;
Preorder(tree);
}
字典树不能为空树
应用:搜索引擎