第五章:树

观看青岛大学-王卓老师的网课,根据每一章做如下总结:
青岛大学-王卓老师B站上的网课

5.1 树和二叉树的定义

树是一种非线性的数据结构,树(Tree)是n个结点的有限集。

  • 若n=0,称为空树;
  • 若n>0, 满足(1)有且仅有一个根(root)结点(2)其余结点可分为m个互不相交的有限集T1,T2...Tm。每个集合又是一棵树,称为根的子树。

所以树是一个递归(嵌套)的概念。

5.1.1 树的定义

树(Tree)是n个结点的有限集。包含多种表示方式:嵌套集合,广义表,凹入表示。

  • 结点:数据元素以及指向的子树的分支。
    根结点:非空树中无前驱结点的结点。
    结点的度:结点拥有的子树数。
    树的度:树内各结点的度的最大值。
    树的深度:树中结点的最大层次。
    叶子结点(终端结点):度=0
    分支结点(非终端结点):度≠0,根结点以外的分支节点称为内部节点。
  • 双亲:结点的子树的根称为该结点的孩子(child),该结点成为该孩子的双亲(parent)。
    堂兄弟:parent位于同一层的结点
  • 祖先:从root到该node所经分支上的所有结点。
    子孙:以某结点为根结点的子树中任意node(在它之下,除了它自己,全部都是;如果子树的根结点是叶子节点,那该子树的根结点是孩子也是孙子)

    树的分类:
    1.有序树:树中结点各子树从左至右有次序(最左边的第一个孩子)
    2.无序树:树中结点各子树无次序。

    3.森林:m(m≥0)棵互不相交的树的集合。把root删除,树就变成看森林;给森林中各子树加上一个双亲结点,森林就变成了树。一棵树是特殊的森林。

树结构和线性结构的比较:
树结构和线性结构的比较

5.1.2 二叉树的定义

1.二叉树的结构最简单,规律性最强,可以证明,所有树都能转为唯一对应的二叉树。
二叉树:由一个根结点和两颗互不相交的(左子树、右子树)树的二叉树组成。
1)二叉树中的node的度总是≤2。
2)子树有左右之分,次序不能颠倒。即使仅有一颗子树,也要说明左子树和右子树。(而对于树而言,只有一个孩子时,无须区分是左还是右的次序。这是二叉树和树的不同
3)二叉树可以是空集合,可以有空的左子树和右子树。可以看出,二叉树的每个结点位置/次序是固定的,可以是空,但不能没有。
二叉树和树是两个不同的概念

2.二叉树的5中基本形态
空二叉树
根和空的左右子树
根的左子树
根的右子树
根的左右子树

5.2 树的实际应用

1.数据压缩:
将数据文件转换成由0、1组成的二进制串,称之为编码。

  • 等长编码(浪费内存)
  • 不等长编码1(通过前缀码辨别不同子树,哈夫曼树)
  • 不等长编码2

2.利用二叉树求解表达式的值:第一操作数 运算符 第二操作数
双亲结点:存储运算符
左子树:第一操作数
右子树:第二操作数
若为一元运算符,左子树为空

5.3 树和二叉树的抽象数据类型定义

ADT BinaryTree{ 数据对象D: 数据关系R: 基本操作P: }ADT BinaryTree


CreateBiTree(&T, definition) // 二叉树有不同的遍历方式,因而有不同的definition

5.4 二叉树的性质和存储结构

1.性质1:二叉树的第\(i\)层上至多\(2^{i-1}\)个结点(一层的数量)
二叉树的第\(i\)层上至少有1个结点(一层的数量)

2.性质2:深度为\(k\)的二叉树至多\(2^{k}-1\)个结点(所有层求和的数量)
深度为\(k\)的二叉树至少\(k\)个结点(所有层求和的数量)

3.性质3:对任何一棵二叉树T1,如果叶子数为n0,,度为2的结点数为n2,则n0=n2+1
证明如下:
其中n为所有结点的个数,n2为度为2的节点个数,n1为度为1的结点个数
且n0+n1+n2=n

4.性质4:具有n个结点的完全二叉树深度为[\(log_{2}n\)]+1
[]表示取整

说明了结点编号与深度之间的关系

