数据结构学习笔记(二)——树与二叉树
树
树的基本概念

- 空树:结点数为0的树
- 非空树:
1.有且仅有一个根节点
2.除了根节点外,其余任何一个结点都有且仅有一个前驱
*有序树:逻辑上看,树上结点的各子树从左至右是有次序的,不能互换
*无序树:逻辑上看,树上结点的各子树从左至右是无次序的,可以互换
*森林:很多课互不相交的树的集合,可以有0棵树
树的常考性质
- 考点1:结点数=总度数+1
- 考点2:度为m的树与m叉树的区别:前者至少有一个结点子树为3,而后者只是定义最大的子树限制,其允许所有结点的度可以小于m
- 考点3:度为m的树第i层最多有\(m^{i-1}\)个结点(i大于等于1)
- 考点4:高度为h的m叉树至多\(\frac{m^h-1}{m-1}\)有个结点
- 考点5:高度为h的m叉树至多有h个结点。高度为h、度为m的树至少有h+m-1个结点。
- 考点6:具有n个结点的m叉树的最小高度为\(log_m(n(m-1)+1)\)
二叉树的概念
定义
binary tree由结点有限的集合构成(可以为空集)
或者为一个根结点(root)及其两棵互不相交、分别称作这个根的左子树和右子树的二叉树的集合
左右子树不能颠倒,二叉树为有序树
五种基本形态

相关术语
结点

以上图为例
- 结点(Node):1 2 3 4 5 6 7
包含一个数据元素及若干指向其子树的分支 - 结点的度(Degree):拥有子树的数量
- 叶结点(Leaf):4 5 6 7
结点度为0,没有子树的结点,又称为终端结点 - 分支结点:1 2 3
除叶结点外的其他结点,又称为非终端结点 - 子结点(Child): 1的子结点为2 3
结点x的子树的根结点为x的子结点 - 最左子结点:最左的子结点
- 父节点:有子结点的x成为该子结点的父结点
- 兄弟结点:同一父结点的子结点之间互为兄弟结点
- 祖先结点:从根结点到该结点的所有结点
- 子孙结点:以某个结点为根的子树的任何一个结点称为该结点的子孙结点
- 层数:根为第0层,子结点层数为父结点加一
- 深度:层数最大的叶结点的层数
- 高度:深度加一
- 树的度:结点度的最大值
几个特殊的二叉树
1.满二叉树:一颗高度为h,且含有\(2^h-1\)个结点的二叉树;
如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。

特点:
*(1)只有最后一层有叶子结点
*(2)不存在度为1的结点
*(3)按层序从1开始编号,结点i的左孩子为2i,右孩子为2i+1;结点i的父节点为\([\frac{i}{2}]\)(如果有的话)

完全二叉树与非完全二叉树
如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。

完全二叉树特点:
*(1)只有最后二层可能有叶子结点
*(2)最多只有一个度为1的结点
*(3)同满二叉树,序号与满二叉树结点序号一一对应
*(4)\(i\leq[n/2]\)为分支结点,\(i>[n/2]\) 为叶子结点 n为1——n的结点序号
以上图a为例\([6/2]=3\)小于等于3的结点都是分支结点,大于3的都是叶子结点
二叉排序树

特性:
- 左子树上所有结点的关键字均小于根结点的关键字;
- 右子树上所有结点的关键字均大于根结点的关键字;
- 左右子树又各是一颗二叉排序树
平衡二叉树
树上左子树和右子树的深度之差不超过1。高度低,搜索效率高



