二叉搜索树:
性质:
- 非空左子树的所有键值小于其根节点的键值;
- 非空右子树的所有键值大于其根节点的键值;
- 它的左右子树都是二叉搜索树
二叉搜索树的操作集:
查找:
由于非递归的执行效率高,因此可将尾递归函数改为迭代函数
递归方式
BinTree Find2(BinTree BT, int Ele) {//Non-recursion非递归
//只要树不是空的就一直找
while (BT)
{
if (Ele > BT->Data) {
BT = BT->Right;
}
else if (Ele < BT->Data) {
BT = BT->Left;
}
else break;//找到了退出
}
return BT;
}
非递归方式
BinTree Find1(BinTree BT, int Ele) {//recursion递归
//如果是空树,返回空值表示失败。
if (BT == NULL) {
printf("查找失败");
return 0;
}
if (Ele > BT->Data) {//如果要找的值比根节点数据大就去右子树找
return Find1(BT->Right, Ele);//传入右子树
}
else if (Ele < BT->Data) {//如果要找的值比根节点数据小就去左子树找
return Find1(BT->Left, Ele);
}
else //Ele==BT->Data
return BT;//表示找到结点,返回这个结点的地址
}
找最大:非递归方式
BinTree FindMax(BinTree BT) {
//用非递归方式执行
while (BT->Right) {
BT = BT->Right;
}
return BT;
}
找最小:递归方式
BinTree FindMin(BinTree BT) {
//递归方式
if (!BT) return NULL;//作为根节点是空的时候返回NULL,非空执行else
else if (!BT->Left) return BT;//如果根节点的左树为空,代表该结点是最左边的树了
else return FindMin(BT->Left);//往做走
}
插入:
BinTree Insert(BinTree BT, int Ele) {
if (!BT) {//如果插入位置为空就生成一个结点
//递归到一个空的结点
BT = InitialTree();
BT->Data = Ele;
return BT;
}
else {//没找到就比较左右树然后进去遍历
if (BT->Data > Ele) {
//插入的元素比结点值小就往左树找
BT->Left = Insert(BT->Left, Ele);
}
else if (BT->Data < Ele) {
//插入的元素比结点值大就往右树找
BT->Right = Insert(BT->Right, Ele);
}
//不然就只剩下相等的情况
return BT;//返回树的地址
}
}
删除:
注意执行IF语句删除时,对左边删除那返回值的要给左边,别漏了。
BinTree Delete(BinTree BT, int Ele) {
BinTree Tempt = NULL;
if (!BT)//如果树空的话
//递归遍历到空的时候还没找到
printf("未找到要删除的元素");
else {
if (BT->Data > Ele)//如果根节点数据比要删除的大
//往左边找要删除的元素
BT->Left=Delete(BT->Left, Ele);
else if (BT->Data < Ele)//如果根节点数据比要删除的小
//往右边找要删除的元素
BT->Right=Delete(BT->Right, Ele);
else {//找到了相等的情况
//如果相等的根节点下面只有一个节点或者没有结点
if (BT->Left && BT->Right) {
//否则就是左右均不为空的情况
//找左边最大的或右边最小的,用最值结点替换该节点,然后删除最值结点
Tempt = FindMax(BT->Left);
BT->Data = Tempt->Data;
BT->Left = Delete(BT->Left, BT->Data);
}
else {
//只要有一边是空的
//说明三种情况:都空,右子树和空的,左子树和空的
//那就让另一边上去替代即可
Tempt = BT;
if (!BT->Left)//左树是空的
//那么往右树走判断
BT = BT->Right;
else if (!BT->Right)//右树是空的
//就往左树走判断
BT = BT->Left;
//要是两边空就说明该结点就是最值结点,直接删除就完事了
free(Tempt);
Tempt = NULL;
}
}
}
return BT;//返回根结点的地址
}
一点错题:
注意完:完全二叉树是叶节点全在底层,但是不一定满的,所以二叉搜索树的最右边的树可能会右一个左子树比他小,导致他不是叶节点。
平衡二叉树(AVL树):
定义:
- 给定节点数为N的AVL树,其最大高度为
- 插入,删除,查找操作均为O(logN)
- 是一种二叉搜索树的优化版
对于一颗平衡二叉树中的任意一个结点T,他的平衡因子记为BF,有BF(T)=HL-HR,其中HL和HR分别为左右树的高度。且BF(T)<=1.
设为对于高度h的平衡二叉树所需要的最少结点数:
联立斐波那契数列:
则有:
代码实现:
注意插入元素即使不需要调整树的结构也要重新计算树的平衡因子。
思路:对于插入的元素,在插入后计算结点的平衡因子,找到平衡因子最里面被破坏超出范围的那一层,判断他和插入结点的位置关系,然后选取旋转方式。
结构:
//创建AVL树的结构体
typedef struct AVLNode * AVLTree;//链式AVL树
typedef struct AVLNode {
char Data;//记录数据
AVLTree Left;//指向左树
AVLTree Right;//指向右树
int BF;//表示平衡因子
};
计算平衡因子:
//计算树高度
int GetHeight(AVLTree TT) {
//利用递归的方式求树高,叶节点的高度记为1
int HL, HR, MAXH;
if (TT) {
HL = GetHeight(TT->Left);
HR = GetHeight(TT->Right);
MAXH = HL > HR ? HL : HR;
return MAXH + 1;//返回的是输入结点的高度
}
else return 0;
}
//计算平衡因子
int CalculateBF(AVLTree TT) {
int BF,HL,HR;
HL = GetHeight(TT->Left);
HR = GetHeight(TT->Right);
BF = HL - HR;
return BF;
}
左单旋和右单旋:
左旋的思路:把对于要再调整的结点A利用一个中间结点B来记录A的左子树下面的信息,因为进行了左旋,所以A的左子树就变成了根结点,而A根据二叉搜索树的性质就成了B的右子树,但是因为B他原先的右子树一定比A小,所以做一个处理就是先把B的右子树接到A的左子树上,然后再把A节到B的右子树上就完成了旋转。
//左旋算法
AVLTree LL(AVLTree TT) {
//对一棵树做左旋,那么这这棵树一定要有左子树
//输入的是BF最先>=2的发现者结点,对他进行左单旋
//利用一个中继节点存储调整后的样子
AVLTree TEMPT = TT->Left;//左为左旋的根节点
TT->Left = TEMPT->Right;//因为TEMPT的右子树都比TT小,所以节到TT的左侧
TEMPT->Right = TT;//把TT接到TEMPT的右侧,实现结点的转接
//调整平衡因子
TT->BF = CalculateBF(TT);
TEMPT->BF = CalculateBF(TEMPT);
return TEMPT;
}
//右旋算法
AVLTree RR(AVLTree TT) {
AVLTree TEMPT = TT->Right;
TT->Right = TEMPT->Left;//因为tempt原先的left一定比TT大,所以放在TT的RIGHT
TEMPT->Left = TT;//把TT接到TEMPT的左边
//调整BF
TT->BF = CalculateBF(TT);
TEMPT->BF = CalculateBF(TEMPT);
return TEMPT;
}
双旋算法:
//左右旋算法
AVLTree LR(AVLTree TT) {
//先右旋再左旋
//就是从里到外的方式旋转调整结点,
TT->Left = RR(TT->Left);//先对结点的左子树做右旋
return LL(TT);//再对结点做左旋,并且返回
}
//右左旋算法
AVLTree RL(AVLTree TT) {
TT->Right = LL(TT->Right);
return RR(TT);
}
AVL树的插入:
插入后注意及时更新BF的值
//AVL树的插入
AVLTree Insert(AVLTree TT, char x) {
//首先先判断插入位置是否为空
if (!TT) {
//如果是空树,那么就生成一个结点插入
TT = (AVLTree)malloc(sizeof(struct AVLNode));
TT->Data = x; TT->Left = TT->Right = NULL;
TT->BF = 0;
}
//如果树不空,那就根据二叉搜索树的性质找合适的位置插入
else if (x < TT->Data) {
//插入数据比结点数据小,就往左边找数据插入
TT->Left = Insert(TT->Left, x);
TT->BF = CalculateBF(TT);//插入后注意即使更新BF
//因为是递归的方式,所以每次return都会从内往外更新BF的值
//在成功插入的判断BF的值考虑是否要调整二叉树
if (TT->BF == 2) {//说明需要左旋
//判断是左单选还是左右双旋
//判断方式:看插入结点时再左子树的左边还是右边
//左边采取左单旋,右边采取左右双旋
if (x < TT->Left->Data) {
TT = LL(TT);
}
else
TT = LR(TT);
}
}
else if (x > TT->Data) {
TT->Right = Insert(TT->Right, x);
TT->BF = CalculateBF(TT);
if (TT->BF == -2) {
if (x > TT->Right) {
TT = RR(TT);
}
else
TT = RL(TT);
}
}
//否则就是找到了一样的结点
//更新BF
TT->BF = CalculateBF(TT);
return TT;//返回树结点
}
汇总:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
//创建AVL树的结构体
typedef struct AVLNode * AVLTree;//链式AVL树
typedef struct AVLNode {
char Data;//记录数据
AVLTree Left;//指向左树
AVLTree Right;//指向右树
int BF;//表示平衡因子
};
//计算树高度
int GetHeight(AVLTree TT) {
//利用递归的方式求树高,叶节点的高度记为1
int HL, HR, MAXH;
if (TT) {
HL = GetHeight(TT->Left);
HR = GetHeight(TT->Right);
MAXH = HL > HR ? HL : HR;
return MAXH + 1;//返回的是输入结点的高度
}
else return 0;
}
//计算平衡因子
int CalculateBF(AVLTree TT) {
int BF,HL,HR;
HL = GetHeight(TT->Left);
HR = GetHeight(TT->Right);
BF = HL - HR;
return BF;
}
//左旋算法
AVLTree LL(AVLTree TT) {
//对一棵树做左旋,那么这这棵树一定要有左子树
//输入的是BF最先>=2的发现者结点,对他进行左单旋
//利用一个中继节点存储调整后的样子
AVLTree TEMPT = TT->Left;//左为左旋的根节点
TT->Left = TEMPT->Right;//因为TEMPT的右子树都比TT小,所以节到TT的左侧
TEMPT->Right = TT;//把TT接到TEMPT的右侧,实现结点的转接
//调整平衡因子
TT->BF = CalculateBF(TT);
TEMPT->BF = CalculateBF(TEMPT);
return TEMPT;
}
//右旋算法
AVLTree RR(AVLTree TT) {
AVLTree TEMPT = TT->Right;
TT->Right = TEMPT->Left;//因为tempt原先的left一定比TT大,所以放在TT的RIGHT
TEMPT->Left = TT;//把TT接到TEMPT的左边
//调整BF
TT->BF = CalculateBF(TT);
TEMPT->BF = CalculateBF(TEMPT);
return TEMPT;
}
//左右旋算法
AVLTree LR(AVLTree TT) {
//先右旋再左旋
//就是从里到外的方式旋转调整结点,
TT->Left = RR(TT->Left);//先对结点的左子树做右旋
return LL(TT);//再对结点做左旋,并且返回
}
//右左旋算法
AVLTree RL(AVLTree TT) {
TT->Right = LL(TT->Right);
return RR(TT);
}
//AVL树的插入
AVLTree Insert(AVLTree TT, char x) {
//首先先判断插入位置是否为空
if (!TT) {
//如果是空树,那么就生成一个结点插入
TT = (AVLTree)malloc(sizeof(struct AVLNode));
TT->Data = x; TT->Left = TT->Right = NULL;
TT->BF = 0;
}
//如果树不空,那就根据二叉搜索树的性质找合适的位置插入
else if (x < TT->Data) {
//插入数据比结点数据小,就往左边找数据插入
TT->Left = Insert(TT->Left, x);
TT->BF = CalculateBF(TT);
//因为是递归的方式,所以每次return都会从内往外更新BF的值
//在成功插入的判断BF的值考虑是否要调整二叉树
if (TT->BF == 2) {//说明需要左旋
//判断是左单选还是左右双旋
//判断方式:看插入结点时再左子树的左边还是右边
//左边采取左单旋,右边采取左右双旋
if (x < TT->Left->Data) {
TT = LL(TT);
}
else
TT = LR(TT);
}
}
else if (x > TT->Data) {
TT->Right = Insert(TT->Right, x);
TT->BF = CalculateBF(TT);
if (TT->BF == -2) {
if (x > TT->Right) {
TT = RR(TT);
}
else
TT = RL(TT);
}
}
//否则就是找到了一样的结点
//更新BF
TT->BF = CalculateBF(TT);
return TT;//返回树结点
}
void PreTraversal(AVLTree TT) {
if (TT) {
printf("%c", TT->Data);
PreTraversal(TT->Left);
PreTraversal(TT->Right);
}
}
int main() {
AVLTree TT = (AVLTree)malloc(sizeof(struct AVLNode));
AVLTree T1 = (AVLTree)malloc(sizeof(struct AVLNode));
AVLTree T2 = (AVLTree)malloc(sizeof(struct AVLNode));
AVLTree T3 = (AVLTree)malloc(sizeof(struct AVLNode));
TT->Data = '1'; TT->Left = T3; TT->Right = T1; TT->BF = -2;
T1->Data = '2'; T1->Left = NULL; T1->Right = T2; T1->BF = -1;
T2->Data = '4'; T2->Left = T2->Right = NULL; T2->BF = 0;
T3->Data = '0'; T3->Left = T3->Right = NULL; T3->BF = 0;
char X = '3';
Insert(TT, X);
PreTraversal(TT);
return 0;
}
线索二叉树:
由于N个结点的二叉链表中,有2N个指针域且有N-1个元素,因此就有N+1个指针域时空的,为了有效利用剩余的空指针域,就有了线索二叉树。
线索二叉树的作用:
可以把树转成线性表,可以将遍历信息在首次遍历时线索化。这样可以在需要时直接获取结点的前驱和后继元素。根据他的遍历序列建立线索二叉树,同时利用空指针记录遍历序列,在二叉树改变的同时对记录的序列可以做动态的改变。
因为如果遍历序列要多次使用,对于序列的获取就要多次调用递归函数,这个过程中计算机会多次调用堆栈,就会造成空间的浪费。而利用指针建立线索就把原先的二叉树转成了一个类双向链表,以线性的方式去找遍历的前驱和后继。
线索二叉树的种类:
- 前序线索二叉树
- 中序线索二叉树
- 后序线索二叉树
- 层序线索二叉树
创建二叉搜索树的思路:
在二叉树建立后,通过遍历二叉树利用空指针建立线索树,然后利用两个指针Cur和Pre。其中Cur表示正在访问的结点,Pre表示之前刚访问的结点。对于Cur指针是按照中序遍历的顺序去指的。然后对于Cur访问的结点用来设置前驱结点,先判断左子树是否为空,为空的话就让他指向前驱结点,然后让Pre指针记录Cure的位置,Cur根据遍历次序移动到下一个位置。对于Cur的每次移动做三件事:
- 检查Cur的左子树是否为空,若为空就设置前驱线索指向Pre,更改Tag
- 检查Pre的右子树是否为空,若为空就设置后继线索指向Cur,更改Tag
- 最后Pre记录Cur的位置然后Cur移动到下一个位置
注意在程序递归结束的时候,最后一个结点的右子树为空。
代码实现的方式就是按照中序遍历的模板,从根节点出发,建立左子树的线索,在建立右子树的线索。对于建立后继的时候注意下判断Pre他的初值是空的,会造成一开始就读取后继异常,要加一个判断。
以中序为例:
左节点NULL的代表中序的起点,右节点NULL代表中序的终点
输入(先序建树):ABD0G000CE00F00
结构:
//线索二叉树的结构体
typedef struct ThreadTreeNode* ThreadTree;
struct ThreadTreeNode
{
char Data;
int LFlag;//为1表示指向前驱元素
ThreadTree Left;
int RFlag;
ThreadTree Right;//为1表示指向后继元素
};
生成树:
//先创建一颗二叉树
ThreadTree InitialTree() {
//对结点初始化
ThreadTree TT = (ThreadTree)malloc(sizeof(struct ThreadTreeNode));
TT->LFlag = TT->RFlag = 0;
TT->Left = TT->Right = NULL; TT->Data = 0;
return TT;
}
ThreadTree PreCreat() {
//先序创建树
ThreadTree TT = InitialTree();
char data;
scanf("%c", &data);
if (data == '0') return 0;
TT->Data = data;
TT->Left = PreCreat();
TT->Right = PreCreat();
return TT;
}
线索化二叉树:
//线索化二叉树
void InorderThread(ThreadTree Cur) {
//输入的参数是树根节点进行中序线索化
static ThreadTree Pre=NULL;
/*注意这里涉及到一个传参的问题,如果不是全局变量或者静态变量
那么Pre在这个函数内部就相当于局部变量,每一次的递归调用就进入了一个新的程序
对于上次的Pre的地址就丢失了,而且每一次程序结束Pre作为局部变量的地址值就被销毁了*/
if (Cur) {
InorderThread(Cur->Left);//中序方式
//线索化步骤:三步走
//建立Cur的前驱线索
if (Cur->Left==NULL) {
//说明无左子树,那就建立前驱
Cur->LFlag = 1;
Cur->Left = Pre;
}
//建立Pre的后继线索
if (Pre != NULL && Pre->Right == NULL) {
//判断Pre不为空可以防止读取判断他的右子树是否为空的异常
Pre->RFlag = 1;
Pre->Right = Cur;
}
Pre = Cur;
InorderThread(Cur->Right);
}
}
查找后继:
//查找一个结点的后继元素是谁
ThreadTree FindNext(ThreadTree TT) {
//输入要查找后继的结点
ThreadTree Rear;
if (TT->RFlag == 1) {
//表示这个结点的右子树是线索,那它接的就是后继
Rear = TT->Right;
}
else {
//利用中序遍历的性质:左-根-右
//所以对于一个结点右子树不是线索树的话
//那他的后续节点一定在右子树的最左子树上
Rear = TT->Right;
while (Rear->LFlag==0)
{
Rear = Rear->Left;
}
}
return Rear;
}
线索化遍历:
//中序遍历线索链表
void InOrderThreadTraversal(ThreadTree TT) {
ThreadTree Cur;
Cur = TT;//先接收,不改变树根的地址,切记
//因为是中序遍历,最开始的结点一定是最左下角的元素
//而且线索二叉树的起点和终点一定是空的
if (!Cur)return 0;//结点为空啥也不做
while (Cur->LFlag==0)//表示左边有左子树
{
Cur = Cur->Left;
}//循环结束TT就到了最左端结点,即中序的起点
printf("%c", Cur->Data);
//查找后继
while (Cur->Right != NULL) {
//因为右子树不空,是线索
Cur = FindNext(Cur);//接收后继元素
printf("%c", Cur->Data);
}
}
反中序输出:
找前驱:
ThreadTree FindFront(ThreadTree TT) {
//找前驱线索,注意先线索化后根据Flag找的线索树
//LFlag表示接的是前驱,RFlag标记接的是后继
ThreadTree Front;//存储前驱结点用来返回
if (TT->LFlag == 1) {
//说明左边接的是前驱结点
Front = TT->Left;
}
else {
//如果他不是线索树,那他的前驱线索就是这个节点的左子树的最右子树
//因为按照中序遍历的顺序:左-根-右
//把该节点看作根节点,他是上一个结点的右边遍历完才到这个根的
Front = TT->Left;
while (Front->RFlag == 0) {
//一直遍历到线索结点
Front = Front->Right;
}
}
return Front;
}
反序遍历:
注意二叉树在线索化后最后一个线索结点虽然指空了但是右子树标记是没修改的,还是0,所以找前驱是到最有端不可以用flag判断,会让函数读入空指针。
void ReverseInOrder(ThreadTree TT) {
ThreadTree Cur = TT;
if (!Cur) return 0;
//因为是反中序所以是从最右边的结点开始
while (Cur->Right != NULL)
{
//找到到最右端的线索树
Cur = Cur->Right;
}
//根据线索找前驱输出
printf("%c", Cur->Data);
while (Cur->Left != NULL)
{
//只要没到中序遍历的起点就一直循环
Cur = FindFront(Cur);
printf("%c", Cur->Data);
}
}
汇总:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
//线索二叉树的结构体
typedef struct ThreadTreeNode* ThreadTree;
struct ThreadTreeNode
{
char Data;
int LFlag;//为1表示指向前驱元素
ThreadTree Left;
int RFlag;
ThreadTree Right;//为1表示指向后继元素
};
//先创建一颗二叉树
ThreadTree InitialTree() {
//对结点初始化
ThreadTree TT = (ThreadTree)malloc(sizeof(struct ThreadTreeNode));
TT->LFlag = TT->RFlag = 0;
TT->Left = TT->Right = NULL; TT->Data = 0;
return TT;
}
ThreadTree PreCreat() {
//先序创建树
ThreadTree TT = InitialTree();
char data;
scanf("%c", &data);
if (data == '0') return 0;
TT->Data = data;
TT->Left = PreCreat();
TT->Right = PreCreat();
return TT;
}
//线索化二叉树
void InorderThread(ThreadTree Cur) {
//输入的参数是树根节点进行中序线索化
static ThreadTree Pre = NULL;
/*注意这里涉及到一个传参的问题,如果不是全局变量或者静态变量
那么Pre在这个函数内部就相当于局部变量,每一次的递归调用就进入了一个新的程序
对于上次的Pre的地址就丢失了,而且每一次程序结束Pre作为局部变量的地址值就被销毁了*/
if (Cur) {
InorderThread(Cur->Left);//中序方式
//线索化步骤:三步走
//建立Cur的前驱线索
if (Cur->Left == NULL) {
//说明无左子树,那就建立前驱
Cur->LFlag = 1;
Cur->Left = Pre;
}
//建立Pre的后继线索
if (Pre != NULL && Pre->Right == NULL) {
//判断Pre不为空可以防止读取判断他的右子树是否为空的异常
Pre->RFlag = 1;
Pre->Right = Cur;
}
Pre = Cur;
InorderThread(Cur->Right);
}
}
//查找一个结点的后继元素是谁
ThreadTree FindNext(ThreadTree TT) {
//输入要查找后继的结点
ThreadTree Rear;
if (TT->RFlag == 1) {
//表示这个结点的右子树是线索,那它接的就是后继
Rear = TT->Right;
}
else {
//利用中序遍历的性质:左-根-右
//所以对于一个结点右子树不是线索树的话
//那他的后续节点一定在右子树的最左子树上
Rear = TT->Right;
while (Rear->LFlag == 0)
{
Rear = Rear->Left;
}
}
return Rear;
}
//中序遍历线索链表
void InOrderThreadTraversal(ThreadTree TT) {
ThreadTree Cur;
Cur = TT;//先接收,不改变树根的地址,切记
//因为是中序遍历,最开始的结点一定是最左下角的元素
//而且线索二叉树的起点和终点一定是空的
if (!Cur)return 0;//结点为空啥也不做
while (Cur->LFlag == 0)//表示左边有左子树
{
Cur = Cur->Left;
}//循环结束TT就到了最左端结点,即中序的起点
printf("%c", Cur->Data);
//查找后继
while (Cur->Right != NULL) {
//因为右子树不空,是线索
Cur = FindNext(Cur);//接收后继元素
printf("%c", Cur->Data);
}
}
ThreadTree FindFront(ThreadTree TT) {
//找前驱线索,注意先线索化后根据Flag找的线索树
//LFlag表示接的是前驱,RFlag标记接的是后继
ThreadTree Front;//存储前驱结点用来返回
if (TT->LFlag == 1) {
//说明左边接的是前驱结点
Front = TT->Left;
}
else {
//如果他不是线索树,那他的前驱线索就是这个节点的左子树的最右子树
//因为按照中序遍历的顺序:左-根-右
//把该节点看作根节点,他是上一个结点的右边遍历完才到这个根的
Front = TT->Left;
while (Front->RFlag == 0) {
//一直遍历到线索结点
Front = Front->Right;
}
}
return Front;
}
void ReverseInOrder(ThreadTree TT) {
ThreadTree Cur = TT;
if (!Cur) return 0;
//因为是反中序所以是从最右边的结点开始
while (Cur->Right != NULL)
{
//找到到最右端的线索树
Cur = Cur->Right;
}
//根据线索找前驱输出
printf("%c", Cur->Data);
while (Cur->Left != NULL)
{
//只要没到中序遍历的起点就一直循环
Cur = FindFront(Cur);
printf("%c", Cur->Data);
}
}
int main() {
ThreadTree TT;
TT = PreCreat();
InorderThread(TT);
InOrderThreadTraversal(TT);
printf("\n\n");
ReverseInOrder(TT);
return 0;
}
递归程序中的小技巧:
保留上一次递归结果的方法:
- 利用全局变量
- 利用stastic