《王道》数据结构之树和二叉树(五)

数据结构入门之树和二叉树(五)


大纲


一、树的概念和性质

1.1 树的概念

1.1.1 树的定义

树是n(n≥0)个结点的有限集(n=0时为空树)。在任意一棵非空树中:(1)有且仅有一个根结点Root,树下面有很多子树(递归)(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、…Tn,其中每一个集合本身又是一棵树,称为根结点的子树(SubTree)。
树是一种递归定义的数据结构。除了根结点外,任何一个结点都有且仅有一个前驱。每个结点可以有0个或多个后继

注意1:n>0时根结点是唯一的
注意2:m>0时,子树个数没有限制,但他们一定互不相交

1.1.2 结点分类与结点间关系

1. 结点分类
树的结点包含一个数据元素若干指向其子树的分支

结点分类解释
根结点有且仅有一个根结点(非空树)
分支结点(非终端结点)有后继的结点
叶子结点(终端结点)没有后继的结点

2. 结点间关系

结点间关系解释
孩子结点(Child)结点的子树的称为该结点的孩子
双亲结点(Parent)上述的该结点就是孩子的双亲
兄弟结点(Sibling)同一个双亲的孩子之间互称兄弟
堂兄弟结点其双亲在同一层的结点互为堂兄弟
祖先结点结点的祖先是从根结点到该结点 所经分支的所有结点
子孙结点以该结点为根的子树中的任一结点都称为 该结点的子孙
两结点间的路径路径只能从上往下
路径长度经过几条“边”(层次的差值)

根结点:无双亲,唯一
中间结点:一个双亲多个孩子
叶结点:无孩子,可以多个

1.1.3 树的其他相关概念

术语解释
结点的层次(深度)从上往下数(根结点层次为1)
结点的高度从下往上数(区别树的高度)
(结点的)度结点有几个孩子/子树
树的度树内各结点的度的最大值
树的深度/高度树中结点的最大层次
  • 有序树和无序树:若将树中结点的各子树看成从左至右有次序的,不能互换,则称为有序树,否则为无序树
  • 树和森林 :m(m≥0)棵互不相交的树的集合(对树中每个结点而言,其子树的集合就是森林)
    在这里插入图片描述

1.2 (非空)树的性质

1. 结点数 = 度数+1(只有根结点头上没有“天线”)

2. 度为m的树、m叉树的区别

  • 树的度:各结点的度的最大值
  • m叉树:各结点最多只能m个孩子的树
度为m的树m叉树
任意结点的度≤m(最多m个孩子)任意结点的度≤m(最多m个孩子)
至少有一个结点的度=m(有m个孩子)允许所有结点的度都<m
一定是非空树,至少m+1个结点可以是空树

相同点m叉树第i层至多有mi-1结点; 度为m的树第i层至多有mi-1结点;**(每个结点都有m个孩子)

不同点: 高度为h的m叉树至多有 (mh-1/m-1) 个结点(等比数列求和m0+m1+ … +mh-1),至少有h个结点; 高度为h、度为m的树至少有h+m-1个结点

3. 具有n个结点的m叉树的最小高度(即完全m叉树)为 [logm(n(m - 1) + 1)]向上取整 或者 [logmn]向下取整+1

  • 法一:和上层比较
    在这里插入图片描述
  • 法二:和下层比较请添加图片描述

1.3 树的存储结构

1.3.1 双亲表示法(顺序存储)

顺序存储结点数据, 结点中保存父结点在数组中的下标
在这里插入图片描述
(除了根结点外,其余每个结点一定有且仅有一个双亲,我们约定根结点的双亲指针域设置为-1)

增/删/改/查优缺点:

  • 新增数据元素,无需按逻辑上的次序存储
  • 删除数据元素两种方案:删除数据元素,父结点数组下标置为-1;将数组最下面的结点替换删除的结点删除的不是叶子结点时,要删除其所有子树的结点
  • 查:找父结点方便;找孩子不方便

代码实现:

#define MAX_TREE_SIZE 100 	//树中最多结点数
typedef int ElemType; 		//树结点的数据类型暂定为整型

//1.结点结构
typedef struct PTNode
{
	ElemType data; 		//结点数据元素
	int parent; 		//双亲位置域(数组下标)
}PTNode;

//2.树结构
typedef struct 
{
	PTNode nodes[MAX_TREE_SIZE];  //结点数组
	int n;  //结点数
	int r;  //根的位置,可不写(默认为0的位置)
}PTree;

1.3.2 孩子表示法

顺序存储结点数据, 结点中保存孩子链表头指针(顺序+链式存储)
在这里插入图片描述
增/删/改/查优缺点:

  • 查:找孩子方便;找父结点不方便

代码实现:

#define MAX_TREE_SIZE 100 	//树中最多结点数
typedef int ElemType; 		//树结点的数据类型暂定为整型

//1.堂兄弟结构(某一结点的所有孩子组成一个链表)
struct CTNode
{	
	int child;				//孩子结点在数组中的位置
	struct CTNode *next;	//下一个孩子
}

//2.结点结构
typedef struct 
{
	ElemType data; 					//结点数据元素
	struct CTNode *firstChild;  	//指针域指向第一个孩子(实际指向所有孩子)
}CTBox;

//3.树结构
typedef struct 
{
	CTBox nodes[MAX_TREE_SIZE];  //结点数组
	int n;  //结点数
	int r;  //根的位置
}CTree;

1.3.3 孩子兄弟表示法(树与二叉树的转换)

用二叉链表存储树——左孩子右兄弟
(孩子兄弟表示法存储的树, 从存储视角来看形态上和二叉树类似,所以可以森林和二叉树的转换(1.4节),也可以用二叉树的操作来处理树(第四节))
在这里插入图片描述
代码实现:

//1.树的结点结构和树结构
typedef struct CSNode{
	ElemType data;								//数据域
	struct CSNode *firstchild, *nextsibling; 	//第一个孩子和右兄弟指针
}CSNode,*CSTree;

//...

1.4 森林与(二叉)树的转换

本质就是用二叉链表存储森林(孩子兄弟表示法):先将每棵树用孩子转化成二叉树,然后每棵树的根结点为兄弟关系(森林中各个树的根结点之间视为兄弟关系)
在这里插入图片描述

二、二叉树的概念和性质

2.1 二叉树的概念

2.1.1 二叉树的定义

二叉树是n(n≥0)个结点的有限集合:
① n = 0 为空二叉树,n = 1 只有根结点
② n > 0 由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。

特点:①每个结点至多只有两棵子树 ②左右子树不能颠倒(二叉树是有序树

2.1.2 几个特殊的二叉树

1. 满二叉树:一棵高度为h,且含有2h - 1个结点的二叉树
2. 完全二叉树:当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应(如果某结点只有一个孩子,那么一定是左孩子)
在这里插入图片描述
3. 二叉排序树:空二叉树或者是具有如下性质的二叉树:
①左子树上所有结点的关键字均小于根结点的关键字;
②右子树上所有结点的关键字均大于根结点的关键字。
③左子树和右子树又各是一棵二叉排序树。
二叉排序树可用于元素的排序、搜索、插入新的结点
在这里插入图片描述
4. 平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1
平衡二叉树能有更高的搜索效率:往宽处长而不往深处长,搜索更快

2.2 二叉树的性质

  1. 二叉树第i层最多有2i-1结点 (m叉树第i层至多有mi-1结点)
  2. 高度为h的二叉树至多有 (2h-1) 个结点 (高度为h的m叉树至多有 (mh-1/m-1) 个结点)
  3. 非空二叉树中度为0、1、2的结点个数分别为n0、n1、n2,则n0 = n2 - 1(叶子结点比二分支结点多一个)
    在这里插入图片描述

  1. 具有n个(n > 0)结点的完全二叉树的高度h为[log2(n + 1)]向上取整 或[log2n]向下取整 + 1 (同完全m叉树)
  2. 对于完全二叉树,可以由的结点数n 推出度为0、1和2的结点个数为n0、n1和n2(完全二叉树最多只会有一个度为1的结点)
    在这里插入图片描述

2.3 二叉树的存储结构

2.3.1 二叉树的顺序存储

1. 完全二叉树
按照从上至下、从左至右的顺序依次存储完全二叉树中的各结点
在这里插入图片描述
在这里插入图片描述
结点编号一般从1开始,方便找结点的左孩子/右孩子/双亲等

代码实现:

#define MAX_SIZE 100
typedef int ElemType; //树结点的数据类型,暂定为整型

//1.结点结构
struct TreeNode{
	ElemType value;     //结点中的数据元素
	bool isEmpty;		//判断结点是否为空
};

//2.树结构
TreeNode T[MAX_SIZE];	//定义一个长度为MAX_SIZE的数组T,按照从上至下、从左至右的顺序依次存储完全二叉树中的各结点

//3.初始化时所有空结点标记为空
for(int i = 0; i < MAX_SIZE; i++)
{
	T[i].isEmpty = true ;
}

常考操作(对于一共n个结点的完全二叉树):

查找
𝑖 的左孩子2𝑖
𝑖 的右孩子2𝑖+1
𝑖 的父节点[𝑖/2]向下取整
𝑖 所在的层次[log2(n + 1)]向上取整 或 [log2n]向下取整 + 1
判断
𝑖 是否有左孩子2𝑖 ≤ n
𝑖 是否有右孩子2𝑖+1 ≤ n
i 是否是叶子结点𝑖 > [n/2]向下取整

2. 非完全二叉树
非完全二叉树的顺序存储结构,一定要把二叉树的结点编号与完全二叉树对应起来
在这里插入图片描述
在这里插入图片描述
代码实现:
同上完全二叉树

常考操作(对于一共n个结点的非完全二叉树):
在找到该结点的左孩子/右孩子/父结点的基础上,判断isEmpty元素是否为真(非完全二叉树的树数组不连续存储结点)

缺点:
高度为h且只有h个结点的单支树(所有结点只有右孩子),也至少需要2h-1 个存储单元。故二叉树的顺序存储结构,只适合存储完全二叉树

2.3.2 二叉树的链式存储(二叉链表)

n个结点的二叉链表共有n+1个空链域,可用于构造线索二叉树
在这里插入图片描述

代码实现:

typedef int ElemType; //树结点的数据类型,暂定为整型

//1.二叉树的结点结构和树结构
typedef struct BiTNode{
	ElemType data;						//数据域
	struct BiTNode *lchild, *rchild; 	//左右孩子指针
}BiTNode,*BiTree;

//2.定义一棵空树
BiTree root = NULL;

//3.插入根结点
root = new BiTNode;
root->data = 1;
root->lchild = NULL;
root->rchild = NULL;

//4.插入新结点
BiTNode *p = new BiTNode;
p->data = 2;
p->lchild = NULL;
p->rchild = NULL;
	//与其他结点的关系
root->lchild = p;

//5.释放开辟在堆区的指针

优缺点:

  • 对于指定结点的左/右孩子,可以很方便找到
  • 对于指定结点的双亲,只能从根结点开始遍历寻找(可以将结点结构定义为三叉链表,即多定义一个parent指针指向双亲)

三、二叉树的遍历及线索二叉树

3.1 二叉树的四种遍历

先/中/后序遍历是基于树的递归特性确定的次序规则,空间复杂度为O(n);而层次遍历是基于树的层次特性确定的次序规则

3.1.1 二叉树的先序遍历(根-左-右)

遍历过程:

  1. 若二叉树为空,则什么也不做;
  2. 若二叉树非空:
    ①访问根结点;
    ②先序遍历左子树;
    ③先序遍历右子树
    在这里插入图片描述
    先序遍历:第一次路过时访问结点
    中序遍历:第二次路过时访问结点
    后序遍历:第三次路过时访问结点

代码实现:

//对于链式存储结构(二叉链表)的先序遍历--递归
void PreOrder(BiTree T)
{
	if(T!=NULL)
	{
		visit(T);				//访问当前根结点
		PreOrder(T->lchild);	//递归遍历左子树
		PreOrder(T->rchild);	//递归遍历右子树
	}
}

在这里插入图片描述

3.1.2 二叉树的中序遍历(左-根-右)

遍历过程:

  1. 若二叉树为空,则什么也不做;
  2. 若二叉树非空:
    ①先序遍历左子树;
    ②访问根结点;
    ③先序遍历右子树
    (第二次路过时访问结点)

代码实现:

//对于链式存储结构(二叉链表)的中序遍历--递归
void InOrder(BiTree T)
{
	if(T!=NULL)
	{
		PreOrder(T->lchild);	//递归遍历左子树
		visit(T);				//访问当前根结点
		PreOrder(T->rchild);	//递归遍历右子树
	}
}

3.1.3 二叉树的后序遍历(左-右-根)

遍历过程:

  1. 若二叉树为空,则什么也不做;
  2. 若二叉树非空:
    ①先序遍历左子树;
    ②先序遍历右子树;
    ③访问根结点
    (第三次路过时访问结点)

代码实现:

//对于链式存储结构(二叉链表)的后序遍历--递归
void PostOrder(BiTree T)
{
	if(T!=NULL)
	{
		PreOrder(T->lchild);	//递归遍历左子树
		PreOrder(T->rchild);	//递归遍历右子树
		visit(T);				//访问当前根结点
	}
}

3.1.4 二叉树的层次遍历

遍历过程:

  1. 初始化一个辅助队列
  2. 根结点入队
  3. 若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
  4. 重复③直至队列为空
    在这里插入图片描述
    代码实现:
//对于链式存储结构(二叉链表)的层次遍历--递归
void LevelOrder(BiTree T)
{
	//1.创建并初始化辅助队列(链队列)
	LinkQueue Q;
	InitQueue(Q);
	
	//2.根结点入队
	EnQueue(Q,T);         	//队列存放的是结点的指针(4字节),而非结点本身(最少8字节)

	//3.若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话),直至为空
	BiTree p;				//存放出队的队头结点
	while(!IsEmpty(Q))
	{
		DeQueue(Q,p);		//队头结点出队
		visit(p);			//访问出队的该结点
		if(p->lchild != NULL)
		{
			EnQueue(Q,p->lchild);	//左孩子入队
		}
		if(p->rchild != NULL)
		{
			EnQueue(Q,p->rchild);	//右孩子入队
		}
	}
}

//ps:链式队列的结点
typedef struct LinkNode
{
	BiTNode * data;			//队列存的是指针,而非二叉树的结点,减小内存
	struct LinkNode* next;
}LinkNode;

3.1.5 由二叉树的遍历序列确定二叉树

若只给出一棵二叉树的前/中/后/层序遍历序列中的一种,不能唯一确定一棵二叉树。但中序+其他一种可以实现唯一确定一棵二叉树
1. 前序+中序遍历序列
在这里插入图片描述

  • 先由前序遍历序列,确定根结点在中序遍历序列的位置
  • 中序遍历序列根结点左侧为该结点的左子树,右侧为该结点的右子树

2. 后序+中序遍历序列
在这里插入图片描述

  • 先由后序遍历序列,确定根结点在中序遍历序列的位置
  • 中序遍历序列根结点左侧为该结点的左子树,右侧为该结点的右子树

3. 层序+中序遍历序列
在这里插入图片描述

  • 先由层序遍历序列,确定根结点在中序遍历序列的位置
  • 中序遍历序列根结点左侧为该结点的左子树,右侧为该结点的右子树,画出左右子树
  • 再由层序遍历序列,确定左子树根结点、右子树根结点在中序遍历序列的位置
  • 中序遍历序列继续根据结点划分左右子树直到底(画图解决)

3.1.6 二叉树遍历的应用

1.求树的深度

//递归的方法求树的深度
int treeDepth(BiTree T)
{
	if(T==NULL)
	{	
		return 0;
	}
	else
	{
		int l = treeDepth(T->lchild);
		int r = treeDepth(T->rchild);
		return (l>r ? l+1 ; r+1)		//树的深度=Max(左子树深度,右子树深度)+1
	}
}

3.2 线索二叉树

3.2.1 线索二叉树是什么、为什么

普通二叉树的缺点:找前驱、后继很不方便;遍历操作必须从根开始,而不能从一个指定结点开始中序遍历——这就是为什么有线索二叉树

1. 线索二叉树
n个结点的二叉树,有n+1个空链域,可用来记录前驱、后继的信息,线索化(怎么做见下一节)后,可以很方便地找到前驱、后继,遍历操作也不必从根开始(如下图为中序线索二叉树)
在这里插入图片描述
线索:指向前驱/后继的指针

2. 三种线索二叉树
各个结点的前驱、后继关系是按先序/中序/后序的遍历来确定,可将线索二叉树分为
先序线索二叉树(对应的前驱、后继叫做先序前驱/先序后继)
中序线索二叉树(对应的前驱、后继叫做中序前驱/中序后继)
后序线索二叉树(对应的前驱、后继叫做后序前驱/后序后继)

3.2.2 线索二叉树怎么做

1. 线索二叉树(结点)的存储结构

typedef int ElemType; //树结点的数据类型,暂定为整型

//线索二叉树的结点(创建出来的树叫线索链表)
typedef struct ThreadNode{
	ElemType data;							//数据域
	struct ThreadNode *lchild, *rchild; 	//左右孩子指针
	int ltag,rtag;							//左右线索标志:tag==0表示指针指向孩子,tag==1表示指针是“线索”
}ThreadNode,*ThreadTree;

2. 三种二叉树的线索化(一边遍历一边线索化)
先序的PreThread函数有一点不同,先序是根-左-右,访问根结点时若把左孩子给了前驱,那左时访问回去导致无限环绕——可通过ltag增加判断

中序线索化代码实现:

//1.设置全局变量pre,指向当前访问结点的前驱,一开始指向NULL
ThreadNode *pre = NULL        	

//2.中序线索化二叉树T
void CreateInThread(ThreadTree T)
{
	pre = NULL;					//每次调用pre初始化为NULL
	if(T!=NULL)					//非空二叉树才能线索化
	{
		InThread(T);			//中序线索化二叉树
		if(pre->rchild == NULL)	//处理遍历的最后一个结点
			pre->rtag = 1; 
	}
}

//主函数的辅助函数InThread(T):中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T)
{
	if(T!=NULL)
	{	
		InThread(T->lchild);		//中序遍历左子树
		visis(T);					//访问当前根结点,线索化由visit函数完成
		InThread(T->rchild);		//中序遍历右子树
	}
}

//InThread(T)的辅助函数visit(T):完成线索化
void visit(ThreadNode *q)
{
	if(q->lchild == NULL)			//左子树为空,建立当前结点的前驱线索
	{
		q->lchild = pre;
		q->ltag = 1;
	}
	if(pre->rchild == NULL && pre !=NULL)	//建立前驱结点的后继线索
	{						//pre !=NULL表示第一个结点没有前驱,或者说最开始的pre的后继不能指向第一个结点
		pre->rchild = q;
		pre->rtag = 1;
	}
	pre = q;		//更新pre结点
	//最后还要检查pre的rchild是否为NULL,如果是,则令rtag=1;
}

先序线索化代码实现:

//1.设置全局变量pre,指向当前访问结点的前驱,一开始指向NULL
ThreadNode *pre = NULL        	

//2.先序线索化二叉树T
void CreatePreThread(ThreadTree T)
{
	pre = NULL;					//每次调用pre初始化为NULL
	if(T!=NULL)					//非空二叉树才能线索化
	{
		PreThread(T);			//先序线索化二叉树
		if(pre->rchild == NULL)	//处理遍历的最后一个结点
			pre->rtag = 1; 
	}
}


//主函数的辅助函数PreThread(T):先序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T)
{
	if(T!=NULL)
	{	
		visis(T);					//先访问当前根结点,线索化由visit完成
		if(T->ltag == 0)			//lchild不是前驱线索时!!!
		{
			PreThread(T->lchild);
		}		
		PreThread(T->rchild);		
	}
}

//PreThread(T)的辅助函数visit(T):完成线索化
void visit(ThreadNode *q)
{
	if(q->lchild == NULL)			//左子树为空,建立当前结点的前驱线索
	{
		q->lchild = pre;
		q->ltag = 1;
	}
	if(pre->rchild == NULL && pre !=NULL)	//建立前驱结点的后继线索
	{						//pre !=NULL表示第一个结点没有前驱,或者说最开始的pre的后继不能指向第一个结点
		pre->rchild = q;
		pre->rtag = 1;
	}
	pre = q;		//更新pre结点
	//最后还要检查pre的rchild是否为NULL,如果是,则令rtag=1;
}

后序线索化代码实现:

//1.设置全局变量pre,指向当前访问结点的前驱,一开始指向NULL
ThreadNode *pre = NULL        	

//2.后序线索化二叉树T
void CreatePostThread(ThreadTree T)
{
	pre = NULL;					//每次调用pre初始化为NULL
	if(T!=NULL)					//非空二叉树才能线索化
	{
		PostThread(T);			//后序线索化二叉树
		if(pre->rchild == NULL)	//处理遍历的最后一个结点
			pre->rtag = 1; 
	}
}


//主函数的辅助函数PostThread(T):后序遍历二叉树,一边遍历一边线索化
void PostThread(ThreadTree T)
{
	if(T!=NULL)
	{	
		PostThread(T->lchild);		//后序遍历左子树
		PostThread(T->rchild);		//后序遍历右子树
		visis(T);					//访问当前根结点,线索化由visit函数完成
	}
}

//PostThread(T)的辅助函数visit(T):完成线索化
void visit(ThreadNode *q)
{
	if(q->lchild == NULL)			//左子树为空,建立当前结点的前驱线索
	{
		q->lchild = pre;
		q->ltag = 1;
	}
	if(pre->rchild == NULL && pre !=NULL)	//建立前驱结点的后继线索
	{						//pre !=NULL表示第一个结点没有前驱,或者说最开始的pre的后继不能指向第一个结点
		pre->rchild = q;
		pre->rtag = 1;
	}
	pre = q;		//更新pre结点
	//最后还要检查pre的rchild是否为NULL,如果是,则令rtag=1;
}

3.2.3 在线索二叉树中找前驱后继

在这里插入图片描述
1. 中序线索二叉树(左-根-右)
1)在中序线索二叉树中找到指定结点*p的中序前驱pre

  • 若p->ltag == 1,则pre = p -> lchild;
  • 若p->ltag == 0,则肯定有左孩子,故pre=当前结点的左子树中最右下结点(左子树按照中序遍历的最后一个结点)

代码实现:

//在中序线索二叉树中找到结点p的前驱结点
ThreadNode *PreNode(ThreadNode *p)
{
	if(p->ltag==0)
	{
		return LastNode(p->lchild);
	}
	else
	{
		return p->lchild;
	}
}

//辅助函数:找到以p为根的子树中,最后一个被中序遍历的结点
ThreadNode *LastNode(ThreadNode *p)
{
	while(p->rtag == 0)
	{
		p = p->rchild;
	}
	return p;
}

(已经知道任意结点的中序前驱,可以对中序线索二叉树进行逆向中序遍历(利用线索实现而非递归算法),其他遍历同理)
void RevInorder(ThreadNode *T)
{
for(ThreadNode *p = LastNode(T) ; p != NULL ; p = PreNode(p) )
visit(p);
}

2)在中序线索二叉树中找到指定结点*p的中序后继next

  • 若p->rtag==1,则next = p->rchild
  • 若p->rtag==0,则肯定有右孩子,故next=当前结点的右子树中最左下结点(右子树按照中序遍历的最后一个结点)

