数据结构笔记——第五章 树与二叉树

目录

5 树与二叉树

5.1 树的基本概念

5.1.1 树的定义和基本术语

5.1.2 树的性质

5.2 二叉树的概念

5.2.1 二叉树的定义和基本术语

5.2.2 二叉树的性质

5.2.3 二叉树的存储结构

5.3 二叉树的遍历和线索二叉树

5.3.1 二叉树的先中后序遍历

5.3.2 二叉树的层次遍历

5.3.3 由遍历序列构造二叉树

5.3.4 线索二叉树的概念

5.3.5 二叉树的线索化

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

5.4 树,森林

5.4.1 树的存储结构

5.4.2 树和森林的遍历

5.5 树与二叉树的应用

5.5.1 二叉排序树

5.5.2 平衡二叉树

5.5.3 哈夫曼树


5 树与二叉树

5.1 树的基本概念

5.1.1 树的定义和基本术语

树的基本概念

空树:节点数为0的树

非空树的特性:

1.有且仅有一个根结点

2.没有后继的结点称为“叶子结点”(或终端结点)

3.有后继的结点称为“分支结点”(或非终端结点)

4.除了根节点外,任何一个结点都有且仅有一个前驱

5.每个结点可以有0个或多个后继

树是n个结点的有限集合,n=0时,称为空树,这是一种特殊情况。在任意一棵非空树中应满足:

1.有且仅有一个特定的称为根的结点

2.当n>1时,其余的结点可分为m个互不相交的有限集合,其中每个集合本身又是一棵树,并且称为根结点的子树

两个结点之间的路径:只能从上往下

属性:

结点的层次(深度)——从上往下数

结点的高度——从下往上数

数的高度(深度)——总共多少层

结点的度——有几个分支

数的度——各结点的度的最大值

有序树:逻辑上看,数中结点的各子树从左至右是有序的,不能互换

无序树:逻辑上看,数中结点的各子树从左至右是无序的,可以互换

选择谁具体看用数存什么,是否需要用结点的左右位置反映某些逻辑关系

森林:是m棵互不相交的数的集合

5.1.2 树的性质

结点数=总度数+1

度为m的树和m叉树的区别:

前者至少有一个结点度=m,后者允许所有结点的度都

度为m的树第i层至多有m^{i-1}个结点。

5.2 二叉树的概念

5.2.1 二叉树的定义和基本术语

二叉树是n个结点的有限集合:

1.或者为空二叉树,即n=0

2.或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。

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

几个特殊的二叉树:

1.满二叉树:

一棵高度为h,且含有2^{h}-1个结点的二叉树。

特点:只有最后一层有叶子结点,不存在度为1的结点

按层序 从1开始编号,结点i的左孩子为2i,有孩子为2i+1,结点i的父节点为i/2(如果有的话)

2.完全二叉树:

当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树

特点:只有最后两层肯可能有叶子结点,最多只有一个度为1的结点。i<=n/2为分支结点,i>n/2为叶子结点

3.二叉排序树:

一棵二叉树或者是空二叉树,或者是具有如下特性的二叉树

左子树上所有结点的关键字均小于根结点的关键字

右子树上所有结点的关键字均大于根结点的关键字

左子树和右子树又各是一棵二叉排序树

4.平衡二叉树:

树上任一结点的左子树和右子树的深度之差不超过1

5.2.2 二叉树的性质

1.设非空二叉树中度为0,1,2的结点个数分别为n0,n1,n2,则n0=n2+1(叶子结点比二分支结点多一个)

2.二叉树第i层至多有2^{i-1}个结点,m叉树第i层至多有m^{i-1}个结点

3.高度为h的二叉树至多有2^{h}-1个结点(满二叉树)

高度为h的m叉树至多有\frac{m^{h}-1}{m-1}个结点

4.具有n个结点的完全二叉树的高度h为[log_{2}(n+1)][log_{2}n]+1

5.对于完全二叉树,可以由的结点数n推出度为0,1,2的结点个数为n0,n1和n2

5.2.3 二叉树的存储结构

顺序存储

#define Maxsize 100;
struct TreeNode {
	ElemType value;//结点中的数据元素
	bool isEmpty;//结点是否为空
};

几个重要的基本操作:

i的左孩子——2i

i的右孩子——2i+1

i的父节点——[i/2]

i所在的层次—— [log_{2}(n+1)][log_{2}n]+1

若完全二叉树中共有n个结点,则

判断i是否有左孩子?——2i<=n?

判断i是否有右孩子?——2i+1<=n?

判断i是否是叶子/分支结点?——i>[n/2]?

二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来

结论:二叉树的顺序存储结构,只适合存储完全二叉树

链式存储

