树是以分支关系定义的层次结构.

重点内容有: 二叉树的存储结构及其各种操作; 树和森林与二叉树的相互转换.

树的定义

树是 n (n≥0) 个结点的有限集.

n=0 时为空树, n>0 时为非空树.

对于非空树 T:

  • 有且仅有一个称之为根的结点;
  • 除了根结点之外的其余节点可分为 m (m>0) 个互不相交的有限集 T1,T2,...,Tm, 其中每一个集合本身又是一棵树, 并且称为根的子树 (SubTree).

树的结构定义是一个递归的定义, 在树的定义中又用到了树的定义.


树的基本术语

结点: 树中的一个独立单元, 包含一个数据元素及若干指向其子树的分支.

根结点: 非空树中没有前驱结点的结点.

结点的度: 结点拥有的子树数. 结点的度, 既是它的子树的个数, 也是它的后继的个数, 也是它的分支的个数, 即从这个结点出来了几根线.

树的度: 树内各个结点度的最大值.

叶子: 度为 0 的结点, 也称为终端结点.

非终端结点: 度不为 0 的结点称为非终端结点, 也称为分支结点. 除了根结点之外的非终端结点也称为内部结点.

双亲和孩子: 和某一个结点直接相连的后继都是这个结点的孩子, 和某一个结点直接相连的前驱称为这个结点的双亲. 在线性结构中的前驱和后继的说法, 到了树中, 换成了双亲和孩子.

兄弟: 同一个双亲的所有孩子互为兄弟.

祖先: 从树的根结点到某一个结点的路径上经过的所有结点都是这个结点的祖先.

子孙: 以某一个结点为根结点的子树中的所有结点都是该结点的子孙.

层次: 结点的层次从根开始定义起, 根为第一层. 树中任一结点的层次, 等于双亲的层次加 1.

堂兄弟: 双亲不同但是双亲在同一层次的结点互为堂兄弟.

树的深度: 树的最大层次数称为树的深度或高度.

有序树和无序树: 如果将树中各个结点看成是从左到右是有次序的, 即不能互换, 则称该树为有序树, 否则为无序树.

森林: m (m≥0) 棵互不相交的树的集合. m=0 则森林为空. m=1 则该森林只有一棵树. 因此, 树一定是森林, 但森林不一定是树. 一棵树也可以看成是特殊的森林.

树中任意一个结点的子树的集合就是一个森林.

给森林中的各个树加上一个双亲结点, 则森林就变成了树. 如果把一棵树的根结点删除了, 树就变成了森林.

二叉树的定义

二叉树 (Binary Tree) 是 n(n≥0) 个结点所构成的集合.

n=0 时该二叉树为空树.

对于非空二叉树 T:

  • 有且仅有一个被称为根的结点;
  • 除了根结点之外的其余结点分为两个互不相交的子集 T1T2, 分别称为 T 的左子树和右子树. T1T2 本身又都是二叉树.

二叉树的定义同样具有递归性质.

二叉树和树的主要区别:

  • 二叉树中的结点度只有三种可能: 0, 1, 2. 二叉树中不存在度大于 2 的结点.
  • 二叉树的子树有左右之分, 次序不能颠倒. 如果颠倒了, 那就是另外一棵二叉树.

一棵树如果每一个结点都只有一个叉, 那就是一个线性表.

二叉树有且仅有 5 种基本形态, 任意一个二叉树都在这 5 种基本形态中, 如图:

任何一棵树都可以转换为一棵唯一对应的二叉树, 二叉树也能还原回去.



二叉树的基本术语

树的基本术语都适用于二叉树.

满二叉树: 深度为 k 且含有 2k1 个结点的二叉树.

满二叉树的特点:

  • 每一层的结点数都是最大结点数, 即每一层的都具有最大结点数 2i1.
  • 叶子结点全部在最底层.
  • 满二叉树里面只有度为 0 和度为 2 的结点, 不存在度为 1 的结点.
  • 满二叉树在同样深度的二叉树中, 结点数最多, 叶子结点数也是最多的.

对满二叉树进行编号:

  • 编号规则: 从根结点开始, 从上向下, 从左到右;
  • 每一个结点位置都有元素, 所以都有一个编号.

完全二叉树: 深度为 k 的, 有 n 个结点的二叉树, 当且仅当其每一个结点都与深度为 k 的满二叉树中编号从 1 至 n 的结点一一对应时, 称为完全二叉树.

完全二叉树的定义是基于满二叉树的, 满二叉树是一种特殊的完全二叉树, 是在层数一定时结点个数达到最大的完全二叉树.

完全二叉树的特点是:

  • 叶子结点只可能出现在层次最大的两层上.
  • 对任一结点, 若其右子树的最大层次为 m, 则其左子树的最大层次必为 m 或 m+1.
  • 最后一层可以不是满的, 但是倒数第二层一定是满的.

完全二叉树在顺序存储方式下可以复原.

在满二叉树中, 从最后一个结点开始, 连续地去掉任意个结点, 得到的就是一棵完全二叉树.

二叉树的性质

性质 1, 2, 3 是关于普通二叉树的, 性质 3 和 4 是关于完全二叉树的.

性质1

二叉树的第 i 层最多有 2i1 个结点.

证明:

二叉树每一个结点的度最多为 2, 因此第 i 层的最大结点个数为第 i-1 层的最大结点个数的 2 倍.

利用数学归纳法进行证明. 当 i=1 时, 即第一层, 有 1 个结点, 显然 2i1=20=1 是对的.

假设第 i-1 层最多有 2i11=2i2 个结点, 则第 i 层最多有 2i1 个结点.

每一层至少有 1 个结点.

性质2

深度为 k 的二叉树, 最多有 2k1 个结点.

证明:

对于一棵树, 如果每一层的结点数都达到最大值, 则整棵树的结点总数达到了最大值. 对于深度为 k 的二叉树, 当每一层结点数都达到了最大值时, 总的结点数为: i=0k12i=20+21++2k1=2k1

深度为 k 的二叉树, 最少有 k 个结点.

性质3

任何一棵二叉树, 设度为 0 的结点有 n0 个, 度为 2 的结点有 n2 个, 则 n0=n2+1.

证明:

假设一个二叉树的度为 0 的结点个数为 n0, 度为 1 的结点个数为 n1, 度为 2 的结点个数为 n2, 则总的结点个数为 n0+n1+n2. 除了根结点之外的每一个结点, 都有一个分支进入, 则分支总数为 n0+n1+n21. 另一方面, 度为 0 的结点出来 0 个分支, 度为 1 的结点出来 1 个分支, 度为 2 的结点出来 2 个分支, 所以分支总数为 n1+2×n2. 则 n0+n1+n21=n1+2×n2, 所以 n0=n2+1.

性质4

具有 n 个结点的完全二叉树的深度为 log2n+1.

证明:

假设深度为 k, 完全二叉树的倒数第二层一定是满的, 所以前 k-1 层的结点总数为 2k11, 第 k 层至少一个结点, k 层二叉树总共最多 2k1 个结点. 因此, 2k11<n<=2k1, 即 2k1<=n<2k, 两边取 2 的对数, 得到 k1<=log2n<k, 所以有 log2n<k<=log2n+1, 所以 k=log2n+1.

性质 4 表明了完全二叉树的结点个数和层数之间的关系.

性质5

编号为 i 的结点, 如果非根结点, 则双亲结点编号为 i/2, 如果有左孩子, 则左孩子的编号为 2i, 如果有右孩子, 则右孩子的编号为 2i+1.

如果一个结点没有左孩子, 则该节点为叶子结点; 如果没有右孩子, 则可能有左孩子, 不一定是叶子.

如果第 i 个结点有左孩子,则左孩子的编号一定是 2i. 如果没有左孩子, 说明 2i>n. 如果一个结点没有左孩子, 说明它一定没有右孩子, 说明它一定是一个叶子. 如果第 i 个结点有右孩子, 则右孩子的编号一定是 2i+1, 如果没有右孩子, 说明 2i+1>n. 但是可能有左孩子, 即可能不是叶子. 没有左孩子就一定没有右孩子, 就一定是叶子.

性质 5 表明了完全二叉树中双亲结点的编号和孩子结点的编号之间的关系.

简单证明:

依然是使用数学归纳法. 分结点 i 和结点 i+1 在同一行和不在同一行两种情况. 两个结点之间的相对位置关系一定是下图中的一种:

在或不在同一行, 先假设结论对于第 i 个结点是成立的, 很容易推出结论对于第 i+1 个结点也成立.

二叉树的存储结构

顺序存储结构

用一个数组来存储二叉树中的全部元素. 为了能够在存储结构中反映出结点之间的逻辑关系, 必须将二叉树中的结点按照一定的规律安排在数组内.

对于完全二叉树, 只要从根起, 依次从上到下, 从左到右存储结点元素, 即将编号为 i 的结点存储在数组中索引为 i-1 的位置.

图示:

对于一般的二叉树, 应将其每一个结点与完全二叉树上的结点相对照, 存储在一维数组的相应位置上, 但是中间不存在的结点需要在数组中空出这个位置.

因此顺序存储结构仅仅适用于完全二叉树.