二叉树的性质
- 性质1:在二叉树的第i层上最多有\(2^i\)个结点(i≥0)
- 性质2:深度为k的二叉树至多有\(2^{k+1}-1\)个结点(k≥0)
- 性质3:对任何一个非空二叉树,度为0,1,2的结点个数设为\(n_0,n_1和n_2\) 则\(n_0=n_2+1\)
推理:- 结点总数\(n=n_0+n_1+n_2\)
- 树的结点树等于总度数+1=>\(n=n_1+2n_2+1\)
两式相减得出\(n_0=n_2+1\)
叶子结点个数比二分支结点个数多一个
- 性质4:具有n个结点的完全二叉树的深度
上限为\({log_2}^{n+1}-1\)
下限为\({log_2}^{n+1}\)
由满二叉树的定义我们可以知道,深度为k的满二叉树的结点数n一定是\(n=2^{k+1}-1\)。
因为这是最多的结点个数。那么对于\(n=2^{k+1}-1\)。
倒推得到满二叉树的深度为\({log_2}^{n+1}-1\),比如结点数为15的满二叉树,深度为3。
最少的结点个数为\(n=2^{k}-1\)
所以最小深度\({log_2}^{n+1}\) - 性质5:如果一颗有n个结点的完全二叉树(其深度为\({log_2}^{n+1}-1\)的结点按层序编号(从第0层开始,每层从左到右),对任一结点i(1<=i<=n),有
1.如果i=1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点。
2.如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i。
3.如果2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1
二叉树的存储结构
作为一种抽象数据类型,二叉树可以采用顺序存储结构和链式存储结构两种。
顺序储存结构
适合二叉树大小和形态不发生剧烈的动态变化的情况
利用数组对二叉树自顶而下,从左到右的连续给各个结点进行编号,得到一个顺序序列,再在相应结点位置存放二叉树的数值。

对完全二叉树,数组中的每一个位置都存放了一个数值,空间利用率较高,但是对于一般二叉树,会存在很多“空结点”,对于有很多单子树的树结构,会导致了很多数组空间的浪费,使得空间利用率较低。
链式存储结构
顺序存储结构对于单子树的结构会造成空间冗余,同时在进行插入删除操作时,可能需要移动很多结点的位置,这降低了算法的效率。链式存储结构可以很有效的克服这些缺点。
二叉链表
每一个结点都有三个域,一个数据域data, 两个指针域,左子结点Lchild,右子结点Rchild,
| Lchild-> | data | Rchild-> |
|---|
template <class type>
struct BintreeNode
{
type data;
BintreeNode *Lchild;
BintreeNode *Rchild;
}
三叉链表
二叉链表很难找到其父结点,而三叉结点添加了一个Parent指针域
| Lchild -> | data | Parent-> | Rchild-> |
|---|
template <class type>
struct BintreeNode
{
type data;
BintreeNode *Lchild;
BintreeNode *Rchild;
BintreeNode *Parent;
}
在建立二叉数数据类型的时候需要一个表头指针root,它指向二叉树的跟结点。

二叉树的数据结构c++代码构建
二叉树的遍历
二叉树的递归特性
①要么是空二叉树
②要么是由“根节点+左子树+右子树”组成的二叉树
遍历顺序
- 先序遍历:根左右(NLR)
- 中序遍历:左根右(LNR)
- 右序遍历:左右根(LRN)

分支结点逐层展开法

二叉树的层序遍历

算法思想:
①初始化一个辅助队列
②根结点入列
③若队列非空,则队头结点出队,访问该节点,并将其左、右孩子结点插入队尾
④重复步骤③直至队列为空
由遍历序列构造二叉树
- 一个遍历序列可能对应多种二叉树的形态,即无法通过前,中,后,层序遍历序列的一种无法确定二叉树的形态,其中任何一个序列与中序序列可以确定一个二叉树。其他两两组合无法确定一个二叉树
树的存储结构
双亲表示法(顺序存储)


孩子表示法(顺序+链式存储)
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,*nextsibling;//第一个孩子和右兄弟指针
}CSNode,*CSTree;
}
森林与二叉树的转换


线索二叉树

需要重新从头遍历
中序线索二叉树
对于n个结点的链式二叉树,会有n+1个空链域,将其利用起来

如何找中序后继
某一结点p,有两种情况
- 其右指针被线索化 那么其后继为p->rchild;
- 右指针没有被线索化,则其后继为其右子树的最左下结点
代码实现:
- 找任意子树的最左下结点-第一个被中序遍历的结点
- 在中序线索二叉树中找到结点p的后继结点
- 对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
ThreadNode *Firstnode(ThreadNode *p)
{
while(p->ltag==0) p=p->lchild;
return p;//返回本身
}
ThreadNode *Nextnode(ThreadNode *p)
{
if(p->rtag==0) return Firstnode(p->rchild); //右子树中最左下结点
else return p->rchild;
}
void Inorder(ThreadNode *T)//空间复杂度为O(1)
{
for(ThreadNode *p=Firstnode(T);p!=NULL;p=Nextnode(p))
visit(p);//访问结点
}
线索二叉树的存储结构
//二叉树的链式存储
typedef struct BiTNode{
ElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
//线索二叉树的链式存储
typedef struct ThreadNode{
ElemType data;
struct ThreadNode*lchild,*rchild;
int ltag,rtag;//左、右线索标志
}ThreadNode,*ThreadTree;


树和森林的遍历
树的遍历
- 先根遍历:若树非空,先访问根结点,再依次对每颗子树进行先根遍历
- 后根遍历:若树非空,先依次对每颗子树进行后根遍历,最后再访问根结点
- 层次遍历:
1.若树非空,则根结点入队
2.若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
3.重复2直至队列为空
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);
}
}
森林的遍历
- 先序遍历森林:
若森林非空,则按如下规则进行遍历:
访问森林中的第一棵树的根结点。
先序遍历第一棵树中根结点的所有子树森林
先序遍历除去第一棵树之后剩余的树构成的森林 - 中序遍历森林:
若森林非空,则按如下规则进行遍历:
中序遍历第一棵树中根结点的所有子树森林
访问森林中的第一棵树的根结点。
中序遍历除去第一棵树之后剩余的树构成的森林 - 后序遍历森林:
若森林非空,则按如下规则进行遍历:
后序遍历第一棵树中根结点的所有子树森林
后序遍历除去第一棵树之后剩余的树构成的森林
访问森林中的第一棵树的根结点。
二叉排序树(BST,binary search tree)
性质:
- 左子树上所有结点上的关键字均小于根结点的关键字
- 右子树上所有结点上的关键字均大于根结点的关键字
- 各左右子树各是一个二叉排序树