//二叉树的结点(链式存储)
typedef struct BiTNode {
	ElemType data;//数据域
	struct BiTNode* lchild, * rchild;//左右孩子指针
}BiTNode, * BiTree;

n个结点的二叉链表共有n+1个空链域,可以用于构造线索二叉树

初始化一个二叉树

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

//插入根节点
root = (BiTree)malloc(sizeof(BiTNode));
root->data = { 1 };
root->lchild = NULL;
root->rchild = NULL;

//插入新结点
BiTNode* p = (BiTree*)malloc(sizeof(BiTNode));
p->data = { 2 };
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p;//作为根节点的左孩子

找到指定结点p的左右孩子很简单,但是如果要找到指定结点p的父结点,只能从根开始遍历寻找

可以添加一个父结点指针——三叉列表,用来找父结点

5.3 二叉树的遍历和线索二叉树

5.3.1 二叉树的先中后序遍历

遍历:按照某种次序把所有结点都访问一遍

层次遍历:基于树的层次特性确定的次序规则

先中后序遍历:基于树的递归特性确定的次序规则

二叉树的递归特性:

1.要么是个空二叉树

2.要么就是由根节点+左子树+右子树组成的二叉树

先序遍历:根左右

中序遍历:左根右

后序遍历:左右根

先序遍历:

//先序遍历
void PreOrder(BiTree T) {
	if (T != NULL) {
		visit(T);//访问根结点
		PreOrder(T->lchild);//递归遍历左子树
		PreOrder(T->rchild);//递归遍历右子树
	}
}

中,后序遍历只需将visit()函数调整位置即可

树的递归走法:

脑补空结点,从根节点出发,画一条路:

如果左边还有没走的路,优先往左边走

走到路的尽头(空结点)就往回走

如果左边没路了,就往右走

如果左右都没路了,则往上走

先序遍历——第一次路过时访问结点

5.3.2 二叉树的层次遍历

算法思想:

1.初始化一个辅助队列

2.根结点入队

3.若队列非空,则头结点出队,访问该结点,并将其左,右孩子插入队尾(如果有的话)

4.重复3直至队列为空

代码实现:

//二叉树的结点(链式存储)
typedef struct BiTNode {
	ElemType data;//数据域
	struct BiTNode* lchild, * rchild;//左右孩子指针
}BiTNode, * BiTree;
//链式队列结点
typedef struct LinkNode {
	BiTNode* data;
	struct LinkNode* next;
}LinkNode;

typedef struct {
	LinkNode* front, * rear;//队头队尾
}LinkQueue;

//层序遍历
void LevelOrder(BiTree T) {
	LinkQueue Q;
	InitQueue(Q);//初始化辅助队列
	BiTree p;
	EnQueue(Q, T);//将根结点入队
	while (!IsEmpty(Q)) {//队列不空则循环
		DeQueue(Q, p);//队头结点出队
		visit(p);//访问出队结点
		if (p->lchild != NULL)
			EnQueue(Q, p->lchild);//左孩子入队
		if (p->rchild != NULL)
			EnQueue(Q, p->rchild);//右孩子入队
	}
}

5.3.3 由遍历序列构造二叉树

不同二叉树的中序遍历序列

一个前/中/后/层序遍历序列可能对应多种二叉树状态

如果给出中序+任意一种其他的遍历序列,就可得出与之对应的唯一的二叉树

key:找到树的根节点,并根据中序序列划分 左右子树,再找到左右子树根节点

5.3.4 线索二叉树的概念

中序线索二叉树

指向前驱,后继的指针称为“线索”

线索二叉树的数据结构

//线索二叉树结点
typedef struct ThreadNode {
	ElemType data;//数据域
	struct ThreadNode* lchild, * rchild;//左右孩子指针
	int ltag, rtag;//左右线索标志
}ThreadNode, * ThreadTree;

三种线索二叉树的对比

先序线索二叉树——线索指向先序前驱,先序后驱

中序线索二叉树——线索指向中序前驱,中序后驱

后序线索二叉树——线索指向后序前驱,后序后驱

5.3.5 二叉树的线索化

用土办法找到中序前驱

//中序遍历
void findPre(BiTree T) {
	if (T != NULL) {
		findPre(T->lchild);
		visit(T);
		findPre(T->rchild);
	}
}
//访问结点q
void visit(BiTNode* q) {
	if (q == p)//当前访问结点刚好是结点p
		final = pre;//找到p的前驱
	else
		pre = q;//pre指向当前访问的结点
}

//辅助全局变量,用于查找结点p的前驱
BiTNode* p;//p指向目标结点
BiTNode* pre = NULL;//指向当前访问结点的前驱
BiTNode* final = NULL;//用于记录最终结果