在最坏的情况下, 一个深度为 k 的单支树, 具有 k 个结点, 但是却要占用长度为 2k1 的一维数组. 显然造成了存储空间的极大浪费.

所以对于一般的二叉树, 更适合采取链式存储结构.

链式存储结构

结点结构不同则链式存储结构也不同.

表示二叉树的链表的结点, 至少包含三个域, 一个数据域和两个指针域, 左指针域指示左孩子的位置, 右指针域指示右孩子的位置. 利用这种结点结构所得二叉树的存储结构称为二叉链表.

有时, 为了方便找到一个结点的双亲, 还可以在结点结构中增加一个指针域, 指向该结点的双亲. 利用这种结点结构所得二叉树的存储结构称为三叉链表.

链表的头指针指向根结点.

对于有 n 个结点的二叉链表, 有 2n 个链域, n-1 个分支, 则有 n-1 个非空链域, 则有 2n-(n-1)=n+1 个空链域. 即 n 个结点的二叉链表, 有 n-1 个非空链域和 n+1 个空链域.


二叉树的二叉链表存储表示:

typedef struct BiTNode {
int val; // 数据域
struct BinaryTreeeNode* lchild, * rchild; // 左右孩子域
}BiTNode;

二叉树的三叉链表存储表示:

typedef struct TriTNode {
int val; // 数据域
struct TriTNode* lchild, * rchild, * parent;
}TriTNode;

遍历二叉树

遍历二叉树 (traversing binary tree) 是指按某一条搜索路径巡访树中的每一个结点, 使得每一个结点均被访问一次, 而且仅被访问一次.

"访问" 的含义很广, 可以是对结点作各种处理, 如: 输出结点的信息, 修改结点的数据值等, 但要求这种访问不破坏原来的数据结构.

遍历的实质是对二叉树进行线性化的过程, 即遍历的结果是将非线性结构的树中结点排成一个线性序列.

遍历的目的: 得到树中所有结点的一个线性序列.

遍历的用途: 它是树结构插入, 删除, 修改, 查找和排序运算的前提, 是二叉树一切运算的基础和核心.

二叉树是由三个基本单元组成: 根结点, 左子树, 右子树. 若能依次遍历这三个部分, 便是遍历了整个二叉树.

以 L, D, R 分别表示遍历左子树, 访问根结点, 遍历右子树, 则可以有六种遍历二叉树的方案: DLR, LDR, LRD, DRL, RDL, RLD. 前三种和后三种是对称的, 前三种研究明白了, 那么后三种就都是一样的.

若限定先左后右, 则只有前三种情况.

DLR -- 先(根)序遍历
LDR -- 中(根)序遍历
LRD -- 后(根)序遍历

三种遍历算法的不同之处仅在于访问根结点和遍历左子树, 右子树的先后关系.




要想将中缀表达式转换为前缀或者后缀, 可以用表达式树作为中转.

先序遍历的递归算法:

void PreOrderTraverse(BiTree T) {
// 先序遍历二叉树T的递归算法
if (T) {
visit(T); // 访问根结点
InOrderTraverse(T->lchild); // 中序遍历左子树
InOrderTraverse(T->rchild); // 中序遍历右子树
}
}

中序遍历的递归算法:

void InOrderTraverse(BiTree T) {
// 中序遍历二叉树T的递归算法
if (T) {
InOrderTraverse(T->lchild); // 中序遍历左子树
visit(T); // 访问根结点
InOrderTraverse(T->rchild); // 中序遍历右子树
}
}

后序遍历的递归算法:

void InOrderTraverse(BiTree T) {
// 后序遍历二叉树T的递归算法
if (T) {
InOrderTraverse(T->lchild); // 中序遍历左子树
InOrderTraverse(T->rchild); // 中序遍历右子树
visit(T); // 访问根结点
}
}

可利用栈将递归算法改写成非递归算法.

从中序遍历递归算法执行过程中递归工作栈的状态可见:

(1) 工作记录中包含两项, 其一是递归调用的语句编号, 其二是指向根结点的指针, 则当栈顶记录中的指针非空时, 应遍历左子树, 即指向左子树根的指针进栈;
(2) 若栈顶记录中的指针值为空, 则应退至上一层, 若是从左子树返回, 则应访问当前层(即栈顶记录)中指针所指的根结点;
(3) 若是从右子树返回, 则表明当前层的遍历结束, 应继续退栈。从另一个角度看, 这意味着遍历右子树时不再需要保存当前层的根指针, 直接修改栈顶记录中的指针即可。

前序遍历的非递归算法:

void PreOrderTraverse(BiTree* T) {
BiTree* p = T; // 指针p指向根结点
BiTree* q = (BiTree*)malloc(sizeof(BiTree*));
BiTree* stack[100]; // 初始化一个空栈stack
int top = 0;
while (p || top != 0) {
if (p) {
stack[top++] = p; // 将p进栈
visit(p); // 在此处访问根结点
p = p->lchild; // p指向该结点的左孩子
} else {
q = stack[--top]; // 弹出栈顶元素
p = q->rchild; // p指向该结点的右孩子
}
}
}

中序遍历的非递归算法:

void InOrderTraverse(BiTree* T) {
BiTree* p = T; // 指针p指向根结点
BiTree* q = (BiTree*)malloc(sizeof(BiTree*));
BiTree* stack[100]; // 初始化一个空栈stack
int top = 0;
while (p || top != 0) {
if (p) {
stack[top++] = p; // 将p进栈
p = p->lchild; // p指向该结点的左孩子
} else {
q = stack[--top]; // 弹出栈顶元素
visit(q); // 在此处访问根结点
p = q->rchild; // p指向该结点的右孩子
}
}
}

后序遍历的非递归算法:

void PostOrderTraverse(BiTree* root) {
BiTree* stack[100];
int top = 0;
BiTree* p = root;
BiTree* flag = NULL; // 作为标记,等于刚刚访问过的元素,初始化为NULL
while (p || top != 0) {
if (p) {
stack[top++] = p;
p = p->lchild;
} else {
p = stack[top - 1]; // 获取栈顶元素
if (p->right && p->right != flag) { // 栈顶元素有右子树且右子树还没有被访问过
p = p->rchild;
stack[top++] = p;
p = p->lchild;
} else {
p = stack[--top];
visit(p); // 在此处访问根结点
flag = p;
p = NULL;
}
}
}
}

无论是递归还是非递归遍历二叉树, 因为每个结点被访问一次, 则不论按哪一种次序进行遍历, 对含 n 个结点的二叉树, 其时间复杂度均为 O(n).

所需辅助空间为遍历过程中栈的最大容量, 即树的深度, 最坏情况下为 n, 则空间复杂度也为 O(n).

还有一种遍历二叉树的方式, 叫做层次遍历法.

这是按照层次遍历二叉树的方式, 按照 "从上到下, 先左后右" 的顺序遍历二叉树, 即先遍历二叉树的第一层的结点, 然后是第二层的结点, 直到最底层的结点, 对每一层的遍历按照从左到右的次序进行.层次遍历算法借助队列实现.

层次遍历算法的代码:

#include <stdio.h>
#define MAXQSIZE 100
typedef struct BiTNode {
int val; // 数据域
struct BinaryTreeeNode* lchild, * rchild; // 左右孩子域
}BiTNode;
typedef struct SqQueue {
int* base;
int front;
int rear;
}SqQueue;
int InitQueue(SqQueue* Q) {
Q->base = (int*)malloc(sizeof(int) * MAXQSIZE);
int rear = 0;
int front = 0;
return 0;
}
int EnQueue(SqQueue* Q, int e) {
if ((Q->rear + 1) % MAXQSIZE == Q->front)
return -1;
Q->base[Q->rear] = e;
Q->rear = (Q->rear + 1) % MAXQSIZE;
return 0;
}
int DeQueue(SqQueue* Q, int* e) {
if (Q->rear == Q->front)
return -1;
*e = Q->base[Q->front];
Q->front = (Q->front + 1) % MAXQSIZE;
return 0;
}
bool isEmpty(SqQueue* Q) {
if (Q->rear == Q->front)
return true;
else return false;
}
// 二叉树层次遍历算法
void LevelOrder(BiTNode* b) {
BiTNode* p; // 定义一个临时的结点指针,用于保存出队元素
SqQueue* qu; // 定义一个顺序队列
InitQueue(qu); // 初始化队列
EnQueue(qu, b); // 根结点指针入队
while (!isEmpty(qu)) { // 当队列不为空时,则进行循环
DeQueue(qu, p); // 出队,用p保存出队结点
printf("%d\n", p->val); // 访问p结点
if (p->lchild) // 如果有左孩子,则将左孩子入队
enQueue(qu, p->lchild);
if (p->rchild) // 如果有右孩子,则将右孩子入队
enQueue(qu, p->rchild);
}
}

根据遍历序列确定二叉树

若二叉树中各个结点的值均不相同, 任意一棵二叉树的结点的先序序列, 中序序列和后序序列都是唯一的.

由先序序列和中序序列, 或者由后序序列和中序序列均能唯一确定一棵二叉树.

由先序序列和后序序列无法唯一确定一棵二叉树, 因为无法确定左右子树两部分.

根据先序序列和后序序列, 找到根结点; 根据中序序列, 找到左右子树.

