数据结构——树与二叉树

这一篇主要介绍了数据结构中树的知识点



			|- 二叉树-------|- 概念
			|				|- 操作:三种遍历(先序、中序、后序);线索二叉树
			|				|- 应用:排序二叉树;平衡二叉树;哈夫曼树
树形结构-----|
			|				|- 概念		|- 与二叉树的转换
			|				|- 操作:---|- 树遍历(先根、后根);森林遍历(先序、中序)
			|- 树和森林-----|- 应用:并查集

一、树的基本概念

1. 树的定义

是一种递归的数据结构
根节点没有前驱,其他节点只有一个前驱;所有节点有零个或多个后继。

2. 基本术语

节点的度:树中一个节点的子节点个数;
树的度:树中节点的最大度数;
节点的深度:从根节点自顶向下逐层累加;
节点的高度:从叶子节点自底向上逐层累加;

3. 树的性质

(1)树中节点数等于所有节点的度数加1;
(2)度为m的树第 i 层至多有 m^(i-1) 个节点;
(3)高度为h的m叉树 至多有 (m^h-1)/(m-1) 个节点;

二、二叉树的概念

1. 二叉树的定义及主要特性

二叉树是有序树,二叉树可以是空树

(1)满二叉树

在这里插入图片描述

(2)完全二叉树

特点如下:

  • 叶子节点只可能在层次最大的两层,且最大层次的叶子节点都在左边。
  • 如果有度为1的节点,只可能有一个,且该节点只有左孩子没有右孩子。
  • 节点个数为n,若n为奇数,则每个分支节点都有左孩子和右孩子;若n为偶数,则编号最大的节点只有左孩子没有右孩子,其余节点左右孩子都有。
(3)二叉排序树

左子树上所有节点的关键字 < 根节点的关键字 < 右子树上所有节点的关键字

(4)平衡二叉树

任一节点的左右子树深度之差不超过1.

(5)二叉树的性质
  • 非空二叉树叶子上节点数 N0 等于度为2的节点数 N1 加1,即 N0 = N1 +1;
2. 二叉树的存储结构

在这里插入图片描述

(1)顺序存储
  • 用一组地址连续的存储单元依次自上而下、自左至右 存储完全二叉树的节点;
  • 完全二叉树和满二叉树适合采用顺序存储,既能最大可能的节省存储空间,又可以利用数组元素的下标值确定节点在二叉树的位置,以及节点之间的位置关系;
(2)链式存储
  • 由于顺序存储的空间利用率第,所以通常采用链式存储;
  • 有 n 个节点的二叉链表中含有 n+1 个空链域;
    链式存储结构如下:
typedef struct BiTNode{
	ElemType data;		//数据域
	struct BiTNode *lchild, *rchild;	//左、右孩子域
} BiTNode, *BiTree;

三、 二叉树的遍历

1. 先序遍历

时间复杂度O(n),空间复杂度O(n)

void PreOrder(BiTree T){
	if(T != NULL){
		visit(T);
		PreOrder(T->lchild);
		PreOrder(T->rchild);
	}
}
2. 中序遍历

时间复杂度O(n),空间复杂度O(n)

void InOrder(BiTree T){
	if(T != NULL){
		InOrder(T->lchild);
		visit(T);
		InOrder(T->rchild);
	}
}
3. 后序遍历

时间复杂度O(n),空间复杂度O(n)

void PostOrder(BiTree T){
	if(T != NULL){
		PostOrder(T->lchild);
		PostOrder(T->rchild);
		visit(T);
	}
}
4. 递归和非递归的转换

