课程设计——赫夫曼编码/译码系统
大一时候写的报告了,可能存在很多问题……
一、题目
编写一个赫夫曼编码/译码系统
二、实验目的
深化学生对课程中基本概念、理论和方法的理解,培养学生独立编写程序的能力,训练学生灵活地将课程内外所学知识和技术运用到实践,解决实际问题。
三、需求分析
利用赫夫曼编码进行通信可以大大提高信道利用率,缩短信息传输时间,降低传输成本。这要求在发送端通过一个编码系统对待传输数据预先编码,在接收端将传来的数据进行译码(复原)。对于双工信道(即可以双向传输信息的信道),每端都需要一个完整的编/译码系统。试为这样的信息收发站编写一个赫夫曼码的编/译码系统。
一个完整的系统应具有以下功能:
(1) I:初始化(Initialization):从终端读入字符集大小n,以及n个字符和n个权值,建立赫夫曼树,并将它存于文件hfmTree中。
(2) E:编码(Encoding):利用已建好的赫夫曼树(如不在内存,则从文件hfmTree中读入),对文件ToBeTran中的正文进行编码,然后将结果存入文件CodeFile中。
(3) D:译码(Decoding):利用已建好的赫夫曼树将文件CodeFile中的代码进行译码,结果存入文件Textfile中。
(4) P:打印代码文件(Print):将文件CodeFile以紧凑格式显示在终端上,每行50个代码。同时将此字符形式的编码文件写入文件CodePrin中。
(5) T:打印赫夫曼树(Tree printing):将已在内存中的赫夫曼树以直观的方式(比如树)显示在终端上,同时将此字符形式的赫夫曼树写入文件TreePrint 中。
四、概要设计
4.1 存储结构说明
4.1.1 定义哈夫曼树结点结构
struct Node {
char data; //记录数据
int weight; //记录权值(出现次数)
int leftchild; //左儿子结点
int rightchild; //右儿子结点
int parent; //双亲结点
};
4.1.2 定义哈夫曼树结构
typedef struct HuffmanTree {
std::vector<Node> nodes; //结点
int total; //记录叶子结点个数
} *Tree;
4.1.3 定义图结点的坐标
struct point {
int x; //记录x坐标
int y; //记录y坐标
};
4.2 初始化
void Init() // 初始化数据
清除原先的树,申请新的树指针访问空间,并将叶子节点数和树高度归零。
void InitTree() // 无原始数据情况初始化
输入叶子结点个数n,申请各结点的存储空间。其中,总结点数为2n-1,考虑0位置存放最大值,故申请2n的空间。然后,按照题目所给数据记录各结点的值和权值。最后,需注意将根结点的双亲结点记为空。
void InitTree(std::string str) // 输入一段文本情况初始化
为方便起见,可以直接将各字符在一段文本中出现的次数作为相应字符的权值。利用双重循环,外层循环遍历输入的文本,内层循环遍历前面已存储的字符。若当前字符在前面已被记入结点,则权值加1;若当前字符第一次出现,则记入结点,值为1。遍历结束后,即可得到各字符的权值。最后,记得插入n-1个空结点为后续建树使用。
4.3 建树
void BuildTree()
n个叶子结点需要合并n-1次才可以构成哈夫曼树。为了判断各结点是否合并过,增加一个bool类型的visited[]数值进行记录合并情况。采用双重循环,外层循环完成n-1次的合并,即i从n+1开始到2n-1部分的求解;内层循环遍历1到i-1的结点,找到未被合并过的结点中权值最小的两个,记录它们的结点位置(即序号)。最后,将这两个结点合并所得的内容存至nodes[i]内,并记得记录下合并结点。
4.4 编码
std::string Encoding(std::string s)
采用双层循环,外层循环遍历所输入的字符,内层循环先查找当前字符在记录中对应的下标,再利用while循环从当前字符对应的叶子结点开始向上追溯直到到达根结点。若当前结点为自身双亲结点的左孩子,则记为0;若当前结点为自身双亲结点的右孩子,则记为1。因为是从叶子结点到根节点的追溯,所以在循环结束后,需要对追溯所得结果进行反转,才是最终的编码。
4.5 译码
std::string Decoding(std::string s)
利用哈夫曼树从根节点开始读取,根据哈夫曼码0/1判断下一指针位置在左子树还是右子树。若编码为0,则遍历左子树;若编码为1,则遍历右子树。每次向下一层,都需要判断当前结点是否为叶子结点,若为叶子结点,则读取该叶子结点对应的字符并存入result中。然后再回到根结点,重复上述操作直到哈夫曼码全部读取完成。
4.6 打印树
打印哈夫曼树首先需要确定树中每一个结点的位置,即结点圆心的x、y坐标。考虑到各结点之间不可出现重叠现象并方便计算,可先假定所建的哈夫曼树是满二叉树。所以,在计算存在的结点的坐标位置时,只需要计算它在对应的满二叉树图像中的坐标即可。
由于非终端结点的横坐标难以通过一个数学公式计算出来,但通过上下两层之间横坐标的递归关系,即当前结点的横坐标为左儿子结点横坐标与右儿子结点横坐标的平均值,只需要知道最后一层结点的横坐标,就可以知道其他非终端结点的横坐标。为实现以上算法,我定义了以下两个函数:
int CalculateX(int deep, int number, map<int, int> &temp)
//计算横坐标
int DeepTree (int root) //计算树的高度
第一个函数用于计算在满二叉树中编号为number的结点的横坐标。依据上面的分析,采用递归的方法。递归规律如上所述;递归出口为到达叶子结点。
是否到达递归出口的判断,根据当前结点的深度是否等于树的高度。故需要计算出树的高度。树的高度可以通过递归方法计算,递归规律是当前所求树的高度等于此树的根结点的左子树和右子树高度的较大值+1,递归出口是当前树为空树,此时高度为0。
因计算根节点的横坐标时已计算出了所有点的横坐标,所以在求其他点横坐标时无需重复求解,只需调用已求出的结果。故我们附设map temp用来缓存已计算出的横坐标,便于后续调用,以减少计算量。
满二叉树叶子结点的横坐标计算公式:
int CalculateY(int deep, int number) //计算纵坐标
因为每一层之间,结点的间距是固定的,故可以直接得到纵坐标的计算公式,如下:
void PreOrderTraverse(int number, int index, int deep, map<int, point> &points, map<int, int> &temp)
//用先序遍历遍历哈夫曼树,记录X、Y坐标,并将其保存在map中
Map points用于存储计算出的横纵坐标,其中每一个元素是一个结构体。
QPixmap DrawTree() //画图
计算出各结点的横纵坐标后,我们便可以开始绘制哈夫曼树的图像。考虑到先画圆再画线,会出现线覆盖圆的问题,故选取先画线再画圆的方法。
void Drawline(QPainter &p, QPixmap &pic, map<int, point> &points, int number, int index) //利用先序遍历画线
利用先序遍历,若当前结点为非终端结点,则从points中读取已缓存的自身和左右儿子的x、y坐标,并画出它与左右儿子的连线。若当前结点为终端结点,则递归结束。
void DrawCircle(QPainter &p, QPixmap &pic, map<int, point> &points, int number, int index) //利用先序遍历画圆
利用先序遍历,若当前结点为非终端结点,则直接从points中读取已缓存的当前结点的x、y坐标,画出结点圆;若当前结点为终端结点,则先画结点圆,再画权值对应的字符。
4.7 用户界面
为改善用户体验,设计了图形界面便于交互。根据题目要求,程序既需要实现根据默认权值进行编/译码的功能,也需要实现通过统计用户输入文本中各个字符出现的频度来计算权值,并进行编/译码的功能。因此,在用户界面中设计了“默认情况”和“自定义”两个模块,在每个模块中,分别放置了“编码”、“译码”、“绘制哈夫曼树”、“导入原文”、“导入编码”、“输出编码文件”和“输出译码文件”这几个按钮。
五、详细设计
5.1 头文件
#ifndef JIEMIAN_HUFFMANCODER_H
#define JIEMIAN_HUFFMANCODER_H
#include <string>
#include <vector>
#include <QPainter>
#include <QPixmap>
//定义哈夫曼树结点
struct Node {
char data; //记录数据
int weight; //记录权值(出现次数)
int leftchild; //左儿子结点
int rightchild; //右儿子结点
int parent; //双亲结点
};
//定义哈夫曼树结构
typedef struct HuffmanTree {
std::vector<Node> nodes; //结点
int total; //记录叶子结点个数
} *Tree;
//定义图结点的坐标
struct point {
int x; //记录x坐标
int y; //记录y坐标
};
//操作集
void Init(); //初始化数据
void InitTree(); //无原始数据情况初始化
void InitTree(std::string str); //输入一段文本情况初始化
void BuildTree(); //建树
std::string Encoding(std::string s); //编码
std::string Decoding(std::string s); //译码
int DeepTree(int root); //计算结点深度
QPixmap DrawTree(); //打印哈夫曼树
#endif //JIEMIAN_HUFFMANCODER_H
5.2 源文件
#include "HuffmanCoder.h"
#include <iostream>
#include <cstdlib>
#include <algorithm>
#include <vector>
#include <map>
#include <cmath>
using namespace std;
//定义全局变量
Tree t;
int Deep; //树的高度
int r = 30, space = 5; //圆的半径像素、叶节点间距
//建树
void BuildTree()
{
vector<bool> visited(2 * t->total, false);
//初始化结点是否合并的判断
//找到未被合并过的结点中权值最小的两个并合并
for (int i = t->total + 1; i <= 2 * t->total - 1; i++)
{
int min1 = 0, min2 = 0;
//min1为最小权值的下标,min2为第二小权值的下标
for (int j = 1; j <= i - 1; j++) //遍历前面已被记入的结点
{
if (!visited[j]) //当前结点未被合并过
{
if (t->nodes[j].weight < t->nodes[min1].weight) //当前结点权值比最小权值小
{
min2 = min1;
min1 = j;
}
else if (t->nodes[j].weight < t->nodes[min2].weight) min2 = j;
//当前结点权值比最小权值大,比第二小权值小
}
}
t->nodes[i].weight = t->nodes[min1].weight + t->nodes[min2].weight;
t->nodes[min1].parent = t->nodes[min2].parent = i;
t->nodes[i].leftchild = min1;
t->nodes[i].rightchild = min2; //合并结点
visited[min1] = visited[min2] = true; //记录此次合并的结点
}
}
//初始化树
void Init()
{
delete t; //清除原来的树
t = new HuffmanTree; //申请树指针的访问空间
t->total = 0; //树的叶子结点个数
Deep = 0; //树的高度
}
//无原始数据情况初始化
void InitTree()
{
t->total = 27; //记录树的叶子结点个数
//申请树各结点的存储空间,总结点数=叶子节点个数*2-1,再加一个0位置(用于放置最大值)
t->nodes = vector<Node>(2 * t->total);
//初始化各叶子结点
t->nodes[0] = {' ', INT_MAX, 0, 0, 0};
t->nodes[1] = {' ', 186, 0, 0, 0};
t->nodes[2] = {'A', 64, 0, 0, 0};
t->nodes[3] = {'B', 13, 0, 0, 0};
t->nodes[4] = {'C', 22, 0, 0, 0};
t->nodes[5] = {'D', 32, 0, 0, 0};
t->nodes[6] = {'E', 103, 0, 0, 0};
t->nodes[7] = {'F', 21, 0, 0, 0};
t->nodes[8] = {'G', 15, 0, 0, 0};
t->nodes[9] = {'H', 47, 0, 0, 0};
t->nodes[10] = {'I', 57, 0, 0, 0};
t->nodes[11] = {'J', 1, 0, 0, 0};
t->nodes[12] = {'K', 5, 0, 0, 0};
t->nodes[13] = {'L', 32, 0, 0, 0};
t->nodes[14] = {'M', 20, 0, 0, 0};
t->nodes[15] = {'N', 57, 0, 0, 0};
t->nodes[16] = {'O', 63, 0, 0, 0};
t->nodes[17] = {'P', 15, 0, 0, 0};
t->nodes[18] = {'Q', 1, 0, 0, 0};
t->nodes[19] = {'R', 48, 0, 0, 0};
t->nodes[20] = {'S', 51, 0, 0, 0};
t->nodes[21] = {'T', 80, 0, 0, 0};
t->nodes[22] = {'U', 23, 0, 0, 0};
t->nodes[23] = {'V', 8, 0, 0, 0};
t->nodes[24] = {'W', 18, 0, 0, 0};
t->nodes[25] = {'X', 1, 0, 0, 0};
t->nodes[26] = {'Y', 16, 0, 0, 0};
t->nodes[27] = {'Z', 1, 0, 0, 0};
t->nodes[2 * t->total - 1].parent = 0; //定义根结点的双亲结点为空
BuildTree(); //建树
}
//输入一段文本情况初始化
void InitTree(std::string str)
{
t->nodes.push_back({' ', INT_MAX, 0, 0, 0});
for (int i = 0; i < str.size(); i++)
{
int flag = 0; //判断当前字符是否已出现过
for (int j = 1; j <= t->total; j++) //遍历已被记入的结点
{
if (str[i] == t->nodes[j].data)
//当前字符在前面已被记入结点
{
t->nodes[j].weight++; //对应的权值+1
flag++;
break;
}
}
if (flag == 0) //当前字符在前面未出现过则插入新结点
{
t->total++;
t->nodes.push_back({str[i], 1, 0, 0, 0}); //加入新的结点
}
}
//插入n-1个空结点为后续建树使用
for (int i = 0; i < t->total - 1; i++)
{
t->nodes.push_back({' ', 0, 0, 0, 0});
}
BuildTree(); //建树
}
//计算树的高度
int DeepTree(int root)
{
if (!root) return 0; //递归出口
else
{
int deep1 = DeepTree(t->nodes[root].leftchild); //左子树高度
int deep2 = DeepTree(t->nodes[root].rightchild); //右子树高度
return max(deep1, deep2) + 1; //当前结点高度
}
}
//计算横坐标
//将哈夫曼树想象成满哈夫曼树进行计算
int CalculateX(int deep, int number, map<int, int> &temp) {
if (temp.find(number) != temp.end())
{
return temp[number];
}
else if (deep == Deep) //当前结点为叶子结点
{
int i = number - pow(2, Deep - 1) + 1;
int x = 2 * r + (2 * r + space) * (i - 1);
temp[number] = x;
return x;
}
else //当前结点不是叶子结点
{
int x1 = CalculateX(deep + 1, number * 2, temp);
//左结点的横坐标
int x2 = CalculateX(deep + 1, number * 2 + 1, temp);
//右结点的横坐标
temp[number * 2] = x1;
temp[number * 2 + 1] = x2;
return (x1 + x2) / 2; //当前结点的横坐标
}
}
//计算纵坐标
int CalculateY(int deep, int number)
{
return 3 * deep * r - r;
}
//用先序遍历遍历哈夫曼树,记录X、Y坐标,并将其保存在map中
void PreOrderTraverse(int number, int index, int deep, map<int, point> &points, map<int, int> &temp)
{
if (index != 0)
{
points[number] = {0, 0}; //申请一个新的存储空间
points[number].x = CalculateX(deep, number, temp);
//记录当前结点的x坐标
points[number].y = CalculateY(deep, number);
//记录当前结点的y坐标
if (t->nodes[index].leftchild != 0)
PreOrderTraverse(number * 2, t->nodes[index].leftchild, deep + 1, points, temp); //遍历左子树
if (t->nodes[index].rightchild != 0)
PreOrderTraverse(number * 2 + 1, t->nodes[index].rightchild, deep + 1, points, temp); //遍历右子树
}
}
//利用先序遍历画线
void Drawline(QPainter &p, QPixmap &pic, map<int, point> &points, int number, int index)
{
if (index != 0)
{
if (t->nodes[index].leftchild != 0)
{
int x1 = points[number].x; //当前结点的x坐标
int x2 = points[number * 2].x; //当前结点左子树的x坐标
int y1 = points[number].y; //当前结点的y坐标
int y2 = points[number * 2].y; //当前结点左子树的y坐标
p.drawLine(x1, y1, x2, y2); //连接当前结点和其左子树
Drawline(p, pic, points, number * 2, t->nodes[index].leftchild);
}
if (t->nodes[index].rightchild != 0)
{
int x1 = points[number].x; //当前结点的x坐标
int x2 = points[number * 2 + 1].x;
//当前结点右子树的x坐标
int y1 = points[number].y; //当前结点的y坐标
int y2 = points[number * 2 + 1].y;
//当前结点右子树的y坐标
p.drawLine(x1, y1, x2, y2);
//连接当前结点和其右子树
Drawline(p, pic, points, number * 2 + 1, t->nodes[index].rightchild);
}
}
}
//利用先序遍历画圆
void DrawCircle(QPainter &p, QPixmap &pic, map<int, point> &points, int number, int index)
{
if (index != 0)
{
int x = points[number].x; //当前结点的x坐标
int y = points[number].y; //当前结点的y坐标
p.drawEllipse(x - r, y - r, 2 * r, 2 * r);
//当前结点为叶子结点
if (t->nodes[index].leftchild == 0 && t->nodes[index].rightchild == 0)
{
p.drawText(QRect(x - r, y - r, 2 * r, 2 * r),
Qt::AlignCenter,
QString(t->nodes[index].data));
//输出权值对应的字符
}
if (t->nodes[index].leftchild != 0)
DrawCircle(p, pic, points, number * 2, t->nodes[index].leftchild);
if (t->nodes[index].rightchild != 0)
DrawCircle(p, pic, points, number * 2 + 1, t->nodes[index].rightchild);
}
}
//画图
QPixmap DrawTree() {
map<int, point> points;
map<int, int> temp;
Deep = DeepTree(t->total * 2 - 1);
PreOrderTraverse(1, t->total * 2 - 1, 1, points, temp);
auto width = (2 * r + space) * (pow(2, Deep - 1) - 1) + 4 * r; //哈夫曼树图像的总宽度
auto height = 3 * r * Deep + r; //哈夫曼树图像的总高度
auto picture = QPixmap(width, height); //新建空图片
picture.fill(Qt::white); //绘制背景
QPainter p;
p.begin(&picture); //设置绘图设备
p.setRenderHint(QPainter::Antialiasing, true); //开启抗锯齿
p.setPen(QPen(Qt::black, 2, Qt::SolidLine, Qt::RoundCap)); //设置画笔颜色和宽度
p.setBrush(Qt::white); //设置画刷颜色
QFont font;
font.setFamily("Microsoft YaHei"); //设置字体
font.setPointSize(static_cast<int>(r * 0.8)); //设置字号
p.setFont(font);
Drawline(p, picture, points, 1, t->total * 2 - 1);
//利用先序遍历画线
DrawCircle(p, picture, points, 1, t->total * 2 - 1);
//利用先序遍历画圆
return picture;
}
//编码
std::string Encoding(std::string s)
{
string result;
for (int i = 0; i < s.size(); i++)
{
int j;
//查找当前字符在记录中对应的下标
for (j = 1; j <= t->total; j++)
{
if (s[i] == t->nodes[j].data) break;
}
string code;
//从叶子结点到根节点的追溯结果
while (t->nodes[j].parent != 0)
//从某个叶子结点开始向上找到到达根节点的路径
{
if (t->nodes[t->nodes[j].parent].leftchild == j) //当前结点为自身双亲结点的左孩子
{
code += "0";
}
else if (t->nodes[t->nodes[j].parent].rightchild == j) //当前结点为自身双亲结点的右孩子
{
code += "1";
}
j = t->nodes[j].parent; //指向当前节点的双亲结点位置
}
reverse(code.begin(), code.end()); //最终编码为追溯结果的反转
result += code;
}
return result;
}
//译码
std::string Decoding(std::string s)
{
int now = t->total * 2 - 1; //当前指针位置(初始从根节点开始)
string result;
for (int i = 0; i < s.size(); i++)
{
if (s[i] == '0') now = t->nodes[now].leftchild;
//左孩子
else now = t->nodes[now].rightchild; //右孩子
//指针到达叶子结点
if (t->nodes[now].rightchild == 0 && t->nodes[now].leftchild == 0)
{
result += t->nodes[now].data;
now = t->total * 2 - 1; //初始化指针位置,继续从根节点开始
}
}
return result;
}
5.3 主函数
#include <QApplication>
#include <QPushButton>
#include "mainwindow.h"
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MainWindow w;
w.show();
return QApplication::exec();
}
5.4 主窗口类
#ifndef JIEMIAN_MAINWINDOW_H
#define JIEMIAN_MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr); //初始化类
~MainWindow() override;
void encode1(); //编码(默认情况)
void decode1(); //译码(默认情况)
void Draw1(); //绘制哈夫曼树(默认情况)
void inputOrigin1(); //导入原文(默认情况)
void inputCode1(); //输出编码文件(默认情况)
void encode2(); //编码(自定义)
void decode2(); //译码(自定义)
void Draw2(); //绘制哈夫曼树(自定义)
void inputOrigin2(); //导入原文(自定义)
void inputCode2(); //输出编码文件(自定义)
private:
Ui::MainWindow *ui;
};
#endif //JIEMIAN_MAINWINDOW_H
5.5 窗口函数
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "HuffmanCoder.h"
#include "showresult.h"
#include "treedraw.h"
#include "inputweight.h"
#include <memory>
#include <QFileDialog>
#include <fstream>
#include <sstream>
using namespace std;
//构造函数
MainWindow::MainWindow(QWidget *parent):
QMainWindow(parent), ui(new Ui::MainWindow){
ui->setupUi(this);
//初始化默认哈夫曼树
Init();
InitTree();
//当用户选择"默认情况"选项卡时,将哈夫曼树重置为默认值
connect(ui->choose, &QTabWidget::currentChanged, this, [=](int index)
{
if (index == 0)
{
Init();
InitTree();
}
});
//"默认情况"选项卡信号槽
connect(ui->printencode1, &QPushButton::clicked, this, &MainWindow::encode1);
connect(ui->printdecode1, &QPushButton::clicked, this, &MainWindow::decode1);
connect(ui->draw1, &QPushButton::clicked, this, &MainWindow::Draw1);
connect(ui->inputencode1, &QPushButton::clicked, this, &MainWindow::inputOrigin1);
connect(ui->inputdecode1, &QPushButton::clicked, this, &MainWindow::inputCode1);
//"自定义"选项卡信号槽
connect(ui->printencode2, &QPushButton::clicked, this, &MainWindow::encode2);
connect(ui->printdecode2, &QPushButton::clicked, this, &MainWindow::decode2);
connect(ui->draw2, &QPushButton::clicked, this, &MainWindow::Draw2);
connect(ui->inputencode2, &QPushButton::clicked, this, &MainWindow::inputOrigin2);
connect(ui->inputdecode2, &QPushButton::clicked, this, &MainWindow::inputCode2);
}
//析构函数
MainWindow::~MainWindow()
{
delete ui;
}
//编码(默认情况)
void MainWindow::encode1()
{
Init(); //初始化
QString origin = ui->scanencode1->toPlainText(); //读取输入内容
InitTree(); //默认情况下的初始化
string result = Encoding(origin.toStdString());
if (ui->saveencode1->isChecked()) //获取输出编码文件的路径
{
QString fileName = "";
fileName = QFileDialog::getSaveFileName(this,QString("请选择保存路径"),
QString("./"),QString("text(*.txt)"));
if (fileName != "")
{
//创建文件
ofstream huffmanCode;
huffmanCode.open(fileName.toStdString(), ios::out | ios::trunc);
huffmanCode << result; //将编码文件存入文件
huffmanCode.close(); //关闭文件
}
}
auto s = new ShowResult(); //创建弹出的新窗口
s->show(); //显示窗口
s->setText(QString::fromStdString(result));
}
//译码(默认情况)
void MainWindow::decode1()
{
QString huffmanCode = ui->scandecode1->toPlainText(); //读取输入内容
string result = Decoding(huffmanCode.toStdString());
if (ui->savedecode1->isChecked())
{
QString fileName = "";
fileName = QFileDialog::getSaveFileName(this,QString("请选择保存路径"),
QString("./"),QString("text(*.txt)"));
if (fileName != "")
{
//创建文件
ofstream origin;
origin.open(fileName.toStdString(), ios::out | ios::trunc);
origin << result; //将译码文件存入文件
origin.close(); //关闭文件
}
}
auto s = new ShowResult(); //创建弹出的新窗口
s->setWindowTitle("译码结果");
s->show(); //显示窗口
s->setText(QString::fromStdString(result));
}
//绘制哈夫曼树(默认情况)
void MainWindow::Draw1()
{
auto w = new TreeDraw;
w->show();
auto pic = DrawTree();
w->SetPicture(pic);
}
//导入原文(默认情况)
void MainWindow::inputOrigin1()
{
QString fileName = "";
fileName = QFileDialog::getOpenFileName(this,QString("请选择编码文件"),
QString("./"),QString("text(*.txt)"));
if (fileName != "")
{
ifstream file(fileName.toStdString());
stringstream ss;
ss << file.rdbuf();
file.close();
string text = ss.str();
ui->scanencode1->setPlainText(QString::fromStdString(text));
}
}
//输出编码文件(默认情况)
void MainWindow::inputCode1()
{
QString fileName = "";
fileName = QFileDialog::getOpenFileName(this,QString("请选择编码文件"),
QString("./"),QString("text(*.txt)"));
if (fileName != "")
{
ifstream file(fileName.toStdString());
stringstream ss;
ss << file.rdbuf();
file.close();
string text = ss.str();
ui->scandecode1->setPlainText(QString::fromStdString(text));
}
}
//编码(自定义)
void MainWindow::encode2()
{
Init();
string result;
QString origin;
if (ui->inputWeightByHand2->isChecked()) //选中手动输入权值按钮
{
auto i = new inputWeight; //新建一个输入窗口
connect(i, &inputWeight::sendData, this, [&](const QString &str)
{
istringstream s(str.toStdString()); //字符串转输入流
int n, weight; //定义输入个数、对应权值
char elem; //定义输入字符
s >> n;
for (int i = 0; i < n; i++)
{
s >> elem;
s >> weight;
origin += QString(weight, elem);
}
InitTree(origin.toStdString());
});
i->exec();
}
origin = ui->scanencode2->toPlainText(); //读取输入内容
InitTree(origin.toStdString()); //输入一段文本情况的初始化
result = Encoding(origin.toStdString());
if (ui->saveencode2->isChecked())
{
QString fileName = "";
fileName = QFileDialog::getSaveFileName(this,QString("请选择保存路径"),
QString("./"),QString("text(*.txt)"));
if (fileName != "")
{
//创建文件
ofstream huffmanCode;
huffmanCode.open(fileName.toStdString(), ios::out | ios::trunc);
huffmanCode << result; //将编码文件存入文件
huffmanCode.close(); //关闭文件
}
}
auto s = new ShowResult(); //创建弹出的新窗口
s->show(); //显示窗口
s->setText(QString::fromStdString(result));
}
//译码(自定义)
void MainWindow::decode2()
{
QString huffmanCode = ui->scandecode2->toPlainText(); //读取输入内容
string result = Decoding(huffmanCode.toStdString());
if (ui->savedecode2->isChecked())
{
QString fileName = "";
fileName = QFileDialog::getSaveFileName(this,QString("请选择保存路径"),
QString("./"),QString("text(*.txt)"));
if (fileName != "")
{
//创建文件
ofstream origin;
origin.open(fileName.toStdString(), ios::out | ios::trunc);
origin << result; //将译码文件存入文件
origin.close(); //关闭文件
}
}
auto s = new ShowResult(); //创建弹出的新窗口
s->setWindowTitle("译码结果");
s->show(); //显示窗口
s->setText(QString::fromStdString(result));
}
//绘制哈夫曼树(自定义)
void MainWindow::Draw2()
{
auto w = new TreeDraw;
w->show();
auto pic = DrawTree();
w->SetPicture(pic);
}
//导入原文(自定义)
void MainWindow::inputOrigin2()
{
QString fileName = "";
fileName = QFileDialog::getOpenFileName(this,
QString("请选择编码文件"),
QString("./"),
QString("text(*.txt)"));
if (fileName != "")
{
ifstream file(fileName.toStdString());
stringstream ss;
ss << file.rdbuf();
file.close();
string text = ss.str();
ui->scanencode2->setPlainText(QString::fromStdString(text));
}
}
//输出编码文件(自定义)
void MainWindow::inputCode2()
{
QString fileName = "";
fileName = QFileDialog::getOpenFileName(this,
QString("请选择编码文件"),
QString("./"),
QString("text(*.txt)"));
if (fileName != "")
{
ifstream file(fileName.toStdString());
stringstream ss;
ss << file.rdbuf();
file.close();
string text = ss.str();
ui->scandecode2->setPlainText(QString::fromStdString(text));
}
}
六、调试分析
6.1 运行结果
6.1.1 默认模块检验
导入原文:
默认情况下的编码:
输出编码文件:
在文件中打开检验:
导入译码文件:
默认情况下的译码:
输出译码文件:
打开文件检验:
打印哈夫曼树:
保存哈夫曼树:
6.1.2 自定义模块检验
自定义权值输入:
编码结果:
输出编码文件:
打开文件检验:
译码、输出译码文件并检验:
绘制哈夫曼树:
保存结果:
6.2 时间复杂度分析
建树:O(n^2)
编码:最好O(nlogm) 最差O(n*m)
(n:需编码文本的长度 m:字符的种类数)
译码:O(n) (n:需译码文本的长度)
打印哈夫曼树:
① 计算x坐标:最好O(m) 最差O(2^m)
② 计算y坐标:O(m)
③ 画线:O(m)
④ 画圆:O(m)
综上:最好O(m) 最差O(2^m)
6.3 改进设想
在建树时,也利用堆查找两个最小的权值,故建树时的时间复杂度可降为O(nlogn);在哈夫曼树时,如果能推导出结点横坐标的变化规律,就可将时间复杂度降为稳定O(n)。
七、实验心得与体会(总结)
7.1 设计过程的收获
首先,在整个程序的设计过程中,锻炼了我的编码能力,培养了我调试代码的能力,加深了我对相关知识点的掌握。从起初只打算完成应有的要求,到后来尝试着去做附加的要求;从程序一堆运行错误,到现在可以顺利弹出界面,成功运行,我学习收获了很多。对于每一个结构体为什么这么定义,每一个函数为什么存在,都明确地知道了原委。
同时,程序的代码量较为巨大,有时候写着写着就会恍惚,这极大地锻炼了我的整体思路布局能力。为了使编码更高效,调试更方便,我将代码按照不同的功能划分为不同类,写入不同的源文件里。
其次,在编写打印哈夫曼树代码的过程中,我遇到了不少困难,也尝试了不少方法,最终确定了以满二叉树计算坐标进行构造的方式。虽然它最终的实现并不够符合我的预期,但也为我拓宽了新思路,并且对哈夫曼树有了更深的理解。在绘制结点圆和两结点连线时,我通过查阅资料学习了QPainter类的使用,掌握了新的知识。
最重要的是,本次课设让我意识到,程序的设计要充分考虑使用者的使用感受。在一开始准备做交互界面时,我询问了学姐,她向我推荐了Qt框架。后续通过自己在网上查看Qt-GUI的相关资料,模仿网上其他的交互界面设计样式,以及询问学姐,最终花了很长时间才完成。
7.2 遇到问题及解决问题过程的思考
(1) 问题:不能正确打印哈夫曼树,出现覆盖或输不出内容的现象。
思考:起初固定结点圆半径、两结点间距离,从根节点开始,从上往下绘制哈夫曼树。但这一思想没有考虑树太大会放置不下的情况。后来,打算对结点圆半径以及两结点间距离进行缩放,若出现防止不下的情况,减小它们的数值。但这样,若遇到极大的树运算量巨大。在思考了多种方法后,采用了先假定为一棵满二叉树,再进行计算坐标继而绘制的方法。虽然最终的实现效果在一些情况下,绘制的哈夫曼树不太美观,但最终也算是运行成功。
(2) 问题:交互界面中弹出的新窗口上的代码运行失效。
思考:出现问题的原因在于没有考虑到主界面和新弹出的窗口界面中一些函数的全局与私有的关系。主界面上的申请,对于新窗口来说是无效的。需要定义新的私有函数来完成代码运行。
7.3 程序调试能力的思考
由于代码量太大,我经常完成一个要求或者写完几个函数,就去运行一下代码,便于及时地找出存在的错误。
代码中经常会出现一些语法错误或者细微的小错误。根据调试代码时报错的提示,去修改语法错误;通过定位一些可能导致错误发生的位置,打截断点进行debug模式调式,并观察每一步中的输入参数数据以及返回值数据是否正确,来判断程序错误发生的实际位置。
代码调试能力,是一种需要量化积累、锻炼的能力。在每一次的程序设计中慢慢提升,不能操之过急。
7.4 对数据结构这门课程的思考
在原来C语言的基础上,数据结构在要求答案正确的同时,更关注过程。程序的复杂程度、程序的可视化、用户的交互情况,是更高水平、高质量的追求。
并且,书中类C语言更多的是提供一个思路,真正的编码运行还需要我们自己动手去实现。课程上,我们对于逻辑结构和存储结构进行了学习;实验中,我们对于所学知识进行了实际运用。能让平时的内容付诸到实际编码中进行加强巩固,有了更深的理解和体会。
同时,数据结构对其他专业课也有所涉猎,是一门计算机打基础的课程。它对于我们后续学习库的调用、面向对象的设计、用户交互等有极大的帮助,更明白其中的原理。
数据结构课程,不仅增长了课本上的知识内容,也培养了我的自学能力与编码能力,锻炼了我的细心与坚持,让我成长和收获了很多。