先序序列中, 根结点一定在树或子树的头部; 后序序列中, 根结点一定在树或子树的尾部.

根结点在中序序列中必然将中序序列分割成两个子序列, 前一个子序列是根结点的左子树的中序序列, 而后一个子序列是根结点的右子树的中序序列. 根据这两个子序列, 在先序序列中找到对应的左子序列和右子序列. 在先序序列中, 左子序列的第一个结点是左子树的根结点, 右子序列的第一个结点是右子树的根结点. 这样, 就确定了二叉树的三个结点. 同时, 左子树和右子树的根结点又可以分别把左子序列和右子序列划分成两个子序列, 如此递归下去, 当取尽先序序列中的结点时, 便可以得到一棵二叉树.

同理, 由二叉树的后序序列和中序序列也可唯一地确定一棵二叉树. 因为, 依据后序遍历和中序遍历的定义, 后序序列的最后一个结点, 就如同先序序列的第一个结点一样, 可将中序序列分成两个子序列, 分别为这个结点左子树的中序序列和右子树的中序序列, 再拿出后序序列的倒数第二个结点, 并继续分割中序序列, 如此递归下去, 当倒着取尽后序序列中的结点时, 便可以得到一棵二叉树.

二叉树遍历算法的应用

"遍历" 是二叉树各种操作的基础, 假设访问结点的具体操作不仅仅局限于输出结点数据域的值, 而把 "访问" 延伸到对结点的判别, 计数等其他操作, 可以解决一些关于二叉树的其他实际问题.

建立二叉树

如果在遍历过程中生成结点, 这样便可建立二叉树的存储结构.

为简化问题, 设二叉树中结点的元素均为一个单字符. 假设按先序遍历的顺序建立二叉链表, T 为指向根结点的指针, 对于给定的一个字符序列, 依次读入字符, 从根结点开始, 递归创建二叉树.

只知道先序建立出来的二叉树不是唯一的, 必须输入一些别的信息, 补充一些空结点, 这样建立出来的二叉树才是唯一的. 如图:


代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
//按照先序遍历的顺序建立二叉链表
//二叉树的二叉链表存储表示
typedef struct BiNode {
char data; //结点数据域
struct BiNode* lchild, * rchild; //左右孩子指针
}BiTNode;
void CreateBiTree(BiTNode** T) {
//按先序次序输入二叉树中结点的值(一个字符),创建二叉链表表示的二叉树T
char ch;
scanf("%c", &ch);
if (ch == '#') *T = NULL; //递归结束,建空树
else {
*T = (BiTNode*)malloc(sizeof(BiTNode));
(*T)->data = ch; //生成根结点
CreateBiTree(&((*T)->lchild)); //递归创建左子树
CreateBiTree(&((*T)->rchild)); //递归创建右子树
}
}
//用中序遍历的递归算法
void InOrderTraverse(BiTNode* T) {
//中序遍历二叉树T的递归算法
if (T) {
InOrderTraverse(T->lchild);
printf("%c ", T->data);
InOrderTraverse(T->rchild);
}
}
int main() {
BiTNode* tree = (BiTNode*)malloc(sizeof(BiTNode));
printf("请输入建立二叉链表的序列:\n");
CreateBiTree(&tree);
printf("所建立的二叉链表中序序列:\n");
InOrderTraverse(tree);
return 0;
}

执行结果:

请输入建立二叉链表的序列:
ABC##DE#G##F###
所建立的二叉链表中序序列:
C B E G D F A

复制二叉树

复制二叉树就是利用已有的一棵二叉树复制得到另外一棵与其完全相同的二叉树. 根据二叉树的特点, 复制步骤如下: 若二叉树不空, 则首先复制根结点, 这相当于二叉树先序遍历算法中访问根结点的语句; 然后分别复制二叉树根结点的左子树和右子树, 这相当于先序遍历中递归遍历左子树和右子树的语句. 因此, 复制函数的实现与二叉树先序遍历的实现非常类似.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
//二叉树的二叉链表存储表示
typedef struct BiNode {
char data; //结点数据域
struct BiNode* lchild, * rchild; //左右孩子指针
}BiTNode;
//按照先序遍历的顺序建立二叉链表
void CreateBiTree(BiTNode** T) {
//按先序次序输入二叉树中结点的值(一个字符),创建二叉链表表示的二叉树T
char ch;
scanf("%c", &ch);
if (ch == '#') *T = NULL; //递归结束,建空树
else {
*T = (BiTNode*)malloc(sizeof(BiTNode));
(*T)->data = ch; //生成根结点
CreateBiTree(&((*T)->lchild)); //递归创建左子树
CreateBiTree(&((*T)->rchild)); //递归创建右子树
}
}
//用中序遍历的递归算法
void InOrderTraverse(BiTNode* T) {
//中序遍历二叉树T的递归算法
if (T) {
InOrderTraverse(T->lchild);
printf("%c ", T->data);
InOrderTraverse(T->rchild);
}
}
//复制二叉树
void Copy(BiTNode* T, BiTNode** NewT) {
if (T == NULL) //如果是空树,递归结束
*NewT = NULL;
else {
*NewT = (BiTNode*)malloc(sizeof(BiTNode));
(*NewT)->data = T->data; //复制根结点
Copy(T->lchild, &((*NewT)->lchild)); //递归复制左子树
Copy(T->rchild, &((*NewT)->rchild)); //递归复制右子树
}
}
int main() {
BiTNode* tree = (BiTNode*)malloc(sizeof(BiTNode));
BiTNode* newtree = (BiTNode*)malloc(sizeof(BiTNode));
printf("请输入建立二叉链表的序列:\n");
CreateBiTree(&tree);
printf("所建立的二叉链表中序序列:\n");
InOrderTraverse(tree);
printf("\n");
Copy(tree, &newtree);
printf("复制得到的新树的中序序列:\n");
InOrderTraverse(newtree);
return 0;
}

执行结果:

请输入建立二叉链表的序列:
ABC##DE#G##F###
所建立的二叉链表中序序列:
C B E G D F A
复制得到的新树的中序序列:
C B E G D F A

计算二叉树的深度

如果是空树, 递归结束, 深度为 0.否则,执行:

  1. 递归计算左子树的深度, 记为 m.
  2. 递归计算右子树的深度, 记为 n.
  3. 如果 m 大于 n, 二叉树的深度为 m+1, 否则为 n+1.

计算二叉树的深度是在后序遍历二叉树的基础上进行的运算.

代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
//二叉树的二叉链表存储表示
typedef struct BiNode {
char data; //结点数据域
struct BiNode* lchild, * rchild; //左右孩子指针
}BiTNode;
//按照先序遍历的顺序建立二叉链表
void CreateBiTree(BiTNode** T) {
//按先序次序输入二叉树中结点的值(一个字符),创建二叉链表表示的二叉树T
char ch;
scanf("%c", &ch);
if (ch == '#') *T = NULL; //递归结束,建空树
else {
*T = (BiTNode*)malloc(sizeof(BiTNode));
(*T)->data = ch; //生成根结点
CreateBiTree(&((*T)->lchild)); //递归创建左子树
CreateBiTree(&((*T)->rchild)); //递归创建右子树
}
}
//用中序遍历的递归算法
void InOrderTraverse(BiTNode* T) {
//中序遍历二叉树T的递归算法
if (T) {
InOrderTraverse(T->lchild);
printf("%c ", T->data);
InOrderTraverse(T->rchild);
}
}
//计算二叉树的深度
int Deepth(BiTNode* T) {
int m, n;
if (T == NULL) return 0;
else {
m = Deepth(T->lchild);
n = Deepth(T->rchild);
return m > n ? (m + 1) : (n + 1);
}
}
int main() {
BiTNode* tree = (BiTNode*)malloc(sizeof(BiTNode));
BiTNode* newtree = (BiTNode*)malloc(sizeof(BiTNode));
printf("请输入建立二叉链表的序列:\n");
CreateBiTree(&tree);
printf("所建立的二叉链表中序序列:\n");
InOrderTraverse(tree);
printf("\n");
printf("树的深度为:%d\n", Deepth(tree));
return 0;
}

执行结果:

请输入建立二叉链表的序列:
ABC##DE#G##F###
所建立的二叉链表中序序列:
C B E G D F A
树的深度为:5

统计二叉树中结点的个数

如果是空树, 则结点个数为 0; 否则, 结点个数为左子树的结点个数加上右子树的结点个数再加上 1.

