五、树与二叉树

本文深入解析了二叉树的基础概念、特殊形态如满二叉树和完全二叉树,重点讲解了二叉树的遍历方法,包括先序、中序和后序,以及层序遍历。此外,涵盖了完全二叉树的性质和二叉排序树(BST)的查找、插入操作。还介绍了平衡二叉树(AVL)的定义、插入调整策略,以及哈夫曼树的构造和编码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

树与二叉树

一、基本概念

树:n(n>=0)个结点的有限集合,n = 0时为空树,树的特点:
有且只有一个根节点
当n>1时,其余结点可分为m(m>0)个互不相交的有限集合,每个集合又是一个树,称为子树
在这里插入图片描述

二、基本术语

(一)结点之间的关系描述:

在这里插入图片描述

(二)结点、树的属性描述:

在这里插入图片描述

(三)有序树、无序树:

在这里插入图片描述

(四)森林:

在这里插入图片描述

三、树的常考性质


考点一:结点数 = 总度数+1,结点的度——结点有几个孩子


考点二:度为m的数与m叉树的区别

在这里插入图片描述


考点三:度为 m 的树第 i 层至多有 mi+1 个结点(i>=1), m 叉树第 i 层至多有 mi+1 个结点(i>=1)

在这里插入图片描述


考点四:高度为h的m叉树至多有(mh-1)/(m-1)个结点
在这里插入图片描述


考点五:高度为 h 的 m 叉树至少有 h 个结点;
               高度为 h ,度为 m 的树至少有 h+m-1 个结点


考点六:具有 n 个结点的 m 叉树的最小高度为 logm(n(m-1)+1)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


四、二叉树

(一)基本概念:

n(n>=0)个结点的有限集合
可以为空二叉树,即n=0
有一个根节点和两个互不相交的左右子树组成,左右子树又分别是一颗二叉树

(二)几种特殊的二叉树:

1.满二叉树:

只有最后一层有叶子节点且不存在度为1的结点
若按层序编号,结点 i 的左孩子为 2i ,右孩子为 2i+1 ,父节点为 i/2

在这里插入图片描述

2.完全二叉树:

必须要求所有的层序编号和满二叉树一一对应
只有最后两层有叶子节点;最多只有一个度为1的结点
若按层序编号,结点 i 的左孩子为 2i ,右孩子为 2i+1 ,父节点为 i/2
i<=(n/2)为分支结点,i>(n/2)为叶子结点

在这里插入图片描述不是完全二叉树的情况:
在这里插入图片描述

3.二叉排列树:

在这里插入图片描述

4.平衡二叉树:

在这里插入图片描述

五、二叉树的常考性质

常考点一:

设非空二叉树度为0、1、2的结点个数为n0、n1、n2,则n0 = n2 + 1(叶子节点比二分支节点多一个)(树的结点数 = 总度数 + 1):
假设树中结点总数为n,则:
A、n = n0 + n1 + n2
                  用B - A得 n0 = n2 + 1
B、n = n1 + 2n2 + 1

常考点二:

二叉树的第i层至多有2i-1个结点(i>=1)
m叉树的第i层至多有mi-1个结点(i>=1)

常考点三:

高度为h的二叉树至多有2h-1个结点(满二叉树)
高度为h的m叉树至多有(mh-1)/(m-1)个结点

六、完全二叉树的常考性质

常考考点一:

具有n个结点的完全二叉树的高度为【log2(n+1)】或【(log2n)+1】


对【log2(n+1)】的推导:

高为h的满二叉树共有2h-1个结点

         则2h-1-1 < n <= 2h-1,然后给此不等式分别加1后取对数,得:h-1< log2(n+1) <= h

高为h-1的满二叉树共有2h-1-1个结点


对【(log2n)+1】的推导:

高为h-1的满二叉树共有2h-1-1个结点

                      则2h-1 <= n < 2h,同时取对数得:h-1 <= (log2n) < h