中序线索化

//中序遍历
void InThread(BiTree T) {
	if (T != NULL) {
		InThread(T->lchild);
		visit(T);
		InThread(T->rchild);
	}
}
void visit(ThreadNode* q) {
	if (q->lchild == NULL) {//左子树为空,建立前驱线索
		q->lchild = pre;
		q->ltag = 1;
	}
	if (pre != NULL && pre->rchild == NULL) {
		pre->rchild = q;//建立前驱结点的后继线索
		pre->rtag = 1;
	}
	pre = q;
}

//全局变量pre指向当前访问结点的前驱
ThreadNode* pre = NULL;

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

先/后序线索化代码以中序线索化为参照,只需修改Inthread()函数里面语句的顺序即可

线索化的核心:

1.中序/先序/后续遍历算法的改造,当访问一个结点时,连接该结点与前驱结点的线索信息

2.用一个指针pre记录当前访问结点的前驱结点

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

中序线索二叉树找中序后继

//找到以p为根的子树中,第一个被中继遍历的结点
ThreadNode* Firstnode(ThreadNode* p) {
	//循环找到最左下结点(不一定时叶结点)
	while (p->ltag == 0)
		p = p->lchild;
	return p;
}
//在中序线索二叉树中找到结点p的后续结点
ThreadNode* Nextnode(ThreadNode* p) {
	//右子树中最左下结点
	if (p->rtag == 0)
		return Firstnode(p->rchild);
	else
		return p->rchild;//rtag==1直接返回后继线索
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode* T) {
	for (ThreadNode* p = Firstnode(T); p != NULL; p = Nextnode(p))
		visit(p);
}

中序线索二叉树找中序前驱

//找到以p为根的子树中,最后一个被中继遍历的结点
ThreadNode* Lastnode(ThreadNode* p) {
	//循环找到最右下结点(不一定时叶结点)
	while (p->rtag == 0)
		p = p->rchild;
	return p;
}
//在中序线索二叉树中找到结点p前驱结点
ThreadNode* Prenode(ThreadNode* p) {
	//右子树中最右下结点
	if (p->ltag == 0)
		return Lastnode(p->lchild);
	else
		return p->lchild;//rtag==1直接返回前驱线索
}
//对中序线索二叉树进行逆向中序遍历
void Revorder(ThreadNode* T) {
	for (ThreadNode* p = Lastnode(T); p != NULL; p = Prenode(p))
		visit(p);
}

先序线索二叉树找先序后继:

若p有左孩子,则先序后继为左孩子

若p没有左孩子,则先序后继为右孩子

先序线索二叉树找先序前驱:

先序遍历中,左右子树中的结点只可能是根的后续,不可能是前驱,除非从头开始遍历查找

1.如果能找到p的父节点,且p是左孩子,那么p的父节点即为其前驱

2.如果能找到p的父节点,且p是右孩子,其左兄弟为空,那么p的父节点即为其前驱

3.如果能找到p的父节点,且p是右孩子,其左兄弟非空,那么p的前驱为左兄弟子树中最后一个被先序遍历的结点

4.如果p是根节点,则p没有先序前驱

后续线索二叉树找后序前驱:

1.若p结点有右孩子,则后序前驱为右孩子

2.若p结点没有右孩子,那么后序前驱为左孩子

后续线索二叉树找后序后继:

后序遍历中,左右子树中的结点只可能是根的前驱,不可能是后继,除非从头遍历

如果能找到p的父节点

1.且p是右孩子,则p的父节点为其后继

2.且p是左孩子,其右兄弟为空,则p的父节点为其后继

3.且p是左孩子,其右兄弟非空,则p的后继为右兄弟子树中第一个被后序遍历的结点

4.且p是根结点,则p没有后序后继

5.4 树,森林

5.4.1 树的存储结构

双亲表示法:每个结点中保存指向双亲的“指针”

#define MAX_TREE_SIZE 100//树中最多结点数
typedef struct {//树的结点定义
	ElemType data;//数据元素
	int parent;//双亲位置域
}PTNode;
typedef struct {//树的类型定义
	PTNode nodes[MAX_TREE_SIZE];//双亲表示
	int n;//结点数
}PTree;

孩子表示法(顺序+链式存储)

struct CTNode {
	int child;//孩子结点在数组中的位置
	struct CTNode* next;//下一个孩子
};
typedef struct {
	Elemtype data;
	struct CTNode* firstChild;//第一个孩子
}CTBox;
typedef struct {
	CTBox nodes[MAX_TREE_SIZE];
	int n, r;//结点数和根的位置
}CTree;

孩子兄弟表示法(链式存储)

