数据结构-树与二叉树

目录

一、树的性质

1、易错术语

2、基本性质

二、二叉树

1、几种特殊二叉树

2、基本性质

3、存储结构

三、二叉树的遍历

1、递归遍历

2、非递归遍历

3、层次遍历

4、出题

四、线索二叉树

1、存储结构

2、中序线索二叉树的构造

3、中序线索二叉树的遍历

五、树、森林

1、树的存储结构

1.1、双亲表示法

1.2、孩子表示法

1.3、孩子兄弟表示法

2、树、森林、二叉树的转换

2.1、树转换为二叉树

2.2、森林转换为二叉树

2.3、二叉树转换为森林

3、树和森林的遍历

3.1、树-先根遍历

3.2、树-后根遍历

3.3、森林-先序遍历

3.4、森林-中序遍历

3.5、森林与二叉树遍历方法对应关系

六、树与二叉树的应用

1、哈夫曼树和哈夫曼编码

1.1、定义

1.2、哈夫曼树的构造

1.3、性质

1.4、哈夫曼编码

2、并查集

2.1、作用

2.2、存储结构

2.3、实现的优化


一、树的性质

1、易错术语

1.1  树的度:节点的最大度数

1.2  节点的深度:节点所在层次

1.3  树的高度/深度:节点的最大层数

1.4  节点的高度:以该节点为根的子树的高度

1.5  节点间路径长度:路径上所经过的边的个数

1.6  树的路径长度:从树根到每个节点的路径长度的总和

1.7  结点的带权路径长度:从树的根到一个结点的路径长度与该结点上权值的乘积

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

1.9  编码加权平均长度:WPL/结点频次之和

2、基本性质

2.1  n个节点的树有n-1条边   (每个节点上面都有一条边,除去根)

2.2  树的节点数n等于所有节点的度数之和+1  (每个节点上面都有一个度,除去根)(度之和=边之和)

2.3  度为m的树中第i层上至多有m^{i-1}个节点

2.4  高度为h的m叉树至多有(m^{h}-1)/(m-1)个节点(满,等比数列);至少有h个

2.5  度为m,具有n个节点的树的最小高度为\left \lceil log_m{n(m-1)+1} \right \rceil

2.6  度为m,具有n个节点的树的最大高度为n-m+1

2 7  一颗m叉树有N1个度数为1的节点,N2个度数为2的节点....Nm个度数为m的节点,则该树中共有\sum_{i=2}^{m}(i-1)N_{i}+1个叶节点  (出度N1+2N2+...+mNm+1=入度N1+N2+...Nm+x)

2.8  一棵树对应二叉树的分支数=叶节点数=有兄弟节点数+1

二、二叉树

1、几种特殊二叉树

1.1  满二叉树:对于编号i的节点,其双亲为\left \lfloor i/2 \right \rfloor.,左孩子为2,右孩子为2i+1

1.2  完全二叉树:编号按顺序一一对应                           

1.3  二叉排序树:左<根<右

1.4  平衡二叉树:树中任意一个节点的左子树和右子树的高度之差的绝对值不超过1

1.5  正则二叉树:每个分支节点都有2个孩子 

2、基本性质

2.1  非空二叉树:n0=n2+1

2.2  完全二叉树:若i<=\left \lfloor n/2 \right \rfloor,则i为分支节点,否则为叶节点。因此叶节点比非叶节点多1/相等

                            若有度为1的节点,则最多只有一个

                            一旦出现i为叶节点或只有左孩子,则大于i的节点均为叶节点

                             n为奇数时,则每个分支节点都有左右孩子

                             当i>1时,节点i的双亲为\left \lfloor i/2 \right \rfloor

                             节点i所在层次为\left \lfloor log_2{i} \right \rfloor+1

                             具有n个节点的完全二叉树的高度为\left \lceil log_2{(n+1)} \right \rceil\left \lfloor log_2{n} \right \rfloor+1

2.3  一颗有n个节点的满二叉树有(n-1)/2个分支节点,(n+1)/2个叶节点

3、存储结构