代码实现:

//在中序线索二叉树中找到结点p的后继结点
ThreadNode *NextNode(ThreadNode *p)
{
	if(p->rtag==0)
	{
		return FirstNode(p->rchild);
	}
	else
	{
		return p->rchild;
	}
}

//辅助函数:找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode *FirstNode(ThreadNode *p)
{
	while(p->ltag == 0)
	{
		p = p->lchild;
	}
	return p;
}

(已经知道任意结点的中序后继,可以对中序线索二叉树进行中序遍历(利用线索实现而非递归算法),其他遍历同理)
void Inorder(ThreadNode *T)
{
for(ThreadNode *p = FirstNode(T) ; p != NULL ; p = NextNode(p) )
visit(p);
}

2. 先序线索二叉树(根-左-右)
1)在先序线索二叉树中找到指定结点*p的先序前驱pre

  • 若p->ltag == 1,则pre = p -> lchild;
  • 若p->ltag == 0,则肯定有左孩子,但先序线索中,左右孩子只可能是后继
    故只能从头先序遍历或者用三叉链表来找到先序前驱pre(下图为利用三叉链表来找到先序前驱pre)在这里插入图片描述

2)在先序线索二叉树中找到指定结点*p的先序后继next

  • 若p->rtag==1,则next = p->rchild
  • 若p->rtag==0,则肯定有右孩子,左孩子分情况讨论
    ①若p有左孩子,则先序后继为左孩子
    ②若p没有左孩子,则先序后继为右孩子