typedef struct CSNode {
	ElemType data;
	struct CSNode* firstchild, * nextsibling;
}CSNode,*CSTree;

森林的二叉树的转换

森林是m棵互不相交的树的集合

可以将各个树的根节点视为兄弟关系

5.4.2 树和森林的遍历

树的先根遍历:

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

树的后根遍历同理

树的层次遍历:

1.若树非空,则根结点入队

2.若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队

3.重复2直到队列为空

森林的先序遍历:

若森林为非空,则按如下规则进行遍历:

访问森林中第一棵树的根结点。

先序遍历第一棵树中根结点的子树森林

先序遍历除去第一棵树之后剩余的树构成的森林

先序遍历除去第一棵树之后剩余的树

森林的中序遍历:效果等同于依次对二叉树的中序遍历

5.5 树与二叉树的应用

5.5.1 二叉排序树

二叉排序树,又称二叉查找树(BST)

一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:

左子树上所有结点的关键字均小于根结点的关键字;

左子树上所有结点的关键字均大于根结点的关键字。

左子树和右子树又各是一棵二叉排序树。

typedef struct BSTNode {
	int key;
	struct BSTNode* lchild, * rchild;
}BSTNode,*BSTNode;

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

递归实现

//在二叉排序树中查找值为key的结点(递归实现)
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
		return BSTSearch(T->rchild, key);//在右子树中查找
	else
}

二叉排序树的插入

//在二叉排序树中插入值为k的结点(递归实现)
BSTNode* BST_Insert(BSTree T, int k) {
	if (T == NULL) {//原树为空,新插入的结点为根结点
		T = (BSTree)malloc(sizeof(BSTNode));
		T->key = k;
		T->lchlid = T->rchild = NULL;
		return 1;//返回1,插入成功
	}
	else if (k == T->key)//树中存在相同关键字的结点,插入失败
		return 0;
	else if (k < T->key)
		return BST_Insert(T->lchild, k);//插入到T的左子树
	else
		return BST_Insert(T->rchild, k);//插入到T的右子树
}

二叉排序树的构造

//按照str[]中的关键字序列建立二叉排序树
void Creat_BST(BSTree& T, int str[], int n) {
	T = NULL;//初始时T为空树
	int i = 0;
	while (i < n) {//依次将每个关键字插入到二叉排序树中
		BST_Insert(T, str[i]);
		i++;
	}
}

二叉排序树的删除

先搜索找到目标结点:

1.若被删除结点是z结点,则直接删除,不会破坏二叉排序树的性质。

2.若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置。

3.若结点z有左,右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转变成了第一或第二种情况。

查找效率分析

查找长度,在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度

5.5.2 平衡二叉树

平衡二叉树的定义

平衡二叉树,简称平衡树(AVL树)——树上任一结点的左子树和右子树的高度之差不能超过1

结点的平衡因子=左子树高-右子树高,值只可能为-1,0,1

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

平衡二叉树的插入

在插入操作中,只要将最小不平衡子树调整平衡,则其他祖先结点都会恢复平衡

调整最小不平衡子树

1.LL平衡旋转(右单旋转)。由于在结点A的左孩子的左子树上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次向右的旋转操作。将A的左孩子B向右上旋转代替A成为根结点,将A结点向右下旋转成为B的右子树的根结点,而B的原右子树则作为A结点的左子树

2.RR平衡旋转(左单旋转)

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

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

查找效率分析

平衡二叉树的平均查找长度为O(log_{2}n)

5.5.3 哈夫曼树

结点的权:有某种现实意义的数值(如:表示结点的重要性等)

结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积

数的带权路径长度:树中所有叶结点的带权路径长度之和

在含有n个带权叶结点的二叉树中,其中带权路径长度最小的二叉树称为哈夫曼树,也称最优二叉树

给定几个叶子结点,构造哈夫曼树的算法描述如下:

1.将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F

2.构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左,右子树,并且将新结点的权值置为左,右子树上根结点的权值之和。

3.从F中删除刚才选出的两棵树,同时将新得到的树加入到F中

4.重复步骤2和3,直到F中只剩下一棵树为止。

每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。

哈夫曼树的结点总数为2n-1

哈夫曼树中并不存在度为1的结点

哈夫曼树并不唯一,但WPL必然相同且为最优

哈夫曼编码:

可变长度编码——允许对不同字符用不等长的二进制表示

若没有一个编码是另一个编码前缀,则称这样的编码为前缀编码

由哈夫曼树得到的哈夫曼编码——字符集中的每个字符作为一个叶子结点,各个字符出现的频率作为结点得到权值,根据之前介绍的方法构造哈夫曼树

你可能感兴趣的:(数据结构与算法,算法,数据结构)