//顺序存储
#define Maxsize 100
struct TreeNode {
	int value;
	bool isEmpty;
};
//初始化
void Init(TreeNode t[]) {
	for (int i = 0; i < Maxsize; i++)
		t[i].isEmpty = true;
}
//链式存储
typedef struct BiTNode{
	int data;
	struct BiTNode* lchild, * rchild;
}BiTNode,*BiTree

重要结论:在含有n个节点的二叉链表中,含有n+1个空指针域

三、二叉树的遍历

1、递归遍历

void Preorder(BiTree T) {//前序遍历
	if (T) {
		visit(T);
		Preorder(T->lchild);
		Preorder(T->rchild);
	}
}
void Inorder(BiTree T) {//中序遍历
	if (T) {
		Inorder(T->lchild);		
		visit(T);
		Inorder(T->rchild);
	}
}
void Postorder(BiTree T) {//后序遍历
	if (T) {
		Postorder(T->lchild);
		Postorder(T->rchild);
		visit(T);
	}
}

//求树高
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;
	}
}

时间复杂度:O(n),递归工作栈的栈深=树的深度

若要删除二叉树表的所有结点并释放占用的存储空间,后序遍历方法更合适。因为删除一个结点时,需要先递归地删除左右孩子,并释放他们的占用存储空间,再然后删除该结点。

2、非递归遍历

以中序遍历为例:

        ①沿着根的左孩子,依次入栈,直到左孩子为空

        ②栈顶元素出栈访问;若其右孩子为空,重新执行②,也就是往上退。若右孩子不空,将右孩子转为①

void Inorder(BiTree T) {
	InitStack(S);
	BiTree p = T;
	while (p || !IsEmpty(S)){ //树不空且栈不空
		if (p) {  //当前节点不空,那么就入栈,并往左走
			push(S, p);
			p = p->lchild;
		}
		else { //一直走到了左尽头,先出栈,并访问,转为右走
			pop(S, p);
			visit(p);
			p = p->rchild;
		}
	}
}
void Preorder(BiTree T) {
	InitStack(S);
	BiTree p = T;
	while (p || !IsEmpty(S)) { //树不空且栈不空
		if (p) {
			visit(p);	
			push(S, p);
			p = p->lchild;
		}
		else { 
			pop(S, p);
			p = p->rchild;
		}
	}
}

在后序非递归遍历中:访问某个节点p,此时栈中节点恰好是p的所有祖先,从栈底到栈顶再加上p,刚好构成从根节点到p的一条路径、因此后序非递归遍历可以求根到某节点的路径,求两个节点的最近公共祖先等

3、层次遍历

借助队列

        ①根节点入队

        ②若队列不空,则队头节点出队,访问该节点,若有左孩子,左孩子入队;若有右孩子,右孩子入队。重复直至队列为空

void LevelOder(BiTree T) {
	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);
}

4、出题

比如说:先序序列为abcd的不同的二叉树的个数是多少=以abcd入栈次序,则出栈序列的个数为多少。即公式:\frac{1}{n+1}C_{2n}^{n}前序序列和中序序列的关系相当于以前序序列为入栈次序,以中序序列为出栈次序。而可以唯一确定一棵二叉树。

因为:

四、线索二叉树

目的:加快查找结点前驱和后继的速度

二叉树是一种逻辑结构,但线索二叉树是加上线索后的链表结构,即它是二叉树在计算机内部的一种存储机构,是物理结构

规定:若无左子树,lchild指向前驱(此操作是在p指向当前结点时进行);若无右子树,rchild指向后继(此操作是在pre指向当前结点的时候进行)

1、存储结构

//存储结构
typedef struct ThreadNode{
	int data;
	int ltag, rtag;
	struct ThreadNode *lchild, *rchild;
}ThreadNode, *ThreadTree;

2、中序线索二叉树的构造

线索化的实质就是遍历一遍二叉树

在写代码的时候可以画一棵树来模拟一下

void InThread(ThreadTree &p, ThreadTree &pre) {
	if (p) {
		InThread(p->lchild, pre);
		if (p->lchild == NULL) {
			p->lchild = pre;
			p->ltag = 1;
		}
		if (pre != NULL && pre->rchild == NULL) {
			pre->rchild = p;
			pre->rtag = 1;
		}
		pre = p;
		InThread(p->rchild, pre);
	}
}
void CreatInThread(ThreadTree T) {
	ThreadTree pre = NULL;
	if (T != NULL) {
		InThread(T, pre);
		pre->rchild = NULL;//处理最后一个结点
		pre->rtag = 1;
	}
}