3. 后序线索二叉树(左-右-根)
1)在后序线索二叉树中找到指定结点*p的后序前驱pre

  • 若p->ltag == 1,则pre = p -> lchild;
  • 若p->ltag == 0,则肯定有左孩子,右孩子分情况讨论
    ①若p有右孩子,则后序前驱为右孩子
    ②若p没有右孩子,则后序前驱为左孩子

2)在后序线索二叉树中找到指定结点*p的后序后继next

  • 若p->rtag==1,则next = p->rchild
  • 若p->rtag==0,则肯定有右孩子,但后序线索中,左右孩子只可能是前驱
    故只能从头后序遍历或者用三叉链表来找到后序后继next(下图为利用三叉链表来找到后序后继next)
    在这里插入图片描述

四、树和森林的遍历

在这里插入图片描述

4.1 树的三种遍历

4.1.1 树的先序遍历(深度优先遍历)

若树非空,先访问根结点,再依次对每棵子树进行先序遍历

代码实现:

void PreOrder(TreeNode *R)
{
	if(R != NULL)
	{
		visit(R);				//访问根结点
		while(R还有下一个子树T)
		{
			PreOrder(T);		//先序遍历下一棵子树
		}
	}
}

树和二叉树的转化:
树的先序遍历序列与这棵树相应二叉树(孩子兄弟表示的树)的先序遍历序列相同

