数据结构笔记五:树与二叉树
树
树的基本概念
树是\(n(n\ge0)\)个结点的有限集合,\(n=0\)时,称为空树,这是一种特殊情况。
在任意一颗非空树应满足:
- 有且仅有一个特定的称为根的结点
- 当\(n\ge1\)时,其余结点可分为\(m(m>0)\)个互不相交的有限集合\(T_1,T_2,...,T_m\),其中每个集合本身又是一棵树,并且称为根结点的子树
结点,树的属性描述
- 结点的层次(深度)——从上往下数
- 结点的高度——从下往上数
- 数的的高度(深度)——总共多少层
- 结点的度——有几个孩子(分支)
- 数的度——各结点的度的最大值
有序树——从逻辑上看,树中结点的各子树从左至右是有次序的,不能互换。
无序树——从逻辑上看,树中结点的各子树从左至右是五次序的,可以互换。
森林——是\(m(m\ge0)\)课互不相交的树的集合
树的性质
-
结点数=总度数+1
-
度为m的树,m叉树的区别
树的度——各结点的度的最大值
m叉树——每个结点最多只能有m个孩子的树
度为m的树 m叉树 任意结点的度\(\le m\)(最多\(m\)个孩子) 任意结点的度\(\le m\)(最多\(m\)个孩子) 至少一个结点度\(=m\)(有\(m\)个孩子) 允许所有结点的度都\(<m\) 一定是非空树,至少有\(m+1\)个结点 可以是空树 -
度为\(m\)的树(\(m\)叉树)第i层至多有\(m^{i-1}(i\ge1)\)个结点
-
高度为\(h\)的\(m\)叉树至多有\(\frac{m^h-1}{m-1}\)个结点
-
高度为\(h\)的\(m\)叉树至少有\(h\)个结点
高度为\(h\),度为\(m\)的树至少有\(h+m-1\)个结点
-
具有n个结点的m叉树的最小高度为\(\lceil log_m(n(m-1)+1) \rceil\)
树的存储结构
双亲表示法(顺序存储)
每个结点中保存指向双亲的”指针“
#define MAX_TREE_SIZE 100 //树种最多结点数
typdef struct{
Elemtype data;
int parent; //双亲位置域
}PTNode;
typedef struct{
PTNode nodes[MAX_TREE_SIZE]; //双亲表示
int n; //结点数
}PTree;
优点:找父节点方便
缺点:找孩子不方便
孩子表示法(顺序+链式)
顺序存储各个节点,每个结点种保存孩子链表头指针
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 CSNode{
Elemtype data;
struct CSNode *firstchild,*nextchild; //第一个孩子和右兄弟指针
}CSNode,*CSTree;
树和二叉树的转换
森林与二叉树的转换
树、森林的遍历
树的先根遍历
若树非空,先访问根结点,再依次对每颗子树进行先根遍历
//树的先根遍历(伪代码)
void PreOrder(TreeNode *R)
{
if(R!=NULL)
{
visit(R); //访问根节点
while(R还有下一个子树T)
PreOrder(T); //先根遍历下一棵子树
}
}
树的先根遍历序列与这棵树相应二叉树的先序序列相同
树的后根遍历
若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点
//树的后根遍历(伪代码)
void PostOrder(TreeNode *R)
{
if(R!=NULL)
{
while(R还有下一个子树T)
PostOrder(T); //先根遍历下一棵子树
visit(R); //访问根节点
}
}
树的后根遍历序列与这棵树相应二叉树的中序序列相同
先根遍历和后根遍历是树的深度优先遍历
树的层次遍历
- 若树非空,则根结点入队
- 若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
- 重复②直到队列为空
树的广度优先遍历
森林的先序遍历
先序遍历效果等同于依次对各个树进行先根遍历(依次对二叉树进行先序遍历)
森林的中序遍历
中序遍历效果等同于依次对各个树进行后根遍历(依次对二叉树进行中序遍历)
总结
**
二叉树
二叉树的基本概念
二叉树是\(n(n\ge 0)\)个结点的有限集合:
- 或者为空二叉树,即\(n=0\)
- 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分布是一颗二叉树
特点:
- 每个结点至多只有两颗子树
- 左右子树不能颠倒(二叉树是有序树)
几种特殊的二叉树
满二叉树:一颗高度为h,且含有\(2^h-1\)个结点的二叉树
特点:
- 只有最后一层有叶子结点
- 不存在度为1的结点
- 按层从1开始编号,结点\(i\)的左孩子为\(2i\),右孩子为\(2i+1\);结点\(i\)的父结点为\(\lfloor i/2\rfloor\)
完全二叉树:当且仅当其每个结点都与高度为\(h\)的满二叉树中编号\(1\)~\(n\)的结点一一对应时,称为完全二叉树
特点:
- 只有最后两层可能有叶子结点
- 最多只有一个度为1的结点
- 同上
二叉排序树:一颗二叉树或者是空二叉树,或者具有如下性质的二叉树:
- 左子树上的所有结点的关键字均小于根结点的关键字
- 右子树上的所有结点的关键字均大于根结点的关键字
- 左子树和右子树又各是一颗二叉排序树
平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1(平衡二叉树具有更高的搜索效率)
二叉树的性质
-
设非空二叉树中度为0、1和2的结点个数分别为\(n_0\),\(n_1\)和\(n_2\),则\(n_0=n_2+1\)(叶子结点比二分支结点多一个)
-
二叉树第\(i\)层至多有\(2^{i-1}\)个结点(\(i\ge1\))
-
高度为\(h\)的二叉树至多有\(2^h-1\)个结点(满二叉树)
-
具有\(n\)个结点的完全二叉树的高度\(h\)为\(\lceil log_2(n+1) \rceil\)或者\(\lfloor log_2n \rfloor+1\)
-
对于完全二叉树,可以由结点数\(n\)推出度为\(0,1,2\)的结点数为\(n_0,n_1,n_2\)
若完全二叉树由\(2k\)个(偶数)个结点,则必有\(n_1=1,n_0=k,n_2=k-1\)
若完全二叉树由\(2k-1\)个(奇数)个结点,则必有\(n_1=0,n_0=k,n_2=k-1\)
二叉树的存储结构
顺序存储
#define MaxSize 100
struct TreeNode{
Elemtype value; //结点中的数据元素
bool isEmpty; //结点是否为空
};
TreeNode t[MaxSize];
定义一个长度为\(MaxSize\)的数组\(t\),按照从上至下,从左至右的顺序依次存储完全二叉树中的各个结点。
二叉树的顺序存储中,一定要把二叉树的结点编号与完全二叉树对应起来(顺序存储,只适合存储完全二叉树)
- \(i\)的左孩子——\(2i\)
- \(i\)的右孩子——\(2i+1\)
- \(i\)的父结点——\(\lfloor i/2 \rfloor\)
最坏情况:高度为\(h\)且只有\(h\)和结点的单支树,也至少需要\(2^h-1\)个存储单元
链式存储
//二叉树的结点(链式存储)
typedef struct BiTNode{
Elemtype data; //数据域
struct BiTNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;
//二叉树的结点(链式存储)
typedef struct BiTNode{
Elemtype data; //数据域
struct BiTNode *lchild,*rchild; //左右孩子指针
struct BiTNode *parent; //三叉链表(便于找父节点)
}BiTNode,*BiTree;
\(n\)个结点的二叉链表共有\(n+1\)个空链域(可以用于构造线索二叉树)
遍历算法
先序遍历
根左右(NLR)
//递归算法
void PreOrder(BiTree T)
{
if(T!=NULL)
{
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
中序遍历
左根右(LNR)
//递归算法
void InOrder(BiTree T)
{
if(T!=NULL)
{
InOrder(T->lchild);
visit(T);
InOrder(T->rchild);
}
}
后序遍历
左右根(LRN)
//递归算法
void PostOrder(BiTree T)
{
if(T!=NULL)
{
PostOrder(T->lchild);
PostOrder(T->rchild);
visit(T);
}
}
层次遍历
算法思想:
- 初始化一个辅助队列
- 根节点入队
- 若队列非空,则队头结点出队,访问该结点,并将其左右孩子插入队尾(有的话)
- 重复③直至队列为空
void LevelOrder(BiTree T)
{
LinkeQueue Q;
InitQueue(Q);
BiTree p;
EnQueue(Q,T);
while(!Istemp(Q))
{
DeQueue(Q,p);
if(p->lchild!=NULL)
EnQueue(Q,p->lchild);
if(p->rchild!=NULL)
EnQueue(Q,p->rchild);
}
}
应用举例
求树的深度
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;
}
}
由遍历序列构造二叉树
若只给出一棵二叉树的前/中/后/层次序列的一种,不能唯一确定一颗二叉树。
前序+中序遍历序列
后序+中序遍历序列
层次+中序
线索二叉树
存储结构
//线索二叉树结点
typedef struct ThreadNode{
ELemtype data;
struct ThreadNode *lchild,*rchlid;
int ltag,rtag; //左、右线索标志
}ThreadNode,*ThreadTree;
中序线索二叉树
先序线索二叉树
后序线索二叉树
三种线索对比
二叉树的线索化
中序线索化
//中序线索化
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); //递归,线索化右子树
}
}
//中序线索化二叉树T
void CreateInThread(ThreadTree T)
{
ThreadTree pre=NULL;
if(T!=NULL)
{
InThread(T,pre);
pre->rchild=NULL: //处理遍历的最后一个结点
pre->rtag=1;
}
}
先序线索化
//先序线索化
void PreThread(ThreadTree p,ThreadTree &pre)
{
if(p!=NULL)
{
if(p->lchild==NULL) //左子树为空,建立前驱线索
{
p->lchild=pre;
p->ltag=1;
}
if(pre!=NULL&&pre->rchild==NULL) //建立前驱结点的后继线索
{
pre-rchild=p;
pre->rtag=1;
}
pre=p;
if(p->ltag==0) //防止循环死锁
PreThread(p->lchild,pre); //递归,线索化左子树
PreThread(p->rchild,pre); //递归,线索化右子树
}
}
//先序线索化二叉树T
void CreatePreThread(ThreadTree T)
{
ThreadTree pre=NULL;
if(T!=NULL)
{
PreThread(T,pre);
if(pre->rchild==NULL) //处理遍历的最后一个结点
pre->rtag=1;
}
}
后序线索化
//后序线索化
void PostThread(ThreadTree p,ThreadTree &pre)
{
if(p!=NULL)
{
PostThread(p->lchild,pre); //递归,线索化左子树
PostThread(p->rchild,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;
}
}
//后序线索化二叉树T
void CreatePostThread(ThreadTree T)
{
ThreadTree pre=NULL;
if(T!=NULL)
{
PostThread(T,pre);
if(pre->rchild==NULL) //处理遍历的最后一个结点
pre->rtag=1;
}
}
线索二叉树找前驱后继
中序线上二叉树找中序后继
在中序线索二叉树中找到指定结点\(*p\)的中序后继next
- 若
p->rtag==1
,则next=p->rchild
- 若
p->rtag==0
,则next=p
的右子树中最左下结点
//找到以P为根的子树中,第一个被中序遍历的结点
ThreadNode *Firstnode(ThreadNode* p)
{
//循环找到最左下结点(不一定是叶结点)
while(p->ltag==0)
p=p->lchild;
return p;
}
//在中序线索二叉树中找到结点p的后继结点
ThreadNode *Nextnode(ThreadNode* p)
{
//右子树中最左下结点
if(p->rtag==0)
return FirstNode(p->rchild);
else
return p->rchild;
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode* T)
{
for(ThreadNode* p=FirstNode(T);p!=NULL;p=NextNode(p))
visit(p);
}
中序线上二叉树找中序前驱
在中序线索二叉树中找到指定结点\(*p\)的中序前驱pre
- 若
p->ltag==1
,则pre=p->lhild
- 若
p->ltag==0
,则pre=p
的左子树中最右下结点
//找到以P为根的子树中,最后一个被中序遍历的结点
ThreadNode *Lastnode(ThreadNode* p)
{
//循环找到最右下结点(不一定是叶结点)
while(p->rtag==0)
p=p->rchild;
return p;
}
//在中序线索二叉树中找到结点p的前驱结点
ThreadNode *Prenode(ThreadNode* p)
{
//左子树中最右下结点
if(p->ltag==0)
return Lastnode(p->lchild);
else
return p->lchild;
}
//对中序线索二叉树进行逆向中序遍历(利用线索实现的非递归算法)
void RevInorder(ThreadNode* T)
{
for(ThreadNode* p=Lastnode(T);p!=NULL;p=Prenode(p))
visit(p);
}
先序线索二叉树找先序后继
在先序线索二叉树中找到指定结点\(*p\)的先序后继next
- 若
p->rtag==1
,则next=p->rchild
- 若
p->rtag==0
,则- 若
p
有左孩子,则先序后继为左孩子 - 若
p
没有左孩子,则先序后继为右孩子
- 若
先序线索二叉树找先序前驱
在先序线索二叉树中找到指定结点\(*p\)的先序前驱pre
- 若
p->ltag==1
,则pre->lhild
- 若
p->ltag==0
,则若是二叉链表,则找不到前驱
若能找到p的父节点(三叉链表)
p
是左孩子时候:p
的父节点即为其前驱p
是右孩子且左兄弟为空时候:p
的父节点即为其前驱p
是右孩子且左兄弟非空时候:p
的前驱为左兄弟子树中最后一个被先序遍历的结点p
是根节点:p
没有先序前驱
后序线索二叉树找后序前驱
在后序线索二叉树中找到指定结点\(*p\)的后序前驱pre
- 若
p->ltag==1
,则pre->lhild
- 若
p->ltag==0
,则- 若
p
有右孩子,则先序后继为右孩子 - 若
p
没有右孩子,则先序后继为左孩子
- 若
后序线索二叉树找后序后继
在后序线索二叉树中找到指定结点\(*p\)的后序后继next
- 若
p->rtag==1
,则next=p->rchild
- 若
p->rtag==0
,则若是二叉链表,则找不到后继
若能找到p的父节点(三叉链表)
p
是右孩子时候:p
的父节点即为其后继p
是左孩子且右兄弟为空时候:p
的父节点即为其后继p
是左孩子且右兄弟非空时候:p
的后继为右兄弟子树中第一个被后序遍历的结点p
是根节点:p
没有后序后继
总结
二叉排序树(BST)
一颗二叉树或者是空二叉树,或者具有如下性质的二叉树:
- 左子树上的所有结点的关键字均小于根结点的关键字
- 右子树上的所有结点的关键字均大于根结点的关键字
- 左子树和右子树又各是一颗二叉排序树
进行中序遍历,可以得到一个递增的有序序列
二叉排序树的查找
若树非空,目标值与根结点的值比较:
- 若相等,则查找成功
- 若小于根结点,则在左子树上查找,否则在右子树上查找
//在二叉排序树中查找值为key的结点(非递归实现)
//最坏空间复杂度O(1)
BSTNode *BST_Search(BSTree T,int key)
{
while(T!=NULL&&key!=T->key) //若树空或等于根结点值,则结束循环
{
if(key<T->key)
T=T->lchild;
else
T=T->rchild;
}
}
//递归实现
//最坏空间复杂度O(n)
BSTNode *BSTSearch(BSTree T,int key)
{
if(T==NULL)
return NULL;
if(key==T->key)
return T; //查找成功
else if(key<T->key)
return BST_Search(T->lchild,key); //在左子树中找
else
return BST_Search(T->rchild,key); //在右子树中找
}
查找效率分析
查找长度——在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度
查找成功的平均查找长度ASL(Average Search Length)
-
最好情况:n个结点的二叉树最小高度为\(\lfloor log_2n \rfloor+1\),平均查找长度为\(O(log_2 n)\)
-
最坏情况:每个结点只有一个分子,树高h=结点数n,平均查找长度为\(O(n)\)
查找失败情况:需要补充失败结点
二叉排序树的插入
若原二叉排序树为空,则直接插入结点;
若关键字k小于根节点值,则插入到左子树,若关键字k大于根结点值,则插入到右子树
//在二叉排序树插入关键字为k的新结点(递归实现)
//最坏空间复杂度O(h)
int BST_Insert(BSTree &T,int k)
{
if(T==NULL) //原树为空,新插入的结点为根节点
{
T=(BSTree)malloc(sizeof(BSTNode));
T->key=k;
T->lchild=T->rchild=NULL;
return 1;
}
else if(K==T->key) //树种存在关键字的结点,插入失败
return 0;
else if(key<T->key)
return BST_Insert(T->lchild,k);
else
return BST_Insert(T->rchild,k);
}
二叉排序树的构造
//按照str[]种的关键字序列建立二叉排序树
void Create_BST(BSTree &T,int str[],int n)
{
T=NULL; //初始化为空树
int i=0;
while(i<n) //依次插入
{
BST_Insert(T,str[i]);
i++;
}
}
二叉排序树的删除
先搜索找到目标结点:
- 若被删除结点z是叶结点,则直接删除,不会破坏二叉排序的性质
- 若结点z只有一颗左子树或右子树,则让z的子树称为z父节点的子树,替代z的位置
- 若结点z有左,右两颗子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树种山区这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
- z的后继:z的右子树中最左下结点(这个结点一定没有左下结点)
- z的前驱:z的左子树中最右下结点(这个结点一定没有右下结点)
平衡二叉树(AVL)
树上任一结点的左子树和右子树的深度之差不超过1(平衡二叉树具有更高的搜索效率)
结点的平衡因子=左子树高-右子树 (-1,0,1)
平衡二叉树的插入
调整最小不平衡子树
调整最小不平横子树(LL)
由于在结点A的左孩子(L)的左子树上(L)上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡。
LL平衡旋转(右单旋转):将A的左孩子B向右上旋转替代A成为根结点,将A结点向右下旋转成为B的右子树的根结点,而B的原右子树则作为A结点的左子树。
//实现f向右下旋转,p向右上旋转
//其中f是爹,p是左孩子,gf是f他爹
f->lchild=p->rchild;
p->rchild=f;
gf->lchild/rchild=p;
调整最小不平横子树(RR)
由于在结点A的右孩子(R)的右子树上(R)上插入了新结点,A的平衡因子由-1增至-2,导致以A为根的子树失去平衡。
RR平衡旋转(左单旋转):将A的右孩子B向左上旋转替代A成为根结点,将A结点向左下旋转成为B的左子树的根结点,而B的原左子树则作为A结点的右子树。
//实现f向左下旋转,p向左上旋转
//其中f是爹,p是左孩子,gf是f他爹
f->rchild=p->lchild;
p->lchild=f;
gf->lchild/rchild=p;
调整最小不平横子树(LR)
由于在结点A的左孩子(L)的右子树上(R)上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡。
LR平衡旋转(先左后右旋转):先左旋然后右旋,先将A结点的左孩子B的右子树的根节点C向左上旋转提升到B结点的位置,然后再把该C结点向右上旋提升到A结点的位置。
调整最小不平横子树(RL)
由于在结点A的右孩子(R)的左子树上(L)上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡。
RL平衡旋转(先右后左旋转):先右旋然后左旋,先将A结点的右孩子B的左子树的根节点C向右上旋转提升到B结点的位置,然后再把该C结点向左上旋提升到A结点的位置。
查找效率分析
若树高为h,则最坏情况下,查找一个关键字最多需要对比n次,即查找曹祖的时间复杂度不可能超过\(O(n)\)
以\(n_h\)表示深度为h的平衡树中含有的最少结点数。
则有\(n_0=0,n_1=1,n_2=2\),并且有\(n_h=n_{h-1}+n_{h-2}+1\)
含有n个结点的平衡二叉树的最大深度为\(O(log_2n)\),平衡二叉树的平均查找长度为\(O(log_2 n)\)
哈夫曼树
带权路径长度
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
树的带权路径长度:树种所有叶结点带权路径长度之和(WPL,Weighted Path Length)
哈夫曼树的定义
在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。
哈夫曼树的构造
给定n个权值分别是\(w_1,w_2,...,w_n\)的结点,构造哈夫曼树的算法描述如下:
- 将这n个结点分别作为n课仅含一个结点的二叉树,构成森林F
- 构造一个新的新结点,从F中选取两颗根结点权值最小的树作为新的结点的左,右子树,并且将新结点的权值置为左,右子树上根结点的权值之和。
- 从F中删除刚才选出的两棵树,同时将新得到的树加入F中
- 重复步骤2和3,直至F中只剩下一棵树为止
性质:
- 每个初始结点最终都称为叶结点,且权值越小的结点到根结点的路径长度越大
- 哈夫曼树的结点总数为\(2n-1\)
- 哈夫曼树中不存在度为1的结点
- 哈夫曼树并不唯一,但WPL必然相同且为最优
哈夫曼编码
固定长度编码——每个字符用相等长度的二进制表示
可变长度编码——允许对不同字符用不等长的二进制位表示
若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码
有哈夫曼树得到哈夫曼编码——字符集中的每个字符作为一个叶子结点,各个字符出现的频次作为结点的权值,根据之前介绍的方法构造哈夫曼树