5.性质5:具有n个结点的完全二叉树(深度为[\(log_{2}n\)]+1)按层序编号,每层从左到右。则对任一结点i:(1≤i≤n)

  • 若i=1,则结点i是二叉树的根,无双亲;如果i>1,则双亲是[i/2]

  • 若2i>n,则结点i为叶子结点,无左孩子;否则,其左孩子结点为2i

  • 若2i+1>n,则结点i无右孩子;否则,其右孩子结点为2i+1。
    说明了双亲结点编号与孩子结点编号之间的关系
    完全二叉树编号示意图

    两种特殊形式的二叉树
    1.满二叉树:总结点数达到最大结点数的树。(深度为k且有2^k-1个结点的二叉树)

    特点:

    • 每一层都满:每一层上的结点数都是最大结点数
    • 叶子结点全部都在最底层
      对结点位置进行编号
      编号规则:从根结点开始,自上而下,自左而右

    2.完全二叉树:深度为k的具有n个结点的二叉树,当且仅当每一个结点都与深度为k的满二叉树中编号为1~n的结点一一对应时,该树为完全二叉树。
    完全二叉树
    注:在满二叉树中,从最后一个结点开始,连续去掉任意个结点,就是一棵完全二叉树。

    什么是连续?
    满二叉树有4、5、6、7叶节点,需要去掉5、6、7(如果去掉的是5、7,就不是连续去掉)
    不是完全二叉树

    特点:

    • 叶子结点只可能分布在层次最大的两层上(最后一层、倒数第二层)
    • 对任一结点,如果右子树最大层次为i,则左子树最大层次必为i或i+!(最多相差一层)

二叉树的存储结构
1.顺序存储结构
实现:按照满二叉树的结点层次编号,依次存放二叉树中的数据元素。

缺点:大小固定,若树中的元素个数变化大,顺序存储不适用;空的内部结点也要存储null,浪费空间(最坏的情况:深度为k且只有k个结点的单支树需要长度为\(2^{k}-1\)的一维数组)适用于存储满二叉树和完全二叉树
优点:结点间的前驱和后继关系蕴含在存储位置中

2.链式存储结构:二叉链表/三叉链表

  • 二叉链表:寻找结点的后继关系(child)
    一个数据存储形式:数据域+指针域,包含数据本身+两个指针(嵌套/递归的定义)


    | lchild |data | rchild |

    • 定义BiNode结点类型为普通的结点类型BiNode和指向具有三个成员的结点类型的指针*BiTree
      在n个结点的二叉链表中,有n+1个空指针域。(每个结点有2个链域,共有2n个链域;每个结点有且只有一个双亲,故有n-1个结点的链域存放指针
  • 三叉链表:寻找结点的后继关系(child)和前驱关系(parent)
    一个数据域+3个指针域

5.5 二叉树的遍历

5.5.1 二叉树的遍历类型

遍历目的:得到树中所有结点的线性排列,这是树结构“增删改查”和排序运算的前提。
遍历方法:依次遍历二叉树的三个组成部分,共有6种方案(3!,重点研究DLR、LDR、LRD)
确定先左后右,有DLR先序遍历、LDR中序遍历和LRD后序遍历三种。
1.DLR先序遍历(自上而下)
首先树分为三个部分,左(B为根结点的树) + 右(D为根结点的树) + 根(A),先访问根结点A,然后处理左子树得到BEL,最后处理右子树得到DHMIJ。处理左右子树都根据DLR,先根再左右的方式。
先序遍历二叉树
算法实现
二叉树先序遍历算法/理论
二叉树先序遍历算法/代码

具体实现过程

2.LDR中序遍历(自下而上)
首先分为三个主要部分,先看左子树,如果左子树的左右子树不全为空,则继续往下搜寻,对每个孙子结点循环判断左右子树是否为空,直至存在一个叶子结点的左右子树为空。然后从叶结点所在的根结点的子树开始往上遍历(图中左子树中,最早的子树是以E为根结点的子树)。 找到根结点A之后,在右子树去找最底层的左子树。以下图中,树先分为三个部分,左(B为根结点的树) + 右(D为根结点的树) + 根(A),先处理左子树得到ELB,然后到根结点A,之后处理右子树得到MHIDJ。
中序遍历二叉树

算法实现
二叉树中序遍历算法/代码

3.LRD后序遍历
首先树分为三个部分,左(B为根结点的树) + 右(D为根结点的树) + 根(A),先处理左子树得到LEB,然后处理右子树得到MIHJD,最后是根结点A。

算法实现
二叉树后序遍历算法/代码

这三种遍历方式只有输出语句位置不一样,如果去掉输出语句,从递归的角度看这三种算法的访问路径是相同的,只是访问结点的时机不同。
遍历算法的分析

5.5.2 根据遍历序列确定二叉树

若二叉树各结点的值均不相同,则二叉树结点的先序序列、中序序列和后序序列都是唯一的。
由二叉树的先序序列和中序序列,或者由二叉树的中序序列和后序序列可以唯一确定一棵二叉树。
1.根据先序序列和中序序列
首先,从先序中确定根结点A;然后,在中序中找到左子树包含的序列CDBEF,右子树包含的序列IHFJ。
对于左子树,在先序中找到根结点B,在中序中找到左子树CD,右子树FE;
同理,对于右子树,在先序中找到根结点G,在中序中找到左子树IH,右子树J。
按照这个规律类推,得到二叉树。
先序序列和中序序列
2.根据中序序列和后序序列
首先,从后序序列尾部找到根结点A;在中序中找到左子树包含的序列BDCE,右子树包含的序列FHG。
对于左子树,在后序中找到根结点B,在中序中找到右子树DCE,左子树为空;
同理,对于右子树,在后序中找到根结点F,在中序中找到右子树HG,左子树为空。
按照这个规律类推,得到二叉树。
中序序列和后序序列

5.5.3 遍历二叉树的非递归算法(以中序遍历为例)

在中序遍历过某结点的整个左子树后,如何找到该结点的根以及右子树?

1.基本思想:遇到根不访问,入栈;去左子树,若左子树为空,则访问根->右子树。
1)建立一个栈
2)根结点进栈,遍历左子树
3)根结点出栈,输出根节点,遍历右子树