如何找前驱:p==x时,pre为x的前驱   pre==x时,q为x的后继

3、中序线索二叉树的遍历

①找到遍历的第一个结点

②找后继:若其右标志为1,则右链为线索,指向后继;否则,遍历右子树中第一个访问的结点为后继。

ThreadNode* FirstNode(ThreadNode* p) {
	while (p->ltag == 0)p = p->lchild;
	return p;
}
//找遍历的最后结点
ThreadNode* LastNode(ThreadNode* p) {
	while (p->rtag == 0)p = p->rchild;
	return p;
}
//找结点p的后继
ThreadNode* Nextnode(ThreadNode* p) {
	if (p->rtag == 0)return FirstNode(p->rchild);//右子树中遍历的第一个结点为其后继
	else return p->rchild;
}
//找结点p的前驱
ThreadNode* Prenode(ThreadNode* p) {
	if (p->ltag == 0)return LastNode(p->lchild);//左子树中遍历的最后一个结点为其前驱
	else return p->lchild;
}
void Inorder(ThreadNode* T) {
	for (ThreadNode* p = FirstNode(T); p; p = Nextnode(p))
		visit(p);
}

同上,编写代码时可以画树模拟

在先序线索二叉树中,若有左孩子,左孩子就是后继;若无左孩子但是有右孩子,右孩子为后继;若为叶节点,其右链指向后继。但查找前驱需要直到该结点的双亲

在后序线索二叉树中,找后继需要直到结点的双亲。后序线索树的遍历需要栈的支持

五、树、森林

1、树的存储结构

1.1、双亲表示法

每个结点增加一个parent域,表示双亲结点在数组中规定位置

规定:根结点的下标为0,parent域为-1

//双亲表示法
#define Max_Tree_Size 100
typedef struct { 
	int data;
	int parent;
}PTNode;
typedef struct {  //树的定义
	PTNode nodes[Max_Tree_Size];
    int n; //结点数
}PTree;

此方法可以很快地找到结点的双亲,但求结点的孩子时要遍历整个结构(并查集)

注意:二叉树可以用树的存储结构来存储,但树不能都用二叉树的存储结构来存储

1.2、孩子表示法

将每个结点的孩子排成一个单链表,n个结点就有n个孩子链表。n个头指针组成一个线性表

//孩子表示法
struct CTNode{  //孩子链表结点的定义
	int child;  //孩子结点在数组中的位置
	struct CTNode* next;
};
typedef struct {  //头指针的定义
	int data;
	struct CTNode* FirstChild;
}CTBox;
typedef struct {  //树的类型定义
	CTBox nodes[Max_Tree_Size];
	int n, r; //结点数和根的位置
}CTree;

寻找孩子的操作非常方便,而寻找双亲的操作则需要遍历n个结点中孩子链表的指针域所指向的n个孩子链表

1.3、孩子兄弟表示法

//孩子兄弟表示法
typedef struct CSNode {
	int data;
	struct CSNode* firstchild, * nextsibling; //指向结点第一个孩子的指针,指向结点下一个兄弟结点的指针
}CSNode,*CSTree;

优点:可以方便地实现树转换为二叉树的操作,易于查找结点的孩子。但找双亲比较麻烦。

在森林中,根结点看作平级的兄弟

2、树、森林、二叉树的转换

2.1、树转换为二叉树

                        左孩子右兄弟,注:树转换为二叉树没有右子树

2.2、森林转换为二叉树

                        把每一棵树转换为二叉树,而树的根为平级的兄弟。相连即可

2.3、二叉树转换为森林

                        二叉树的右子树的右边均是各树的头节点。二叉树转换为树或森林是唯一的

3、树和森林的遍历

3.1、树-先根遍历

                        先根,再依次遍历根节点的每棵子树。其遍历序列与对应二叉树的先序序列相同