4.1.2 树的后序遍历(深度优先遍历)

若树非空,先依次对每棵子树进行后序遍历,最后再访问根结点

代码实现:

void PostOrder(TreeNode *R)
{
	if(R != NULL)
	{
		while(R还有下一个子树T)
		{
			PreOrder(T);		//后序遍历下一棵子树
		}
		visit(R);				//访问根结点
	}
}

树和二叉树的转化:
树的后序遍历序列与这棵树相应二叉树(孩子兄弟表示的树)的中序遍历序列相同
在这里插入图片描述

4.1.3 树的层序遍历(广度优先遍历)

遍历过程(与二叉树的层序遍历类似):

  1. 初始化一个辅助队列
  2. 若树非空,则根节点入队
  3. 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队(如果有的话)
  4. 重复③直至队列为空

树和二叉树的转化:
转化成二叉树后,没有对应相同的遍历序列

4.2 森林的两种遍历

4.2.1 森林的先序遍历

遍历过程:(效果等同于依次对各个树进行先根遍历)

  1. 访问森林中第一棵树的根结点(森林为非空)
  2. 先序遍历第一棵树中根结点的子树森林
    先序遍历除去一棵树根结点之后剩余的树构成的森林
  3. 重复直至结束

森林和二叉树的转化:
森林的先序遍历序列与森林对应二叉树(孩子兄弟表示的森林)的先序遍历序列相同。

