上一篇中主要是二叉树的一些介绍,本来想把Huffman编码放进去,介于篇幅过长,故移到这里。
1.Huffman编码
PFC编码以及解码
以下基于二叉树结构,按照图5.28的总体框架。介绍PFC编码和解码算法的具体实现。
如图5.29所示,若字符集E1和E2之间没有公共字符,且PFC编码方案对应于二叉树T1和T2,则通过引入一个根节点合并T1和T2 之后所得到的二叉树,就是对应于E1UE2的一种PFC编码方案。请注意,无论T1和T2的高度和规模是否相等,这一性质总是成 立。
利用上述性质,可自底向上地构造PFC编码树。首先,由每一字符分别构造一颗单节点二叉树,并将它们视作一个森林。此后,反复从森林中取出两棵树并将其合二为一,如此,经|E| - 1步迭代之后。初始森林中的|E|棵树将合并成为一棵完整的PFC编码树。接下来,再将PFC编码树转译为编码表,以便能够根据待编码字符快捷确定与子对应的编码串。至此,对于任何待编码文本,通过反复查询编码表,即可高效地将其转化为二进制编码串。
与编码过程相对应地,接受方也可以借助同一棵编码树来记录双方约定的编码方案。于是,每当接收到经信道传送过来的编码串后,(只要传送过程无误)接收方都可通过在编码树中反复从根节点出发做相应的漫游,依次完成对信息文本中各字符的解码。
总体框架
以上编码和解码过程可描述为如下代码,这也是同类编码,解码算法的统一测试入
int main(int argc,char* argv[]) //PFC编码。解码算法统一测试入口
{
PFCForest* forest = initForest(); //初始化PFC森林
PFCTree* tree = generateTree(forest);
release(forest); //生成PFC编码树
for(int i = 1;i < argc;i++) //对于命令行传入的每一明文串
{
Bintmap codeString;//二进制编码串
int n = encode(table,codeString,argv[i]);//将根据编码表生成(长度为n)
decode(tree,codeString,n);//利用编码树,对长度为n的二进制编码串解码(直接输出)
}
release(table);
release(tree);
return 0; //释放编码表,编码树
}
数据结构的选取与设计
如下面代码所示,这里使用向量实现PFC森林,其中各元素分别对应于一棵编码树:使用了跳转表式词典结构实现编码表。其中的词条各以某一待编码字符为关键码。以对应的编码串为数据项;使用位图Bitmap,实现各字符的二进制编码串。
//PFC编码使用的数据结构
#include "../BinTree/BinTree.h"//用BinTree实现PFC树
typedef BinTree<char> PFCTree;//PFC树
#include "../Vector/Vector.h"//用Vector实现PFC森林
typedef Vector<PFCTree*> PFCForest;//PFC森林
#include "../Bitmap/Bitmap.h"//使用位图结构实现二进制编码串
#include "../Skiplist/Skiplist.h" //引入Skiplist式词典结构实现
typedef Skiplist<char,char*> PFCTable; //PFC编码表,词条格式为:(key = 字符,value = 编码串)
#define N_CHAR (0x80 - 0x20) //只考虑可打印字符
//初始化PFC森林
PFCForest* initForest()//PFC编码森林初始化
{
PFCForest* forest = new PFCForest;//首先创建空森林,然后
for(int i = 0;i < N_CHAR;i++) //对每一个可打印字符[0x20,0x80)
{
forest->insert(i,new PFCTree());//创建一棵对应的PFC编码树,初始时其中只包含对应的一个(叶,根)节点
(*forest)[i]->insertAsRoot(0x20 + i);//只包含对应的一个(叶,根)节点
}
return forest;//返回包含N_CHAR棵树的森林,其中每棵树各包含一个字符
}
//构造PFC编码树
PFCTree* generateTree(PFCForest* forest) //构造PFC树
{
srand((unsigned int )time(NULL)); //这里将随机取树合并,故先设置随机种子
while(1 < forest->size()) //共做|forest| - 1次合并
{
PFCTree* s = new PFCTree;
s->insertAsRoot('^');//创建新树(根标记为'^')‘
Rank r1 = rand() % forest->size();//随机选取r1,且
s->attackAsLC(s->root(),(*forest)[r1]);
forest->remove(r1);//作为左子树后随即剔除
Rank r2 = rand() % forest->size();//随机选取r2,且
s->attackAsRC(s->root(),(*forest)[r2]);
forest->remove(r2);//作为右子树接入后随即剔除
forest->insert(forest->size(),s);//合并后的PFC重新植入PFC森林
}
return (*forest)[0];//至此,森林中尚存的最后一棵树,即全局PFC编码树
}
//生成PFC编码表
void generateCT //通过遍历获取各字符的编码
(Bitmap* code,int lenght,PFCTable* table,BinNodePosi(char) v)
{
PFCTable* table = new PFCTable;//创建以Skiplist实现的编码表
Bitmap* code = new Bitmap;//用于记录RPS的位图
generateCT(code,0,table,tree->root());//遍历以获取各字符(叶节点)的RPS
release(code);
return table;//释放编码位图,返回编码表
}
//编码
int encode(PFCTable* table,Bitmap& codeString,char* s) //PFC编码算法
{
int n = 0;
for(size_t m = strlen(s),i = 0;i < m;i++) //对于明文s[]中的每个字符
{
char** pCharCode = table->get(s[i]);//取出其对应的编码串
if(!pCharCode)
pCharCode = table->get(s[i] + 'A' - 'a');//小写字母转为大写
if(!pCharCode)
pCharCode = table->get(' ');//无法识别的字符统一视作空格
printf("%s",*pCharCode);//输出当前字符的编码
for(size_t m = strlen(*pCharCode),j = 0;j < m;j++) //将当前字符的编码接入编码串
'1' == *(*pCharCode + j) ? codeString.set(n++):codeString.clear(n++);
}
return n;//二进制编码串记录于codeString中,返回编码串总长
}
//解码
void decode(PFCTree* tree,Bitmap& code,int n) //PFC解码算法
{
BinNodePosi(char) x = tree->root();//根据PFC编码树
for(int i = 0;i < n;i++) //将编码(二进制为图)
{
x = code.test(i) ? x->rChild :x->lChild;//转译为明码并
if(IsLeat(*x))
{
printf("%c",x->data);
x = tree->root(); //打印输出
}
}
}
优化
在介绍过PFC及其实现方法后,以下将就其编码效率做一分析,并设计出更佳的编码方法。
同样地,我们依然暂且忽略硬件成本和信道误差等因素,而主要考查如何高效率地完成文本信息地编码和解码,不难理解,在计算资源固定的条件下,不同编码方法的效率主要体现于所生成二进制编码串的总长,或者更精确的,体现于二进制码长与原始文本长度的比率。
那么。面对千变万化,长度不一的待编码文本,从总体上我们应该按照何种尺度来衡量这一因素呢?基于这一尺度,又该应用哪些数据结构来实现相关的算法呢?
2.最优编码树
在实际的通讯系统中,信道的使用效率是个很重要的问题,这在很大程度上取决于编码算法本身的效率,比如,高效的编码算法生成的编码串应该尽可能地短。那么,如何做到这一点呢?
平均编码长度于叶节点平均深度
由前文中得出地结论,字符x的编码长度|rps(x)|就是其对应的叶节点的深度depth(v(x))。于是,各字符的平均编码长度就是编码树T中各叶节点的平均深度(average leaf depth):
最优编码树
同一字符集的所有编码方案中,编码长度最小者称作最优方案:对应编码树的平均编码长度也达到最短,故称之为最优编码树