数据结构与算法课的一个简单实验,记录一下,以供参考。
文章目录
- 要求
- 测试样例
- 统计字母出现次数
- 建立哈夫曼树
- 对字符编码
- 对原文进行编码
- 译码
要求
- 输入一段100—200字的英文短文,存入一文件a中。
- 统计短文出现的字母个数n及每个字母的出现次数。
- 以字母出现次数作权值,建Huffman树(n个叶子),给出每个字母的Huffman编码。
- 用每个字母编码对原短文进行编码,码文存入文件b中。
- 用Huffman树对b中码文进行译码,结果存入文件c中,比较a,c是否一致,以检验编码、译码的正确性。
测试样例
节选自J·阿尔弗瑞德·普鲁弗洛克的情歌,存放在文件a.txt中:
Let us go then, you and I,
When the evening is spread out against the sky
Like a patient etherized upon a table;
Let us go, through certain half-deserted streets,
The muttering retreats
Of restless nights in one-night cheap hotels
And sawdust restaurants with oyster-shells:
Streets that follow like a tedious argument
Of insidious intent
To lead you to an overwhelming question ...
Oh, do not ask, “What is it?”
Let us go and make our visit.
这段诗是游戏赛博朋克2077节制结局奥特对v说的,印象深刻,找到英文原版拿来用了。
统计字母出现次数
这块用哈希表来存比较简单,字母为key,出现的次数为valve,借助stl容器unordered_map实现,从文件中一个字符一个字符读,然后将其存到哈希表中。
代码如下:
void analysis()
{
ifstream ifs("a.txt", ios::in);
char c;
while (ifs.get(c))
m[c]++;
ifs.close();
}
测试代码如下:
for (auto& kv : m)
cout << kv.first << '>' << kv.second << '\t';
cout << endl;
结果如下:
第一行最后的换行是因为回车符。原文正好12行,换了11次行。
建立哈夫曼树
采用链式存储,先给出节点的定义:
struct treeNode
{
char data;
int weight;
treeNode* parent;
treeNode* lchild;
treeNode* rchild;
treeNode(char d = 0, int w = 0, treeNode* p = nullptr, treeNode* l = nullptr, treeNode* r = nullptr)
: data(d), weight(w), parent(p), lchild(l), rchild(r)
{}
};
vector<treeNode*> v;
在给出字符的哈夫曼编码时采取的从叶子到根的方式,所以需要保存父节点指针,同时需要记录所有叶子节点,借助stl容器vector保存。
建树的过程很简单,先建立n个叶子结点,然后创建n-1个非叶子节点,每次从节点堆里取出两个权值最低的节点作为新节点,然后加入节点堆重复这个过程,一共循环n-1次。
每次都需要取权值最小的节点,考虑使用小根堆,借助stl容器priority_queue,由于优先队列中存放的是自定义类型指针,需要自定义比较方式,用仿函数实现:
struct treeNodeCompare
{
bool operator()(treeNode* lhs, treeNode* rhs)
{
return lhs->weight > rhs->weight;
}
};
所以建树的代码就好写了:
void createTree()
{
priority_queue<treeNode*, vector<treeNode*>, treeNodeCompare> q;
treeNode *e;
for (auto& kv : m)
{
e = new treeNode(kv.first, kv.second);
q.push(e);
v.push_back(e);
}
treeNode *l, *r, *p;
for (int i = 0; i < m.size() - 1; i++)
{
l = q.top();
q.pop();
r = q.top();
q.pop();
p = new treeNode(0, l->weight + r->weight, nullptr, l, r);
l->parent = p;
r->parent = p;
q.push(p);
}
}
对字符编码
对字符进行编码,这里采取从下至上的方式,在左分支给0,右分支给1。
所以代码思路就很简单,遍历先前保存的叶子结点集合,一直向上找parent,如果是parent的left就+0,如果是parent的right就+1,直到走到根节点,这样得到的序列是逆过来的,可以选择转置,这个无所谓。
得到字符的编码之后还需要保存一下,后面对短文进行编码和译码的时候都要用。
用哈希表存的话需要存两份,一份字符为key编码为value,对文章编码的时候用;一份编码为key字符为value,译码的时候用,这份其实也可以不要,后续给出不用这个的译码方式。
代码如下:
unordered_map<char, string> ch_hf;
unordered_map<string, char> hf_ch;
void createHfCode()
{
for (auto e : v)
{
string tmp;
treeNode* p = e;
while (p->parent)
{
if (p->parent->lchild == p)
tmp += '0';
else
tmp += '1';
p = p->parent;
}
reverse(tmp.begin(), tmp.end());
ch_hf[e->data] = tmp;
hf_ch[tmp] = e->data;
}
}
也可以使用先序遍历,遍历到根节点就存储,然后回溯,这样就不需要父节点了,但是需要保存一下根节点:
void dfs(treeNode* p, string& s)
{
if (p->lchild == nullptr && p->rchild == nullptr)
{
hf_ch[s] = p->data;
ch_hf[p->data] = s;
return;
}
if (p->lchild)
{
s += '0';
dfs(p->lchild, s);
s.pop_back();
}
if (p->rchild)
{
s += '1';
dfs(p->rchild, s);
s.pop_back();
}
}
void createHfCode()
{
string s;
dfs(root, s);
}
用下面的代码进行这部分的测试:
for (auto& kv : ch_hf)
printf("%c->%-15s", kv.first, kv.second.c_str());
cout << endl;
结果如下:
第三行突然换行还是因为换行符。
对原文进行编码
这个就很简单了,还是一个一个字符读取,直接向文件b中写入字符对应的编码。
哈希表还是很方便的,代码如下:
void code()
{
ifstream ifs("a.txt", ios::in);
ofstream ofs("b.txt", ios::out);
char c;
while (ifs.get(c))
ofs << ch_hf[c];
ifs.close();
ofs.close();
}
编译运行得到文件b,一长串01序列,全部都在一行不好展示,用下面代码进行测试:
ifstream ifs2("b.txt", ios::in);
while (ifs2.get(c))
cout << c;
cout << endl;
ifs2.close();
结果如下:
译码
第一种方法,使用多保存的那个哈希表。
还是一个字符一个字符地从文件b读取,读一个就加到临时字符串中,然后从哈希表中查询一下是否有以当前字符串为key的kv对,如果有就将其value值写入到文件c中。代码如下:
void encode()
{
ifstream ifs("b.txt", ios::in);
ofstream ofs("c.txt", ios::out);
string tmp;
char c;
while (ifs.get(c))
{
tmp += c;
auto it = hf_ch.find(tmp);
if (it != hf_ch.end())
{
ofs << it->second;
tmp.clear();
}
}
ifs.close();
ofs.close();
}
第二种方法,不使用哈希表,直接从树中查询,需要保存根节点。
还是一个字符一个字符读取,如果读到0就走到左子树,如果读到1就走到右子树。当走到叶子节点说明已经读完了一个字符的完整哈夫曼编码,可以进行译码,将叶子节点存的字符写入到文件c中。代码如下:
void encode2()
{
ifstream ifs("b.txt", ios::in);
ofstream ofs("c.txt", ios::out);
char c;
treeNode* p = root;
while (ifs.get(c))
{
if (c == '0')
p = p->lchild;
else
p = p->rchild;
if (p->data)
{
ofs << p->data;
p = root;
}
}
ifs.close();
ofs.close();
}
编译运行得到c.txt:
与原文完全一致。