树
- 从根节点发展到分支节点,再发展到叶子节点。
- 空树:结点数为 0 。
- 非空树:有且仅有一个根节点。(只有分支节点有前驱和后继;每个分支节点和叶子结点有且仅有一个前驱)
- 子树:根节点下的子树一定互不相交。
- 节点关系:祖先节点、子孙节点、双亲(父)节点(族谱)、兄弟节点、孩子节点。
- 节点路径:只能从上往下。(单向)
- 树的路径长度:从根节点到每个叶子节点的路径长度之和。
- 属性:节点的层次(从上往下数)、节点的高度(深度)(从下往上数)、节点的度(有几个分支)、树的度(max(节点的度),树最宽的节点)
- 有序树:各子树从左到右有次序,不能互换。
- 无序树:各子树从左到右无次序,能互换。
- 森林:由 m(m≥0) 棵互不相交的树的集合。
- 性质:
-
节点数=总度数+1(总度数:总共有多少条分支(边);节点数:全树有多少节点)(总的节点的度+1)
-
度为 m 的树、m 叉树的区别:
度为 m 的树 m 叉树 任意节点的度 ≤ m 任意节点的度 ≤ m 至少有一个节点度 = m 允许所有节点的度都 < m 一定是非空树,至少有 m+1 个节点 可以是空树 度为 m 的树中至少有一个节点有 m 个子节点 m 叉树每个节点最多有 m 个子节点 -
度为 m 的树第 i 层最多只有 m i − 1 ~m^{i-1}~ mi−1 个节点。
-
高度为 h 的 m 叉树最多有 m h − 1 m − 1 ~\frac{m^h-1}{m-1}~ m−1mh−1 个节点。
m 0 + m 1 + m 2 + . . . + m h − 1 = a 1 ( 1 − q n ) 1 − q = 1 ∗ ( 1 − m h ) 1 − m = m h − 1 m − 1 m^0+m^1+m^2+...+m^{h-1}=\frac{a_1(1-q^n)}{1-q}=\frac{1*(1-m^h)}{1-m}=\frac{m^h-1}{m-1} m0+m1+m2+...+mh−1=1−qa1(1−qn)=1−m1∗(1−mh)=m−1mh−1
-
高度为 h 的 m 叉树至少有 h h h 个节点。(节点 ≠ 子节点(分支数))
高度为 h 、度为 m 的树至少有 h + m − 1 h+m-1 h+m−1 个节点。
-
有 n 个节点的 m 叉树的最小高度为 ⌈ log m ( n ( m − 1 ) + 1 ) ⌉ \lceil\log_m(n(m-1)+1)\rceil ⌈logm(n(m−1)+1)⌉
m h − 1 − 1 m − 1 ≤ n ≤ m h − 1 m − 1 = > m h − 1 ≤ n ( m − 1 ) + 1 ≤ m h = > h − 1 ≤ log m ( n ( m − 1 ) + 1 ) ≤ h \frac{m^{h-1}-1}{m-1}\le n \le\frac{m^h-1}{m-1}=>m^{h-1}\le n(m-1)+1\le m^h\\=>h-1\le\log_m(n(m-1)+1)\le h m−1mh−1−1≤n≤m−1mh−1=>mh−1≤n(m−1)+1≤mh=>h−1≤logm(n(m−1)+1)≤h
-
二叉树
- 概念:要么为空树,要么为二叉树。左右子树不能颠倒,是有序树。(空二叉树:节点数为0)
- 特殊二叉树:
- 满二叉树:除开叶子节点,其他都满了。
- 只有最后一层有叶子节点,
- 如果根为 1 ,则第 i i i 层的左分支为 2 i 2i 2i,右分支为 2 i + 1 2i+1 2i+1;父节点为 ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋
- 完全二叉树:当且仅当其中每个节点都与高度为 h 的满二叉树中编号为 1~n 的节点一 一对应。
- 最后两层才会出现叶子节点,
- 至多只有一个度(分支)为 1 的节点。(多一个编号就不是一 一对应)
- 如果根为 1 ,则第 i i i 层的左分支为 2 i 2i 2i,右分支为 2 i + 1 2i+1 2i+1;父节点为 ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋
- 编号 ≤ ⌊ n / 2 ⌋ \le\lfloor n/2\rfloor ≤⌊n/2⌋ 为分支节点,编号 > ⌊ n / 2 ⌋ >\lfloor n/2\rfloor >⌊n/2⌋ 为叶子节点。
- 如果某个节点只有一个分支,则其一定是左分支。(否则不会编号一 一对应)
- 若完全二叉树有偶数个节点,则必有一个单分支节点,其编号为 ⌊ n / 2 ⌋ \lfloor n/2\rfloor ⌊n/2⌋。
- 若完全二叉树有奇数个节点,则没有单分支节点。
- 二叉排序树:(二叉搜索树)
- 所有分支节点的左子树上的关键字均小于根节点的关键字,右子树的均大于。
- 适用于元素的排序、搜索等。
- 平衡二叉树:(特殊二叉排序树)
- 所有节点的左子树和右子树的深度差 ∈ { 0 , 1 } ~\in\{0,1\} ∈{0,1}。
- 有更高的搜索效率。
- 满二叉树:除开叶子节点,其他都满了。
- 性质:
-
非空二叉树中叶子节点比二分支节点多 1 个。 ( n = n 0 + n 1 + n 2 = 2 n 0 + n 1 − 1 n = n_0 + n_1 + n_2=2n_0+n_1-1 n=n0+n1+n2=2n0+n1−1)
-
高度为 h 的二叉树最少有 2 h − 1 2^{h-1} 2h−1个节点。
-
高度为 h 的二叉树最多有 2 h − 1 2^h-1 2h−1 个节点。(满二叉树)
-
有 n 个节点的完全二叉树高度 h 为 ⌈ log 2 ( n + 1 ) ⌉ 或 ⌊ log 2 n ⌋ + 1 ~\lceil\log_2(n+1)\rceil或\lfloor\log_2n\rfloor+1 ⌈log2(n+1)⌉或⌊log2n⌋+1。
2 h − 1 − 1 < n ≤ 2 h − 1 = > h − 1 < log 2 ( n + 1 ) ≤ h = > h = ⌈ log 2 ( n + 1 ) ⌉ 2 h − 1 ≤ n < 2 h = > h − 1 ≤ log 2 n < h = > h = ⌊ log 2 n ⌋ + 1 2^{h-1}-1<n\le2^h-1=>h-1<\log_2(n+1)\le h=>h=\lceil\log_2(n+1)\rceil\\2^{h-1}\le n<2^h=>h-1\le\log_2n<h=>h=\lfloor\log_2n\rfloor+1 2h−1−1<n≤2h−1=>h−1<log2(n+1)≤h=>h=⌈log2(n+1)⌉2h−1≤n<2h=>h−1≤log2n<h=>h=⌊log2n⌋+1
-
第 i 个节点所在层次为 ⌈ log 2 ( i + 1 ) ⌉ 或 ⌊ log 2 i ⌋ + 1 ~\lceil\log_2(i+1)\rceil或\lfloor\log_2i\rfloor+1 ⌈log2(i+1)⌉或⌊log2i⌋+1。
-
- 存储结构:
- 顺序存储:在顺序存储中一定要将二叉树编号和完全二叉树编号一一对应。
- 适用范围:只适用存储完全二叉树。(适用于完全二叉树性质)
- 链式存储:
- n 个节点的 m 叉链表总共有 m n − ( n − 1 ) ~mn-(n-1)~ mn−(n−1) 个空指针(空链域)。
- 顺序存储:在顺序存储中一定要将二叉树编号和完全二叉树编号一一对应。
- 二叉树遍历:
-
层次遍历:基于树的层次特性确定的次序规则。
-
先序遍历:根左右(NLR)(波兰式)
-
中序遍历:左根右(LNR)(常规式)
-
后序遍历:左右根(LRN)(逆波兰式)
-
递归实现:
void perorder(myerca *temp){ //空间复杂度O(h+1)=O(h) if (temp!=NULL){ visit(temp); perorder(temp->left); perorder(temp->right); } }
-
层序遍历:采用队列。从根开始,节点依次入队,若节点为非空则出队一个元素,并把它的左右孩子依次入队,然后下一轮。
void cengxu(BiTree s){ Queue *q; InitQueue(&q); InsertQueue(q,&s); //辅助队列 BiTNode *a; //临时变量 while (q->next!=NULL){ //辅助队列不为空 Output(q,&a); if (a==NULL){ //最后一次出队列 a=NULL break; } printf("%d ",a->data); if (s->lchild!=NULL){ InsertQueue(q,&a->lchild); //左孩子不为空,将其加入队列 } if (s->rchild!=NULL){ InsertQueue(q,&a->rchild); //右孩子不为空,将其加入队列 } } }
-
前序(后序)(层序) + 中序遍历序列唯一确定一棵二叉树。(先找到根节点再划分左右子树)(先序和后序不能确定)(根据前序、后序、层序依次套入中序中确定)
- 根据序列求二叉树的个数:卡特兰数: 1 n + 1 C 2 n n \frac{1}{n+1}C_{2n}^n n+11C2nn,唯一 一个中序确定一个二叉树
-
线索二叉树:(物理结构)
-
在普通二叉树中查找线性前驱只能再次进行前中后遍历。
-
线索:
- 由左孩子指针充当前驱,
- 由右孩子指针充当后继。
-
结构体:
typedef struct xiansuo{ DATATYPE data; struct xiansuo *left,*right; int ltag,rtag; //tag=1则表示指针是线索,tag=0表示指针指向孩子 }myxiansuo;
-
线索化:(递归版)
void threadtree(zizen t){ //线索化"主函数" pre=NULL; if (t!=NULL){ traversetreem(t); if (pre->right==NULL) //处理最后一个节点元素 pre->rtag=1; } } void printtree(zizen *t){ //中序遍历式线索化 if ((*t)->left==NULL){ //左分支为空,则将左指针指向pre的地址 (*t)->left=pre; (*t)->ltag=1; //设置线索标记 } if(pre->right==NULL && pre!=NULL){ //pre右分支为空且pre不为NULL,则将pre指向后继p pre->right = *t; pre->rtag=1; } pre=*t; //pre前移到p处 } void traversetreem(zizen t){ //中序递归遍历 if (t!=NULL){ traversetreem(t->left); printtree(&t); traversetreem(t->right); // printtree(&t); //后序递归遍历 } } void traversetreel(zizen t){ //前序递归遍历 if (t!=NULL){ printtree(&t); if (pre->ltag!=1) //防止前序转圈访问 traversetreel(t->left); traversetreel(t->right); } }
-
线索二叉树前趋、后继查找:
**//前趋查找:** mytree * firstnodem(zizen *t){ //中序查找 if (t->ltag==0){ t=t->left; while (t->rtag==0){ t=t->right; } return t; }else { return t->left; } } mytree * firstnodef(zizen *t){ //先序查找 if (t->ltag==1){ return t->left; }else{ //遍历 或 用三叉链表 } } mytree * firstnodel(zizen *t){ //后序查找 if (rtag==1){ return t->left; } if (rtag==0){ return t->right; } } **//后继查找:** mytree * lastnodem(zizen *t){ //中序查找 if (t->rtag==0){ t=t->right; while (t->ltag==0){ t=t->left; } return t; }else { return t->right; } } mytree * lastnodef(zizen *t){ //先序查找 if (t->ltag==0){ return t->left; } else{ return t->right; } } mytree * firstnodel(zizen *t){ //后序查找 if (t->rtag==1){ return t->right; } if (t->rtag==0){ //遍历 或 用三叉链表 } }
线索二叉树 前序 中序 后序 找前趋 × √ √ 找后继 √ √ × -
只有后序线索化的树的遍历需要栈的支持。
-
-
树的存储结构
- 双亲表示法:在每个节点中存储数据和指向父节点的指针。(顺序存储)
- 找爹方便,找孩子不方便
- 除开根的所有节点的指针都指向父节点
- 增加数据方便
- 删除分支节点不方便
- 孩子表示法:在每个节点中存储数据和指向它儿子们的指针。(顺序+链式存储)
- 找孩子方面,找爹不方便
- 找孩子方面,找爹不方便
- 孩子兄弟表示法:每个节点中存储数据、指向孩子的指针(左)、指向兄弟的指针(右)。(链式存储)(二叉树和树的转换)(表示方式唯一)
- 森林和二叉树的转换:用兄弟孩子表示法,同级的都是兄弟。
- 遍历:
-
先根遍历(深度优先):树为非空,先访问根,在递归访问子树。(二叉树先序)
void printtree(zizen *t){ if (!t){ visit(t); while (t还有子树){ printtree(t); } }
-
后根遍历(深度优先):(二叉树中序)
void printtree(zizen *t){ if (!t){ while(t还有子树){ printtree(t); } visite(t); } }
-
层次遍历(广度优先):等于二叉树层次
-
- 森林遍历:
- 树的先序遍历 = 森林的先序 = 二叉树的先序
- 树的后序遍历 = 森林的中序 = 二叉树的中序
- 森林的后序 = 二叉树的中序遍历
哈夫曼树
- 哈夫曼树:
-
节点的权:某种现实意义的数值。
-
节点的带权路径长度:由根到节点的路径(经过的边个数)* 节点权值
-
树的带权路径长度:所有叶子节点的带权路径长度之和。
W P L = ∑ i = 1 n w i l i WPL=\sum^n_{i=1}w_i~l_i WPL=i=1∑nwi li
-
在 n 个带权叶子节点的二叉树中,WPL 值最小的树称为哈夫曼树,也就是最优二叉树。
-
哈夫曼树的构造:依次结合最小的权值节点,一路滚雪球。(带权路径最小)(权值越大路径越短)(哈夫曼树不唯一,但WPL相同且最优)(不存在度为 1 的节点(只有一个分支))
-
编码时所有分支按照左小右大或右小左大,不要混用。
-
应用:
- 哈夫曼编码:字符集中每一个字符作为叶子,出现频度作为叶子权值 进行构造。(只能对叶子节点进行编码)(属于变长编码)
- 哈夫曼解码:
- 字符的出现频次跟所在的层级没关系。(每次选的是最小的两个频次的)
- 变长编码(只能将权值节点当做叶子节点处理(防止发生歧义))(若没有一个编码是另一个编码的前缀,则称为前缀编码(不会发生歧义))
- 用途:数据压缩、求最优解
- 定长编码:
- 所构成的二叉树是满二叉树。
- 所有字符(叶子)节点在相同层。(所有字符被赋予相同长度的编码)
-