高为h的完全二叉树至少有2h-1个结点、至多有2h-1个结点


常考考点二:

对于完全二叉树,可以由结点总数n推出度为0、1、2得结点个数是n0、n1、n2

完全二叉树最多只有一个度为1得结点,即:
n1 = 0或1
n0 = n2 + 1 可得:n0+n1一定是奇数,则有:
若完全二叉树有2k(偶数)个结点,则必有n1 = 1,n0 = k,n2 = k+1
若完全二叉树有2k-1(奇数)个结点,则必有n1 = 0,n0 = k,n2 = k-1

七、二叉树和完全二叉树得比较

在这里插入图片描述

八、二叉树的存储结构

(一)顺序存储(只适合完全二叉树)【很少用】:


使用静态数组,一层一层依次存储:
在这里插入图片描述


在这里插入图片描述


在这里插入图片描述一般二叉树判断是否有左右孩子,可以判断isEmpty是否为true来确定


结论:
在这里插入图片描述


(二)链式存储【常用】:


在这里插入图片描述若没有左(右)孩子,则把对应指针设为NULL即可


#include <stdio.h>
#include <stdlib.h>

//二叉树的顺序存储:使用静态数组
#define MaxSize 100
struct TreeNode{
	int value;//结点的数据元素 
	bool isEmpty;//结点是否为空 
}; 

//二叉树的链式存储:使用链表
typedef struct BiTNode{
	int data;//数据域
	struct BiTNode* lchild;//左孩子
	struct BiTNode* rchild;//右孩子 
}BiTNode,*BiTree; 

int main(){
	//定义一个空树 
	BiTree root = NULL;
	
	//插入根结点
	root = (BiTree)malloc(sizeof(BiTree)); 
	root->data = {1};
	root->lchild = NULL;
	root->rchild = NULL;
	
	//插入新节点
	BiTNode* p = (BiTNode*)malloc(sizeof(BiTNode));
	p->data = {2};
	p->lchild = NULL;
	p->rchild = NULL;
	root->lchild = p;//p成为根节点root的左孩子 
	return 0;
}

在这里插入图片描述以上代码找指定结点的左(右)孩子很简单,只需判断左(右)指针是否为NULL,但是找指定结点的父节点却需要从根结点开始依次遍历,解决方法如下:

typedef struct FBiTNode{
	int data;
	struct FBiTNode* lchild;
	struct FBiTNode* rchild;
	struct FBiTNode* patent;//父节点指针 
}FBiTNode,*FBiTree;

注意:n个结点的二叉链表共有n-1个空链域


九、二叉树的遍历

遍历:按照某种次序所有结点 都访问一遍
二叉树的递归特性:要么是空二叉树,要么是由根节点+左子树+右子树组成的二叉树

(一)先序遍历【根 左 右】(NLR) :

(二)中序遍历【左 根 右】(LNR):

(三)后序遍历【左 右 根】(LRN):

(四)应用举例:


1.手算练习:

1.1.写出满二叉树的所有遍历:

在这里插入图片描述


1.2.写出一般二叉树的所有遍历:

在这里插入图片描述


1.3.算数表达式中的应用:

在这里插入图片描述


2.机算练习:

//定义一个二叉树
typedef struct BiTNode{
	int data;
	struct BiTNode* lchild;
	struct BiTNode* rchild;
}BiTNode,*BiTree; 
2.1.先序遍历:

若为空二叉树,什么都不做
若为非空二叉树,则先访问根节点,后访问左结点,最后访问右结点

void PreOrder(BiTree T){
	if(T != NULL){
		visit(T);//访问根结点 
		PreOrder(T->lchild);//访问左子树 
		PreOrder(T->rchild);//访问右子树 
	}
} 
2.2.中序遍历:
void InOrder(BiTree T){
	if(T != NULL){
		InOrder(T->lchild);//访问左子树 
		visit(T);//访问根结点 
		InOrder(T->rchild);//访问右子树 
	}
} 
2.3.后序遍历:
void PostOrder(BiTree T){
	if(T != NULL){
		PostOrder(T->lchild);//访问左子树  
		PostOrder(T->rchild);//访问右子树 
		visit(T);//访问根结点
	}
}  

