树
定义

结点的度数其实就是这个结点下连的线,比如:A的度 = AB+AC+AD = 3
树的度就是MAX(结点的度)
叶子结点就是没后代的结点
树的基本性质
-
所有结点数 = 所有结点的度数之和 + 1(这个1也就是根节点)
习题:
所有节点数 =4 + 20 * 4 + 10 * 3 + 1 * 2 + 10 * 1 + 1 = 123
123 - 20 - 10 - 1 - 10 = 82
-
所有结点数 = 不同度的节点数 之和
假设所有节点数为n,度0~4的个数为n0~n4,则n = n0 + n1 + n2 + n3 + n4
-
第一层m0,第二层m1,第i层m^(i-1)
-
等比数列求和公式
二叉树
定义

就是每个分支只有二叉的树,子树有左右之分
基本形态

二叉树的性质
-
这是某一层的最多结点数
-
- 这是整个二叉树的最多结点数
-
n0 = n2 + 1
对于任何非空的二叉树,度0(叶子)和度2的结点数为n0、n2,那么 n0 = n2 + 1
n2 = 0 的时候,n0 = 1,每把一个叶子画一个二叉,n2++,n0++n = 1 * n1 + 2 * n2 + 1
n = n0 + n1 + n2
解得,n0 = n2 + 1
特殊二叉树
满二叉树

完全二叉树

没有左子树,不能有右子树,上一层没铺满,不能有下一层
判断完全二叉树

不是

是
完全二叉树的性质
1.2.3.就是二叉树的性质
- 如果总结点数-1是奇数,说明有一个度为1的结点
习题

叶子结点出现在最后2层,这里求的是最多,所以最后一层是第7层
第6层最多的结点数=2^(6-1)=32
第6层的非叶子结点=32-8=24
第7层的最多结点数= 24*2 = 48
前6层的最多结点数=2^6 - 1 = 63
总计=63+48 = 111,选c

n = n0+n1+n2
n0=n2+1
所以,n = 2n2 + n1 + 1
而768-1是奇数,所以有一个度为1的结点,即n1 = 1
解得 n2 = 383
n0 = n2 + 1 = 384,选c

n=n0+n1+n2
n1 = 0
n0=n2+1
所以,n = 2n0 - 1 = 2k -1 ,选a
二叉树的实现
顺序结构实现------除了满二叉树和完全二叉树的其他场景比较浪费空间
链式结构实现

c
//链式结构实现
typedef char ElemType;
//树结点
typedef struct TreeNode {
ElemType data;
TreeNode *lchild;
TreeNode *rchild;
} TreeNode;
//用树结点指针表示二叉树
typedef TreeNode* BiTree;
创建二叉树
二级指针概念:

指针pp 存的是 指针p的地址
那么*pp 就是 得到p的地址,**pp就是得到p的值
c
char str[] = "ABDH#K###E##CFI###G#J##";
int idx = 0;
//创建二叉树
// T是二级指针(BiTree**)
// *T就是对二叉树的结点进行操作
void createTree(BiTree *T)
{
ElemType ch;
ch = str[idx++];
if (ch == '#')
{
*T = NULL;
}
else
{
*T = (BiTree)malloc(sizeof(TreeNode));
(*T)->data = ch;
createTree(& ( (*T)->lchild ) );
createTree(& ( (*T)->rchild ) );
}
}
遍历
前序遍历
NLR / NRL (根-左-右)/(根-右-左)
下面都以先左的遍历为例
从根节点开始,先从左子结点开始一层层向下递(进栈)并打印,如果左子节点是空就归(出栈),然后开始递右结点,进行如上同样操作,并向上一层层归,归到根节点后,对根节点的右子节点进行同样的操作。
具体动画演示可以看《数据结构(C 语言描述)》的55:00左右进度条
c
//前序遍历
void preOrder(BiTree T){
if(T == NULL) return;
printf("%c ", T->data);
//递归子树
preOrder(T->lchild);
preOrder(T->rchild);
}
中序遍历
LNR / RNL
从根节点开始,先从左子结点开始一层层向下递(进栈),如果左子节点是空就归(出栈)并打印,然后开始递右结点,进行如上同样操作,并向上一层层归,归到根节点后,对根节点的右子节点进行同样的操作。
c
//中序遍历
void inOrder(BiTree T){
if(T == NULL) return;
inOrder(T->lchild);
printf("%c ", T->data);
inOrder(T->rchild);
}
后序遍历
LRN / RLN
从根节点开始,先从左子结点开始一层层向下递(进栈),如果左子节点是空就归(出栈),然后开始递右结点,进行如上同样操作,在右结点空的时候归(出栈)并打印,并向上一层层归,归到根节点后,对根节点的右子节点进行同样的操作。
c
//后序遍历
void postOrder(BiTree T){
if(T == NULL) return;
postOrder(T->lchild);
postOrder(T->rchild);
printf("%c ", T->data);
}
习题

