以下的内容做为学习笔记,复制别人的,感觉总结的比较好:
第5章 树和二叉树
本章中主要介绍下列内容:
1.树的定义和存储结构
2.二叉树的定义、性质、存储结构
3.二叉树的遍历、线索算法
4.树和二叉树的转换
5.哈夫曼树及其应用
课时分配:
1、2两个学时,3四个学时,4两个学时, 5两个学时,上机两个学时
重点、难点:
二叉树的遍历、线索算法、哈夫曼树及其应用
第一节 树
1.树的定义和基本运算
1.1 定义
树是一种常用的非线性结构。我们可以这样定义:树是n(n≥0)个结点的有限集合。若n=0,则称为空树;否则,有且仅有一个特定的结点被称为根,当n>1时,其余结点被分成m(m>0)个互不相交的子集T1,T2,...,Tm,每个子集又是一棵树。由此可以看出,树的定义是递归。
结点:数据元素的内容及其指向其子树根的分支统称为结点。
结点的度:结点的分支数。
终端结点(叶子):度为0的结点。
非终端结点:度不为0的结点。
结点的层次:树中根结点的层次为1,根结点子树的根为第2层,以此类推。
树的度:树中所有结点度的最大值。
树的深度:树中所有结点层次的最大值。
有序树、无序树:如果树中每棵子树从左向右的排列拥有一定的顺序,不得互换,则称为有序树,否则称为无序树。
森林:是m(m≥0)棵互不相交的树的集合。
在树结构中,结点之间的关系又可以用家族关系描述,定义如下:
孩子、双亲:结点子树的根称为这个结点的孩子,而这个结点又被称为孩子的双亲。
子孙:以某结点为根的子树中的所有结点都被称为是该结点的子孙。
祖先:从根结点到该结点路径上的所有结点。
兄弟:同一个双亲的孩子之间互为兄弟。
堂兄弟:双亲在同一层的结点互为堂兄弟。
1.2 树的基本运算
常用操作:
(1)构造一个树 CreateTree (T)
(2)清空以T为根的树 ClearTree(T)
(3)判断树是否为空 TreeEmpty(T)
(4)获取给定结点的第i个孩子 Child(T,linklist,i)
(5)获取给定结点的双亲 Parent(T,linklist)
(6)遍历树Traverse(T)
对树遍历的主要目的是将非线性结构通过遍历过程线性化,即获得一个线性序列。树的遍历顺序有两种,一种是先序遍历,即先访问根结点,然后再依次用同样的方法访问每棵子树;另一种是后序遍历,即先依
2. 树的存储结构
2.1 双亲表示法
类型定义:
#define MAX_TREE_LINKLIST_SIZE 100
typedef struct {
TElemtype info;
int parent;
} ParentLinklist;
typedef struct {
ParentLinklist elem[MAX_TREE_LINKLIST_SIZE];
int n; //树中当前的结点数目
}ParentTree;
这种存储方法的特点是寻找结点的双亲很容易,但寻找结点的孩子比较困难。
算法实现举例:
int Parent(ParentTree T,int linklist)
{ if (linklist<0||linklist>=T.n) return -2;
else return T.elem[linklist].parent;
}
2.2 孩子表示法
在C语言中,这种存储形式定义如下:
#define MAX_TREE_LINKLIST_SIZE 10
typedef struct ChildLinklist{
int child; //该孩子结点在一维数组中的下标值
struct ChileLinklist *next; //指向下一个孩子结点
}CLinklist;
typedef struct{
Elemtype info; //结点信息
CLinklist *firstchild; //指向第一个孩子结点的指针
}TLinklist;
typedef struct {
TLinklist elem[MAX_TREE_LINKLIST_SIZE];
int n,root; //n为树中当前结点的数目,root为根结点在一维数组中的位置
}ChildTree;
这种存储结构的特点是寻找某个结点的孩子比较容易,但寻找双亲比较麻烦,所以,在必要的时候,可以将双亲表示法和孩子表示法结合起来,即将一维数组元素增加一个表示双亲结点的域parent,用来指示结点的双亲在一维数组中的位置。
获取给定结点第i个孩子的操作算法实现:
int Child(ChildTree T, int linklist, int i)
{
if(linklist<0||linklist>=T.n) return -2;
p=T.elem[linklist].firstchild; j=1;
while (p&&j!=i) { p=p->next; j++;}
if(!p) return -2;
else return p->child;
}
2.3 孩子兄弟表示法
孩子兄弟表示法也是一种链式存储结构。它通过描述每个结点的一个孩子和兄弟信息来反映结点之间的层次关系,其结点结构为:
在C语言中,这种存储形式定义如下:
typedef struct CSLinklist{
Elemtype elem;
struct CSLinklist *firstchild,*nextsibling;
}CSLinklist,*CSTree;
void AllChild(CSTree T, CSTree p) //输出树中p指针所指结点的所有孩子信息
{
q=p->fisrtchild;
while(q) {
printf("%c",q->elem); q=q->nextsibling;
}
}
第二节 二叉树
1.二叉树的定义和基本运算
1.1 定义
定义:二叉树是另一种树形结构。它与树形结构的区别是:
(1)每个结点最多有两棵子树;
(2)子树有左右之分。
二叉树也可以用递归的形式定义。即:二叉树是n(n≥0)个结点的有限集合。当n=0时,称为空二叉树;当n>0时,有且仅有一个结点为二叉树的根,其余结点被分成两个互不相交的子集,一个作为左子集,另一个作为右子集,每个子集又是一个二叉树。
二叉树的5种形态:
1.2 二叉树的基本运算(略见教材)
(1) 构造一棵二叉树 CreateBTree ( BT)
(2)清空以BT为根的二叉树 ClearBTree(BT)
(3)判断二叉树是否为空 BTreeEmpty(BT)
(4)获取给定结点的左孩子和右孩子 LeftChild(BT,linklist),RightChild(BT,linklist)
(5)获取给定结点的双亲 Parent(BT,linklist)
(6)遍历二叉树Traverse(BT)
2.二叉树的性质
二叉树具有下列5个重要的性质。
【性质1】 在二叉树的第i层上最多有2i-1个结点(i≥1)。
证明:二叉树的第1层只有一个根结点,所以,i=1时,2i-1=21-1=20=1成立。
假设对所有的j,1≤j<i< span="">成立,即第j层上最多有2j-1个结点成立。若j=i-1,则第j层上最多有2j-1=2i-2个结点。由于在二叉树中,每个结点的度最大为2,所以可以推导出第i层最多的结点个数就是第i-1层最多结点个数的2倍,即2i-2*2=2i-1。
【性质2】 深度为K的二叉树最多有2K-1个结点(K≥1)。
证明:由性质1可以得出,1至K层各层最多的结点个数分别为: 20,21,22,23,...,2K-1。这是一个以2为比值的等比数列,前n项之和的计算公式为:
【性质3】 对于任意一棵二叉树BT,如果度为0的结点个数为n0,度为2的结点个数为n2,则n0=n2+1。
证明:假设度为1的结点个数为n1,结点总数为n,B为二叉树中的分支数。
因为在二叉树中,所有结点的度均小于或等于2,所以结点总数为:
n=n0+n1+n2 (1)
再查看一下分支数。在二叉树中,除根结点之外,每个结点都有一个从上向下的分支指向,所以,总的结点个数n与分支数B之间的关系为:n=B+1。
又因为在二叉树中,度为1的结点产生1个分支,度为2的结点产生2个分支,所以分支数B可以表示为:B=n1+2n2。
将此式代入上式,得:
n=n1+2n2+1 (2)
用(1)式减去(2)式,并经过调整后得到:n0=n2+1。
满二叉树:
如果一个深度为K的二叉树拥有2K-1个结点,则将它称为满二叉树。
完全二叉树:有一棵深度为h,具有n个结点的二叉树,若将它与一棵同深度的满二叉树中的所有结点按从上到下,从左到右的顺序分别进行编号,且该二叉树中的每个结点分别与满二叉树中编号为1~n的结点位置一一对应,则称这棵二叉树为完全二叉树。
【性质4】 具有n个结点的完全二叉树的深度为 (log2n)+1。其中,(log2n) 的结果是不大于log2n的最大整数。
证明:假设具有n个结点的完全二叉树的深度为K,则根据性质2可以得出:
2K-1-1<n≤2k-1
将不等式两端加1得到:
2K-1≤n<2K
将不等式中的三项同取以2为底的对数,并经过化简后得到:
K-1≤log2n<k
由此可以得到:(log2n) =K-1。整理后得到:K= (log2n)+1。
【性质5】 对于有n个结点的完全二叉树中的所有结点按从上到下,从左到右的顺序进行编号,则对任意一个结点i (1≤i≤n),都有:
(1)如果i=1,则结点i是这棵完全二叉树的根,没有双亲;否则其双亲结点的编号为 i/2。
(2)如果2i>n,则结点i没有左孩子;否则其左孩子结点的编号为2i。
(3)如果2i+1>n,则结点i没有右孩子;否则其右孩子结点的编号为2i+1。
下面我们利用数学归纳法证明这个性质。
我们首先证明(2)和(3)。
证明:当i=1时,若n≥3,则根的左、右孩子的编号分别是2,3;若n<3,则根没有右孩子;若n<2,则根将没有左、右孩子;以上对于(2)和(3)均成立。
假设:对于所有的1≤j≤i 结论成立。即:结点j的左孩子编号为2j;右孩子编号为2j+1。
由完全二叉树的结构可以看出:结点i+1或者与结点i同层且紧邻i结点的右侧,或者i位于某层的最右端,i+1位于下一层的最左端。
可以看出,i+1的左、右孩子紧邻在结点i的孩子后面,由于结点i 的左、右孩子编号分别为2i和2i+1,所以,结点i+1的左、右孩子编号分别为2i+2和2i+3,经提取公因式可以得到:2(i+1)和2(i+1)+1,即结点i+1的左孩子编号为2(i+1);右孩子编号为2(i+1)+1。
又因为二叉树由n个结点组成,所以,当2(i+1)+1>n,且2(i+1)=n时,结点i+1只有左孩子,而没有右孩子;当2(i+1)>n,结点i+1既没有左孩子也没有右孩子。
以上证明得到(2)和(3)成立。
下面利用上面的结论证明(1)。
证明:对于任意一个结点i,若2i≤n,则左孩子的编号为2i,反过来结点2i的双亲就是i,而 2i/2=i;若2i+1≤n,则右孩子的编号为2i+1,反过来结点2i+1的双亲就是i,而 (2i+1)/2 =i,由此可以得出(1)成立。
3.二叉树的存储结构
二叉树也可以采用两种存储方式:顺序存储结构和链式存储结构。
3.1 顺序存储结构
这种存储结构适用于完全二叉树。其存储形式为:用一组连续的存储单元按照完全二叉树的每个结点编号的顺序存放结点内容。下面是一棵二叉树及其相应的存储结构
在C语言中,这种存储形式的类型定义如下所示:
#define MAX_TREE_LINKLIST_SIZE 100
typedef struct {
Elemtype elem[MAX_TREE_LINKLIST_SIZE]; //根存储在下标为1的数组单元中
int n; //当前完全二叉树的结点个数
}QBTree;
这种存储结构的特点是空间利用率高、寻找孩子和双亲比较容易。下面我们给出完全二叉树在这种存储形式下的操作算法。
(1)构造一棵完全二叉树
void CreateBTree(QBTree *BT,Elemtype elem[ ],int n)
{
if(n>=MAX_TREE_LINKLIST_SIZE)n=MAX_TREE_LINKLIST_SIZE-1;
for (i=1; i<=n;i++)
BT->elem[i]=elem[i];
BT->n=n;
}
(2)获取给定结点的左孩子
int LeftCHild(QBTree BT,int linklist)
{
if (2*linklist>BT.n) return 0;
else return 2*linklist;
}
RightChild(BT,linklist)与这个操作类似,读者可试着自行完成。
(3)获取给定结点的双亲
int Parent(QBTree BT,int linklist)
{
if(1<=linklist&&linklist<=BT.n) return i/2;
else return -1;
}
3.2 链式存储结构
在顺序存储结构中,利用编号表示元素的位置及元素之间孩子或双亲的关系,因此对于非完全二叉树,需要将空缺的位置用特定的符号填补,若空缺结点较多,势必造成空间利用率的下降。在这种情况下,就应该考虑使用链式存储结构。
常见的二叉树结点结构如下所示:
其中,Lchild和Rchild是分别指向该结点左孩子和右孩子的指针,elem是数据元素的内容。在C语言中的类型定义为:
typedef struct BTLinklist{
Elemtype elem;
struct BTLinklist *Lchild,*Rchlid;
}BTLinklist,*BTree;
下面是一棵二叉树及相应的链式存储结构
这种存储结构的特点是寻找孩子结点容易,双亲比较困难。因此,若需要频繁地寻找双亲,可以给每个结点添加一个指向双亲结点的指针域,其结点结构如下所示。
4. 遍历二叉树
二叉树是一种非线性的数据结构,在对它进行操作时,总是需要逐一对每个数据元素实施操作,这样就存在一个操作顺序问题,由此提出了二叉树的遍历操作。所谓遍历二叉树就是按某种顺序访问二叉树中的每个结点一次且仅一次的过程。这里的访问可以是输出、比较、更新、查看元素内容等等各种操作。
二叉树的遍历方式分为两大类:一类按根、左子树和右子树三个部分进行访问;另一类按层次访问。下面我们将分别进行讨论。
4.1 按根、左子树和右子树三部分进行遍历
遍历二叉树的顺序存在下面6种可能:
TLR(根左右), TRL(根右左)
LTR(左根右), RTL(右根左)
LRT(左右根), RLT(右左根)
其中,TRL、RTL和RLT三种顺序在左右子树之间均是先右子树后左子树,这与人们先左后右的习惯不同,因此,往往不予采用。余下的三种顺序TLR、LTR和LRT根据根访问的位置不同分别被称为先序遍历、中序遍历和后序遍历。
(1)先序遍历
若二叉树为空,则结束遍历操作;否则
访问根结点;
先序遍历左子树;
先序遍历右子树。
(2)中序遍历
若二叉树为空,则结束遍历操作;否则
中序遍历左子树;
访问根结点;
中序遍历右子树。
(3)后序遍历
若二叉树为空,则结束遍历操作;否则
后序遍历左子树;
后序遍历右子树;
访问根结点。
下面是一棵二叉树及其经过三种遍历得到的相应序列
下面我们再给出两种遍历二叉树的方法:
(1)对一棵二叉树中序遍历时,若我们将二叉树严格地按左子树的所有结点位于根结点的左侧,右子树的所有结点位于根右侧的形式绘制,就可以对每个结点做一条垂线,映射到下面的水平线上,由此得到的顺序就是该二叉树的中序遍历序列
(2)任何一棵二叉树都可以将它的外部轮廓用一条线绘制出来,我们将它称为二叉树的包线,这条包线对于理解二叉树的遍历过程很有用。
由此可以看出:(1)遍历操作实际上是将非线性结构线性化的过程,其结果为线性序列,并根据采用的遍历顺序分别称为先序序列、中序序列或后序序列;(2)遍历操作是一个递归的过程,因此,这三种遍历操作的算法可以用递归函数实现。
(1)先序遍历递归算法
void PreOrder(BTree BT)
{
if(BT) { Visit(BT);
PreOrder(BT->Lchild);
PreOrder(BT->Rchild); }
}
(2)中序遍历递归算法
void InOrder(BTree BT)
{
if (BT) {
InOrder(BT->Lchild);
Visit(BT);
InOrder(BT->Rchild);
}
}
(3)后序遍历递归算法
void PostOrder(BTree BT)
{
if (BT) {
PostOrder(BT->Lchild);
PostOrder(BT->Rchild);
Visit(BT);
}
}
4.2 按层次遍历二叉树
实现方法为从上层到下层,每层中从左侧到右侧依次访问每个结点。下面我们将给出一棵二叉树及其按层次顺序访问其中每个结点的遍历序列。
void LevelOreder(QBTree BT)
{
for (i=1;i<=BT.n;i++)
if (BT.elem[i]!='#') Visite(BT.elem[i]);
}
二叉树用链式存储结构表示时,按层遍历的算法实现,访问过程描述如下:
(1)访问根结点,并将该结点记录下来;
(2)若记录的所有结点都已处理完毕,则结束遍历操作;否则重复下列操作。
(3)取出记录中第一个还没有访问孩子的结点,若它有左孩子,则访问左孩子,并将记录下来;若它有右孩子,则访问右孩子,并记录下来。
在这个算法中,应使用一个队列结构完成这项操作。所谓记录访问结点就是入队操作;而取出记录的结点就是出队操作。这样一来,我们的算法就可以描述成下列形式:
(1)访问根结点,并将根结点入队;
(2)当队列不空时,重复下列操作:
①从队列退出一个结点;
②若其有左孩子,则访问左孩子,并将其左孩子入队
③若其有右孩子,则访问右孩子,并将其右孩子入队;
void LevelOrder(BTree *BT)
{
if (!BT) exit;
InitQueue(Q); p=BT; //初始化
Visite(p); EnQueue(&Q,p); //访问根结点,并将根结点入队
while (!QueueEmpty(Q)) { //当队非空时重复执行下列操作
DeQueue(&Q,&p); //出队
if(!p->Lchild) {Visite(p->Lchild);EnQueue(&Q,p->Lchild); //处理左孩子
if(!p->Rchild) {Visite(p->Rchild);EnQueue(&Q,p->Rchild); //处理右孩子
}
}
5. 典型二叉树的操作算法
5.1 输入一个二叉树的先序序列,构造这棵二叉树
为了保证唯一地构造出所希望的二叉树,在键入这棵树的先序序列时,需要在所有空二叉树的位置上填补一个特殊的字符,比如,'#'。在算法中,需要对每个输入的字符进行判断,如果对应的字符是'#',则在相应的位置上构造一棵空二叉树;否则,创建一个新结点。整个算法结构以先序遍历递归算法为基础,二叉树中结点之间的指针连接是通过指针参数在递归调用返回时完成。
【算法5-12】
BTree Pre_Create_BT( )
{
getch(ch);
if (ch=='#') return NULL; //构造空树
else{ BT=(BTree)malloc(sizeof(BTLinklist)); //构造新结点
BT->data=ch;
BT->lchild =Pre_Create_BT( ); //构造左子树
BT->rchild =Pre_Create_BT( ); //构造右子树
return BT;
}
}
5.2 计算一棵二叉树的叶子结点数目
这个操作可以使用三种遍历顺序中的任何一种,只是需要将访问操作变成判断该结点是否为叶子结点,如果是叶子结点将累加器加1即可。下面这个算法是利用中序遍历实现的。
【算法5-13】
void Leaf(BTree BT,int *count)
{
if (BT) {
Leaf(BT->child,&count); //计算左子树的叶子结点个数
if (BT->lchild==NULL&&BT->rchild==NULL) (*count)++;
Leaf(BT->rchild,&count); //计算右子树的叶子结点个数
}
}
5.3 交换二叉树的左右子树
许多操作可以利用三种遍历顺序的任何一种,只是某种遍历顺序实现起来更加方便一些。而有些操作则不然,它只能使用其中的一种或两种遍历顺序。将二叉树中所有结点的左右子树进行交换这个操作就属于这类情况。
void change_left_right(BTree BT)
{
if (BT) {
change_left_right(BT->lchild);
change_left_right(BT->rchild);
BT->lchild<->BT->rchild;
}
}
5.4 求二叉树的高度
这个操作使用后序遍历比较符合人们求解二叉树高度的思维方式。首先分别求出左右子树的高度,在此基础上得出该棵树的高度,即左右子树较大的高度值加1。
int hight(BTree BT)
{//h1和h2分别是以BT为根的左右子树的高度
if (BT==NULL) return 0;
else {
h1=hight(BT->lchild);
h2=hight(BT->right);
return max{h1,h2}+1;
}
}
6.树、森林与二叉树的转换
6.1 树、森林转换成二叉树
将一棵树转换成二叉树的方法:将一棵树转换成二叉树实际上就是将这棵树用孩子兄弟表示法存储即可,此时,树中的每个结点最多有两个指针:一个指针指向第一个孩子,另一个指针指向右侧第一个兄弟。当你将这两个指针看作是二叉树中的左孩子指针和孩子右指针时,就是一棵二叉树了。
特点:一棵树转换成二叉树后,根结点没有右孩子。
将森林转换成二叉树的方法与一棵树转换成二叉树的方法类似,只是把森林中所有树的根结点看作兄弟关系,并对其中的每棵树依依地进行转换。
6.2 二叉树还原成树或森林
这个过程实际上是树、森林转换成二叉树的逆过程,即将该二叉树看作是树或森林的孩子兄弟表示法。比如,若二叉树为空,树也为空;否则,由二叉树的根结点开始,延右指针向下走,直到为空,途经的结点个数是相应森林所含树的棵数;若某个结点的左指针非空,说明这个结点在树中必有孩子,并且从二叉树中该结点左指针所指结点开始,延右指针向下走,直到为空,途经的结点个数就是这个结点的孩子数目。
第三节 哈夫曼树及其应用
1.哈夫曼树的定义
在二叉树中,一个结点到另一个结点之间的分支构成这两个结点之间的路径。
这三棵二叉树的带权路径长度分别为:
WPL1=10*2+11*2+3*3+6*3+7*3+9*3=117
WPL2=3*1+6*2+7*3+9*4+10*5+11*5=177
WPL3=9*1+7*2+6*3+3*4+10*5+11*5=158
构造哈夫曼树的过程:
(1)将给定的n个权值{w1,w2,...,wn}作为n个根结点的权值构造一个具有n棵二叉树的森林{T1,T2,...,Tn},其中每棵二叉树只有一个根结点;
(2)在森林中选取两棵根结点权值最小的二叉树作为左右子树构造一棵新二叉树,新二叉树的根结点权值为这两棵树根的权值之和;
(3)在森林中,将上面选择的这两棵根权值最小的二叉树从森林中删除,并将刚刚新构造的二叉树加入到森林中;
(4)重复上面(2)和(3),直到森林中只有一棵二叉树为止。这棵二叉树就是哈夫曼树。
假设有一组权值{5,29,7,8,14,23,3,11},下面我们将利用这组权值演示构造哈夫曼树的过程。
这就是以上述8个权值为叶子结点权值构成的哈夫曼树,它的带权的路径长度为:
WPL=(23+29)*2+(11+14)*3+(3+5+7+8)*4=271
2.判定树
在很多问题的处理过程中,需要进行大量的条件判断,这些判断结构的设计直接影响着程序的执行效率。例如,编制一个程序,将百分制转换成五个等级输出。大家可能认为这个程序很简单,并且很快就可以用下列形式编写出来:
if (socre<60) printf("bad");
else if (socre<70) printf("pass");
else if (score<80) printf("general");
else if (score<90) printf("good");
esle printf("very good");
在实际应用中,往往各个分数段的分布并不是均匀的。下面就是在一次考试中某门课程的各分数段的分布情况:
3.前缀编码
在电文传输中,需要将电文中出现的每个字符进行二进制编码。在设计编码时需要遵守两个原则:(1)发送方传输的二进制编码,到接收方解码后必须具有唯一性,即解码结果与发送方发送的电文完全一样;(2)发送的二进制编码尽可能地短。下面我们介绍两种编码的方式。
(1)等长编码
这种编码方式的特点是每个字符的编码长度相同(编码长度就是每个编码所含的二进制位数)。假设字符集只含有4个字符A,B,C,D,用二进制两位表示的编码分别为00,01,10,11。若现在有一段电文为:ABACCDA,则应发送二进制序列:00010010101100,总长度为14位。当接收方接收到这段电文后,将按两位一段进行译码。这种编码的特点是译码简单且具有唯一性,但编码长度并不是最短的。
(2)不等长编码
在传送电文时,为了使其二进制位数尽可能地少,可以将每个字符的编码设计为不等长的,使用频度较高的字符分配一个相对比较短的编码,使用频度较低的字符分配一个比较长的编码。例如,可以为A,B,C,D四个字符分别分配0,00,1,01,并可将上述电文用二进制序列:000011010发送,其长度只有9个二进制位,但随之带来了一个问题,接收方接到这段电文后无法进行译码,因为无法断定前面4个0是4个A,1个B、2个A,还是2个B,即译码不唯一,因此这种编码方法不可使用。
假设有一个电文字符集中有8个字符,每个字符的使用频率分别为{0.05,0.29,0.07,0.08,0.14,0.23,0.03,0.11},现以此为例设计哈夫曼编码。
哈夫曼编码设计过程为:
(1)为方便计算,将所有字符的频度乘以100,使其转换成整型数值集合,得到{5,29,7,8,14,23,3,11};
(2)以此集合中的数值作为叶子结点的权值构造一棵哈夫曼树,如图5-27所示;
(3)由此哈夫曼树生成哈夫曼编码,如图5-28所示。
最后得出每个字符的编码为:
比如,发送一段编码:0000011011010010,
接收方可以准确地通过译码得到:⑥⑥⑦⑤②⑧。