2.算法实现:

Status InOrderTraverse(BiTree T){
  BiTree p; InitStack(S); p=T;
  // 左子树为空 或 栈为空 则结束循环
  while(p|| !StackEmpty(S)){
    if(p){Push(S,p); p=p->lchild;}
    //将栈顶元素弹出存到q中
    else {Pop(S,q); printf("%c", q->data); p=q->rchild; }
  }
  return OK;
}

5.5.4 二叉树的层次遍历

1.基础思想:从根结点开始,按从上到下、从左到右的顺序访问每一个结点,每个结点仅访问一次。(使用队列存储)
1)根节点入队
2)队不空时循环:从队列中出队一个结点*p,访问它。若它有左孩子结点,将左孩子结点入队;若它有右孩子结点,将右孩子结点入队。
二叉树的层次遍历示意图
使用顺序循环队列存储二叉树结点:
队列类型定义

2.算法实现

void LevelOrder(BTNode *b){
  BTNode *p; SqQueue *qu;
  InitQueue(qu);  //初始化队列
  enQueue(qu, b);  //根结点指针进入队列
  while(!QueueEmpty(qu)){
    // 队头元素出队
    deQueue(qu,p);
    // 访问结点p
    printf("%c", p->data); 
    // 若结点有左孩子则将其入队
    if(p->lchild!==NULL) enQueue(qu,p-lchild);
    // 若结点有右孩子则将其入队
    if(p->rchild!==NULL) enQueue(qu,p->rchild);
  }
}

5.5.5 二叉树遍历算法应用

1.建立二叉树
1)按先序遍历序列建立二叉树的二叉链表
先序序列:ABCDEGF,可能会有多个不相同的二叉树,为了避免这种情况,在构建二叉树时,要补充空的节点信息,用"#"填充。(满二叉树?)
建立唯一的二叉树

// 图(a)的二叉树,可以表示为ABC##DE#G##F###,按顺序读入这些字符
Status CreateTree(BiTree &T){
  cin>>ch;
  if(ch=="#") T=NULL;
  else {
    if(!(T=(BiTNode *)malloc(sizeof(BiTNode))))
      exit(OVERFLOW);
    T->data = ch; //生成根结点
    CreateBiTree(T->lchild);  //构造左子树
    CreateBiTree(T->rchild);  //构造右子树
 }
  return OK;
}

2.复制二叉树
采用递归的方法复制二叉树,若是空树,递归结束;否则申请新结点空间,复制根结点,先递归复制左子树,再递归复制右子树。

int Copy(BiTree T, BiTree &New T){
  // 空树则返回0
  if(T==NULL) {NewT=NULL; return 0;}
  else {
    // 建立新结点
    NewT = new BiTNode;
    NewT->data = T->data;
    Copy(T->lchild, NewT->lchild);
    Copy(T->rchild, NewT->rchild);
  }
}

3.计算二叉树的深度
对于空树,深度为0;对于非空树,递归计算左子树的深度,记为m;递归计算右子树的深度,记为n,那么二叉树的深度为max(m,n)+1。

