此文章基于C语言实现的,参考书籍:《数据结构(C语言版)》——清华大学出版社 严蔚敏 吴伟民
树的一些操作
1.左旋
(1)对A进行左旋
其右子节点转为自己父亲,这个右子节点继承A的父亲
右子节点的左子节点变为A的右子节点
左旋后其中序遍历不变
(2)算法实现过程:
①新建节点,并赋值为当前节点的值
②设置新建节点的左子节点为当前节点的左子节点
③设置新建节点的右子节点为当前节点的右子节点的左子节点
④把当前节点的值换为其右子节点的值
⑤把当前节点的右子树设为右子节点的右子节点
⑥把当前节点的左子树设为新节点
2.右旋
对A进行右旋
其左子节点转为自己父亲,这个左子节点继承A的父亲
左子节点的右子节点变为A的左子节点
** 右旋后其右根左遍历不变 **
①新建节点,并赋值为当前节点的值
②设置新建节点的右子节点为当前节点的右子节点
③设置新建节点的左子节点为当前节点的左子节点的右子节点
④把当前节点的值换为其左子节点的值
⑤把当前节点的左子节点设为左子节点的左子节点
⑥把当前节点的右子节点设为新节点
一、树
6.1 树的定义和基本术语
1.树和子树:树(Tree)是n(n>=0)各界底单的有限集。在任意一颗非空树中 :(1)有且只有一个特定的称为根的(Root)的结点;(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1,T2,…Tm,其中每一个集合本身又是一棵树,并且称为子树。
2.度:节点拥有的子树数,称为结点的度。
3.叶子、终端结点:度为0的结点。
4.非终端结点、分支结点:度不为0的结点。
5.内部结点:除根结点以外,分支结点也称为内部结点。
6.孩子、双亲:结点的子树的根称为该节点的孩子,该结点称为孩子的双亲。
7.兄弟:同一个双亲的不同孩子,之间互相为兄弟。
8.祖先、子孙:是从根到该结点所经分支上的所有结点,相应的以某结点为根的子树中的任一结点都是该结点的子孙。
9.堂兄弟:两结点的双亲不为同一结点,且其在同一层,这样的两个结点之间互为堂兄弟。
10.深度(Depth)、高度:树中结点的最大层次称为树的深度或高度。
11.有序树、无序树:树中结点的各子树看成从左至右是有次序的(既不能互换),则称该树为有序树,否则称为无序树。
12.森立(Forest):是m(m>=0)棵互不相交的树的集合。
13.二叉树(Binary Tree)特点是每个结点至多只有两棵子树,且二叉树的子树有左右之分,次序不能颠倒,即为有序树。
14.满二叉树:一棵深度为k且有2k-1个结点的二叉树为满二叉树。
6.2二叉树
6.3.1二叉树的定义
二叉树(Binary Tree)特点是每个结点至多只有两棵子树,且二叉树的子树有左右之分,次序不能颠倒,即为有序树。
6.3.2二叉树的性质
1.性质1:在二叉树的第i层上至多有2i-1个结点。
2.性质2:深度为k的二叉树最多有2k-1个结点(k>=1)
3.性质3:对任何一棵二叉树T,如果其终端节点数为n0,度为2的结点数为n2,则n0=n2+1。
满二叉树:一棵深度为k且有2k-1个结点的二叉树为满二叉树。
完全二叉树:当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称为完全二叉树。
(1)性质1:叶子结点只可能在层次最大的两层上出现。
(2)性质2:对任一结点,其右分支下的子孙的最大层次为l,则其左分支下的子孙的最大层次必为l或l+1。
4.性质4:具有n个结点的完全二叉树的深度为⌊log2n⌋+1。
5.==性质5:==如果对一棵有n个节点的完全二叉树(其深度为⌊log2n⌋+1)的结点按层序编号(从第1层到第⌊log2n⌋+1层,从左到右),则对任一结点i(1<=i<=n),有
==(1)==如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲PARENT(i)是结点⌊i/2⌋。
==(2)==如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子LCHILD(i)是结点2i。
==(3)==如果2i+1>n,则结点i无右孩子;否则其右孩子RCHILD(i)是结点2i+1。
6.2.3二叉树的存储结构
1.顺序存储结构
就是用数组来存储,按照满二叉树的编号,如果二叉树不满的话,空的结点相对应的数组元素就空出来。那么就可能造成很多空的但是却占用的空间,所以顺序存储结构仅适用于完全二叉树。
2.链式存储结构
typedef struct BiTNode{
TelemType data;
struct BiTNode *lchild,*rchild;
struct BiTNode *parent;//指向双亲的指针,三叉链表
}BiTNode,*BiTree;
6.3遍历二叉树和线索二叉树
6.3.1遍历二叉树
根在哪儿就是什么序,好记一些
1.先序:根左右
2.中序:左跟右
3.后序:左右根
算法6.1
//递归算法
Status PreOrderTraverse(BiTree T,Status (*Visit)(TElemType e)){//这里的Visit是指针函数,方便调用不用的函数,引用时用Visit=函数名
//采用二叉链表存储结构,Visit是对数据元素操作的应用函数
//先序遍历二叉树T的递归算法,对每个数据元素调用函数Visit
//简单的Visit函数是:
// Status PrintElement(TElemType e){
// printf(e);
// return OK;
//}
if(T){ //如果T不为空的话,进if否则return ERROR;
if(Visit(T->data)) //如果T->data不为空,输出
if(PreOrderTraverse(T->lchild,Visit)) //进入左孩子的递归
if(PreOrderTraverse(T->rchild,Visit)) return OK; //
return ERROR;
}else return OK;
}//PreOrderTraverse
如图所示的二叉树表示下述表达式
a+b*(c-d)-e/f
若先序遍历此二叉树,按访问节点的先后次序将结点排列起来,可得到二叉树的
先序序列为: -+ab-cd/ef
中序遍历为: a+bc-d-e/f
后序遍历为: abcd-*+ef/-
可以看出上述三个表达式恰好为表达式的前缀表示(波兰式)、中缀表示和后缀表示(逆波兰式)。
如果在算法中暂且抹去和递归无关的Vist语句,则三个遍历算法完全相同。那么从递归的角度来看先中后序遍历也是完全相同的。
算法 6.2
//非递归中序
Status InOrderTraverse(BiTree T,Status (*Visit)(TElemType e)){
//采用二叉链表结构,Visit是对数据元素操作的应用函数
//中序遍历二叉树T的非递归算法,对每个数据元素调用函数Visit
InitStack(S);
Push(S,T); //根指针进栈
while(!StackEmpty(S)){
while(GetTop(S,p) && p) Push(S,p->lchild); //向左走到尽头,GetTop()是将栈S的栈顶元素拿出来但是不弹出,这句是如果s的栈顶不为空,将栈顶元素的左孩子压入栈。
Pop(S,p); //空指针退栈,最后一个一定是空才到这里,所以弹出一个就是把空指针退出来
if(!StackEmpty(S)){ //访问结点,向右一步
Pop(S,p);
if(!Visit(p->data)) return ERROR; //如果最左孩子,就是应该第一个输出的元素为空,那么就错误
Push(S,p->rchild);
}//if
}//while
return OK;
}//InOrderTraverse
算法6.3
//非递归中序
Status InOrderTraverse(BiTree T,Status (*Visit)(TElemType e)){
//采用二叉链表存储结构,Visit是对数据元素操作的应用函数
//中序遍历二叉树T的非递归算法,对每个数据元素调用Visit
InitStack(S);
p = T;
while(p || !StackEmpty(S)) {
if(p) {
Push(S,p);
p = p -> lchild; //根指针进栈,遍历左子树
}//if
else{ //根指针退栈,访问根结点,遍历右子树
Pop(S,p);
if(!Visit(p->data)) return ERROR;
p = p->rchild;
}//else
}//while
return OK;
}//InOrderTraverse
算法6.4
//先序创造二叉树(遍历中创建结点,结合上面的算法)
Status CreateBiTree(BiTree &T){
//按先序次序输入二叉树中的结点的值(一个字符),空格字符表示空数
//构造二叉链表表示的二叉树T。
scanf(&ch);
if(ch == '#') T = NULL;
else{
if(!(T = (BiTNode *)malloc(sizeof(BiTNode)))) exit(OVERFLOW);
T->date = ch;
CreateBiTree(T->lchild);
CreateBiTree(T->rchild);
}//else
return OK;
}//CreateBiTree
6.3.2 线索二叉树
遍历二叉树的本质是以一定规则将二叉树中结点排列成一个线性序列,得到二叉树中结点的先序序列或者中序序列或者后序序列。实质上是对一个非线性结构进行线性化操作,使每一个结点(除了最后一个和第一个)在这些线性序列中有且只有一个=直接前驱和直接后驱。
中序线索二叉树会添加一个thrt结点,其做指针指向二叉树的头结点(这个头结点使得,可以有指针域可以指向序列的最后一个结点,形成双向链表)
结构:
struct BiTree{
ElemType data;
struct BiTree *lchild,*rchild;
int LTag,RTag; //用作标志为,如果标志位为0则表示所对应的lchild或者rchild表示的是结点的左孩子,
//如果标志位为1对应的指针表示结点的前驱或者后驱
}*PBiTree; //这里是定义了一个BiTree类型的指针PBiTree
令二叉树中序序列中的第一个结点的lchild域指针和最后一个结点的rchild域的指针均指向头结点。这好比为二叉树建立了一个双向线索链表。
算法6.5
//以双向线索俩表为存储结构时对二叉树进行遍历的算法
typedef enum PointerTag( Link,Thread ); //Link==0;指针 Thread==1;线索
Status InOrderTraverse_Thr(BiThrTree T,Status(*Visit)(TElemType e)){
//T指向头结点,头结点的左链lchild指向根节点,可参见线索化算法
//中序遍历二叉线索树T的非递归算法,对每个数据元素都调用函数Visit
p = T->lchild; //p指向根节点
while(p != T){
while(p->LTag==Link)
p = p->lchild;
if(!Visit(p->data)) return ERROR; //表明结构建立时有问题,并且调用了Visit
while(p->RTag==Thread && p->rchild!=T){
p = p->rchild;
Visit(p->data); //访问后继结点
}
}//while
return OK;
}//InOrderTraverse_Thr
算法6.6
//中序遍历建立线索化链表的算法
//先建立头结点,指向根结点,然后将当
Status InOrderThreading(BiThrTree & Thrt,BiThrTree T){
//中序遍历二叉树T,并将其中序线索化,Thrt指向头结点。
if(!(Thrt = (BiThrTree)malloc(sizeof(BiThrNode)))) exit(OVERFLOW);
Thrt->LTag = Link;
Thrt->RTag = Thread; //建头结点
Thrt->rchild = Thrt; //右指针回指
if(!T) Thrt->lchild = Thrt; //若二叉树空,则左指针回指
else{
Thrt->lchild = T;
pre = Thrt; //pre是一个二叉树型指针,用来做temp
InThreading(T); //中序遍历进行中序线索化
pre->rchild = Thrt;
pre->RTag = Thread; //最后一个结点线索化
Thrt->rchild = pre;
}//else
return OK;
}//InOrderThreading
void InThreading(BiThrTree p){
if(p){
InThreading(p->lchild); //左孩子线索化
if(!p->lchild) {
p->LTag = Thread;
p->lchild = pre;
}//前驱线索
if(!pre->rchild){
pre->RTag = Thread;
pre->rchild = p;
}//后继线索
pre = p;
inThreading(p->rchild); //右子树线索化
}//if
}InThreading
6.4 树和森林
6.4.1 树的存储结构
1.双亲表示法
用一组连续空间存储树的结点,同时在每一个结点中附设一个指示器指示其双亲结点在链表中的位置。
//--------树的双亲表存储表示-------------
#define MAX_TREE_SIZE 100
typedef struct PTNode{ //结点结构
TElemType data;
int parent; //双亲位置域
}PTNode;
typedef struct{ //树结构
PTNode node[MAX_TREE_SIZE];
int r,n; //根的位置和结点数
}PTree;
这个算法利用除根以外的结点双亲唯一的性质,实现PARENT(T,X)可以在常量时间内完成,查找根结点时ROOT(x)也很快。
2.孩子表示法
这种办法是把每个节点的孩子结点排列起来,看成是一个线性表,且以单链表作为存储结构,则n个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表,为了便于查找,可采用顺序存储结构。
//----------树的孩子链表存储表示----------
typedef struct CTNode{ //孩子结点
int child;
struct CTNode *next;
}*childPtr;
typedef struct{
TElemType data;
childPtr firstchild; //孩子链表头指针
}CTBox;
typedef struct{
CTBox nodes[MAX_TREE_SIZE];
int n,r; //结点数和根的位置;
}CTree;
3.孩子兄弟表示法
又称二叉树表示法,或二叉链表表示法。即以二叉链表作树的存储结构。链表中结点的两个链域分别指向该节点的第一个孩子和下一个兄弟结点,分别命名为firstchild域和nextsibling域。
//-----------数的二叉链表(孩子兄弟)存储表示-------------
typedef struct CSNode{
ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree;
若要访问结点x的第i个孩子,则只需要从firstchild域找打第一个孩子结点,然后沿孩子结点的nextsibling域连续走i-1步。如果为每个结点增加一个parent域,则同样能方便的实现PARENT(T,x)操作。
6.4.2 森林与二叉树的转换
给定一棵树可以找到唯一的一棵二叉树与之对应,从物理结构来看,他们的二叉链表是相同的,只是解释不同。
1.森林转换成二叉树
如果F={T1,T2,……,Tm}是森林,则可以按如下规则转换成一棵二叉树B={root,LB,RB}。
(1)若F为空,即m=0,则B为空树;
(2)若F非空,即m不为0,则B的根root即为森林中第一棵树的根ROOT(T1),B的左子树LB是从T1中根结点的自述森林F1={T11,T12,……,T1m1}转换成的二叉树;其右子树RB是从森林F’={T2,……,Tm}转换而成的二叉树。
2.二叉树转换成森林
如果B={root,LB,RB}是一棵二叉树,则可按如下规则转换成森林。
(1)若B为空,则F为空;
(2)若B非空,则F中第一棵树T1的根ROOT(T1)即为二叉树的根root;T1中根结点的子树森林F1是由B的左子树LB转换成的森林;F中除T1之外其余树组成的森林F’={T2,……,Tm}是由B的右子树RB转换而成的森林。
6.4.3 树和森林的遍历
1.先序遍历森林
(1)访问森林中第一棵数的根结点。
(2)先序遍历第一棵树中根结点的子树森林。
(3)先序遍历除去第一棵树之后剩余的树构成的森林;
2.中序遍历森林
(1)中序遍历森林中第一棵树的根结点和子树森林。
(2)访问第一棵树的根结点。
(3)中序遍历除去第一棵树之后剩余的树构成的森林。
树的先根遍历和后根遍历可借用二叉树的先序遍历和中序遍历算法实现。
根左右:其中根左是一棵树,右指向下一棵树
左根右:就实现访问了森林的第一棵树的除了根以外的结点,后访问根的时候再访问这棵树的根(先序顺序提取此棵树)
6.5 树与等价问题
离散数学中等价的关系是 :
设R是集合S的等价关系。对任何x∈S,由[x]R={y|y∈S∧xRy}给出的集合[x]R称为由x∈S生成的一个R等价类。
这样的优点在于,可以以等价类的数目即为需要分配的存储单位,而同一等价类中的程序变量可以分配到同一存储单位,此外花粉等价类的算法思想也可以用于求网络的最小生成树等图的算法中。
划分等价类
集合S有n个元素,m个形如(x,y)(x,y∈S)的等价偶对确定了等价关系R
(1)令S中美一个元素形成一个只含单个成员的子集,记作S1,S2,……,Sn。
(2)重复读入m个偶对,对每个读入的偶对(x,y),判定x,y所属子集。不失一般性,假设x∈Si,y∈Sj,若Si≠Sj,则合并,并置另一个为空。则当m个偶对都被处理后,所有非空子集即为S的R等价类。
6.6 赫夫曼树及其应用
赫夫曼(Huffman)树,又称最优树,是一类带权路径最短的树
6.6.1 最优二叉树(赫夫曼树)
特性:具有n个叶子结点时(即一开始的需要构建的结点数有n个),那么赫夫曼树一共有2n-1个结点
1.基本概念:
(1)路径长度:路径上的分支数目
(2)树的路径长度:是从树根到每一结点的路径长度之和
(3)结点的带权路径长度为从该结点到树根之间的路径长度与结点上权的乘积。
(4)树的带权路径长度:树中所有叶子结点的带权路径长度之和,通常记作WPL=∑nk=1 wklk。
2.如何构建赫夫曼树
(就是把每个结点当做一棵树,找权最小的两棵“树”,结合成新树,直到成为一棵树)
(1)根据给定的n个权值{w1,w2,……,wn}构成n棵二叉树的集合{T1,T2,……,Tm},其中每棵二叉树Ti中只有一个带权为wi的根结点,其左右子树均空
。
(2)在F中选取两棵根结点的权值最小的树,作为左右自述构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和。
(3)在F中删除这两棵树,同时将新得到的二叉树加入F中。
(4)重复(2)和(3),直到F中只含一棵树为止。这棵树就是赫夫曼树。
6.6.2 赫夫曼编码
书P146:大意为发送编码需要尽量短,就可以用不等长编码来缩减编码位数,又不会造成多种释义。
(1)前缀编码:任一个字符的编码都不是另一个字符的编码的前缀,这种编码叫做前缀编码。
(2)特性:有n个结点的赫夫曼树共有2n-1个结点
//-------赫夫曼树编码的存储表示--------
typedef struct{
unsigned int weight;
unsigned int parent,lchild,rchild;
}HTNode,*HuffmanTree; //动态分配数组存储赫夫曼树
typedef char ** HuffmanCode; //动态分配数组存储赫夫曼编码表(char*类别的数组,指针数组)
//-----------求赫夫曼编码的算法--------------
void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,int *w,int n){
//w用来接收n个字符的权值的数组(均>0),构造赫夫曼树HT,并求出n个字符的赫夫曼编码HC。
if(n<=1) return;
m = 2 * n - 1; //需要m个结点,赫夫曼树的特性,结点数
HT = (HuffmanTree)malloc((m+1) * sizeof(HTNode)); //0号单元没有用
for(p=HT+1,i=1;i<=n;++i,++p,++w) *p = {*w,0,0,0}; //初始化有数据的n个结点,及诶暗中的weight属性赋值*w,即权值
for(;i<=m;++i,++p) *p = {0,0,0,0}; //初始化剩下没有数据的结点
for(i=n+1;i<=m;++i){ //建赫夫曼树
Select(HT,i-1,s1,s2); //在HT[1. .i-1]选择parent为0且weight最小的两个结点,其序号分别为s1和s2.
HT[s1].parent = i;
HT[s2].parent = i;
HT[i].lchild = s1;
HT[i].rchild = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
//----从叶子到根逆向求每个字符的赫夫曼码----
HC = (HuffmanCode)malloc((n+1)*sizeof(char *)); //分配n个字符编码的头指针向量
cd = (char *)malloc(n*sizeof(char *)); //分配求编码的工作空间
cd[n-1] = "\0"; //编码结束符
for(i=1;i<=n;++i){ //逐个字符求赫夫曼码
start = n-1; //编码结束符位置
for(c=i,f=HT[i].parent;f!=0;c=f,f=HT[f].parent){//从叶子到根逆向求编码,c是当前
if(HT[f].lchild==c) cd[--start] = "0";
else cd[--start] = "1"; //这里就是如果是左孩子就是0,右孩子就是1
}//for
HC[i] = (char *)malloc((n-start)*sizeof(char)); //为第i个字符编码分配空间
strcpy(HC[i],&cd[start]); //从cd复制编码(串)到BC
}
free(cd); //释放工作空间
}//HuffanCoding
二、 平衡二叉树
参考链接:算法其实很简单—平衡二叉树的构建
2.1 特点
是各个节点的平衡因子不大于1的排序二叉树
(1)左右子树深度的绝对值不超过1
(2)左右子树仍为平衡二叉树
平衡因子:节点的左子树深度-右子树深度
2.2 创建的插入逻辑过程和代码实现
按照二叉排序树中创建顺序插入(注意额外标注根节点)
(1)若树为空,插入节点为根节点
(2)递归:插入节点和当前节点比较,如果大则去和右子节点去比较,小则去和左子节点比较,知道找到相等的放弃插入或者找到空节点成功插入。递归过程中,每次递归都在栈中压入一个对当前节点的平衡因子判断,这样,在成功插入节点后会沿着树向上去检查每一个路径上的节点,进行旋转。
具体旋转过程:
这里利用左右子树高度去判断是左左,左右,右右还是右左
即从三角型转为直线型之后再进行旋转。(每一次旋转(左左)一定会降低以这个节点为根节点的树的平衡因子$$$)
// 如果当前节点的右节点比左节点的高度 > 1,则进行左旋转
// 如果当前节点的左节点比右节点的高度 > 1,则进行右旋转
if (rightHeight() - leftHeight() > 1) {
// 如果当前节点的右子节点的左子树高度大于右子树的高度,则先进行右旋转
if (right != null && right.leftHeight() > right.rightHeight()) {
right.leftRotate();
}
leftRotate();
} else if (leftHeight() - rightHeight() > 1) {
// 如果当前节点的左子节点的右子树高度大于左子树的高度,则先进行左旋转
if (left != null && left.rightHeight() > left.leftHeight()) {
left.leftRotate();
}
rightRotate();
}
2.3删除的逻辑过程和代码实现
有要删除的节点是要先去在树中查找这个节点,置为当前节点
父亲节点的获取可以从头开始遍历,也可以在遍历时用一个对象存父亲节点,也可以用双向链表原理存储节点
(1)如果根节点的左右子节点为空,则将root直接置为null。
(2)当查找到后,如果当前节点(就是要删除的节点,下面不再赘述)的左右子节点都为空,则直接将此节点的父亲节点的相对应节点置为null。
(3)如果当前节点的左右子节点都不为空,则将当前节点用其右子树中最小节点值赋值(找最右的那个节点的值),然后删除那个节点(注意这里需要重复(1)(2)(3)过程)
(4)如果左子节点是空的,则将当前节点的值赋值为其右子节点的值,之后由于删除当前节点前这就是一个平衡二叉树,所以其右子节点不会有子节点了(平衡因子问题),就将当前节点的右子节点置为null;
2.4 完整测试类Java实现
package AVL_test;
/**
* @author 浪子傑
* @version 1.0
* @date 2020/6/2
*/
public class AVLTreeDemo {
public static void main(String[] args) {
// int[] arr = {4, 3, 6, 5, 7, 8};
// int[] arr = {10, 12, 8, 9, 7, 6};
int[] arr = {10, 11, 7, 6, 8, 9};
AVLTree avlTree = new AVLTree();
for (int i : arr) {
avlTree.add(new Node(i));
}
System.out.println(avlTree.root.height());
System.out.println(avlTree.root.leftHeight());
System.out.println(avlTree.root.rightHeight());
}
}
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
/**
* 左旋转
*/
public void leftRotate() {
// 创建一个新的节点,值为当前节点的值
Node newNode = new Node(value);
// 把新节点的左子树设为当前节点的左子树
newNode.left = left;
// 把新节点的右子树设为当前右子节点的左子树
newNode.right = right.left;
// 把当前节点的值换位右子节点的值
this.value = right.value;
// 把当前节点的右子树设为右子节点的右子树
right = right.right;
// 把当前节点的左子树设为新节点
left = newNode;
}
/**
* 右旋转
*/
public void rightRotate() {
// 创建一个新的节点并将当前值赋给新节点
Node newNode = new Node(value);
// 把新节点的右子树设为当前节点的右子树
newNode.right = right;
// 把新节点的左子树设为当前节点左子节点的右子树
newNode.left = left.right;
// 把当前值设为左子树节点的值
this.value = left.value;
// 把当前节点的左子树设为左子节点的左子树
this.left = left.left;
// 把当前节点的右子树设为新节点
this.right = newNode;
}
/**
* 返回右子树的高度
*
* @return
*/
public int rightHeight() {
if (right == null) {
return 0;
} else {
return right.height();
}
}
/**
* 返回左子树的高度
*
* @return
*/
public int leftHeight() {
if (left == null) {
return 0;
} else {
return left.height();
}
}
/**
* 获取以当前节点为根节点的树的高度
*递归每次返回左右子树的深度最大值
* @return
*/
public int height() {
return Math.max(left == null ? 0 : left.height(), right == null ? 0 : right.height()) + 1;
}
/**
* 查询要删除的节点
*
* @param value
* @return
*/
public Node search(int value) {
// 如果要查找的值==当前节点的值,返回当前节点
// 如果要查找的值 < 当前节点,则向该节点的左子树查找
// 否则向该节点的右子树查找
if (this.value == value) {
return this;
} else if (value < this.value) {
// 如果该节点的左子节点为null,直接返回null
if (this.left == null) {
return null;
}
return this.left.search(value);
} else {
if (this.right == null) {
return null;
}
return this.right.search(value);
}
}
/**
* 查找要删除节点的父节点
*
* @param value
* @return
*/
public Node searchParent(int value) {
// 如果当前节点的子节点不为空,并且当前节点子节点的值 == value,则返回当前节点
// 如果当前节点的左子节点不为null,并且当前节点的值 > value,则向该节点的左子节点遍历
// 如果当前节点的右子节点不为null,并且当前节点的值不> value,则向该节点的右子节点遍历
// 否则没有找到,返回null
if ((this.left != null && this.left.value == value) ||
(this.right != null && this.right.value == value)) {
return this;
} else if (this.left != null && this.value > value) {
return this.left.searchParent(value);
} else if (this.right != null && this.value <= value) {
return this.right.searchParent(value);
} else {
return null;
}
}
/**
* 添加节点
*
* @param node
*/
public void add(Node node) {
// 如果该节点为null,直接返回
if (node == null) {
return;
}
// add的节点小于当前节点,说明应该在当前节点的左边
// 否则放在当前节点的右边
if (node.value < this.value) {
// 如果当前节点的左边没有子节点,则直接把add节点放在当前节点的左子节点
// 否则的话,遍历当前左子节点,直到找到合适位置
if (this.left == null) {
this.left = node;
} else {
this.left.add(node);
}
} else {
if (this.right == null) {
this.right = node;
} else {
this.right.add(node);
}
}
// 如果当前节点的右节点比左节点的高度 > 1,则进行左旋转
// 如果当前节点的左节点比右节点的高度 > 1,则进行右旋转
if (rightHeight() - leftHeight() > 1) {
// 如果当前节点的右子节点的左子树高度大于右子树的高度,则先进行右旋转
if (right != null && right.leftHeight() > right.rightHeight()) {
right.leftRotate();
}
leftRotate();
} else if (leftHeight() - rightHeight() > 1) {
// 如果当前节点的左子节点的右子树高度大于左子树的高度,则先进行左旋转
if (left != null && left.rightHeight() > left.leftHeight()) {
left.leftRotate();
}
rightRotate();
}
}
/**
* 中序遍历
*/
public void middleOrder() {
if (this.left != null) {
this.left.middleOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.middleOrder();
}
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}
class AVLTree {
Node root;
/**
* 删除节点
*
* @param value
*/
public void delete(int value) {
// 如果父节点为null,直接返回
if (root == null) {
return;
} else {
Node targetNode = search(value);
// 如果要删除的节点为null,直接返回
if (targetNode == null) {
return;
}
// 如果父节点的左右节点都为null,说明只有一个节点,直接将root设为null即可
if (root.left == null && root.right == null) {
root = null;
return;
}
Node parentNode = searchParent(value);
// 如果要删除节点的左右节点都为null,说明该要删除的节点为子节点
if (targetNode.left == null && targetNode.right == null) {
// 如果父节点的左子节点不为null并且是要删除的节点,则将父节点的左子节点设为null
// 如果父节点的右子节点不为null并且是要删除的节点,则将父节点的右子节点设为null
if (parentNode.left != null && parentNode.left.value == value) {
parentNode.left = null;
} else if (parentNode.right != null && parentNode.right.value == value) {
parentNode.right = null;
}
} else if (targetNode.left != null && targetNode.right != null) {
// 如果要删除的左右子节点都不为null,则查找要删除节点右子节点的最小值,删除最小节点并将值赋给要删除节点(这里符合排序二叉树的规定,即右边最小的值一定是大于当前节点且小于当前节点的右子节点)
int treeMin = delRightTreeMin(targetNode.right);
System.out.println("最小的为---" + treeMin);
targetNode.value = treeMin;
} else {
// 如果要删除的节点左右子节点有一个为null
// 如果要删除的子节点为root节点
if (parentNode == null) {
// 如果左子节点不为null,则将左子节点赋给root
// 否则将右子节点赋给root
if (targetNode.left != null) {
root = targetNode.left;
} else {
root = targetNode.right;
}
} else if (parentNode.left.value == value) {//$
// 如果要删除的节点为parentNode的左子节点
// 如果要删除的节点的左子节点不为null,则将parentNode的左子节点指向要删除节点的左子节点
// 否则则指向要删除节点的右子节点
if (targetNode.left != null) {
parentNode.left = targetNode.left;
} else {
parentNode.left = targetNode.right;
}
} else {
if (targetNode.right != null) {
parentNode.right = targetNode.right;
} else {
parentNode.right = targetNode.left;
}
}
}
}
}
/**
* 返回以node节点为根节点的二叉排序树的最小值
*
* @param node
* @return
*/
public int delRightTreeMin(Node node) {
Node target = node;
while (target.left != null) {
target = target.left;
}
delete(target.value);
return target.value;
}
/**
* 查找要删除的节点
*
* @param value
* @return
*/
public Node search(int value) {
if (root == null) {
return null;
} else {
return root.search(value);
}
}
/**
* 查询要删除的父节点
*
* @param value
* @return
*/
public Node searchParent(int value) {
if (root == null) {
return null;
} else {
return root.searchParent(value);
}
}
/**
* 添加子节点
*
* @param node
*/
public void add(Node node) {
if (root == null) {
root = node;
} else {
root.add(node);
}
}
/**
* 中序遍历
*/
public void infixOrder() {
if (root != null) {
root.middleOrder();
} else {
System.out.println("当前root为空");
}
}
}
三、 B树
参考链接:6.3 B树和B+树1
又称为多路平衡查找树,B树中所有节点的孩子节点数的最大值为B树的阶。
一棵m阶树,为满足如下条件的m叉树:
①树中每个节点至多有m棵子树,(即至多有m-1个关键字)
②若根节点不是终端节点,则最少有两棵子树(会有叶子节点作为失败节点做填充)
③除根节点外所有非叶节点至少有⌈m/2⌉棵子树(即⌈m/2⌉-1个关键字)
④非叶节点的结构
⑤所有叶节点都出现同一层上,并不带任何信息
n P0 K1 P1 K2 P2 Kn Pn
n:节点关键字的个数,因为①所以n<=(m-1),n>= ⌈m/2⌉-1,n=叶子节点数-1
Pi(0,1,2…n):指向第i个子树根节点的的指针,pi-1所指向子树的关键字均小于Ki,Pi所指向的子树的关键字均大于Ki
Ki(1,2…n):关键字
1.查找
2.插入
(1)定位
规定定位到最底层非叶子节点中
(2)插入
①若插入后,不破坏m阶二叉树的定义,即插入后几点关键字个数在属于[ ⌈m/2⌉-1,m-1 ],即插入。
②分裂,擦汗入后的节点中间位置(⌈m/2⌉)关键字并入会饿点中,中间节点左侧节点留在原先节点中,右侧节点放入新的节点中,若并入父节点后,父节点关键字数目超出范围,继续向上分裂,知道符合要求为止。
四、 B+树
五、 红黑树
5.1 特点:
(1)根节点必须为黑色
(2)父子不能同为红色
(3)从任一节点出发,到达叶子节点经过的黑色节点一致
5.2 创建
1.插入逻辑过程
插入默认为红色
(1)树内无节点,为根节点(修改颜色为黑色)
(2)父亲节点为黑色的,直接插入(置为默认红色)
(3)父亲为红色(插入后,父亲节点 置为黑色)分为两种情况
①uncle为红——>染黑uncle,染红祖父(递归向上修复)
②uncle为黑或为null——>若为三角型:则对父节点左旋或右旋变为直线型
若为直线型:对父亲节点右旋或左旋(如果为左左则右旋,反之左旋,即左左为父亲为祖父的左子节点,当前节点为父亲节点的左子节点),之后当前节点和祖父互换颜色。
举例如下:
三角型:(这里例子不准确,因为父亲是黑色的时候不用去修复,仅作为例子)
对Parent做左旋,转换为直线型:
之后对当前节点左右旋,之后交换祖父和当前节点的颜色(颜色不对,请注意):
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200818003147352.png#pic_center = 300x)