可以借助栈,将二叉树的递归遍历转化为非递归遍历。

  • 非递归先序遍历
    算法步骤如下:(注意:入栈时打印
    从根开始,当前节点存在或栈不为空,重复下面两个操作。
    (1)访问当前节点,当前节点进栈,进入其左子树,重复至当前节点为空;
    (2)若栈非空,则栈顶节点出栈,并进入其右子树。
void PreOrder(BiTree root){
	SeqStack *S;
	BiTree p;
	InitStack(S);
	p = root;
	while(p != NULL || !IsEmpty(S)){	//当前节点指针和栈均为空,则结束
		while(p != NULL){	
			visit(p->data);		//访问根节点
			Push(S, p);			//根指针进栈
			p = p->lchild;		//进入左子树
		}
		if(!IsEmpty(S)){		//如果栈非空
			Pop(S, &p);			//根指针出栈
			p = p->rchild;		//进入右子树
		}
	}                                                                                                                                                                                                                                                                                                               			
}
  • 非递归中序遍历
    算法步骤如下:(注意:出栈时打印
    从根开始,当前节点存在或栈不为空,重复下面两个操作。
    (1)当前节点进栈,进入其左子树,重复至当前节点为空;
    (2)若栈非空,则栈顶节点出栈,访问该出栈节点,并进入其右子树。
void InOrder(BiTree root){
	SeqStack *S;
	BiTree p;
	InitStack(S);
	p = root;
	while(p != NULL || !IsEmpty(S)){	//当前节点指针和栈均为空,则结束
		while(p != NULL){	
			Push(S, p);			//根指针进栈
			p = p->lchild;		//进入左子树
		}
		if(!IsEmpty(S)){		//如果栈非空
			Pop(S, &p);			//根指针出栈
			visit(p->data);		//访问根节点(和先序遍历的不同之处)
			p = p->rchild;		//进入右子树
		}
	}                                                                                                                                                                                                                                                                                                               			
}
  • 非递归后序遍历
    算法步骤如下:
    从根开始,当前节点存在或栈不为空,重复下面两个操作。
    (1)当前节点进栈,进入其左子树,重复至当前节点为空;
    (2)若栈非空,判断栈顶 p 的右子树是否为空、右子树是否刚访问过。
    若是,则出栈、访问节点 p ,p 赋给 q,p置为空;
    若不是,则进入 p 的右子树。
void PostOrder(BiTree root){
	SeqStack *S;
	BiTree p,q;
	InitStack(S);
	p = root;
	q = NULL;
	while(p != NULL || !IsEmpty(S)){	//当前节点指针和栈均为空,则结束
		while(p != NULL){	
			Push(S, p);			//根指针进栈
			p = p->lchild;		//进入左子树
		}
		if(!IsEmpty(S)){		//如果栈非空
			Top(S, &p);
			//判断栈顶的右子树是否为空,右子树是否访问过
			if((p->rchild==NULL) || (p->rchild==q)){	
				Pop(S, &p);		//根指针出栈
				visit(p->data);	//访问根节点
				q = p;
				p = NULL;
			}
			else{
				p = p->rchild;
			}
		}
	}                                                                                                                                                                                                                                                                                                               			
}
5. 层次遍历

层次遍历需要借助一个队列。
算法步骤如下:
首先根节点入队,当队列非空时,重复如下操作。
(1)队头节点出队,并访问出队节点;
(2)出队节点的非空 左、右孩子依次入队。

void LevelOrder(BiTree T){
	InitQueue(Q);
	BiTree p;
	EnQueue(Q, T);		//根节点入队
	while(!IsEmpty(Q)){	//队列不为空进循环
		DeQueue(Q, p);	//队头元素出队
		visit(p);		//访问当前p所指向的节点
		if(p->lchild != NULL){
			EnQueue(Q, p->lchild);	//左子树不为空,则左子树入队
		}
		if(p->rchild != NULL){
			EnQueue(Q, p->rchild);	//右子树不为空,则右子树入队
		}
	}
}
6. 由遍历构造二叉树
  • 二叉树的先序和中序可以唯一的确定一棵二叉树。
  • 二叉树的后序和中序可以唯一的确定一棵二叉树。
  • 二叉树的层次和中序可以唯一的确定一棵二叉树。
  • 注意:先序和后序,不能唯一确定一棵二叉树。

四、线索二叉树

1. 线索二叉树的基本概念
  • 传统的链式存储只能体现一种父子关系,不能直接得到遍历中的前驱和后继。
  • 引入线索二叉树的目的:为了加快查找前驱和后继节点的速度。
  • 线索化规定:若无左子树,另 lchild 指向其前驱节点;若无右子树,则令 rchild 指向其后继节点。
    在这里插入图片描述
  • ltag = 0, lchild指向节点的左孩子
    ltag = 1, lchild指向节点的前驱
  • rtag = 0, rchild指向节点的右孩子
    rtag = 1, rchild指向节点的后继
typedef struct ThreadNode{
	ElemType data;
	struct ThreadNode *lchild, *rchild;
	int ltag, rtag;
}ThreadNode, *ThreadTree;
2. 线索二叉树的构造

在这里插入图片描述

通过中序遍历对二叉树线索化:

//中序遍历对二叉树线索化的递归算法
void InThread(ThreadTree &p, ThreadTree &pre){
	if(p != NULL){
		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 CreateInThread(ThreadTree T){
	ThreadTree pre = NULL;
	if(T != NULL){			//树非空
		InThread(T, pre);	//线索化二叉树(调用函数)
		pre->rchild = NULL;	//处理便利的最后一个节点
		pre->rtag = 1;
	}
}
3. 线索二叉树的遍历

中序线索二叉树的遍历不需要栈,可以实现非递归遍历。

  • 求中序线索二叉树中 中序序列的第一个节点
ThreadNode *Firstnode(ThreadNode *p){
	while(p->ltag == 0){
		p = p->lchild;		//最左下节点(不一定是叶子节点)
	}
	return p;
}
  • 求中序线索二叉树中 节点 p 在中序序列下的后继节点
ThreadNode *Nextnode(ThreadNode *p){
	if(p->rtag == 0){
		return Fiestnode(p->rchild);
	}
	else{		
		return p->rchild;	//rtag==1,直接返回后继线索
	}
}
  • 二叉树的线索链表也可以带头结点。

五、树和森林

1. 树的存储结构
(1)双亲表示法

在这里插入图片描述

#define MAX_TREE_SIZE 100
typedef struct{			//树的结点定义
	ElemType data;		
	int parent;
}PTNode;
typedef struct{			//树的类型定义
	PTNode nodes[MAX_TREE_SIZE];	//双亲表示
	int n;							//节点数
}PTree;
  • 优点:可以很快得到每个节点的双亲节点。
  • 缺点:求节点的孩子时要遍历整个结构。
(2)孩子表示法

在这里插入图片描述

  • 优点:寻找子女节点操作简单。
  • 缺点:寻找双亲节点需要遍历多。
(3)孩子兄弟表示法

在这里插入图片描述

typedef struct CSNode{
	ElemType data;
	struct CSNode *firstchild, *nextsibling;	//第一个孩子、右兄弟 指针
}CSNode, *CSTree;
  • 优点:可以方便实现树转化为二叉树的操作,易于查找节点的孩子。
  • 缺点:从当前节点查找双亲节点比较麻烦。(如果为每个节点增设一个parent域指向父亲节点,则查找节点的父亲节点也方便。)
2. 树、森林、二叉树的转换
  • 树转换为二叉树:每个节点左指针指向第一个孩子节点,右指针指向它在树中相邻的兄弟节点。(左孩子右兄弟)
    注意:由树转换得到的二叉树没有右子树。
    在这里插入图片描述

  • 森林转换为二叉树:与上述转换类似,先将森林中的每棵树转换为二叉树。然后将第一棵树的根作为二叉树的根,第二棵树的根作为第一个右孩子,以此类推。
    在这里插入图片描述

  • 二叉树转换为森林:上述过程的逆推即可。

3. 树和森林的遍历
  • 树的遍历
    (1)先根遍历
    (2)后根遍历
  • 森林的遍历
    (1)先序遍历
    (2)中序遍历
  • 树、森林、二叉树 各种遍历之间的关系
森林二叉树
先根遍历先序遍历先序遍历
后根遍历中序遍历中序遍历
  • 重要性质:在 n 个节点的树中有 n-1 条边。
    那么对于每棵树来说,节点数=边数+1。
    故如果题目:森林中节点数比边数多5,则森林有5棵树。

六、 树的应用——并查集

七、 二叉树的应用

1. 二叉排序树
(1)二叉排序树定义

二叉排序树(BST),也称二叉查找树,中序遍历是一个有序序列。
左子树节点值 < 根节点值 < 右子树节点值

(2)二叉排序树查找

二叉排序树的非递归查找:从根节点开始,沿某一个分支逐层向下进行比较

//查找函数返回指向关键字值为key的节点指针,若不存在,返回 NULL
BSTNode *BST_Search(BiTree T, ElemType key, BSTNode *&p){
	p = NULL;
	while(T != NULL && key != T->data){
		p = T;		// p 指向被查找节点的双亲,用于插入和删除操作中
		if(key < T->data){
			T = T->lchild;
		}
		else{
			T = T->rchild;
		}
	}
	return T;
}
(3)二叉排序树插入

二叉排序树是一种动态集合,树的结构通常不是一次生成的,而是在查找过程中,逐渐插入的。(插入的新节点一定是某个叶子节点)

//在二叉排序树中插入一个关键字为 k 的节点
int BST_Insert(BiTree &T, KeyType k){
	if(T == NULL){		//原树为空,新插入的记录为根节点
		T = (BiTree)malloc(sizeof(BSTNode));
		T->key = k;
		T->lchild = T->rchild = NULL;
		return 1;		//返回1,表示成功
	}
	else if(k == T->key){	//树中存在相同的关键字节点
		return 0;
	}
	else if(k < T->key){	//小于关键字,插入到T的左子树
		return BST_Insert(T->lchild, k);
	}
	else{					//大于关键字,插入到T的右子树
		return BST_Insert(T->rchild, k);
	}
}
(4)二叉排序树构造

构造一棵二叉排序树就是一次输入数据元素,并将它们插入到二叉排序树中的适当位置上的过程。

//用关键字数组 str[] 建立一个二叉排序树
void Creat_BST(BiTree &T, KeyType str[], int n){
	T = NULL;			//初始时为空树
	int i = 0;
	while(i < n){		//依次将每个元素插入
		BST_Insert(T, str[i]);
		i++;
	}
}
(5)二叉排序树删除

删除操作的过程按三种情况来处理:
(1)若被删节点 k 为叶子节点,则直接删除。
(2)若被删节点 k 只有一棵左子树或右子树,则令 k 的孩子替代 k 的位置。
(3)若被删节点 k 有左、右两棵子树,则令 k 的直接后继(或直接前驱)替代 k ,然后将其删除。
在这里插入图片描述

(6)二叉排序树的查找效率分析

对于高度为 H 的二叉排序树,其插入和删除操作的运行时间都是 O(H)
平均查找长度 ASL 主要取决于树的高度。

如果是一棵单支树,平均查找长度为 O(n)
如果是一棵平衡二叉树,平均查找长度为 O(log2 n)

在这里插入图片描述

优点:无需移动节点,只需修改指针即可完成插入和删除操作。
当有序表是静态查找时,宜用顺序表作为其存储结构,用二分查找;
当有序表是动态查找时,宜用二叉排序树作为其逻辑结构。

2. 平衡二叉树
(1)平衡二叉树定义
  • 平衡二叉树(AVL树):任意节点的左、右子树高度差的绝对值不超过1。
  • 平衡因子:节点左子树和右子树的高度差,平衡二叉树的平衡因子只能为 -1、0、1.
(2)平衡二叉树插入
  • 二叉排序树保证平衡的基本思想:每当插入或删除节点时,首先要检查是否会导致不平衡。① 如果导致了不平衡,则先找到插入路径上离插入节点最近的平衡因子绝对值大于1的节点A;② 再对以A为根的子树,在保持二叉排序树特性的前提下,调整各节点位置关系,重新达到平衡。
  • 注意:每次调整的对象都是最小不平衡子树。
  • 一般可将失去平衡后进行调整的规律归纳为以下四种情况:

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

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

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

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

(3)平衡二叉树查找

在平衡二叉树上的查找过程和二叉排序树相同。
平衡二叉树的平均查找长度为 O(log2 n)

3. 哈夫曼树
(1)哈夫曼树定义

节点的带权路径长度:从树的根节点到任意节点的路径长度与该节点上权值的乘积。
树的带权路径长度:树中所有叶子节点的带权路径长度之和。
在这里插入图片描述
在这里插入图片描述

(2)哈夫曼树构造
  • 哈夫曼树的构造过程
    在这里插入图片描述

  • 哈夫曼树的特点
    (1)每个初始节点都成为叶子节点,并且权值越小到根节点路径越长。
    (2)构造过程中共新建了 N-1 个节点,因此哈夫曼树中节点总数为 2N-1。
    (3)每次构造都选择2棵树作为新节点的孩子,因此哈夫曼树中不存在度为1的节点。

(3)哈夫曼编码
  • 可变长度的编码比固定长度的编码好,其特点是对频率高的字符用短编码,频率低的字符用长编码。可以起到压缩数据的效果。
  • 前缀编码:没有一个编码是另一个编码的前缀。如:0、101、100是前缀编码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值