4.2.2 森林的中序遍历

遍历过程:(效果等同于依次对各个树进行后根遍历)

  1. 中序遍历森林中第一棵树的根结点的子树森林(森林为非空)
  2. 访问第一棵树的根结点
  3. 重复直至结束

森林和二叉树的转化:
森林的中序遍历序列与森林对应二叉树(孩子兄弟表示的森林)的中序遍历序列相同。

五、二叉排序树BST

5.1 二叉排序树的定义

二叉排序树,又称二叉查找树(BST,Binary Search Tree),一棵空二叉树,或者是具有如下性质的二叉树:
①左子树上所有结点的关键字均小于根结点的关键字;
②右子树上所有结点的关键字均大于根结点的关键字。
③左子树和右子树又各是一棵二叉排序树。
在这里插入图片描述
重要性质:
由于左子树结点值< 根结点值< 右子树结点值,则对二叉排序树进行中序遍历,可以得到一个递增的有序序列

5.2 二叉排序树的操作

5.2.1 二叉排序树的查找

若树非空,目标值与根结点的值比较:
①若小于根结点,则在左子树上查找;若大于根结点,则在右子树上查找。
②若相等,则查找成功,返回结点指针;查找失败返回NULL

代码实现:

//二叉排序树结点
typedef struct BSTNode{
	int key;
	struct BSTNode *lchild, *rchild;
}BSTNode,*BSTree;