十、二叉树的层序遍历

层序遍历:一层一层依次遍历
在这里插入图片描述A先入队,没有兄弟结点,则A出队;然后A的左右孩子BC入队;然后B出队;然后B的左右孩子DE入队,然后然后C出队,然后C的左右孩子FG入队;然后D出队,然后D的左右孩子HI入队…

#include <stdio.h> 
#include <stdlib.h>

//定义一个二叉树
typedef struct BiTNode{
	int data;
	struct BiTNode* lchild;
	struct BiTNode* rchild;
}BiTNode,*BiTree; 

//定义一个队列
typedef struct LinkNode{
	BiTBode* data;
	struct LinkNode* next;
}LinkNode; 
typedef struct{
	LinkNode* front;
	LinkNode* rear;
}LinkQueue;

//初始化队列
void InitQueue(LinkQueue &Q){
	Q.front = Q.rear= (LinkNode*)malloc(sizeof(LinkNode)); 
	Q.front->next = NULL;
} 

//入队 
void EnQueue(LinkQueue &Q,int x){
	LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));
	s->data = x;
	s->next = NULL;
	Q.rear->next = s;
	Q.rear = s;
	} 

//出队
bool DeQueue(LinkQueue &Q,int &x){
	if(Q.front == Q.rear){
	   	return false;
	}
	LinkNode* p = Q.front->next;
	x = p->data;
	Q.front->next = p->next;
	if(Q.rear == p){
		Q.rear = Q.front;
	}
	free(p);
	return true;
} 

//判空
bool isEmpty(LinkQueue Q){
	if(Q.front == NULL){
		return true;
	}else{
		return false;
	}
} 

//层序遍历 
void LevelOrder(BiTree T){
	LinkQueue Q;
	InitQueue(Q);
	BiTree p;
	EnQueue(Q,T);
	while(!isEmpty(Q)){
		DeQueue(Q,p);
		vist(p);
		if(p->lchild != NULL){
			EnQueue(Q,p->lchild);
		}
		if(p->rchild != NULL){
			EnQueue(Q,p->rchild);
		}
	}
}

int main(){
	return 0;
}

算法思想:
1)初始化一个辅助队列
2)根节点入队
3)若非空,则队头结点访问该节点,并将其左右孩子入队
4)重复3)至队列为空

十一、由遍历序列构造二叉树

(一)不同二叉树的遍历序列:


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


(二)结论:

给定二叉树,它的遍历序列是唯一的,但给定遍历序列,出现的二叉树并不唯一
即:若只给出一颗二叉树的前/中/后/层序遍历中的一种,并不能唯一确定一颗二叉树

那么,如何通过遍历确定一颗二叉树呢:可以通过前序+中序/后序+中序/层序+中序

(三)通过层序确定二叉树:

1.前序+中序:


已知:
前序遍历:A D B C E
中序遍历:B D C A E


根据中序,只能知道所有结点都有可能是根节点,但根据前序,确定出A为根结点,则根据中序的左-根-右可知:B D C 为左子树,E为右子树,即:
在这里插入图片描述


用以上方法判断B D C:可知D为根结点(现已判断出A,故可除去A),则根据中序遍历,B为左子树,即:
在这里插入图片描述


进一步练习:


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


2.后序+中序:

后序序列最后一个出现的是根节点:


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


3.层序+中序:

层序序列第一个被访问的是根节点,接下来是左子树,最后是右子树:


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


(四)总结:

在这里插入图片描述

十二、线索二叉树

(一)作用(以中序为例):

方便找出某一结点的前驱和后继:

(二)存储结构:

在这里插入图片描述

(三)三种线索二叉树:

1.先序线索二叉树:

在这里插入图片描述

2.后序线索二叉树:

在这里插入图片描述

3.对比:

在这里插入图片描述

(四)二叉树的线索化:

1.中序线索化:

在这里插入图片描述

#include <stdio.h> 

//定义中序线索二叉树
typedef struct ThreadNode{
	int data;
	struct ThreadNode* lchild;
	struct ThreadNode* rchild;
	int ltag,rtag;//左右线索标志 
}ThreadNode,*ThreadTree; 

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

//建立线索
void visit(ThreadNode* q){
	if(q->lchild == NULL){//左孩子为空 
		q->lchild = pre;//建立前驱结点的后继结点
		pre->ltag = 1; 
	}
	if(pre != NULL && pre->rchild == NULL){
		q->rchild = q;//建立前驱结点的后继结点
		pre->rtag = 1; 
	}
	pre = q;//pre指向当前结点 
} 

//遍历二叉树
void InThread(ThreadTree T){
	if(T != NULL){
		InThread(T->lchild);//遍历左 
		visit(T);//遍历根 
		InThread(T->rchild);//遍历右 
	}
} 

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

int main(){
	return 0;
}

2.先序线索化:

在这里插入图片描述在这里插入图片描述

3.后序线索化:

在这里插入图片描述

(五)总结:

在这里插入图片描述

十三、线索二叉树找前驱/后继

(一)中序:

在这里插入图片描述在这里插入图片描述

(二)先序:

在这里插入图片描述在这里插入图片描述

(三)后序:

在这里插入图片描述在这里插入图片描述

十四、树

(一)树的逻辑结构:

树是n个结点的有限集合,n=0时,为空树,非空树需要满足下列条件:
1)有且仅有一个根结点;
2)当n>1时,其余结点可分为m个互不相交的有限集合,每个集合又是一棵树,称为根节点的子树;
3)树是一种递归定义的数据结构。

(二)双亲表示法(顺序存储):

每个结点中保存指向双亲的指针

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

优点:查指定结点的双亲很方便
缺点:查指定结点的孩子结点只能从头遍历

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

顺序存储各结点,每个结点中保存孩子链表的头指针

#define MAX_TREE_SIZE 100

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


int main(){
	return 0;
}

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

typedef struct CSNode{
	int data;
	struct CSNode* firstchild;//左指针
	struct CSNode* nextchild;//右指针
}CSNode,*CSTree;

(五)树、森林、二叉树的转换:

1.树和二叉树的转换:

在这里插入图片描述

2.森林和二叉树的转换:

森林:m个互不相交的树的集合。
在这里插入图片描述在这里插入图片描述

十五、树和森林的遍历

(一)树的遍历:

1.先根遍历:

先访问根节点,然后依次对每颗子树进行先根遍历

void PreOrder(TreeNode* R){
	if(R != NULL){
		visit(R);
		while(R还有下一个子树T){
			PreOrder(T);
		}
	}
}

在这里插入图片描述

2.后根遍历:

先依次对每颗子树进行后根遍历,最后访问根节点

void PostOrder(TreeNode* R){
	if(R != NULL){
		while(R还有下一个子树T){
			PostOrder(T);
			visit(R);
		}
	}
}

在这里插入图片描述

3.层序遍历:

用队列实现,若树非空,则根节点入队;若队列非空,则队头元素出并访问,同时将该元素的孩子入队

(二)森林的遍历:

1.先序遍历:

访问森林中第一颗树的根节点,然后先序遍历第一颗树中根节点的子树森林,最后先序遍历除第一颗树之后的剩余的树构成的森林;等同于依次对各个树进行先根遍历
在这里插入图片描述

2.中序遍历:

中序遍历森林中第一颗树的根节点的子树森林,然后访问第一棵树的根节点,最后中序遍历除第一棵树之后的剩余树构成的森林;等同于依次对各个树进行后根遍历

在这里插入图片描述

十六、二叉排序树(BST)

(一)定义:

二叉排序树,又称二叉查找树(Binary Search Tree):
 1)左子树上所有结点的关键字均小于根节点的关键字;
 2)右子树上所有结点的关键字均大于根节点的关键字;
 3)左子树和右子树又个是一颗二叉排序树。
即:左子树结点值 < 根节点值 < 右子树结点值

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

(二)查找操作:

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

//查找值为n的结点
BSTNode* BST_Search0(BSTree T,int n){
	while(T != NULL && n != T->key){//查找前提:树非空且查找值不等根节点 
		if(n < T->key){
			T = T->lchild;
		} else{
			T = T->rchild;
		}
	}
	return T;
} 

//查找值为n的结点(递归实现) 
BSTNode* BST_Search1(BSTree T,int n){
	if(T == NULL){
		return NULL;//查找失败 
	}
	if(n == T->key){
		return T;//查找成功 
	}else if(n < T->key){
		return BST_Search1(T->lchild,n); 
	}else{
		return BST_Search1(T->rchild,n);
	}
} 

(三)插入操作:

若原二叉排序树为空,则直接插入结点;
否则,若关键字小于根节点的值,则插入到左子树;若大于根节点的值,则插入到右子树。

//二叉排序树的插入(递归实现) 
int BST_Insert0(BSTree &T,int k){
	if(T == NULL){
		T = (BSTree)malloc(sizeof(BSTree));
		T->key = k;
		T->lchild = T->rchild = NULL;
		return 1;
	}else if(k  == T->key){
		return 0;//插入的值相同,则插入失败 
	}else if(k < T->key){
		return BST_Insert0(T->lchild,k);
	} else{
		return BST_Insert0(T->rchild,k);
	}
} 

//按照str = {50,66,60,26,21,30,70,68}建立BST
void Creat_BST(BSTree &T,int str[],int n){
	T = NULL;
	int i = 0;
	while(i < n){
		BST_Insert0(T,str[i]);
		i ++;
	}
} 

(四)删除操作:

1)若被删除结点是叶子结点,则直接删除;
2)若被删除结点只有一颗左子树右子树,则让其子树成为其父结点的子树,代替其位置;
3)若被删除结点右左右子树,则令其的直接后继或直接前驱代替其,然后从二叉排序树中删除这个直接后继或直接前驱,转换为第一种或者第二种情况
在这里插入图片描述
如下所示:
在这里插入图片描述

(五)查找效率分析:

查找长度:在查找运算中,需要对比关键字的次数,其反应查找操作的时间复杂度
在这里插入图片描述

十七、平衡二叉树(AVL)

(一)定义:

平衡二叉树(Balanced Binary Tree):简称平衡树(AVL树),其树上任意一结点的左右子树高度之差不超过1;
结点的平衡因子:左子树高 - 右子树高
平衡二叉树结点的平衡因子的值只可能是-1、0、1

typedef struct AVLNode{
	int data;//数据域
	int balance;//平衡因子
	struct AVLNode* lchild;
	struct AVLNode* rchild; 
}AVLNode,*AVLTree; 

(二)插入操作:

从插入点往回找到第一个不平衡结点,调整以该结点为根的子树(最小不平衡子树)
在这里插入图片描述


(三)插入新结点后如何调整"不平衡"问题:


1.在A的左孩子的左子树中插入(LL):

在这里插入图片描述代码思路:
在这里插入图片描述


2.在A的右孩子的右子树中插入(RR):

在这里插入图片描述代码思路:
在这里插入图片描述


3.在A的左孩子的右子树中插入(LR):

在这里插入图片描述


4.在A的右孩子的左子树中插入(RL):

在这里插入图片描述


十八、哈夫曼树

(一)带权路径长度:

结点的权:有某种现实含义的数值
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
树的带权路径长度:树中所有叶子结点的带权路径长度之和(WPL):
在这里插入图片描述在这里插入图片描述

(二)哈夫曼树的定义:

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

(三)哈夫曼树的构造:

在这里插入图片描述

(四)哈夫曼编码:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值