代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
//二叉树的二叉链表存储表示
typedef struct BiNode {
char data; //结点数据域
struct BiNode* lchild, * rchild; //左右孩子指针
}BiTNode;
//按照先序遍历的顺序建立二叉链表
void CreateBiTree(BiTNode** T) {
//按先序次序输入二叉树中结点的值(一个字符),创建二叉链表表示的二叉树T
char ch;
scanf("%c", &ch);
if (ch == '#') *T = NULL; //递归结束,建空树
else {
*T = (BiTNode*)malloc(sizeof(BiTNode));
(*T)->data = ch; //生成根结点
CreateBiTree(&((*T)->lchild)); //递归创建左子树
CreateBiTree(&((*T)->rchild)); //递归创建右子树
}
}
//用中序遍历的递归算法
void InOrderTraverse(BiTNode* T) {
//中序遍历二叉树T的递归算法
if (T) {
InOrderTraverse(T->lchild);
printf("%c ", T->data);
InOrderTraverse(T->rchild);
}
}
//统计二叉树中结点的个数
int NodeCount(BiTNode* T) {
if (T == NULL) return 0;
else return NodeCount(T->lchild) + NodeCount(T->rchild) + 1;
}
int main() {
BiTNode* tree = (BiTNode*)malloc(sizeof(BiTNode));
BiTNode* newtree = (BiTNode*)malloc(sizeof(BiTNode));
printf("请输入建立二叉链表的序列:\n");
CreateBiTree(&tree);
printf("所建立的二叉链表中序序列:\n");
InOrderTraverse(tree);
printf("\n");
printf("结点个数为:%d\n", NodeCount(tree));
return 0;
}

执行结果:

请输入建立二叉链表的序列:
ABC##DE#G##F###
所建立的二叉链表中序序列:
C B E G D F A
结点个数为:7

求叶子结点个数

思路:

  1. 如果是空树, 则叶子结点数为 0.
  2. 否则, 为左子树的叶子结点个数 + 右子树的叶子结点个数.

代码:

#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
//二叉树的二叉链表存储表示
typedef struct BiNode {
char data; //结点数据域
struct BiNode* lchild, * rchild; //左右孩子指针
}BiTNode;
//按照先序遍历的顺序建立二叉链表
void CreateBiTree(BiTNode** T) {
//按先序次序输入二叉树中结点的值(一个字符),创建二叉链表表示的二叉树T
char ch;
scanf("%c", &ch);
if (ch == '#') *T = NULL; //递归结束,建空树
else {
*T = (BiTNode*)malloc(sizeof(BiTNode));
(*T)->data = ch; //生成根结点
CreateBiTree(&((*T)->lchild)); //递归创建左子树
CreateBiTree(&((*T)->rchild)); //递归创建右子树
}
}
//用中序遍历的递归算法
void InOrderTraverse(BiTNode* T) {
//中序遍历二叉树T的递归算法
if (T) {
InOrderTraverse(T->lchild);
printf("%c ", T->data);
InOrderTraverse(T->rchild);
}
}
//求叶子结点的个数
int CountLeaf(BiTNode* T) {
if (T == NULL) return 0;
if ((T->lchild == NULL) && (T->rchild == NULL)) return 1;
else return CountLeaf(T->lchild) + CountLeaf(T->rchild);
}
//或者:
//int CountLeaf(BiTNode* T) {
// if (T == NULL) return 0;
// else {
// if ((T->lchild == NULL) && (T->rchild == NULL)) return 1;
// else return CountLeaf(T->lchild) + CountLeaf(T->rchild);
// }
//}
int main() {
BiTNode* tree = (BiTNode*)malloc(sizeof(BiTNode));
BiTNode* newtree = (BiTNode*)malloc(sizeof(BiTNode));
printf("请输入建立二叉链表的序列:\n");
CreateBiTree(&tree);
printf("所建立的二叉链表中序序列:\n");
InOrderTraverse(tree);
printf("\n");
printf("叶子结点个数为:%d\n", CountLeaf(tree));
return 0;
}

执行结果:

请输入建立二叉链表的序列:
ABC##DE#G##F###
所建立的二叉链表中序序列:
C B E G D F A
叶子结点个数为:3

度为 2 的结点个数为叶子个数加 1.

度为 1 的结点个数为结点总数减去叶子结点个数再减去度为 2 的结点个数.

线索二叉树

遍历二叉树是以一定规则, 将二叉树中的结点排列成一个线性序列, 得到二叉树中结点的先序序列, 中序序列或后序序列. 这实质上是对非线性结构进行线性化操作, 使每一个结点 (除第一个结点和最后一个结点外) 在这些线性序列中有且仅有一个直接前驱和直接后继.

当以二叉链表作为存储结构时, 只能找到结点的左右孩子信息, 而不能得到结点在任一序列中的前驱和后继的信息, 这种信息只有在遍历的动态过程中才能得到. 为此, 引入线索二叉树来保存这些在动态过程中得到的有关前驱和后继的信息.

虽然可以在每个结点中增加两个指针域来存放在遍历时得到的有关前驱和后继信息, 但是这样将使得结构的存储密度大大降低.

由于有 n 个结点的二叉链表中必定存在 n+1 个空链域, 因此可以充分利用这些空链域来存放结点的前驱和后继信息.

规定: 若结点有左子树, 则 lchild 域指向其左孩子, 若结点没有左孩子, 则 lchild 域由空改为指向其前驱. 若结点有右孩子, 则其 rchild 域指向其右孩子, 若结点没有右孩子, 则 rchild 域由空改为指向其后继.

同时, 改变结点的结构, 增加两个数据域作为标志, 分别为 LTag 和 RTag. 并规定:

  • LTag=0: lchild 域指向该结点的左孩子.
  • LTag=1: lchild 域指向该结点的前驱.
  • RTag=0: rchild 域指向该结点的右孩子.
  • RTag=1: rchild 域指向该结点的后继.

这种改变了指向的指针 (由 NULL 改为指向前驱或后继) 称为线索.

加上线索的二叉树称为线索二叉树 (Threaded Binary Tree).

对二叉树按照某种遍历次序使其变为线索二叉树的过程称为线索化.

二叉树的二叉线索存储表示:

typedef struct BiThrNode {
ElemType data; // 根据具体的元素类型修改ElemType
struct BiThrNode* lchild, * rchild;
int LTag, RTag; // 这两个标志域一定为int类型
}BiThrNode;

凡是谈线索二叉树, 都一定要带上某种序列, 如中序线索二叉树, 后序线索二叉树等.

加上线索后, 可以将指针和线索区分开, 指针指向左右子树, 线索指向前驱后继.

为了方便起见, 仿照线性表的存储结构, 在二叉树的线索链表上也添加一个头结点, 并令其 lchild 域的指针指向二叉树的根结点, 其 rchild 域的指针指向中序遍历时访问的最后一个结点; 同时, 令二叉树中序序列中第一个结点的 lchild 域指针和最后一个结点 rchild 域的指针均指向头结点. 这好比为二叉树建立了一个双向线索链表, 既可从第一个结点起顺后继进行遍历, 也可从最后一个结点起顺前驱进行遍历.

构造线索二叉树

由于线索二叉树的构造的实质是将二叉链表中的空指针改为指向前驱或后继的线索, 而前驱或者后继的信息只有在遍历时才能获得, 因此线索化的过程即为在遍历的过程中修改空指针的过程, 可用递归算法.

对二叉树按照不同的遍历次序进行线索化, 可以得到不同的线索二叉树.

比如, 按照先序遍历构造线索二叉树, 也叫先序线索化, 得到先序线索二叉树; 按照中序遍历构造线索二叉树, 也叫中序线索化, 得到中序线索二叉树; 按照后序遍历构造线索二叉树, 也叫后序线索化, 得到后序线索二叉树.

为了记下遍历过程中结点访问的先后关系, 附设一个结点 pre 始终指向刚刚访问过的结点, 而指针 p 指向当前访问的结点, 由此记录下遍历过程中结点访问的先后关系.

算法 5.7 是对树中任意一个结点 p 为根的子树中序线索化的过程, 算法 5.8 通过调用算法 5.7 来完成整个二叉树的中序线索化.

算法5.7, 以结点 p 为根的子树中序线索化.

代码:

//以结点P为根的子树中序线索化 (算法5.7)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
//二叉树的二叉线索类型存储表示
typedef struct BiThrNode {
char data; //结点数据域
struct BiThrNode* lchild, * rchild; //左右孩子指针
int LTag, RTag;
}BiThrNode, * BiThrTree;
//全局变量pre
BiThrNode pri = { 'a',NULL,NULL,0,0 };
BiThrNode* pre = &pri;
//按先序次序建立二叉链表
void CreateBiTree(BiThrNode** T) {
//按先序次序输入二叉树中结点的值(一个字符),创建二叉链表表示的二叉树T
char ch;
scanf("%c", &ch);
if (ch == '#') *T = NULL; //递归结束,建空树
else {
*T = (BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->data = ch; //生成根结点
CreateBiTree(&((*T)->lchild)); //递归创建左子树
CreateBiTree(&((*T)->rchild)); //递归创建右子树
}
}
void InThreading(BiThrNode* p) {
//pre是全局变量,初始化时其右孩子指针为空,便于在树的最左点开始建线索
if (p) {
InThreading(p->lchild); //左子树递归线索化
if (!p->lchild) { //p的左孩子为空
p->LTag = 1; //给p加上左线索
p->lchild = pre; //p的左孩子指针指向pre(前驱)
}
else
p->LTag = 0;
if (!pre->rchild) { //pre的右孩子为空
pre->RTag = 1; //给pre加上右线索
pre->rchild = p; //pre的右孩子指针指向p(后继)
}
else
pre->RTag = 0;
pre = p; //保持pre指向p的前驱
InThreading(p->rchild); //右子树递归线索化
}
}
int main() {
pre->RTag = 1;
pre->rchild = NULL;
BiThrNode* tree = (BiThrNode*)malloc(sizeof(BiThrNode));
printf("请输入建立二叉链表的序列:\n");
CreateBiTree(&tree);
InThreading(tree);
printf("线索化完毕!\n");
return 0;
}

执行结果:

请输入建立二叉链表的序列:
ABC##DE#G##F###
线索化完毕!

算法5.8, 带头结点的二叉树中序线索化.

代码:

//带头结点的中序线索化 (算法5.8)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
//二叉树的二叉线索类型存储表示
typedef struct BiThrNode {
char data; //结点数据域
struct BiThrNode* lchild, * rchild; //左右孩子指针
int LTag, RTag;
}BiThrNode, * BiThrTree;
//全局变量pre
BiThrNode pri = { 'a',NULL,NULL,0,0 };
BiThrNode* pre = &pri;
//按先序次序建立二叉链表
void CreateBiTree(BiThrNode** T) {
//按先序次序输入二叉树中结点的值(一个字符),创建二叉链表表示的二叉树T
char ch;
scanf("%c", &ch);
if (ch == '#') *T = NULL; //递归结束,建空树
else {
*T = (BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->data = ch; //生成根结点
CreateBiTree(&((*T)->lchild)); //递归创建左子树
CreateBiTree(&((*T)->rchild)); //递归创建右子树
}
}
void InThreading(BiThrNode* p) {
//pre是全局变量,初始化时其右孩子指针为空,便于在树的最左点开始建线索
if (p) {
InThreading(p->lchild); //左子树递归线索化
if (!p->lchild) { //p的左孩子为空
p->LTag = 1; //给p加上左线索
p->lchild = pre; //p的左孩子指针指向pre(前驱)
}
else
p->LTag = 0;
if (!pre->rchild) { //pre的右孩子为空
pre->RTag = 1; //给pre加上右线索
pre->rchild = p; //pre的右孩子指针指向p(后继)
}
else
pre->RTag = 0;
pre = p; //保持pre指向p的前驱
InThreading(p->rchild); //右子树递归线索化
}
}
void InOrderThreading(BiThrNode** Thrt, BiThrNode* T) {
//中序遍历二叉树T,并将其中序线索化,Thrt指向头结点
*Thrt = (BiThrNode*)malloc(sizeof(BiThrNode)); //建头结点
(*Thrt)->LTag = 0; //头结点有左孩子,若树非空,则其左孩子为树根
(*Thrt)->RTag = 1; //头结点的右孩子指针为右线索
(*Thrt)->rchild = *Thrt; //初始化时右指针指向自己
if (!T) (*Thrt)->lchild = *Thrt; //若树为空,则左指针也指向自己
else {
(*Thrt)->lchild = T; pre = *Thrt; //头结点的左孩子指向根,pre初值指向头结点
InThreading(T); //调用算法5.7,对以T为根的二叉树进行中序线索化
pre->rchild = *Thrt; //算法5.7结束后,pre为最右结点,pre的右线索指向头结点
pre->RTag = 1;
(*Thrt)->rchild = pre; //头结点的右线索指向pre
}
}
int main() {
pre->RTag = 1;
pre->rchild = NULL;
BiThrNode* tree, Thrt;
printf("请输入建立二叉链表的序列:\n");
CreateBiTree(&tree);
InOrderThreading(&Thrt, tree);
printf("线索化完毕!\n");
return 0;
}

执行结果:

请输入建立二叉链表的序列:
ABC##DE#G##F###
线索化完毕!

遍历线索二叉树

由于有了结点的前驱和后继信息, 线索二叉树的遍历和在指定次序下查找结点的前驱和后继算法都变得简单. 因此, 若需经常查找结点在所遍历线性序列中的前驱和后继, 则采用线索链表作为存储结构.

在先序线索化树上找前驱或在后序线索化树上找后继时都比较复杂,此时若需要,可直接建立含 4 个指针的线索链表。

由于有了结点的前驱和后继的信息,线索二叉树的遍历操作无需设栈,避免了频繁的进栈、出栈,因此在时间和空间上都较遍历二叉树节省。如果遍历某种次序的线索二叉树,则只要从该次序下的根结点出发,反复查找其在该次序下的后继,直到叶子结点。

算法5.9,遍历中序线索二叉树:

【算法步骤】

  1. 指针 p 指向根结点。

  2. p 为非空树或遍历未结束时,循环执行以下操作:

    • 沿左孩子向下,到达最左下结点 *p, 它是中序的第一个结点;
    • 访问 *p;
    • 沿右线索反复查找当前结点 *p 的后继结点并访问后继结点,直至右线索为 0 或者遍历结束;
    • 转向 p 的右子树。

代码:

//算法5.9 遍历中序线索二叉树
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
//二叉树的二叉线索类型存储表示
typedef struct BiThrNode {
char data; //结点数据域
struct BiThrNode* lchild, * rchild; //左右孩子指针
int LTag, RTag;
}BiThrNode, * BiThrTree;
//全局变量pre
BiThrNode pri = { 'a',NULL,NULL,0,0 };
BiThrNode* pre = &pri;
//按先序次序建立二叉链表
void CreateBiTree(BiThrNode** T) {
//按先序次序输入二叉树中结点的值(一个字符),创建二叉链表表示的二叉树T
char ch;
scanf("%c", &ch);
if (ch == '#') *T = NULL; //递归结束,建空树
else {
*T = (BiThrNode*)malloc(sizeof(BiThrNode));
(*T)->data = ch; //生成根结点
CreateBiTree(&((*T)->lchild)); //递归创建左子树
CreateBiTree(&((*T)->rchild)); //递归创建右子树
}
}
void InThreading(BiThrNode* p) {
//pre是全局变量,初始化时其右孩子指针为空,便于在树的最左点开始建线索
if (p) {
InThreading(p->lchild); //左子树递归线索化
if (!p->lchild) { //p的左孩子为空
p->LTag = 1; //给p加上左线索
p->lchild = pre; //p的左孩子指针指向pre(前驱)
}
else
p->LTag = 0;
if (!pre->rchild) { //pre的右孩子为空
pre->RTag = 1; //给pre加上右线索
pre->rchild = p; //pre的右孩子指针指向p(后继)
}
else
pre->RTag = 0;
pre = p; //保持pre指向p的前驱
InThreading(p->rchild); //右子树递归线索化
}
}
void InOrderThreading(BiThrNode** Thrt, BiThrNode* T) {
//中序遍历二叉树T,并将其中序线索化,Thrt指向头结点
*Thrt = (BiThrNode*)malloc(sizeof(BiThrNode)); //建头结点
(*Thrt)->LTag = 0; //头结点有左孩子,若树非空,则其左孩子为树根
(*Thrt)->RTag = 1; //头结点的右孩子指针为右线索
(*Thrt)->rchild = *Thrt; //初始化时右指针指向自己
if (!T) (*Thrt)->lchild = *Thrt; //若树为空,则左指针也指向自己
else {
(*Thrt)->lchild = T; pre = *Thrt; //头结点的左孩子指向根,pre初值指向头结点
InThreading(T); //调用算法5.7,对以T为根的二叉树进行中序线索化
pre->rchild = *Thrt; //算法5.7结束后,pre为最右结点,pre的右线索指向头结点
pre->RTag = 1;
(*Thrt)->rchild = pre; //头结点的右线索指向pre
}
}
void InOrderTraverse_Thr(BiThrNode* T) {
//T指向头结点,头结点的左链lchild指向根结点,可参见线索化算法5.8。
//中序遍历二叉线索树T的非递归算法,对每个数据元素直接输出
BiThrNode* p;
p = T->lchild; //p指向根结点
while (p != T) //空树或遍历结束时,p==T
{
while (p->LTag == 0) //沿左孩子向下
p = p->lchild; //访问其左子树为空的结点
printf("%c " ,p->data);
while (p->RTag == 1 && p->rchild != T) {
p = p->rchild; //沿右线索访问后继结点
printf("%c ", p->data);
}
p = p->rchild;
}
} //InOrderTraverse_Thr
void main() {
pre->RTag = 1;
pre->rchild = NULL;
BiThrTree tree, Thrt;
printf("请输入建立二叉链表的序列:\n");
CreateBiTree(&tree);
InOrderThreading(&Thrt, tree);
printf("中序遍历线索二叉树的结果为:\n");
InOrderTraverse_Thr(Thrt);
printf("\n");
}

执行结果:

请输入建立二叉链表的序列:
ABC##DE#G##F###
中序遍历线索二叉树的结果为:
C B E G D F A

遍历线索二叉树的时间复杂度为 O(n) , 空间复杂度为 O(1),这是因为线索二叉树的遍历不需要使用栈来实现递归操作。

树的存储结构

双亲表示法

这种表示法中,以一组连续的存储单元存储树的结点,每个结点除了数据域 data 外,还附设一个双亲域 parent,用于指示该结点的双亲结点的位置,即双亲在数组中的索引。

即结点是一个结构体,有两个成员,分别为结点的数据和结点的双亲的位置。然后用一个结构体数组来存储树。

双亲表示法的结点类型:

存储类型描述为:

#define MAXSIZE 100
typedef struct PTNode {
Elemtype data; // 结点的数据域,具体的类型取决于数据与的数据类型
int parent; // 结点的双亲域,指示结点的双亲的位置
}PTNode;
typedef struct PTree {
PTNode nodes[MAXSIZE]; // 结构体数组
int r; // 根结点位置
int n; // 结点总数,r 和 n 的类型必然都是 int
}PTree;

例子:

每一个结点都是一个结构体,有两个成员,数据成员,即 data 域,存放该结点的数据,双亲成员,即 parent 域,存放该结点的双亲在数组中的索引。因为根结点没有双亲,为了方便起见,就令根结点的 parent 域取值为 -1。

为了操作方便,我们还存储两个值,一个是 r,表示根结点的位置,也就是说根结点在数组中的位置是任意的,不一定必须在第一个位置。另一个是 n,表示结点的总数。

这种存储结构利用了每一个结点(除了根结点以外)只有唯一的双亲的性质。在这种存储结构下,求结点的双亲十分方便,也很容易求树的根,但是求结点的孩子时需要遍历整个结构。即找双亲容易,找孩子难。

孩子表示法

孩子表示法也叫孩子链表法。

由于树中每一个结点可能有多棵子树,则可用多重链表,即每一个结点有多个指针域,每一个指针指向一个子树的根结点。此时的链表中的结点可以有如图所示的两种结点格式:

若采用第一种结点格式,则多重链表中的结点是同构的,其中 d 为树的度。由于树中很多结点的度小于 d, 所以链表中有很多空链域,空间较浪费,不难推出,在一棵有 n 个结点度为 k 的树中必有 n(k-1) + 1 个空链域。【nk-(n-1)=nk-n+1=n(k-1)+1】

若采用第二种结点格式,则多重链表中的结点是不同构的,其中 d 为结点的度,degree 域的值同 d。此时,虽能节约存储空间,但操作不方便。

另一种办法是,把每个结点的孩子结点排列起来,看成是一个线性表,且以单链表做存储结构,则 n 个结点有 n 个孩子链表(叶子的孩子链表为空表)。而 n 个头指针又组成一个线性表,为了便于查找,可采用顺序存储结构(含 n 个元素的结构体数组)。如图:

在链表的结点的数据域中,不必再记录完整的数据内容,因为数据内容已经在保存头指针的线性表中存储好了,而且有的时候数据内容是很大的。在链表的结点的数据域中,只需要存储孩子结点在存储头指针的线性表中的索引。

与双亲表示法相反,孩子表示法便于那些涉及孩子的操作的实现。可以把双亲表示法和孩子表示法结合起来,即将双亲表示和孩子链表合在一起。如图:

代码:

// 带双亲的孩子链表
#define MAXSIZE 100
// 孩子结点
typedef struct CTNode {
int child; // 这个孩子结点的兄弟在头结点数组中的索引
struct CTNode* next; // 指向该孩子结点的下一个兄弟
}CTNode;
// 头结点
typedef struct CTBox {
ElemType data; // 数据域,具体类型要根据数据域存放的数据来确定
int parent; // 该结点的双亲在头结点数组中的索引
CTNode* firstchild;
}CTBox;
// 头结点数组
typedef struct CTree {
CTBox nodes[MAXSIZE];
int r, n; // 根结点的位置和结点的数目
}CTree;

孩子兄弟表示法

又称为二叉树表示法,或二叉链表表示法,即以二叉链表作为树的存储结构。

结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点,分别命名为 firstchild 和 nextsibling。结点形式为:

结点的存储表示:

typedef struct CSNode {
ElemType data; // 数据域
struct CSNode* firstchild, * nextsibling;
}CSNode;

示例:

利用这种存储结构便于实现各种树的操作。首先易于实现找结点孩子等的操作。例如,若要访问结点 x 的第 l 个孩子,则只要先从 firstchild 域找到第 1 个孩子结点,然后沿着孩子结点的 nextsibling 域连续走 i-1 步,便可找到 x 的第 l 个孩子。当然,如果为每个结点增设一个 parent 域,则同样能方便地实现查找双亲的操作。

这种存储结构的优点是它和二叉树的二叉链表表示完全一样,便于将一般的树结构转换为二叉树进行处理,利用二叉树的算法来实现对树的操作。因此孩子兄弟表示法是应用较为普遍的一种树的存储表示方法。

因为根结点没有兄弟,因此这种方法的根结点的右子树必为空。

长兄如父。

树和森林与二叉树的转换

由于树和二叉树都可以用二叉链表作为存储结构,因此可以用二叉链表作为媒介,导出树与二叉树之间的对应关系。将树转化为二叉树进行处理,利用二叉树的算法来实现树的操作。

旋转是以每一个子树的根结点为中心进行旋转。在上图中则是以 B、E、H 为中心将各自的子树进行旋转。

某一个结点原来的兄弟变为了它的右子树,原来的第一个孩子变为了左子树。

其实就是把森林的多个根结点看成兄弟。第一棵树的子树森林转换成左子树,剩余树的森林转换成右子树。

树和森林的遍历

由树结构的定义可引出三种次序遍历树的方法:一种是先根(次序)遍历树,即:先访问树的根结点,然后依次先根遍历根的每棵子树;一种是后根(次序)遍历,即先依次后根遍历每棵子树,然后访问根结点;一种是层次遍历。

树中一个结点可以有多个子树,因此没有中序遍历。

森林有两种遍历方式:先序遍历和中序遍历。

(1) 先序遍历森林

若森林非空,则可按下述规则遍历:

  1. 访问森林中第一棵树的根结点;

  2. 先序遍历第一棵树的根结点的子树森林;

  3. 先序遍历除去第一棵树之后剩余的树构成的森林。

即:依次从左至右对森林中的每一棵树进行先根遍历。

(2) 中序遍历森林

若森林非空,则可按下述规则遍历:

  1. 中序遍历森林中第一棵树的根结点的子树森林;

  2. 访问第一棵树的根结点;

  3. 中序遍历除去第一棵树之后剩余的树构成的森林。

即:依次从左至右对森林中的每一棵树进行后根遍历.

树的先序和后序遍历结果等同于对应的二叉树的先序和中序遍历; 森林的先序和中序遍历的结果等同于对应的二叉树的先序和中序遍历.

哈夫曼树的概念

哈夫曼(Huffman)树又称为最优树,是一种带权路径最短的树。

路径:树中从一个结点到另一个结点之间的分支构成这两个结点之间的路径。

路径长度:路径上的分支数目。

树的路径长度:从根结点到每一个结点的路径长度之和。

由此可见,结点数目相同的二叉树中,树的路径长度也是可能不同的,完全二叉树是路径长度最短的二叉树。这是一个充分条件。完全二叉树是路径最短的二叉树,但是路径最短的二叉树不一定是完全二叉树。

权(weight,也叫权重):赋予某个实体的一个量,是对实体的某个或某些属性的数值化描述。在数据结构中,实体有结点(元素)和边(关系)两大类,所以对应有结点权和边权。结点权或边权具体代表什么意义,由具体情况决定。如果在一棵树中的结点上带有权值,则对应的就有带权树等概念。

结点的带权路径长度:从该结点到树根之间的路径长度与结点上权的乘积。结点的带权路径长度是该结点到树的根结点的路径长度乘以该结点的权值。

树的带权路径长度:树中所有叶子结点的带权路径长度之和,通常记作 WLP=k=1nwklk. WLP: weighted pass length。即叶子结点到根结点的路径长度乘以叶子结点的权值,再将全部叶子结点的这个结果求和。

哈夫曼树:假设有 m 个权值{w1, w2,…, wm},可以构造一棵含 n 个叶子结点的二叉树,每个叶子结点的权为 wi, 则其中带权路径长度 WPL 最小的二叉树称做最优二叉树或哈夫曼树。

"带权路径长度最短" 是在 "度相同" 的树中比较而得的结果, 因此有最优二叉树、最优三叉树之称等等。要在度相同这个前提下比较才有意义。

因为构造这种树的算法是由哈夫曼教授于1952年提出的, 所以被称为哈夫曼树, 相应的算法称为哈夫曼算法。

在哈夫曼树中, 权值越大的结点离根结点越近。

满二叉树不一定是哈夫曼树。

具有相同带权结点的哈夫曼树不唯一。

构造哈夫曼树

两两合并产生一颗新的树,若有 n 个叶子结点,则需要合并 n-1 次,最后才能只剩下一颗树。合并一次产生一个新结点,合并了 n-1 次就产生了 n-1 个新结点。故哈夫曼树总共有结点数为:n+n-1=2n-1。即有 n 个叶子结点的哈夫曼树总共有 2n-1 个结点。哈夫曼树只有度为 0 或 2 的结点,没有度为 1 的结点。通过合并产生的 n-1 个结点,都是度为 2 的结点。原有的这些 n 个叶子结点都是度为 0 的结点。即,在哈夫曼树的这些 2n-1 个结点中,n-1 个为合并产生的度为 2 的结点,n 个为原有的度为 0 的叶子结点。

哈夫曼树是一种二叉树, 可以采用前面介绍过的通用存诸方法, 此处采用一维结构体数组来存储。一棵有 n 个叶子结点的哈夫曼树共有 2n-1 个结点, 可以存储在一个大小为 2n-1 的一维数组中。树中每个结点除了要保存权重信息之外还要包含其双亲信息和孩子结点的信息(此处的权重信息就相当于一般的数据域中的数据内容了), 由此, 每个结点的存储结构如图所示:

采用顺序存储, 用一个一维的结构数组, 数组的每一个元素都是一个结构体,定义如下:

typedef struct HTNode {
int parent, lchild, rchild; // 结点的双亲,左孩子和有孩子的下标
int weight; // 权重
}HTNode;

哈夫曼树的各结点存储在数组中, 为了实现方便, 数组的 0 号单元不使用, 从 1 号单元开始使用, 所以数组的大小为 2n。将叶子结点集中存储在前面部分 1~n 个位置, 后面的 n-1 个位置存储其余非叶子结点。

算法5.10 构造哈夫曼树

【算法步骤】

构造哈夫曼树算法的实现可以分成两大部分。

  1. 初始化: 初始化分两步。首先动态申请 2n 个单元; 然后循环 2n-1 次, 从 1 号单元开始, 依次将 1 至 2n-1 所有单元中的双亲、左孩子、右孩子的下标都初始化为0; 最后再循环 n 次, 输入前 n 个单元中叶子结点的权值。

  2. 创建树: 循环 n-1 次, 通过 n-1 次的选择、删除与合并来创建哈夫曼树。选择是从当前森林中选择双亲为 0 且权值最小的两个树根结点 s1 和 s2; 删除是指将结点 s1 和 s2 的双亲改为非 0; 合并就是将 s1 和 s2 的权值和作为一个新结点的权值依次存人到数组的第 n+1 之后的单元中, 同时记录这个新结点左孩子的下标为 s1, 右孩子的下标为 s2.

代码:

//算法5.10 构造哈夫曼树
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
typedef struct {
int weight;
int parent, lchild, rchild;
}HTNode;
void Select(HTNode* HT, int len, int* s1, int* s2) {
int i, min1 = 0x3f3f3f3f, min2 = 0x3f3f3f3f; //先赋予最大值
for (i = 1; i <= len; i++) {
if (HT[i].weight < min1 && HT[i].parent == 0) {
min1 = HT[i].weight;
(*s1) = i;
}
}
int temp = HT[(*s1)].weight; //将原值存放起来,然后先赋予最大值,防止s1被重复选择
HT[(*s1)].weight = 0x3f3f3f3f;
for (i = 1; i <= len; i++) {
if (HT[i].weight < min2 && HT[i].parent == 0) {
min2 = HT[i].weight;
(*s2) = i;
}
}
HT[(*s1)].weight = temp; //恢复原来的值
}
void CreatHuffmanTree(HTNode** HT, int n) {
//构造哈夫曼树HT
int m, s1, s2, i;
if (n <= 1) return;
m = 2 * n - 1;
//HT = new HTNode[m + 1]; //0号单元未用,所以需要动态分配m+1个单元,HT[m]表示根结点
*HT = (HTNode*)malloc(sizeof(HTNode) * (m + 1));
for (i = 1; i <= m; ++i) //将1~m号单元中的双亲、左孩子,右孩子的下标都初始化为0
{
(*HT)[i].parent = 0;
(*HT)[i].lchild = 0;
(*HT)[i].rchild = 0;
}
printf("请输入叶子结点的权值:\n");
for (i = 1; i <= n; ++i) //输入前n个单元中叶子结点的权值
scanf("%d", &((*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].parent = 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 的权值为左右孩子权值之和
}
}
int main() {
HTNode* HT;
int n;
printf("请输入叶子结点的个数:\n");
scanf("%d", &n);
CreatHuffmanTree(&HT, n);
printf("哈夫曼树建立完毕!\n");
return 0;
}

执行结果:

请输入叶子结点的个数:
8
请输入叶子结点的权值:
5 29 7 8 14 23 3 11
哈夫曼树建立完毕!

另一种代码写法:

//算法5.10 构造哈夫曼树
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
typedef struct {
int weight;
int parent, lchild, rchild;
}HTNode;
void Select(HTNode* HT, int len, int* s1, int* s2) {
int min1 = 0x3f3f3f3f, min2 = 0x3f3f3f3f;
(*s1) = 0;
(*s2) = 0;
for (int i = 1; i <= len; i++) {
if (HT[i].weight < min1 && HT[i].parent == 0) {
min2 = min1;
(*s2) = (*s1);
min1 = HT[i].weight;
(*s1) = i;
}
else if (HT[i].weight < min2 && HT[i].parent == 0) {
min2 = HT[i].weight;
(*s2) = i;
}
}
}
//构造哈夫曼树HT
void CreatHuffmanTree(HTNode** HT, int n) {
int s1, s2;
if (n <= 1) return;
*HT = (HTNode*)malloc(sizeof(HTNode) * (2 * n)); //0 号单元未用,所以需要动态分配 2 * n 个单元
for (int i = 1; i <= 2 * n - 1; ++i) { //将 1 ~ 2 * n - 1 号单元中的双亲、左孩子,右孩子的下标都初始化为 0
(*HT)[i].parent = 0;
(*HT)[i].lchild = 0;
(*HT)[i].rchild = 0;
}
printf("请输入叶子结点的权值:\n");
for (int i = 1; i <= n; ++i) //输入前n个单元中叶子结点的权值
scanf("%d", &((*HT)[i].weight));
/*――――――――――初始化工作结束,下面开始创建哈夫曼树――――――――――*/
for (int i = n + 1; i <= 2 * n - 1; ++i) { //通过n-1次的选择、删除、合并来创建哈夫曼树
//在HT[k](1≤k≤i-1)中选择两个其双亲域为0且权值最小的结点,
//并返回它们在HT中的序号s1和s2
Select(*HT, i - 1, &s1, &s2);
//得到新结点i,从森林中删除s1,s2,将s1和s2的双亲域由0改为i
(*HT)[s1].parent = i;
(*HT)[s2].parent = i;
(*HT)[i].lchild = s1; //s1,s2分别作为i的左右孩子
(*HT)[i].rchild = s2;
(*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight; //i的权值为左右孩子权值之和
}
}
int main() {
HTNode* HT;
int n;
printf("请输入叶子结点的个数:\n");
scanf("%d", &n);
CreatHuffmanTree(&HT, n);
printf("哈夫曼树建立完毕!\n");
return 0;
}

执行结果和上面一样。

再来一个例子:

n 个叶子结点的哈夫曼树,最多有 n-1 层。

哈夫曼编码

前缀编码:如果在一个编码方案中,任一个编码都不是其他任何编码的前缀(最左字串),则称编码是前缀编码。

前缀编码可以保证对压缩文件进行解码时不产生二义性,确保正确解码。

哈夫曼编码:对一棵有 n 个叶子结点的哈夫曼树,若对树中的每一个左分支赋予 0,右分支赋予 1,则从根到每个叶子的路径上,各个分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。

哈夫曼编码满足两个性质:

  1. 哈夫曼编码是前缀编码。因为没有一片树叶是另一片树叶的祖先,即从根到某一个树叶的路径上不可能经过其他任一个树叶,所以每一个叶子结点的编码就不可能是另一个叶子结点的编码的前缀。

  2. 哈夫曼编码是最优前缀编码。最优是指字符编码总长最短。因为哈夫曼树的带权路径总长最短,故字符编码总长最短。

哈夫曼编码的实现:

在构造哈夫曼树之后, 求哈夫曼编码的主要思想是: 依次以叶子为出发点, 向上回溯至根结点为止。回溯时走左分支则生成代码 0, 走右分支则生成代码 1。

由于每个哈夫曼编码是变长编码, 因此使用一个指针数组来存放每个字符编码串的首地址。

typedef char ** HuffmanCode; // 动态分配数组存储哈夫曼编码表

各字符的哈夫曼编码存储在由 HuffmanCode 定义的动态分配的数组 HC 中, 为了实现方便, 数组的 0 号单元不使用, 从 1 号单元开始使用, 所以数组 HC 的大小为 n+1, 即编码表 HC 包括 n+1 行。但因为每个字符编码的长度事先不能确定。所以不能预先为每个字符分配大小合适的存储空间。为不浪费存储空间, 动态分配一个长度为 n (字符编码长度一定小于 n) 的一维数组 cd, 用来临时存放当前正在求解的第 i (1<i<r) 个字符的编码, 当第 i 个字符的编码求解完毕后, 根据数组 cd 的字符串长度分配 HC[i] 的空间, 然后将数组 cd 中的编码复制到 HC[i] 中。

因为求解编码时是从哈夫曼树的叶子出发, 向上回溯至根结点。所以对于每个字符, 得到的编码顺序是从右向左的, 故将编码向数组 cd 存放的顺序也是从后向前的, 即每个字符的第 1 个编码存放在 cd[n-2] 中 (cd[n-1] 存放字符串结束标志 '\0'), 第 2 个编码存放在 cd[n-3] 中, 依此类推, 直到全部编码存放完毕。

算法5.11 根据哈夫曼树求哈夫曼编码

【算法步骤】

  1. 分配存储 n 个字符编码的编码表空间 HC, 长度为 n+1; 分配临时存储每个字符编码的动态数组空间 cd, cd[n-1] 置为'\0'。

  2. 逐个求解 n 个字符的编码,循环 n 次,执行一下操作:

    • 设置变量 start 用于记录编码在 cd 中存放的位置, start 初给时指向最后, 即编码结束符位置 n-1;

    • 设置变量 c 用于记录从叶子结点向上回溯至根结点所经过的结点下标, c 初始时为当前待编码字符的下标 i, f 用于记录 i 的双亲结点的下标;

    • 从叶子结点向上回溯至根结点, 求得字符 i 的编码, 当 f 没有到达根结点时, 循环执行以下操作:

      • 回溯一次 start 向前指一个位置, 即 -start;

      • 若结点 c 是 f 的左孩子, 则生成代码 0, 否则生成代码 1, 生成的代码 0 或 1 保存在 cd[start] 中;

      • 继续向上回溯, 改变 c 和 f 的值。

    • 根据数组 cd 的字符串长度为第 i 个字符编码分配空间 HC[i], 然后将数组 cd 中的编码复制到 HC[i] 中。

  3. 释放临时空间 cd。

代码:

//算法5.11 根据哈夫曼树求哈夫曼编码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
typedef struct {
int weight;
int parent, lchild, rchild;
}HTNode, * HuffmanTree;
typedef char** HuffmanCode;
void Select(HTNode* HT, int len, int* s1, int* s2) {
int i, min1 = 0x3f3f3f3f, min2 = 0x3f3f3f3f; //先赋予最大值
for (i = 1; i <= len; i++) {
if (HT[i].weight < min1 && HT[i].parent == 0) {
min1 = HT[i].weight;
(*s1) = i;
}
}
int temp = HT[(*s1)].weight; //将原值存放起来,然后先赋予最大值,防止s1被重复选择
HT[(*s1)].weight = 0x3f3f3f3f;
for (i = 1; i <= len; i++) {
if (HT[i].weight < min2 && HT[i].parent == 0) {
min2 = HT[i].weight;
(*s2) = i;
}
}
HT[(*s1)].weight = temp; //恢复原来的值
}
//构造哈夫曼树HT
void CreatHuffmanTree(HTNode** HT, int n) {
//构造哈夫曼树HT
int m, s1, s2, i;
if (n <= 1) return;
m = 2 * n - 1;
//HT = new HTNode[m + 1]; //0号单元未用,所以需要动态分配m+1个单元,HT[m]表示根结点
*HT = (HTNode*)malloc(sizeof(HTNode) * (m + 1));
for (i = 1; i <= m; ++i) //将1~m号单元中的双亲、左孩子,右孩子的下标都初始化为0
{
(*HT)[i].parent = 0;
(*HT)[i].lchild = 0;
(*HT)[i].rchild = 0;
}
printf("请输入叶子结点的权值:\n");
for (i = 1; i <= n; ++i) //输入前n个单元中叶子结点的权值
scanf("%d", &((*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].parent = 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 的权值为左右孩子权值之和
}
}
//根据哈夫曼树求哈夫曼编码
void CreatHuffmanCode(HuffmanTree HT, char*** HC, int n) {
//从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中
(*HC) = (char**)malloc(sizeof(char*) * (n + 1));
int start, c, f;
char* cd = (char*)malloc(sizeof(char) * n); //分配临时存放编码的动态数组空间
cd[n - 1] = '\0'; //编码结束符
for (int i = 1; i <= n; ++i) { //逐个字符求哈夫曼编码
start = n - 1; //start开始时指向最后,即编码结束符位置
c = i;
f = HT[i].parent; //f指向结点c的双亲结点
while (f != 0) { //从叶子结点开始向上回溯,直到根结点
--start; //回溯一次start向前指一个位置
if (HT[f].lchild == c)
cd[start] = '0'; //结点c是f的左孩子,则生成代码0
else
cd[start] = '1'; //结点c是f的右孩子,则生成代码1
c = f;
f = HT[f].parent; //继续向上回溯
}
(*HC)[i] = (char*)malloc(sizeof(char) * (n - start)); //为第i个字符编码分配空间
strcpy((*HC)[i], &cd[start]); //将求得的编码从临时空间cd复制到HC的当前行中
}
free(cd); //释放临时空间
}
void show(HuffmanTree HT, char** HC) {
for (int i = 1; i <= sizeof(HC); i++)
printf("%d 编码为 %s\n", HT[i].weight, HC[i]);
}
int main() {
HuffmanTree HT;
char** HC;
int n;
printf("请输入叶子结点的个数:\n");
scanf("%d", &n); //输入哈夫曼树的叶子结点个数
CreatHuffmanTree(&HT, n);
CreatHuffmanCode(HT, &HC, n);
show(HT, HC);
return 0;
}

执行结果:

请输入叶子结点的个数:
8
请输入叶子结点的权值:
5 29 7 8 14 23 3 11
5 编码为 0001
29 编码为 10
7 编码为 1110
8 编码为 1111
14 编码为 110
23 编码为 01
3 编码为 0000
11 编码为 001

另一种写法:

//算法5.11 根据哈夫曼树求哈夫曼编码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
typedef struct {
int weight;
int parent, lchild, rchild;
}HTNode, * HuffmanTree;
typedef char** HuffmanCode;
void Select(HTNode* HT, int len, int* s1, int* s2) {
int min1 = 0x3f3f3f3f, min2 = 0x3f3f3f3f;
(*s1) = 0;
(*s2) = 0;
for (int i = 1; i <= len; i++) {
if (HT[i].weight < min1 && HT[i].parent == 0) {
min2 = min1;
(*s2) = (*s1);
min1 = HT[i].weight;
(*s1) = i;
}
else if (HT[i].weight < min2 && HT[i].parent == 0) {
min2 = HT[i].weight;
(*s2) = i;
}
}
}
void CreatHuffmanTree(HTNode** HT, int n) {
//构造哈夫曼树HT
int m, s1, s2, i;
if (n <= 1) return;
m = 2 * n - 1;
//HT = new HTNode[m + 1]; //0号单元未用,所以需要动态分配m+1个单元,HT[m]表示根结点
*HT = (HTNode*)malloc(sizeof(HTNode) * (m + 1));
for (i = 1; i <= m; ++i) //将1~m号单元中的双亲、左孩子,右孩子的下标都初始化为0
{
(*HT)[i].parent = 0;
(*HT)[i].lchild = 0;
(*HT)[i].rchild = 0;
}
printf("请输入叶子结点的权值:\n");
for (i = 1; i <= n; ++i) //输入前n个单元中叶子结点的权值
scanf("%d", &((*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].parent = 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 的权值为左右孩子权值之和
}
}
//根据哈夫曼树求哈夫曼编码
void CreatHuffmanCode(HuffmanTree HT, char*** HC, int n) {
//从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中
(*HC) = (char**)malloc(sizeof(char*) * (n + 1));
int start, c, f;
char* cd = (char*)malloc(sizeof(char) * n); //分配临时存放编码的动态数组空间
cd[n - 1] = '\0'; //编码结束符
for (int i = 1; i <= n; ++i) { //逐个字符求哈夫曼编码
start = n - 1; //start开始时指向最后,即编码结束符位置
c = i;
f = HT[i].parent; //f指向结点c的双亲结点
while (f != 0) { //从叶子结点开始向上回溯,直到根结点
--start; //回溯一次start向前指一个位置
if (HT[f].lchild == c)
cd[start] = '0'; //结点c是f的左孩子,则生成代码0
else
cd[start] = '1'; //结点c是f的右孩子,则生成代码1
c = f;
f = HT[f].parent; //继续向上回溯
}
(*HC)[i] = (char*)malloc(sizeof(char) * (n - start)); //为第i个字符编码分配空间
strcpy((*HC)[i], &cd[start]); //将求得的编码从临时空间cd复制到HC的当前行中
}
free(cd); //释放临时空间
}
void show(HuffmanTree HT, char** HC) {
for (int i = 1; i <= sizeof(HC); i++)
printf("%d 编码为 %s\n", HT[i].weight, HC[i]);
}
int main() {
HuffmanTree HT;
char** HC;
int n;
printf("请输入叶子结点的个数:\n");
scanf("%d", &n); //输入哈夫曼树的叶子结点个数
CreatHuffmanTree(&HT, n);
CreatHuffmanCode(HT, &HC, n);
show(HT, HC);
return 0;
}

执行结果与上面一样。

文件的编码和译码

编码:有了字符集的哈夫曼编码表之后, 对数据文件的编码过程是: 依次读人文件中的字符 c, 在哈夫曼编码表 HC 中找到此字符, 将字符 c 转换为编码表中存放的编码串。

译码:对编码后的文件进行译码的过程必须借助于哈夫曼树。具体过程是: 依次读入文件的二进制码, 从哈夫曼树的根结点 (即 HT[m]) 出发, 若当前读入 0, 则走向左孩子, 否则走向右孩子。一旦到达某一叶子 HT[i] 时便译出相应的字符编码 HC[i]。然后重新从根出发继续译码, 直至文件结束。

posted @   有空  阅读(12)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
点击右上角即可分享
微信分享提示

目录导航