//在二叉排序树查找值为key的结点
//法一:while循环
BSTNode *BST_Search(BSTree T, int key)
{
	while(T!=NULL && key != T->key)	//若树空或等于根结点值,则结束循环
	{	
		if(key < T->key)
		{	
			T = T->lchild;
		}
		else
		{
			T = T->rchild;
		}
	}
	return T;
}

//法二:递归实现
BSTNode *BSTSearch(BSTree T, int key)
{
	if(T == NULL)
		return NULL;
	if(key == T->key)
	{
		return T;
	}
	else if(key < T->key)
	{
		return BSTSearch(T->lchild , key)
	}
	else (key > T->key)
	{
		return BSTSearch(T->rchild , key)
	}
}

法一:最坏空间复杂度O(1)
法二:空间复杂度为O(h)

查找效率分析:
查找长度:在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度
平均查找长度ASL(Average Search Length):分为查找成功的平均查找长度、查找失败的平均查找长度(需补充失败结点)

复杂度:取决于树的高度

  • 最坏情况:每个结点只有一个分支,树高h=结点数n。平均查找长度=O(n)
  • 最好情况:n个结点的二叉树最小高度为[log2n]向下取整 + 1。平均查找长度= O(log2n)

5.2.2 二叉排序树的插入和构造

二叉排序树的插入:若原二叉排序树为空,则直接插入结点;否则,若关键字k小于根结点值,则插入到左子树,若关键字k大于根结点值,则插入到右子树新插入的结点一定是叶子

