5.1 树的基本概念
树:n个结点的有限集合,n=0时称为空树(从树根生长,逐渐分支)
空树:结点数为0的树
非空树特性:
- 有且仅有一个根节点
- 没有后继 的结点称为叶子节点
- 有后继的结点称为分支结点
- 除根节点外,任何结点都有且仅有一个前驱
- 每个结点可以有0个或多个后继
- 除根节点外,其余结点可分为m(m>0)个互不相交的有限集合T1,T2 … Tm,其中每个集合本身又是一颗树,并称之为根节点的子树
树是一种递归定义的数据结构
两个结点之间的路径只能从上往下
路径长度是经过几条边
树的属性:
- 结点的层次(深度)——从上往下数(默认从1开始)
- 结点的高度——从下往上数
- 树的高度(深度)——总共多少层
- 结点的度——有几个孩子(分支)
- 树的度——个节点的度的最大值
有序树与无序树:
- 有序树:逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
- 无序树:逻辑上看,树中结点的各子树从左至右是无次序的,可以互换
森林:m棵互不相交的树的集合(m=0是空森林)
树的常考性质:
- 节点数=总度数+1
- 度为m的树第i层至多有mi-1个结点(i>=1)
- 高度为h的m叉树至多有(mh-1-1)/(m-1)
- 高度为h的m叉树至少有h个结点;高度为h、度为m的树至少有h+m-1个结点
- 具有n个结点的m叉树的最小高度为[logm(n(m-1)+1)](向上取整)
度为m的树与m叉树的区别:
度为m的树 | m叉树 |
---|---|
任意结点的度<=m(最多m个孩子) | 任意结点的度<=m(最多m个孩子) |
至少有一个结点度=m | 允许所有结点度<m |
一定是非空树,至少m+1个结点 | 可以是空树 |
5.2 二叉树的基本概念
二叉树是n(n>=0)个结点的有限集合:
①或者为空二叉树,即n=0;
②或者由一个根节点和两个互不相交的被称为根的左子树和右子树组成,左右子树又分别是一棵二叉树
特点:
①每个结点至多只有两棵子树;
②左右子树不能颠倒(二叉树是有序树);
③二叉树是递归定义的数据结构
几个特殊的二叉树:
满二叉树: 一棵高度为h,且含有2h-1个结点的二叉树
特点:
①只有最后一层有叶子节点
②不存在度为1的结点
③按层序从1开始编号,结点i的左孩子为2i,右孩子为2i-1;结点 i 的父节点为[ i/2 ](向下取整)
完全二叉树: 当且仅当其每个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称之为完全二叉树(可删去编号最大的几个结点)
特点:
①只有最后两层可能有叶子节点
②最多只有一个度为1的结点
③按层序从1开始编号,结点i的左孩子为2i,右孩子为2i-1;结点 i 的父节点为[ i/2 ](向下取整)
④i<=[n/2](向下取整)为分支结点;i>[n/2](向下取整)为叶子节点
二叉排序树: 一棵空二叉树或者具有以下性质的二叉树:
①左子树上所有节点的关键字均小于根节点的关键字;
②右子树上所有节点的关键字均大于根节点的关键字;
③左右子树又各是一棵二叉排序树
特性:二叉排序树可用于元素的排序、搜索
平衡二叉树: 树上任一结点的左右子树深度之差不超过1
特性:更高的搜索效率
二叉树常考性质:
- 设非空二叉树中度为0、1、2的结点个数为n0、n1和n2,则n0=n2+1(叶子结点比二分支结点多一个)
假设树中结点总数为n,则
①n=n0+n1+n2
②n=n1+2n2+1(树的节点数=总度数<n1+2n2>+1) - 二叉树第 i 层至多有2i-1个结点(i>=1)
m叉树第i层至多有mi-1个结点(i>=1) - 高度为h的二叉树至多有2h-1个结点(满二叉树)
高度为h的m叉树至多有(mh-1-1)/(m-1)
完全二叉树常考性质:
- 具有n个(n>0)结点的完全二叉树的高度为[log2(n+1)](向上取整)或[log2n]+1(向下取整)
- 对于完全二叉树,可以由节点数n推出度为0、1、2的结点个数n0、n1和n2
完全二叉树最多只有一个度为1的结点,即n1=0或1
n0=n2+1 ->n0+n2(2n2+1)一定是奇数
由上可推得:
若完全二叉树由2k(偶数)个结点,则必有n1=1,n0=k,n2=k-1
若完全二叉树由2k-1(奇数)个结点,则必有n1=0,n0=k,n2=k-1
5.3 二叉树存储结构
5.3.1 二叉树的顺序存储
#define MaxSize 100
struct TreeNode{
ElemType value;//节点中数据元素
bool isEmpty;//结点是否为空
};
//初始化二叉链表
TreeNode t[MaxSize];
for(int i=0;i<MaxSize;i++){
t[i].isEmpty=true;//标记所有结点为空
}
几个常考基本操作:
- i的左孩子:2i
- i的右孩子:2i+1
- i的父节点:[i/2]向下取整
- i所在层次:[log2(n+1)]向上取整或[log2n]+1向下取整
若完全二叉树共有n个结点,则
- 判断i是否有左孩子:2i<=n?
- 判断i是否有右孩子:2i+1<=n?
- 判断i是否是叶子结点/分支结点:i>[n/2]向下取整?
二叉树顺序存储结构只适合存储完全二叉树
5.3.2 二叉树的链式存储
typedef struct{
int data;//数据域
struct BiTNode *lchild,*rchild;//左右孩子指针
}BiTNode,*BiTree;
n个结点的二叉链表共有n+1个空链域:
n个结点共有2n个链域,除根节点外其他节点都有父节点,也就是有n-1个实链域,所以有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的左右孩子结点很简单,但找父节点需要从头遍历,所以引用三叉链表
typedef struct{
int data;//数据域
struct BiTNode *lchild,*rchild;//左右孩子指针
struct BiTNode *parent;//父节点指针
}BiTNode,*BiTree;//根据实际要求决定是否要加父节点
5.4 二叉树的遍历
5.4.1 先\中\后序遍历
//先序遍历
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);//访问根节点
}
}
应用:
①前中后缀表达式转换
②求树的深度
//求树的深度
int treeDepth(BiTree T){
if(T==NULL)
return 0;
else{
int l=treeDepth(T->lchild);
int r=treeDepth(T->rchild);
//树的深度=Max(左子树深度,右子树深度)+1
return l>r?l+1:r+1;
}
}
考点:
给定中序遍历序列和层次、先/后序遍历序列,画出二叉树
5.4.2 层序遍历
算法思想:
①初始化一个辅助队列
②根节点入队
③若队列非空,则队头结点出队,访问该结点,并将其左右孩子插入队尾(如果有的话)
④重复③直至队列为空
//二叉树结点(链式存储)
typedef struct{
int 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){
DeQueue(Q,p);//根节点(队头结点)出队
visit(p);//访问根节点
if(p->lchild!=NULL)
EnQueue(Q,p->lchild);//左孩子入队
if(p->rchild!=NULL)
EnQueue(Q,p->rchild);//右孩子入队
}
}
5.5 线索二叉树
5.5.1线索二叉树的基本概念
//线索二叉树结点
typedef struct ThreadNode{
int data;//数据域
struct ThreadNode *lchild,*rchild;//左右孩子指针
int ltag,rtag;//左右线索标志,0为孩子,1为线索
}ThreadNode,*ThreadTree
逻辑结构:
存储结构:
5.5.2 二叉树的线索化
中序线索化:
//线索二叉树结点
typedef struct ThreadNode{
int data;//数据域
struct ThreadNode *lchild,*rchild;//左右孩子指针
int ltag,rtag;//左右线索标志,0为孩子,1为线索
}ThreadNode,*ThreadTree
//全局变量pre,指向当前访问节点的前驱
ThreadNode *pre=NULL;
//结点访问函数
void visit(ThreadNode *p){
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 InThread(ThreadTree T){
if(T!=NULL){
InThread(T->lchild);//中序遍历左子树
visit(T);//访问根节点
InThread(T->rchild);//中序遍历右子树
}
}
//中序线索化二叉树
void CreateInThread(ThreadTree T){
pre=NULL;
if(T!=NULL){
InThread(T);
if(pre->rchild==NULL)
pre->rtag=1;//处理遍历的最后一个结点
}
}
先序线索化:
//线索二叉树结点
typedef struct ThreadNode{
int data;//数据域
struct ThreadNode *lchild,*rchild;//左右孩子指针
int ltag,rtag;//左右线索标志,0为孩子,1为线索
}ThreadNode,*ThreadTree
//全局变量pre,指向当前访问节点的前驱
ThreadNode *pre=NULL;
//结点访问函数
void visit(ThreadNode *p){
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 PreThread(ThreadTree T){
if(T!=NULL){
visit(T);//先访问根节点
if(T->ltag==0)//保证lchild不是前驱线索,就先序线索化特殊
PreThread(T->lchild);//先序遍历左子树
PreThread(T->rchild);//先序遍历右子树
}
}
//先序线索化二叉树
void CreatePreThread(ThreadTree T){
pre=NULL;//pre初始化为NULL
if(T!=NULL){//非空二叉树才可线索化
PreThread(T);//线序线索化二叉树
if(pre->rchild==NULL)
pre->rtag=1;//处理遍历的最后一个结点
}
}
后序线索化:
//线索二叉树结点
typedef struct ThreadNode{
int data;//数据域
struct ThreadNode *lchild,*rchild;//左右孩子指针
int ltag,rtag;//左右线索标志,0为孩子,1为线索
}ThreadNode,*ThreadTree
//全局变量pre,指向当前访问节点的前驱
ThreadNode *pre=NULL;
//结点访问函数
void visit(ThreadNode *p){
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 PostThread(ThreadTree T){
if(T!=NULL){
PostThread(T->lchild);//先序遍历左子树
PostThread(T->rchild);//先序遍历右子树
visit(T);//访问根节点
}
}
//后序线索化二叉树
void CreatePostThread(ThreadTree T){
pre=NULL;//pre初始化为NULL
if(T!=NULL){//非空二叉树才可线索化
PostThread(T);//线序线索化二叉树
if(pre->rchild==NULL)
pre->rtag=1;//处理遍历的最后一个结点
}
}
以上先/中/后序线索化中的visit函数一样
5.6 树和森林
5.6.1 树的存储结构
孩子兄弟表示法:
typedef struct CSNode{
ElemType data;//数据域
struct CSNode *firstchild,*nextsibling;//左孩子和右兄弟指针
}CSNode,*CSTree//相关操作类似二叉树
树与二叉树的相互转换:
森林和二叉树的转换:
5.6.2 树和森林的遍历
树:
- 先根遍历。若树非空,先访问根节点,再依次遍历根节点的每棵子树,遍历子树时仍遵循先根后子树规则。其遍历序列与这棵树对应的二叉树的先序序列相同
- 后根遍历。若树非空,先遍历根节点的每颗子树,再访问根节点,遍历子树时仍遵循先子树后根规则。其遍历序列与这棵树对应的二叉树的中序序列相同
森林:
- 先序遍历森林(先序遍历对应二叉树)
- 中序遍历森林(中序遍历对应二叉树)
5.7 树与二叉树的应用
5.7.1 二叉排序树(BST)
左子树节点值<根节点值<右子树节点值
进行中序遍历可以得到一个递增的有序序列
二叉排序树查找算法:
//二叉树结点
typedef struct BSTNode{
int key;
struct BSTNode *lchild;*rchild;
}BSTNode,*BSTree
//非递归查找
BSTNode *BST_Search(BSTree T,ElemType key){
while(T!=NULL&&key!=T->key){//若树空或等于根结点值,则结束循环
if(key<T->key)
T=T->lchild;//小于,则再左子树中寻找
else
T=T->rchild;//大于,则再右子树中寻找
}
return T;
}
//递归查找
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 if(key>T->key)
return BSTSearch(T->rchild,key);//在右子树中找
}
二叉排序树插入算法:
//非递归实现插入
int BST_Insert(BSTree &T,int k){
while(T->key!=k){//当没有相同值时循环
if(T==NULL){//原树为空,新插入结点为根节点
T=(BSTree)malloc(sizeof(BSTNode));
T->key=k;
T->lchild=T->rchild=NULL;
return 1;//返回1,插入成功
}
else if(k<T->key)
T=T->lchild;//小于,则再左子树中插入
else if(k>T->key)
T=T->rchild;//大于,则再右子树中插入
}
return 0;//有相同值,插入失败
}
//递归实现插入
int BSTInsert(BSTree &T,int k){
if(T==NULL){//原树为空,新插入结点为根节点
T=(BSTree)malloc(sizeof(BSTNode));
T->key=k;
T->lchild=T->rchild=NULL;
return 1;//返回1,插入成功
}
else if(k==T->key)//树中找到相同结点
return 0;//返回0,插入失败
else if(k<T->key)
return BSTInsert(T->lchild,k);//插入到T的左子树
else
return BSTInsert(T->lchild,k);//插入到T的右子树
}
二叉排序树的构造:
略
二叉排序树的删除:
- 若被删除结点z为叶子节点,则直接删除
- 若z只有左子树或只有右子树,则让z的子树成为z父节点的子树
- 若z左右子树都有,则
①左子树最右下结点代替
②右子树最左下结点代替
查找长度:
- 概念:
需要对比关键字的次数称为查找长度,反应查找操作的时间复杂度 - 查找成功的平均查找长度:
ASL=(第一层结点个数*1+第二层结点个数*2+…)/节点个数 - 查找失败平均查找长度:
ASL=(第几层的空孩子个数*第几层+…)/空孩子个数
5.7.2 平衡二叉树
平衡二叉树:简称平衡树(AVL树)——树上任一节点的左子树和右子树的高度之差不超过1
结点的平衡因子=左子树高-右子树高
调整最小不平衡子树:
查找效率分析:
- 若树高为h,则最会情况下,查找一个关键字最多需要对比h次,即查找操作的时间复杂度不可能超过O(h)
- 假设以nh表示深度为h的平衡树中含有的最少结点数
则有n0=0,n1=1,n2=2,n3=4,n4=7,n5=12,并且有nh=nh-1+nn-2+1 - 平衡二叉树最大深度为O(log2n),平均查找长度/查找时间复杂度为O(log2n)
5.7.3 哈夫曼树
结点的权: 有某种现实意义的数值
节点的带权路径长度: 从树的根到到该结点的路径长度(经过的边数)与该结点上权值的乘积
树的带权路径长度: 树中所有叶子结点的带权路径长度之和(WPL)
哈夫曼树: 在含有n个带权叶子结点的二叉树中,其中带权路径长度最小的二叉树称为哈夫曼树,也称最优二叉树
哈夫曼树的构造:
哈夫曼编码:
固定长度编码:每个字符用相等长度的二进制位表示
可变长度编码:允许对不同字符用不等长的二进制位表示
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码
有哈夫曼树得到哈夫曼编码:字符集中的每个字符作为一个叶子结点,各个字符出现的频度作为结点的权值,构造哈夫曼树,左0右1编码