前:ABDHEICFGJK
中:HDBEIAFCJGK
后:HDIEBFJKGCA

前:ABDEGHCFI
中:DBGEHAFIC
后:DGHEBIFCA

先画出二叉树:
c
A
/ \
B D
/ / \
C E F
后:CBEFDA
二叉树遍历性质

习题

先右后左的中序遍历,RNL,选d


ADB都能画出来,所以选c

c
1
\
2
\
3
选b
(这题不要多选,一般情况只需要考虑先左就行)

c
f
/ \
c g
\ /
a d
/ \
e b
选b

因为对于顺序结构来说,没有的子树节点需要填NULL,所以相当于是高度为5的满二叉树需要的存储单元,也就是二叉树的最大结点数公式,即2^5 - 1 = 31
线索二叉树

目标:构建一个双向循环链表

会出现空余空间不够用的情况吗?
不会,n个节点有n+1个空
代码实现

c
typedef char ElemType;
typedef struct ThreadNode {
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag;
} ThreadNode;
typedef ThreadNode *ThreadTree;
ltag:0 指向lchild,1指向前驱
rtag:0 指向rchild,1指向后继
中序遍历线索化

c
#include <stdio.h>
#include <stdlib.h>
typedef char ElemType;
typedef struct ThreadNode {
ElemType data;
struct ThreadNode *lchild, *rchild;
int ltag, rtag;
} ThreadNode;
typedef ThreadNode *ThreadTree;
char str[] = "ABDH##I##EJ###CF##G##";
int idx = 0;
ThreadTree prev;
//创建二叉树
void createTree(ThreadTree *T){
ElemType ch;
ch = str[idx++];
if (ch == '#')
{
*T = NULL;
}
else
{
*T = (ThreadTree)malloc(sizeof(ThreadNode));
(*T)->data = ch;
createTree(& ( (*T)->lchild ) );
//lchild有左孩子,则ltag=0
if( (*T)->lchild != NULL){
(*T)->ltag = 0;
}
createTree(& ( (*T)->rchild ) );
if( (*T)->rchild != NULL){
(*T)->rtag = 0;
}
}
}
//线索化------加前驱后继的逻辑
void threading(ThreadTree T){
if(T != NULL){
//一直往左边遍历
threading(T->lchild);
//当前结点的左孩子为空,当前结点的左孩子设定为指向前驱
if(T->lchild == NULL){
T->ltag = 1;
T->lchild = prev;
}
//前驱结点的右孩子为空,前驱结点的右孩子设定为指向当前结点(当前结点就是前驱节点的后继)
if(prev->rchild == NULL){
prev->rtag = 1;
prev->rchild = T;
}
//更新prev到根节点,往右边遍历
prev = T;
threading(T->rchild);
}
}
//中序遍历线索化
void inOrderThreading(ThreadTree *head ,ThreadTree T){
*head = (ThreadTree)malloc(sizeof(ThreadNode));
(*head)->ltag = 0;
(*head)->rtag = 1;
(*head)->rchild = (*head);
if(T == NULL){
(*head)->lchild = (*head);
}
else{
//头节点的左孩子指向树的根节点
(*head)->lchild = T;
//prev:上一个访问的节点是头节点
prev = (*head);
//加前驱后继的逻辑
threading(T);
//最后一个节点的右孩子指向头节点
prev->rtag = 1;
prev->rchild = (*head);
//头节点的右孩子指向遍历的最后一个节点
(*head)->rchild = prev;
}
}
//基于线索的中序遍历
void inOrder(ThreadTree T){
ThreadTree current = T->lchild;
while(current != T){
//如果当前节点有左孩子,则一直往左边遍历
//没有左孩子,则退出当前循环 输出当前节点
while(current->ltag == 0){
current = current->lchild;
}
printf("%c",current->data);
//往右边遍历, 直到右孩子不为空且当前的右孩子是头节点
while(current->rtag == 1 && current->rchild != T){
current = current->rchild;
printf("%c",current->data);
}
current = current->rchild;
}
printf("\n");
}
int main(){
ThreadTree head;
ThreadTree T;
createTree(&T);
inOrderThreading(&head,T);
inOrder(head);
return 0;
}
习题

