王道数据结构第五章树与二叉树(代码及笔记)

第五章树与二叉树

    • 基本概念
    • 基本术语
    • 树的性质
  • 二叉树
    • 二叉树概念
    • 特殊二叉树
    • 二叉树性质
    • 存储结构
  • 二叉树遍历
    • 先序遍历<根左右>
    • 中序遍历<左根右>
    • 后序遍历<左右根>
    • 层序遍历
    • 遍历序列构造二叉树
  • 线索二叉树
  • 二叉排序树
    • 查找效率分析
  • 平衡二叉树
  • 树的存储结构
    • 双亲表示法(顺序存储)
    • 孩子表示法(顺序+链式存储)
    • 孩子兄弟表示法
    • 森林和二叉树的转换
  • 树、森林的遍历<写出序列>
    • 树的遍历
      • 先根遍历<深度优先遍历>
      • 后根遍历<深度优先遍历>
      • 层次遍历(队列实现)<广度优先遍历>
    • 森林的遍历
      • 先序遍历
      • 中序遍历
  • 哈夫曼树
    • 概念
    • 哈夫曼树的构造
    • 哈夫曼编码
  • 并查集

基本概念

树是一种递归定义的数据结构

非空树的特性
有且仅有一个根节点
除根节点以外,任何节点都有且仅有一个前驱
每个节点可以有0个或多个后继
有序树:节点的各个子树从左至右是有次序的
无序树:节点的各个子树从左至右是无次序的
森林:互不相交的树的集合

基本术语

叶子节点<终端节点>:度为0的节点
分支节点<非终端节点>:度大于0的节点
祖先节点:该节点到根节点路径上的所有节点都是
子孙节点:该分支下的节点都是
双亲节点<父节点>:直接前驱
孩子节点:直接后继
兄弟节点:有相同前驱的节点互称为兄弟节点
堂兄弟节点:同一层的节点互称
路径:(只能从上往下)
路径长度:经过几条边(路径上边的条数)
节点的层次<深度>:从上往下数(默认从1开始)
节点的高度:从下往上数
树的高度<深度>:总共多少层
节点的度:有几个分支
树的度:各节点的度的最大值

树的性质

度为m的树m叉树
至少有一个节点度为m允许所有节点的度都小于m
一定是非空树,至少有m+1个节点可以是空树
任意节点的度≤m任意节点的度≤m

1.节点数 = 总度数 + 1
2.度为m的树,N = 在这里插入图片描述
2.度为m的树第i层最多有个1在这里插入图片描述个节点
3.m叉树第i层最多有在这里插入图片描述个节点
4.高度为h的m叉树最多有在这里插入图片描述个节点
5.高度为h度为m的树最少有h-1+m个节点
6.高度为h的m叉树最少有h个节点
7.具有n个节点的m叉树的最小高度为在这里插入图片描述
证明:在这里插入图片描述

二叉树

二叉树概念

特殊二叉树

满二叉树:
一个高度为h,且含有在这里插入图片描述个节点的二叉树
特性:
①只有最后一层有叶子节点
②不存在度为1的节点
③按层序从1开始,节点i的左孩子为2i,右孩子为2i+1,父节点为在这里插入图片描述

完全二叉树:
特性:
①只有最后两层可能有叶子节点
②最多只有一个度为1的节点
③按层序从1开始,节点i的左孩子为2i,右孩子为2i+1,父节点为 在这里插入图片描述
④i ≤ 在这里插入图片描述 为分支节点,i> 在这里插入图片描述 为叶子节点

二叉排序树(功能上特殊):用于元素的排序、搜索
左子树上所有节点的关键字均小于根节点的关键字
右子树上所有节点的关键字均大于根节点的关键字

**平衡二叉树:**有更高的搜索效率
树上任意节点的左子树和右字数的深度之差不超过1

二叉树性质

①设非空二叉树中度为0、1和2的节点个数分别为n0、n1和n2,则
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述一定为奇数

②高度为h的二叉树至多有在这里插入图片描述个节点;高度为h的m叉树至多有在这里插入图片描述个节点
③具有n个节点的完全二叉树的高度h为在这里插入图片描述在这里插入图片描述
完全二叉树:n1=0或1(最多只有一个度为1的节点)
⑤若完全二叉树有2k(偶数)个节点,则必有n1=1,n0=k,n2=k-1
⑥若完全二叉树有2k-1(奇数)个节点,则必有n1=0,n0=k,n2=k-1

存储结构

  • 顺序存储
#define MaxSize 100
struct TreeNode{
	ElemType value;
	bool isEmpty;
};
TreeNode t[MaxSize];
--初始化--
void InitTreeNode{
	for(int i = 0;i < MaxSize;i++)
		t[i].isEmpty = true;
}
若完全二叉树中共有n个节点则
---

最坏情况:高度为h且只有h个节点的单支树,也至少需要2^h-1个存储单元
故,只适合存储完全二叉树

  • 链式存储