代码实现:

//在二叉排序树插入关键字为k的新结点
//法一:递归实现
int BSTInsert(BSTree &T, int k)
{
	if(T==NULL)
	{
		T = new BSTNode;
		//对开辟的新结点赋值
		T->key = k;
		T->lchild = NULL; 
		T->lchild = NULL; 
		//返回1,插入成功
		return 1;			
	}
	else if(k==T->key)		//树中存在相同关键字的结点,插入失败!
	{
		return 0;
	}
	else if(k < T->key)		//插入到T的左子树上
	{
		return BSTInsert(T->lchild, k);
	}
	else 					//插入到T的右子树上
	{
		return BSTInsert(T->rchild, k);
	}
}

递归实现:最坏空间复杂度O(1)

二叉排序树的构造
可利用插入函数构造二叉排序树

代码实现:

void Create_BST(BSTree &T,int str[],int n)
{
	T = NULL;			//初始时T为空树
	for(int i = 0; i < n; i++)
	{
		BSTInset(T,str[i]);
	}
}

注意:不同的关键字序列可能得到同款二叉排序树,关键字相同顺序不同的序列也可能得到不同款的二叉排序树

5.2.3 二叉排序树的删除

先搜索找到目标结点:
① 若被删除结点z是叶结点,则直接删除,不会破坏二叉排序树的性质。
② 若结点z只有一棵左子树或右子树,则让z的子树 成为 z父结点的子树,替代z的位置。
③ 若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
z的后继:z的右子树中最左下结点(该节点一定没有左子树)
z的前驱:z的左子树中最右下结点(该节点一定没有右子树)

六、平衡二叉树AVL

平衡二叉树高度与完全二叉树同等数量级

6.1 平衡二叉树的定义

树上任一结点的左子树和右子树的高度之差不超过1,即结点的平衡因子=左子树高-右子树高 = 1、0、-1

代码实现:

//平衡二叉树结点
typedef struct AVLNode{
	int key;
	int balance;
	struct AVLNode *lchild, *rchild;
}AVLNode, *AVLTree;

6.2 平衡二叉树的插入