后序遍历:dbca
左虚线是前驱,右虚线是后继
根节点的前驱是头节点NULL
选D

c
根节点
/ \
Y X
后序遍历:YX根
右虚线是后继,所以X的右线索指向根,也就是父节点
选A

中序遍历:debxac
左虚线是前驱,右虚线是后继
所以b、a
选D
哈夫曼树
为什么要学哈夫曼树?

对于这样一个问题,通常用if分支表示

效率很低啊,有没有效率高的方式呢?
有的兄弟有的🤡
基本概念

路径:两个结点之间经过的分支
路径长度:路径上的分支数,也就是看这条路径上有几条线
树的路径长度:从根结点到每一个结点的路径长度之和
结点的权:权重
结点的带权路径长度:从该结点到树根之间的路径长度 * 该结点的权
树的带权路径长度(WPL):树的所有叶子结点的带权路径长度之和
计算WPL

构造哈夫曼树
-
先把有权值的叶子结点从小到大排列,形成有序序列
-
取2个最小权值的结点作为新结点N1的子结点,新结点N1的权值就是这2个最小权值的和
-
把N1替换取出的2个结点,加入到有序序列中重新排列
-
回到步骤2重复操作(取2个最小权值结点,作为新结点)
本质:让权重大的结点更靠近根结点,这样算WPL的时候就可以做到大权重乘小路径长度。
哈夫曼树的性质
- 哈夫曼树是WPL最小的二叉树
- 哈夫曼树只有度0(叶子)和度2的结点
- 哈夫曼树的叶子结点数为n,那么共有2n-1个结点
n = n0 + n2
n0 = n2 + 1
所以,n = 2 n0 - 1
哈夫曼编码
对于如下的表格:
画出哈夫曼树,然后左0右1标号
(也可以是左1右0)

哈夫曼编码结果:

对比原编码,哈夫曼编码显然效率更高

习题

哈夫曼树不一定是完全二叉树,因为不满足 上一层没铺满不能有下一层
选A

前缀编码:任一编码都不是其他编码的前缀
ABC都满足前缀编码
D中110是1100的前缀,选D

0100 | 011 | 001 | 001 | 011 | 11 | 0101 |
---|---|---|---|---|---|---|
a | f | e | e | f | g | d |
选D

选A
(自己画一下)

相当于叶子结点n,总结点115
115 = 2n - 1
解得n = 58
选C

画出哈夫曼树,把每个叶子结点*路径长度加到一起
WPL = 16 2* + 21 * 2 +30 * 2 + 10 * 3 + 12 * 3 = 200
选B
树与二叉树的转换
树-->二叉树


二叉树-->树


森林转二叉树
森林-->二叉树
-
把每个树各自转成二叉树
-
所有兄弟结点连线
-
只保留每个结点与第一个孩子的连线
-
旋转
-
后面的树也这样操作
-
-
整合成一个二叉树
详细操作看《数据结构(C 语言描述)》的53:40左右进度条
二叉树-->森林
- 拆成多个二叉树
-
从根结点开始,右结点存在就删去与右孩子的连线
-
- 每个 二叉树->树
-
从根结点开始,若结点的左孩存在,就把该结点与左孩的所有右孩相连
-
删去兄弟结点的连线
-
对每个二叉树做同样操作
-
旋转
-