int Depth(BiTree T){
  if(T==NULL) return 0;
  else {
    m = Depth(T->lchild);
    n = Depth(T->rchild);
    if(m>n) return (m+1);  //因为根节点比左右子树高一层,整棵树深度要加1
    else return (n+1);
  }
}

4.计算二叉树的结点总数
对于空树,结点个数为0;对于非空树,递归计算左子树的结点数,记为m;递归计算右子树的结点数,记为n,二叉树的结点总数为m+n+1。

int NodeCount(BiTree T){
  if(T==NULL) return 0;
  else return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}

5.计算二叉树的叶子结点数
对于空树,叶子结点个数为0;对于非空树,左子树叶子结点数 + 右子树的叶子结点数。

int LeafCount(BiTree T){
  if(T==NULL) return 0;
  // 如果该结点是叶子节点则返回1
  if(T->lchild == NULL && T->rchild == NULL) 
    return 1;
  else 
    return LeafCount(T->lchild) + LeafCount(T->rchild);
}

5.5.6 线索二叉树

使用二叉树链表存储二叉树时,利用左右孩子的指针域可以很方便地找到某个结点的左右孩子,但如何高效地寻找特定遍历序列中二叉树结点的前驱和后继呢?
-[-]增加额外的两个指针域?浪费空间
-[-]遍历整棵树?太慢

1.二叉树链表的空指针域
n个结点
2n个指针域
n-1个孩子
余下2n-(n-1)=n+1空指针域
线索:改变指向的指针。当某结点的左孩子为空,则将空的指针域改为指向其前驱;当某结点的右孩子为空,则将空的指针域改为指向其后继。

2.线索二叉树 Threaded Binary Tree
对存在空指针域的二叉树进行“线索化”则形成线索二叉树。前驱和后继关系可以将所有结点连接成一个链式结构。
线索二叉树实例
为了区分lchild和rchild指针到底是指向孩子的指针,还是指向前驱/后继的指针,对二叉链表中每个结点增设标志域ltag和rtag,并约定:

ltag = 0 lchild指向该结点的左孩子
ltag = 1 lchild指向该结点的前驱
rtag = 0 rchild指向该结点的右孩子
rtag = 1 rchild指向该结点的后继

线索二叉树结构

3.线索二叉树的存储方式
先序线索二叉树、中序线索二叉树、后序线索二叉树
1)增设头结点:遍历序列中第一个结点的lc域和最后一个结点的rc域都指向头结点。

// 头结点的指针域
ltag = 0, lchild指向根结点
rtag = 1, rchild指向遍历序列中最后一个结点

线索二叉树练习
增加头结点变成循环链表:
增加头结点的二叉树

**关于kd-tree在SLAM中的应用:
一文弄懂kd-tree

5.6 树和森林

森林:m棵互不相交的树的集合。

5.6.1 树的存储结构

1.双亲表示法
使用结构数组存放树的结点,每个结点包含两个域:
数据域:存放结点本身信息。
双亲域:指示本结点的双亲结点在数组中的下标/位置。
双亲表示法存储树
抽象数据类型:
结点结构包含data域和parent域

2.孩子链表
把每个结点的孩子结点排列起来,看成是一个线性表,用单链表存储起来,n个结点有n个孩子链表(叶子的孩子链表为空表)。n个头指针组成一个线性表,用顺序表存储。
抽象数据类型:
抽象数据类型
实例:
孩子链表示例

*带双亲的孩子链表:方便存储双亲和孩子,但牺牲空间
带双亲的孩子链表

3.孩子兄弟表示法(二叉链表表示法)
更常用,用二叉链表作为树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子结点和下一个兄弟结点。
孩子结点(纵向) + 兄弟结点(横向),找所有孩子的过程:孩子域->兄弟域->兄弟域...找双亲的过程:增加一个双亲域,每一个结点都标上指向的双亲域。
孩子兄弟表示法

5.6.2 树与二叉树的转换

1.将树转换成二叉树
首先,将兄弟结点之间添加连线;然后,将除了左孩子的之外的所有孩子与根结点之间的连线去除;最后,以树的根结点为轴心,将整树顺时针转45°。
(兄弟相连留长子)
树转换成二叉树

2.将二叉树变成树
首先,若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩子...所有的右孩子都与p的双亲用线连起来。
然后,去除原二叉树中双亲与右孩子之间的连线。
最后,调整结点的位置,将结点按层次排列,形成树结构。
(左孩右右连双亲,去掉原来右孩线)
将二叉树变成树