typedef struct BiTNode{
	ElemType data;
	struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
BiTree root = NULL;
n个节点的二叉链表共有n+1个空链域<用于构造线索二叉树>

三叉链表<方便寻找父节点>
typedef struct BiTNode{
	ElemType data;
	struct BiTNode *lchild,*rchild;
	struct BiTNode *parent;
}BiTNode,*BiTree;

二叉树遍历

先序遍历<根左右>

void PreOrder(BiTree T){
	if(T != NULL){
		visit(T);
		PreOrder(T->lchild);
		PreOrder(T->rchild);
	}
}

中序遍历<左根右>

void InOrder(BiTree T){
	if(T != NULL){
		InOrder(T->lchild);
		visit(T);
		InOrder(T->rchild);
	}
}

后序遍历<左右根>

void PostOrder(BiTree T){
	if(T != NULL){
		PostOrder(T->lchild);
		PostOrder(T->rchild);
		visit(T);
	}
}

层序遍历

思想:
初始化一个辅助队列
根节点入队
若队列非空,则队头节点出队,访问该节点,并将其左,右孩子插入队尾

void LevelOrder(BiTree T){
	LinkQueue Q;
	InitQueue(Q);
	BiTree p;
	EnQueue(Q,T);
	while(!isEmpet(Q)){
		DeQueue(Q,p);	//对头结点出队
		visit(p);		//访问出队节点
		if(p->lchild != NULL)
			EnQueue(Q,p->lchild);
		if(p->rchild != NULL)
			EnQueue(Q,p->rchild);
	}
}

遍历序列构造二叉树

若只给出一棵二叉树的前,中,后,层序遍历中的一种,不能唯一确定一棵二叉树
只有以下三种可以确定唯一的二叉树
1.前序+中序:前序中第一个出现的一定是根节点,则对应中序中,左边是左子树,右边是右子树
2.后序+中序:后序中最后一个出现的一定是根节点,对应中序中,左边是左子树,右边是右子树
3.层序+中序:每个层序遍历中的节点,对应中序中,从该节点分两块,分别左和右

线索二叉树

Question:如何找到指定节点p在中序遍历中的前驱
思路:从第一个元素出发,重新进行一次中序遍历,指针q记录当前访问的节点,指针pre记录上一个被访问的节点— --- — 当q == p时,pre为前驱(p为所找元素)
n个节点的二叉树,有n+1个空链域用来记录前驱,后继的信息

线索二叉树的存储结构
typedef struct ThreadNode{
	ElemType data;
	struct ThreadNode *lchild,*rchild;
	int ltag,rtag;		//tag为0时表示指针指向其孩子,为1时表示指针是线索
}ThreadNode,*ThreadTree;
一般方法寻找中序前驱节点
BiTNode *p;
BiTNode *pre = NULL;
BiTNode *final = NULL;
void InOrder(BiTree T){
	if(T != NULL){
		InOrder(T->lchild);
		visit(T);
		InOrder(T->rchild);
	}
}
void visit(BiTNode *q){
	if(q == p)
		final = pre;
	else
		pre = q;
}
中序线索化
typedef struct ThreadNode{
	ElemType data;
	struct ThreadNode *lchild,*rchild;
	int ltag,rtag;
}ThreadNode,*ThreadTree;
//全局变量pre
ThreadNode *pre = NULL;
void CreateInThread(ThreadTree T){
	pre = NULL;
	if(T != NULL){
		InThread(T);	//中序线索化
		if(pre->rchild == NULL)
			pre->rtag = 1;
	}
}
//中序遍历二叉树,一边遍历,一边线索化
void InThread(ThreadTree 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;
}
先序线索化
void CreatePreThread(ThreadTree T){
	pre = NULL;
	if(T != NULL){
		InThread(T);	//中序线索化
		if(pre->rchild == NULL)
			pre->rtag = 1;
	}
}
//中序遍历二叉树,一边遍历,一边线索化
void PreThread(ThreadTree T){
	if(T != NULL){
		visit(T);
		if(T->ltag == 0)		!!!
			PreThread(T->lchild);
		PreThread(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;
}

二叉排序树

相同的序列创建的二叉排序树是唯一的
同一集合创建的二叉排序树是不同的
用二叉树的先序遍历序列创建的二叉排序树与原树相同

节点的删除
1.若只有根节点,则删除的节点就是根节点
2.若是叶节点,直接删除,不会破坏二叉排序树性质
3.若删除节点只有左子树或右子树,则让子树代替自己
4.若删除节点有左子树和右子树,则让左子树最右侧的节点代替自己,然后删除左子树最右侧的节点(让右子树最左侧的节点代替自己,然后删除右子树最右侧的节点)

增加删除查找

查找效率分析

查找成功的平均查找长度(ASL)
查找失败的平均查找长度(ASL)

若二叉排序树高为h,最下层节点的查找长度为h,查找操作的时间复杂度为O(h)
具有n个节点的最小高度为log2(n+1)向上取整,最大高度为n
最好情况平均查找长度=O(log2n)
最坏情况平均查找长度=O(n)

平衡二叉树

  • 树上的任意节点的左子树和右子树之差不超过1
  • 节点的平衡因子=左子树的高度-右子树的高度
  • 最小不平衡子树

从AVL最小不平衡子树出发
四种情况:
左左更高:整棵树向右旋转
左右更高:左子树先向左旋转,整棵树再向右旋转
右左更高:右子树先向左旋转,整棵树再向左旋转
右右更高:整棵树向左旋转
方法:
1.若是外侧的子树更高,只需旋转一次
2.若是内测的子树更高,就需要旋转两次
3.哪边的子树高就往另一个方向旋转,这样子树才能变矮
4.把节点用整数表示,方便判断他在调整的时候应该安排什么位置

平衡二叉树节点数与树高
最少节点数递推公式Ch = Ch-1 +Ch-2 + 1
Ch:1 2 4 7 12 20 33 54 88 143

树的存储结构

双亲表示法(顺序存储)

结构体
#define MAX_TREE_SIZE 100
typedef struct{		//树的节点定义
	ElemType data;
	int parent;
}PTNode;
typedef struct{		//树的类型定义
	PTNode nodes[MAX_TREE_SIZE];
	int n;
}PTree;
#S新增数据元素,无需按逻辑上的次序存储
删除方案:	1.parent置为-1
			2.后面元素填充上去√
优点:查指定节点的双亲很方便,查指定节点的孩子只能从头遍历,空数据导致遍历更慢

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

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{
	ElemType data;
	struct CSNode *firstChild,*nextSibling;  //第一个孩子(看作左指针)和右兄弟指针
}CSNode,*CSTree;

森林和二叉树的转换

  • 本质孩子兄弟法,左孩子右兄弟

树、森林的遍历<写出序列>

树的遍历

先根遍历<深度优先遍历>

void PreOrder(TreeNode *R){
	if(R != NULL){
		visit(R);
		while(R还有下一个子树T)
			PreOrder(T);
	}
}
1.树的先根遍历序列与相应二叉树的先序序列相同

后根遍历<深度优先遍历>

void PostOrder(TreeNode *R){
	if(R != NULL){
		while(R还有下一个子树T)
			PostOrder(T);
		visit(R);
	}
}
1.树的后根遍历序列与相应二叉树的中序序列相同

层次遍历(队列实现)<广度优先遍历>

①若树非空,则根节点入队
②若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队

森林的遍历

先序遍历

  • 效果等同于依次对各个树进行先根遍历
  • 树的先根遍历序列与相应二叉树的先序序列相同

中序遍历

  • 效果等同于依次对各个树进行后根遍历
  • 树的后根遍历序列与相应二叉树的中序序列相同

哈夫曼树

概念

节点的权:有某种现实含义的数值
节点的带权路径长度:从树的根到该节点的路径长度 * 该节点上的权值
树的带权路径长度(WPL):树中所有叶子节点的带权路径长度之和
哈夫曼树(最优二叉树):在含有n个带权叶子节点的二叉树中,带权路径长度(WPL)最小的二叉树

哈夫曼树的构造

选取两棵根节点权值最小的树相结合,作为新的左右子树
并将左右权值之和作为新的根节点权值
1.每个初始节点最终都成为叶节点
2.权值越小的节点到根节点的路径长度越大
3.原始n个节点,结合n-1次,故哈夫曼树的节点总数为2n-1
4.哈夫曼树中不存在度为1的节点
5.哈夫曼树并不唯一,但WPL必然相同且为最优

哈夫曼编码

可变长度编码:允许对不同字符用不等长的二进制位表示
前缀编码:没有一个编码是另一个编码的前缀
可用于数据压缩

并查集

//查
int Find(int S[],int x){
	while(S[x] >= 0)
		x = S[x];
	return x;
}
最坏时间复杂度:O(n)
并,将两个集合合并为一个
void Union(int S[],int Root1,int Root2){
	if(Root1 == Root2)
		return;
	S[Root2] = Root1;	//将root2的连接在root1下面
}
最坏时间复杂度:O(1)
优化思路:	1.用根节点的绝对值表示树的节点总数
			2.Union操作,让小树合并到大树
并,优化
void Union(int S[],int root1,int root2){
	if(Root1 == Root2)
		return;
	if(S[Root2] > S[Root1]){	//root1节点更多-大树
		S[Root1] += S[Root2];			//节点总数加在一起
		S[Root2] = Root1;
	}else{
		S[Root2] += S[Root1]
		S[Root1] = Root2;
	}
}
保证Find的最坏复杂度为O(log2n)
Union不变

树高不超过在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java程序员十六

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值