在插入操作中,只要将最小不平衡子树调整平衡,则其他祖先结点都会恢复平衡(每次调整的对象都是“最小不平衡子树”,有四种方式调整)
最小不平衡子树:从插入点往回找到第一个不平衡结点,调整以该结点为根的子树
调整目标:1.恢复平衡;2.保持二叉排序树特性
在这里插入图片描述
要点:只有左孩子才能右上旋,只有右孩子才能左上旋

6.2.1 LL平衡旋转(右单旋转)

LL情况:由于在结点A的左孩子(L)的左子树(L)上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡

操作:需要将A的左孩子B向右上旋转代替A成为根结点,将A结点向右下旋转成为B的右子树的根结点,而B的原右子树则作为A结点的左子树。
靠左的结点(B)的左子树(BL)一直都会是该结点的左子树
靠右的结点(A)的右子树(AR)一直都会是该结点的右子树
在这里插入图片描述
代码思路:
实现f向右下旋转, p向右上旋转(其中f是爹,p为左孩子,gf为f他爹):
① f->lchild = p->rchild;
② p->rchild = f;
③ gf->lchild/rchild = p;
在这里插入图片描述

6.2.2 RR平衡旋转(左单旋转)

RR情况:由于在结点A的右孩子(R)的右子树(R)上插入了新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡

操作:需要将A的右孩子B向左上旋转代替A成为根结点,将A结点向左下旋转成为B的左子树的根结点,而B的原左子树则作为A结点的右子树
靠左的结点(A)的左子树(AL)一直都会是该结点的左子树
靠右的结点(B)的右子树(BR)一直都会是该结点的右子树
在这里插入图片描述
代码思路:
实现f 向左下旋转, p 向左上旋转(其中f是爹,p为右孩子,gf为f他爹):
① f->rchild = p->lchild;
② p->lchild = f;
③ gf->lchild/rchild = p;
在这里插入图片描述

6.2.3 LR平衡旋转(先左后右双旋转)

LR情况:由于在A的左孩子(L)的右子树(R)上插入新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡

操作:先将A结点的左孩子B的右子树的根结点C向左上旋转提升到B结点的位置,然后再把该C结点向右上旋转提升到A结点的位置
在这里插入图片描述

6.2.4 RL平衡旋转(先右后左双旋转)

RL情况:由于在A的右孩子(R)的左子树(L)上插入新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡

操作:先将A结点的右孩子B的左子树的根结点C向右上旋转提升到B结点的位置,然后再把该C结点向左上旋转提升到A结点的位置
在这里插入图片描述

6.3 查找效率分析

树高和查找次数/复杂度挂钩:若树高为h,则最坏情况下,查找一个关键字最多需要对比h次,即查找操作的时间复杂度不可能超过O(h)

假设以nh表示深度为h的平衡树中含有的最少结点数,则有n0 = 0, n1 = 1, n2 = 2,并且有nh = nh−1 + nh−2 + 1,则可以证明含有n个结点的平衡二叉树的最大深度为O(log2n) ,即平衡二叉树的平均查找长度为O(log2n)

七、哈夫曼树(二叉树)

术语解释
结点的权有某种现实含义的数值(如:表示结点的重要性等)
结点的带权路径长度该结点的权值 × 树的根到该结点的路径长度(经过的边数)
(树的)带权路径长度(WPL, Weighted Path Length)树中所有叶子结点的带权路径长度之和

7.1 哈夫曼树的定义

哈夫曼树(最优二叉树):在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树

7.2 哈夫曼树的构造

给定n个权值分别为w1, w2,…, wn的结点,构造哈夫曼树的算法描述如下:

  1. 将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F
  2. 构造一个新结点:从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和
  3. 从F中删除刚才选出的两棵树,同时将新得到的树加入F中
  4. 重复步骤2和3,直至F中只剩下一棵树为止 在这里插入图片描述

哈夫曼树的特点:

  • 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
  • 哈夫曼树的结点总数为2n − 1(合并n-1次,出现n-1个新的结点)
  • 哈夫曼树中不存在度为1的结点
  • 哈夫曼树并不唯一,但WPL必然相同且为最优

7.3 哈夫曼编码

固定长度编码:每个字符用相等长度的二进制位表示
可变长度编码:允许对不同字符用不等长的二进制位表示

前缀编码:没有一个编码是另一个编码的前缀(前缀码解码无歧义)
非前缀编码:有编码是另一个编码的前缀(非前缀码解码有歧义,在哈夫曼树中有字符在非叶子结点上)
在这里插入图片描述
哈夫曼编码:字符集中的每个字符作为一个叶子结点,将字符频次作为字符结点权值,构造哈夫曼树(故哈夫曼编码也不唯一),即可得到赫夫曼编码(可用于数据压缩)。

总结

代码附录

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值