3.森林与二叉树的相互转换
1)森林转换成二叉树
将各棵树分别转为二叉树,连接每棵树的根节点,以第一棵树根节点作为二叉树的根,再以根结点为轴心,顺时针旋转成二叉树结构。
森林与二叉树的转换
(树变二叉根相连)
2)二叉树转换成森林
将二叉树中的根节点与右孩子的连线,以及沿右分支的所有右孩子之间的连线全部抹掉,变成孤立的二叉树。再将孤立的二叉树还原成树。
(去掉全部右孩线,孤立二叉再还原)
二叉树转换成森林

5.6.3 树与森林的遍历

1.树的遍历

  • 先根遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树。
  • 后根遍历:若树不空,则先依次后根遍历各棵子树,然后访问根结点。
  • 层次遍历:自上而下自左至右访问树中每个结点。

树的遍历

2.森林的遍历
第一棵树的根结点、第一棵树的子树森林、其他树构成的森林
1)先序遍历
从左到右依次对森林中的每一棵树进行先序遍历

先序遍历

2)中序遍历
从左到右依次对森林中的每一棵树进行后序遍历
中序遍历

5.7 哈夫曼树(最优二叉树)及其应用

5.7.1 哈夫曼树的基本概念

  • 路径:一个结点到另一个结点之间的分支.
  • 结点的路径长度:两结点间路径上的分支数.
  • 树的路径长度:树根到每一个结点的路径长度之和,记作TL.
    完全二叉树是路径最短的二叉树
  • 权:每一个结点都有一个带着某种含义的数值,这个数值称为该结点的权.
  • 结点的带权路径长度:从根结点到某一结点的路径长度 x 该结点的权.
  • 树的带权路径长度:所有叶子结点的带权路径之和.

1.哈夫曼树(带权路径长度WPL最短的树)
1)最优二叉树
带权路径最短的二叉树是最优二叉树
2)最优三叉树
带权路径最短的二叉树是最优三叉树

特点:
①满二叉树不一定是哈夫曼树。
②哈夫曼树中权值大的叶子离根越近,权值小的叶子离根越远。
③具有相同带权结点的哈夫曼树不唯一。
哈夫曼树

5.7.2 哈夫曼树的构造算法

1.哈夫曼算法
“构造森林全是根,选用两小造新树,删除两小添新人,重复23剩单根”
包含n棵树的森林要合并n-1次才能形成哈夫曼树,共产生n-1个新结点,原n个叶子节点 + 新结点n-1 = 2n-1个节点。
哈夫曼树的结点度数为0或2,没有度为1的结点
哈夫曼算法

2.哈夫曼树构造算法实现
抽象数据类型(顺序存储):
哈夫曼树构造算法实现
实例
代码实现:

// 1. 初始化HT [1,2...2n-1],lch=rch=parent=0;
// 2. 输出初始的n个叶子结点,置HT[1,...,n]的weight值;
// 3. 进行n-1次合并,依次产生n-1个结点HT[i],i=n+1,...,2n-1,更新n-1个结点的权值。
//  a)在[1,i-1]中选两个之前没有被选过的(parent==0)的weight最小的两个结点HT[s1]和HT[s2],s1和s2为两个最小结点的下标。
//  b)修改HT[s1]和HT[s2]的parent值:HT[s1].parent=i,HT[s2].parent=i。
//  c)修改HT[i]的lch和rch值:HT[i].lch=s1,HT[i].rch=s2,HT[i].weight=HT[s1].weight + HT[s2].weight。
void createHuffmanTree (HuffmanTree HT, int n){
  // step1. 初始化哈夫曼树
  // 只有一个根结点就不用构造树了
  if(n<=1)return;
  m = 2*n-1; //数组共2*n-1个元素
  HT = new HTNode[m+1];  //0号单元未用,HT[m]表示根结点
  for(int i=1; i<=m; i++){
    HT[i].lch=0; HT[i].rch=0; HT[i].parent=0;
  }
  for(int i=1; i<=n; i++) cin>>HT[i].weight;
  //step2. 构造哈夫曼树
  int s1,s2;  //权值最小的结点对应的下标
  for(int i=n+1; i<=m; i++){
    // 在HT[k](k\in[1,i-1])中选择两个双亲域为0且权值最小的结点
    // 返回他们在HT中的序号s1和s2
    Select(HT, i-1, s1, s2);
    HT[s1].parent=i; HT[s2].parent=i; 
    HT[i].lch=s1; HT[i].rch=s2; HT[i].weight=HT[s1].weight + HT[s2].weight;
  }
}