3.2、树-后根遍历

                        先依次遍历根结点的每棵子树,再访问根节点、其遍历序列与对应二叉树的中序                              序列相同

3.3、森林-先序遍历

                        先访问森林中第一树根根节点,对该树先序遍历,结束后,遍历下一课树

3.4、森林-中序遍历

                        中序遍历第一棵树的根节点的子树,再访问第一棵树的根节点,依次-

3.5、森林与二叉树遍历方法对应关系

在森林转换为二叉树时,第一棵树的子树是二叉树的左子树,剩余树为二叉树的右子树。所以森林的先序和中序遍历对应其二叉树的先序和中序遍历。

树和森林的后根=二叉树的中序

六、树与二叉树的应用

1、哈夫曼树和哈夫曼编码

1.1、定义

                带权路径长度(WPL)最小的二叉树,也称最优二叉树

1.2、哈夫曼树的构造

                每次选出两个权值最小的结点或树,进行结合,新结点的权值为左右子树上根节点的权值之和,重复

1.3、性质

               ①每个初始结点都称为叶节点,且权值越小的结点到根节点的路径长度越大

                ②构造过程中新建了n-1个结点,因此哈夫曼树的总结点数为2n-1

                ③不存在度为1的结点

                ④度为m的哈夫曼树,叶子节点个数为n,则非叶子结点的个数为\left \lceil (n-1)/(m-1) \right \rceil

                        结点总数为N=n0+nm ,而因N个结点的哈夫曼树有N-1个分支,则m*nm=N-                        1=nm+n0-1 整理可得

1.4、哈夫曼编码

固定长度编码:对每个字符用相等长度的二进制位表示

可变长度编码:对不同字符用不等长的二进制位表示(压缩数据。对频率高的字符赋以短编码,反之长一些的编码,使字符的平均编码长度减短)

前缀编码:没有一个编码是另一个编码的前缀---二叉树设计二进制前缀编码

哈夫曼编码:数据压缩编码;权值为频率,构造相应哈夫曼树,然后将从根到叶节点的路径上分支标记的字符串作为该字符的编码

2、并查集

2.1、作用

①判断连通性,判断环

②Krnskal算法:排序+并查集

2.2、存储结构

通常用树的双亲表示法作为存储结构

数组下标为元素名称,数组内容为双亲下标。根结点的下标代表子集合名

2.2、基本实现

#define SIZE 100
int UPSets[SIZE];
//初始化
void Initial(int S[]) {
	for (int i = 0; i < SIZE; i++)
		S[i] = -1;
}
//在并查集S中查找并返回包含元素X的根
int Find(int S[], int x) { //数组元素的下标代表元素名
	while (S[x] >= 0)   //S[x]双亲的下标
		x = S[x];  //一层一层往上找
	return x;
}
//把集合S中的子集合ROOT2并入子集合ROOT1,要求两者不相交
void Union(int S[], int Root1, int Root2) {
	if (Root1 == Root2)return;
	S[Root2] = Root1; //将根Root2连接到另一根Root1下面
}

Find和Union的时间复杂度分别为O(d),O(1) ,D为树的深度

2.3、实现的优化

①极端情况下,并查集可能是一条线的形式,改进:把小树合并到大树,令根节点的绝对值保存集合树种的成员数量

void Union(int S[], int Root1, int Root2) {
	if (Root1 == Root2)return;
	if (S[Root1] > S[Root2]) {
		S[Root1] += S[Root2];
		S[Root2] = Root1;
	}
	else {
		S[Root2] += S[Root1];
		S[Root1] = Root2;
	}
}

树的高度不超过\left \lfloor log_2{n} \right \rfloor+1,其中Find为O(log_2{n}),Union为O(1)

②压缩路径,为进一步减少确定元素所在集合的时间,把根从元素x路径上的元素都变成根的孩子

int Find(int S[], int x) {
	int root = x;
	while (S[root] >= 0)
		root = S[root];//找到根
	while (x != root) {//压缩路径
		int t = S[x]; //t为x的父节点
		S[x] = root; //将x挂到根节点下面
		x = t;  //进行上一层的路径压缩
	}
	return root;
}

压缩后,集合树的深度不超过O(a(n)),通常a(n)<=4

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值