如果进行中序遍历,将得到一个递增的有序序列
二叉排序树的查找
\\代码实现
typedef struct BSTNode{
int key;
struct BSTNode *lchild,*rchild;
}BSTNode,*BSTree;
//在二叉排序树中查找值为 key 的结点
BSTNode *BST_Search(BSTree T,int key){
while(T!=NULL&&key!=T->key){//若树空或等于根结点值,则结束循环
if(key<T->key) T=T->lchild;//小于,则在左子树上查找
else T=T->rchild;//大于,则在右子树上查找
}
return T;
}
//在二叉排序树中查找值为 key 的结点(递归实现)
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
return BSTSearch(T->rchild, key);//在右子树中找
二叉排序树的插入
若原二叉排序树为空,则直接插入结点;否则,若关键字k小于根结点值,则插入到左子树,若关键字k大于根结点值,则插入到右子树
//在二叉排序树插入关键字为k的新结点(递归实现)最坏的空间复杂度为O(h )
int BST_Insert(BSTree &T,int k){
if(T==NULL){//原树为空,新插入的结点为根结点
T=(BSTree)malloc(sizeof(BSTNode));
T->key=k;
T->child=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 BSTInsert(T->rchild,k);
二叉排序树的构造
void Creat_BST(BSTree &T,int str[],int n){
T=NULL;//初始时T为空树
int i=0;
while(i<n){ //依次将每个关键字插入到二叉排序树中
BST_Insert(T,str[i]);
i++;
}
}
二叉树的删除
先搜索找到目标结点:
1.若被删除结点z是叶结点,则直接删除,不会破坏二又排序树的性质
2.若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置
3.若结点有左、右两棵子树,则令z 的直接后继 (或直接前驱)替代,然后从二又排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
查找效率分析
查找长度-在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度
查找成功
查找成功的平均查找长度(ASL)=每一个结点查找长度的总和除以结点数
树的高度越高,时间复杂度越大
最小高度为\(\lfloor log_2{n} \rfloor+1\)
平均查找长度为\(O(log_2{n})\)

平衡二叉树(AVL树)
平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1。
- 结点的平衡因子=左子树高-右子树高。
- 平衡二叉树结点的平衡因子的值只可能是-1,0或1
平衡二叉树的插入
在二又排序树中插入新结点后,如何保持平衡?
从插入点往回找到第一个不平衡结点,调整以该结点为根的子树,调整最小不平衡子树
调整最小不平衡子树
- LL:在A的左孩子的左子树中插入导致不平衡
- RR:在A的右孩子的右子树中插入导致不平衡
- LR:在A的左孩子的右子树中插入导致不平衡
- RL:在A的右孩子的左子树中插入导致不平衡
LL

在插入新结点不平衡之后,子树高度只有上图的情况
目标:
1.恢复平衡
2.保持二叉排序树的特性
LL平衡旋转(右单旋转):由于在结点A的左孩子(L)的左子树(L) 上插入了新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要一次向右的旋转操作。将A的左孩子B向右上旋转代替A成为根结点,将A结点向右下旋转成为B的右子树的根结点,而B的原右子树则作为A结点的左子树。
RR

AL<A<BL<B<BR
RR平衡旋转(左单旋转):由于在结点A的右孩子(R)的右子树 (R)插入了新结点,A的平衡因子由-1减至-2,导致以A为根的子树失去平衡,需要一次向左的旋转操作。将A的右孩子B向左上旋转代替A成为根结点,将A结点向左下旋转成为B的左子树的根结点,而B的原左子树则作为A结点的右子树
LR

LR平衡旋转(先左后右双旋转): 由于在A的左孩子(L)的右子树(R)上插入新结点,A的平衡因子由1增至2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。先将A结点的左孩子B的右子树的根结点C向左上旋转提升到B结点的位置,然后再把该C结点向右上旋转提升到A结点的位置
RL

总结

查找平均时间复杂度为\(O(log_2{n})\)
哈夫曼树
- 结点的权:某种现实含义的数值,表示结点的重要性
- 结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
- 树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL,Weighted Path Length)
定义:在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL) 最小的二叉树称为哈夫曼树,也称最优二叉树
哈夫曼树的构造
给定n个权值分别为w1, w2,.., wn,的结点,构造哈夫曼树的算法描述如下:
1)将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
2)构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
4)重复步骤2) 和3),直至F中只剩下一棵树为止。

- 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
2)哈夫曼树的结点总数为2n-1
3)哈夫曼树中不存在度为1的结点。
4)哈夫曼树并不唯一,但WPL必然相同且为最优
哈夫曼编码
可变长度编码一一允许对不同字符用不等长的二进制位表示若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码 (无歧义)
总结


浙公网安备 33010602011771号