void Select(HuffmanTree HT, int k, int &s1, int &s2){
  s1 = s2 = 0;  //0表示尚未找到
  for(int i=1; i<=k; ++i){
    if(HT[i].parent !=0) continue;  // 已被合并,跳过该结点
    if(s1==0 || HT[i].weight< HT[s1].weight){
      s2 = s1;  //旧的最小变为次小
      s1 = i;   //更新最小
    }
    else if(s2==0 || HT[i].weight< HT[s2].weight)
      s2 = i;   //更新次小
  }
  // 确保 s1 ≤ s2(按权重,若权重相同则下标小的在前)
  if(HT[s2].weight < HT[s1].weight)
    std::swap(s1, s2);
}

5.7.3 哈夫曼编码及应用

在远程通讯中,要将待传字符转换成由二进制的字符串,会把字母编为等长的二进制表达:A-00,B-01,C-10,D-11,那么ABACCDA-> 0001 0010 1011 00这样的转换会浪费很多空间,如何压缩存储呢?

不同字母用不等长的编码:让待传字符串中出现次数较多的字符采用尽可能短的编码。如A-0,B-00,C-1,D-01,那么ABACCDA-> 0000 1101 0。这样又可能出现重码,0000可能是AAAA也可能是BB,翻译时会出现歧义。

关键:设计长度不等的编码,使任意字符的编码不是另一个字符编码的前缀(前缀编码)。

1.哈夫曼编码
根据哈夫曼树的特点制定的一种前缀码。
1)特点:

  • 前缀码:哈夫曼编码能够保证是前缀编码。(没有一片树叶是另一片树叶的祖先,因此每个叶节点的编码就不可能是其它叶结点编码的前缀)
  • 最优前缀码:哈夫曼编码能够保证字符编码总长最短。(哈夫曼树的带权路径长度最短,因此字符编码总长最短)

哈夫曼编码实例
2)原理实现:

  1. 统计字符集中每个字符在电文中出现的平均概率,概率越大编码长度越短。
  2. 将每个字符的概率值作为权值,构造哈夫曼树,概率越大的结点越靠近根结点,路径越短。
  3. 哈夫曼树的每个分支标0或1,左支为0、右支为1,把从根到叶子的路径标号相连,作为该叶子代表的字符编码。

哈夫曼编码算法实现

// 存储哈夫曼编码的是一个二维数组,i是下标,从1~n,存放n个元素,HC[i]是对应的哈夫曼编码。
// 哈夫曼编码最长的位数是n-1(哈夫曼树最深是n-1个路径),使用循环回溯哈夫曼树,从末尾位开始填0或1,得到哈夫曼编码。
void CreateHuffmanCode(HuffmanTree HT, HuffmanCode &HC, int n){
  // 从叶子到根逆向求每个字符的哈夫曼编码,存储在编码表HC中
  HC = new char *[n+1];  // 分配n个字符编码的头指针,指针下标从1开始,因此n+1长度,即从0到n不用0
  cd = new char[n];  // 临时空间存放n位哈夫曼编码
  cd[n-1] = '\0';  //第n位存停止符
  for(int i=1; i<=n; i++){
    start = n-1; c = i; f = HT[i].parent;
    // 循环回溯叶子结点到根结点的分支编码
    while(f!=0){
      --start;  // 从n-1位开始填编码,回溯一次start向前指一个位置
      if(HT[f].lchild==c) cd[start]='0';
      else cd[start]='1';
      c = f; f = HT[f].parent;  //继续向前回溯
    }
    HC[i] = new char [n-start];  // 为第i个字符串编码分配空间
    strcp(HC[i], &cd[start]);  // 将取得的编码从临时空间cd的start开始的地址,复制到HC当前行中
  }
  delete cd;  //释放临时空间
}

2.文件的编码和解码
1)编码
①输入各字符及权值
②构造哈夫曼树 HT[i]
③进行哈夫曼编码 HC[i]
④查表HC[i],得到各字符的哈夫曼编码

2)解码
①构造哈夫曼树 HT[i],依次读入二进制码。读入0,则走向左孩子;读入1,则走相关右孩子。
②一旦到达某叶子,参考原码报文OC,即可翻译出字符。
③再从根出发继续译码,直到结束。

posted @ 2024-11-29 16:57  soffiya  阅读(81)  评论(0)    收藏  举报