数据结构学习总结--树和二叉树
树的定义
树是n个结点的有限集 它或为空树(n=0)或为非空树,对于非空树T:
- 有且仅有一个称之为根的结点
- 除根结点外的其余结点可以分为m个互不相交的有限集\((T_{1},T_{2},T_{m})\)其中每一个集合本身又是一棵树,并且称为根的子树。
树的基本术语
- 结点:书中一个独立单元,包含一个数据元素及若干指向其子树的分支。
- 结点的度:结点拥有的子树数称为结点的度。
- 树的度:树的度是树内各结点度的最大值。
- 叶子:度为0的结点称为叶子或者终端结点。
- 非终端结点:度不为0的结点称为非终端结点或分支结点,除根结点外,非终端结点也称为内部结点。
- 双亲和孩子:结点的子树的根称为该结点的孩子,相应地该结点称为孩子的双亲。
- 兄弟:同一个双亲的孩子之间互称兄弟。
- 祖先:从根到该节点所经分支上的所有结点。
- 子孙:以某结点为根的子树中任一结点都称为该结点的子孙。
- 层次:结点的层次从根开始定义起,根为第一层,根的孩子为第二层。树中任意结点的层次等于其双亲结点的层次加一。
- 堂兄弟:双亲在同一层的结点互为堂兄弟。
- 树的深度:树中结点的最大层次称为树的深度或高度。
- 有序树和无序树:如果将树中结点的各个子树看成从左至右是有次序(不能互换)则称该树为有序树,否则为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的称为最后一个孩子。
- 森林:森林是m(m>=0)棵互不相交的树的集合。对于树中每个结点而言,其子树的集合即为森林 因此可以用森林和树相互递归的定义来描述树
就逻辑结构而言,任何一棵树都是一个二元组\(Tree=(root,F)\),其中root是数据元素,称为树的根结点;F是m棵树的森林,\(F=(T_{1},T_{2},T_{m})\)其中\(T_{i}=(r_{i},F_{i})\)称为根root的第i棵子树;当m不等于0时,在树根和其子树森林之间存在下列关系:RF={<root,\(r_{i}\)>|i=1,2,4,m, m>0}
二叉树的定义
二叉树是n个结点构成的集合,它或为空树(n=0);或为非空树,对于非空树T:
- 有且仅有一个称为根的结点。
- 除根节点以外的其余结点分为互不相交的子集\(T_{1}\)和\(T_{2}\),分别称为T的左子树和右子树,且\(T_{1}\)和\(T_{2}\)本身又都是二叉树。
二叉树与树一样具有递归的性质,二叉树与树的区别: - 二叉树每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点)。
- 二叉树的子树有左右之分,其次序不能任意颠倒。
二叉树的五种基本形态
五种基本形态:空二叉树,仅有根结点的二叉树,右子树为空的二叉树,左右子树均非空的二叉树,左子树为空的二叉树。
二叉树的性质
性质1:在二叉树的第i层上至多有\(2^{i-1}\)个结点。
性质2:深度为K的二叉树至多有\(2^k-1\)个结点。深度为k时至少有K个结点(如只有左子树的二叉树)。
性质3:对任何一棵二叉树T,如果其终端结点数为\(n_{0}\),度为2的结点数为\(n_{2}\),则\(n_{0}\)=\(n_{2}\);
😀小扩展:
一棵度为四的树:度为四说明该树中结点的子节点最多为4个。
树中结点总个数=(所有结点的度数即子节点个数)+1(根结点)
推广\(\implies\)一个森林所有结点数=(所有结点的度数即子节点个数)+N(N棵树每棵树只有一个根结点);
推广\(\implies\)一个森林的所有叶子结点数=(所有结点的度数+n(n棵树,每棵树只有一个根节点)-m(度数非0的结点个数)
重点来了啊,好好听好好听啊,错过这个村就没这个店了啊啊啊啊
满二叉树
满二叉树的定义:深度为k且含有\(2^k-1\)个结点的二叉树。
满二叉树的特点:每一层上的结点数都是最大结点数(每一层都满)即每一层i的结点都具有最大值\(2^{i-1}\)。 (叶子结点全部都在最底层)


完全二叉树
完全二叉树定义:深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一 一对应时称之为完全二叉树。
完全二叉树的特点是:
- 叶子结点只可能在层次最大的两层出现。
- 对任一结点,若其右分支的子孙的最大层次为l,则其左分支下的子孙的最大层次必为l或者l+1;
性质:
性质一:具有n个结点的完全二叉树的深度为\(\log_2 n\)取底整数(比如3.5取底整数为3)+1;
性质二:如果对于一棵有n个结点的完全二叉树的结点按层序编号,则对任一结点i有以下性质:

二叉树的存储结构
顺序存储结构
`//----二叉树的顺序存储表示----
#define MAXSIZE 100 //二叉树的最大结点数
typedef TElemType SqBiTree [MAXTSIZE]; //0号单元存储根结点
SqBiTree bt;`
顺序存储结构使用一组地址连续的存储单元来存储数据元素,为了能够在存储结构中反映出结点之间的逻辑关系,必须将结点依照一定规律安排在这组单元中。这种顺序存储结构仅仅适用于完全二叉树。
链式存储结构
组成:由二叉树的定义可知,二叉树的结点由一个数据元素和分别指向其左,右子树的两个分支构成,表示二叉树链表中结点至少包含三个域:数据域和左右指针域(或为了便于找到结点的双亲,还可在结点结构中增加一个指向其双亲结点的指针域),利用这两种结点结构所得二叉树的存储结构分别称为二叉链表和三叉链表,链表的头指针指向二叉树的根结点。
结论:在含有n个结点的二叉链表中有n+1个空链域。
两个指针域的结点结构

`///----二叉树的二叉链表存储表示
typedef struct BiNode{
TElemType data; //结点数据域
struct BiNode *lchild,*rchild; //左右孩子指针
}BiTNode,*BiTree;`
遍历二叉树
定义:遍历二叉树是指按某条搜索路径巡防树中的每个结点,使得每个结点均被访问一次,而且仅被访问一次。(访问的含义很广,可以对结点做各种处理,包括输出结点信息,对结点进行运算和修改)
遍历二叉树是二叉树最基本的操作,也是二叉树其他各种操作的基础,遍历的实质是对二叉树进行线性化的过程,即遍历的结果是将非线性结构的树中结点排成一个线性序列。
遍历二叉树的操作:
- 先序遍历:即先访问根结点再访问左子树最后访问右子树(根左右)
- 中序遍历:即先访问左子树再访问根结点最后访问右子树 (左根右)
- 后序遍历:即先访问左子树再访问右子树最后访问根结点(左右根)
二叉树先序遍历算法
`Status PreorderTraverse(BiTree T){
if(T=NULL) return ok; //空二叉树
else {
visit(T);
PreorderTraverse(T->lchild); ///递归遍历左子树
PreorderTraverse(T->rchild); ///递归遍历右子树
}
}`
中序遍历的递归算法
`void InorderTraverse(BiTree T)
{ //中序遍历二树T的递归算法
if(T) //若二叉树非空
{
InorderTraverse(T->lchild); //中序遍历左子树
cout<<T—>data; //访问根结点
InorderTraverse(T->rchild); //中序遍历右子树
}
}`
二叉树的层次遍历(队列类型定义)
`typedef struct
{
BTNode data[maxsize]; //存放队中元素
int front,rear; //队头队尾指针
} SqQueue; ` //顺序循环队列类型
算法 :
`void LevelOrder(BTNode *b)
{BTNOde *p; SqQueue *qu;
InitQueue(qu); ///初始化队列
enQueue(qu,b); //根结点指针进入队列
while(!QueueEmpty(qu)) //队不为空,循环
{ deQueue(qu,p); //出队结点p
printf("%c",p->data); //访问结点p
if(p->lchild!=null)
enQueue (qu,p->lchild); //有左孩子将其进队
if(p->rchild!=null)
enQueue (qu,p->rchild); //有右孩子将其进队
}
}`
先序遍历的顺序建立二叉链表
`void CreateBiTree(BiTree &T)
{ //按先序次序输入二叉树中结点的值(一个字符),创建二叉链表表示的二叉树T
cin>>ch;
if(ch=='#') T=null; ///递归结束,建立空树
else //递归创建二叉树
{
T=new BiTNode; //生成根结点
T->data=ch; //根结点数据域置为ch
CreateBiTree (T->lchild); //递归创建左子树
CreateBiTree (T->rchild); //递归创建右子树
}
}`
复制二叉树
定义:复制二叉树就是利用已有的一棵二叉树复制得到另外一棵与其完全相同的二叉树。
`void Copy(BiTree,BiTree &NewT)
{ //复制一棵和T完全相同的二叉树
if(T==null) //如果是空树,递归结束
{ NEWT=null;
return;
}
else
{
NewT=new BiTNode;
NewT->data=T->data; //复制根结点
Copy(T->lchild,NewT->lchild) //递归复制左子树
Copy(T->rchild,NewT->rchild) //递归复制右子树
}
}`
计算二叉树的深度
二叉树的深度为树中结点的最大层次,二叉树的深度为左右子树深度的较大者加一;
`///----计算二叉树的深度-----
int Depth(BiTree T)
{ //计算二叉树T的深度
if(T=NULL) return 0; //如果是空树,深度为0,递归结束
else
{
m=Depth(T->lchild); ///递归计算左子树的深度记为m
n=Depth(T->rchild); ///递归计算右子树的深度记为n
if(m>n) return (m+1); //二叉树的深度为m与n的较大者加1
else return(n+1);
}
}`
计算二叉树的叶子结点数
`Int LeadCount(BiTree T)
{ if(T==NULL) //空树返回0
return 0;
if(T->lchild==NULL&&T->rchild==NULL)
return 1; //叶子结点返回1
else
return Leafcount (T->lchild)+1
return Leafcount (T->rchild); }`
线索二叉树


以这种结点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表,其中指向结点前驱和后继指针叫做线索。加上线索的二叉树称为线索二叉树,对二叉树以某种次序遍历使其变为线索二叉树的过程叫做线索化。
`///----二叉树的二叉线索存储表示----
typedef struct BiThrNode
{ TElemType data;
struct BiThrNode *lchild,*rchild; //左右孩子指针
int LTag,RTag; //左右标志
}BiThrNode,*BiThrTree;`
三种线索二叉树

中序线索链表

树和森林
(树的知识点真的好多,唉加油吧 奥里给)
树的存储结构
-
双亲表示法:以一组连续的存储单元存储树的结点,每个结点除了数据域data外,还附设一个parent域用以指示其双亲结点的位置。找双亲容易找孩子比较难
![]()
`///----双亲表示法----- typedef struct{ TElemType data; childPtr firstchild; }CTBox;` -
孩子表示法:由于树中每一个结点可能有多棵子树,则可用多重链表,即每个结点有多个指针域其中每个指针指向一棵子树的根结点。
![]()
![]()
第一种结点格式则多重链表中结点是同构的,其中d为树的度,由于树中有很多结点的度小于d造成很多空链域 浪费不难推出在一棵n个结点度为k的树中必有n*(k-1)+1个空链域。
第二种结点格式,虽能节约存储空间但操作不方便。
其二方法:把每个结点的孩子结点排列起来,看成一个线性表 且以单链表做存储结构则n个结点有n个孩子链表

图二是带双亲的孩子链表

`///-----孩子表示法-----
typedef struct CTNode
{ int child;
struct CTNode *next;
}*childPtr;`
3.孩子兄弟表示法:孩子兄弟表示法又称为二叉树表示法或二叉链表表示法,即以二叉链表做树的存储结构,链表中结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点。


优点:这种存储结构的优点是它和二叉树的二叉链表表示完全一样便于将一般的树结构转换为二叉树进行处理,孩子兄弟表示法是应用较为普遍的一种树的存储表示法。
`///------树的二叉链表(孩子兄弟)存储表示----
typedef struct CSNode
{ ElemType data;
struct CSNode *firstchild,*nextsibling;
}CSNode,*CSTree; `
森林与二叉树的转换
-
树转换成二叉树:(兄弟相连留长子)
①加线:在兄弟之间加一条线;
②抹线:对每一个结点,除了其左孩子外去除其与其余孩子之间关系;
③旋转:以树的根节点为轴心,将树顺时针旋转45度; -
二叉树转化为树:(左孩右右连双亲,去掉原来右孩线)
-
森林转化为二叉树:(树变二叉根相连)
-
二叉树转换为森林:(去掉全部右孩子线,孤立二叉再还原

树和森林的遍历


森林的遍历分为先序遍历森林和中序遍历森林



哈夫曼树及其应用
哈夫曼树的基本概念
- 哈夫曼树的定义:哈夫曼树又称为最优树,是一类带权路径长度最短的树。
- 路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。
- 路径长度:路径上的分支数目称作路径长度。
- 树的路径长度:从树根到每一个结点的路径长度之和。
- 权:赋予某个实体的一个量,是对实体某个或某些属性的数值化描述,在数据结构中实体有结点(元素)和边(关系)两大类,所以对应有结点权和边权。如果一个棵树的结点上带有权值,则对应有带权树的概念。
- 结点的带权路径长度:从该结点到树根之间的路径长度与结点上权的乘积。
- 树的带权路径长度:树中所有叶子结点的带权路径长度之和,通常记作WPL。
- 哈夫曼树:假设有m个权重{\(W_{1},W_{2},,,,,,W_{m}\)},可以构造一棵含n个叶子结点的二叉树,每个叶子结点的权为\(W_{i}\),则其中带权路径长度WPL最小的二叉树称作最优二叉树或哈夫曼树。
具有不同带权路径长度的二叉树

结论:满二叉树不一定是哈夫曼树;哈夫曼树中权值越大的叶子结点离根越近;具有相同带权结点的哈夫曼树不唯一;
哈夫曼树的构造
① 构造森林全是根
② 选用两小造新树:(加个根节点)
③ 删除两小添新人
④重复第二,第三步骤直到剩单根为止
(在构造哈夫曼树时,首先选择权小的,保证权大的离根较近,计算树的带权路径长度自然会得到最小带权路径长度,这是一种典型贪心算法)

哈夫曼树算法的实现
由于哈夫曼树中没有度为1的结点,则一棵树有n个叶子结点的哈夫曼树共有2n-1个结点,树中每个结点还要包含其双亲信息和孩子结点信息。
哈夫曼树的结点形式

`////------哈夫曼树的存储形式------
typedef struct{
int weight; //结点权值
int parent,lchild,rchild; //结点双亲 左孩子,右孩子下标
}HTNode,*HuffmanTree;` //动态分配数组存储哈夫曼树
哈夫曼树的各结点存储在由HuffmanTree定义的动态分配数组中,为了方便,从1号单元开始使用,所以数组大小为2n。将叶子结点集中存储在前面部分1~n个位置,而后面的n-1个位置存储其余非叶子结点(这一点知识在后面解答哈夫曼树的初态终态会用到)。
`///----算法------
void CreateHuffmanTree(HuffmanTree &HT,int n)
{ //构造哈夫曼树HT
if(n<=1) return;
m=2*n-1; //数组总共2n-1个元素
HT=new HTNode[m+1]; //0号单元未用,所以需要动态分配m+1个单元,HT[m]表示根结点
for(i=1;i<m;++i) //将1~m单元中的双亲左孩子右孩子下标都初始化为0;
{ HT[i].parent=0;HT[i].lchild=0;HT[i].rchild=0;}
for(i=1;i<=n;++i) //输入前n个单元叶子结点的权值
cin>>HT[i].weight;
//----------初始化结束,开始构建哈夫曼树-------
for(i=n+1;i<m;++i)
{ //通过n-1次的选择删除合并来创建哈夫曼树
Select(HT,i-1,s1,s2);
//在HT[k](1<=k<=i-1)中选择两个其双亲域为0且权值最小的结点,并返回它们在HT中的序号s1和s2
HT[s1].parent=i;HT[s2].oarent=i;
//得到新结点i,从森林中删除s1,s2,将s1和s2的双亲域由0改为i;
HT[i].lchild=s1;HT[i].rchild=s2; //s1,s2分别作为i的左右孩子
HT[i].weight=HT[s1].weight+HT[s2].weight; //i的权值为左右孩子权值之和
}
}`
哈夫曼编码
前缀编码:在一个编码方案里,任一个编码都不是其他任何编码的前缀(最左子串)则称编码是前缀编码。前缀编码可以保证对压缩文件进行解码时不产生二义性,确保正确解码。
哈夫曼编码:对一棵具有n个叶子的哈夫曼树,若对树中的每个左分支赋予0,右分支赋予1,则从根到每个叶子的路径上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。
哈夫曼编码的性质
性质1:哈夫曼编码是前缀编码,任一哈夫曼编码都不会与任意其他哈夫曼编码的前缀部分完全重叠,因此哈夫曼编码是前缀编码。
性质2:哈夫曼编码是最优前缀编码。对于包括n个字符的数据文件,分别以它们的出现次数为权值构造哈夫曼树,则利用该树对应的哈夫曼编码对文件进行编码,能使该文件压缩后对应的二进制文件长度最短(带权路径长度最短)。
哈夫曼编码的思想:依次以叶子结点为出发点,向上回溯至根节点为止,回溯时走左分支则生成代码0,走右分支则生成代码1。
由于每个哈夫曼编码是变长编码,因此使用一个指针数组来存放每个字符编码串的首地址。
因为求解编码时是从哈夫曼树的叶子结点出发,向上回溯至根节点,所以对于每个字符,得到的编码顺序是从右往左,故将编码向数组cd存放的顺序也是从后向前的。




解码时从根节点开始向下解码直至叶子结点为止,代码为0走左分支,代码为1走右分支。

(国庆节快乐啊,啊啊啊啊啊树和二叉树写了好久了啊啊啊啊,不过梳理一遍知识点觉得很清晰)加油




浙公网安备 33010602011771号