news 2026/10/18 1:29:25

哈夫曼编译码器完整课设:从建树到编码译码的C语言实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
哈夫曼编译码器完整课设:从建树到编码译码的C语言实现

简介:一份面向数据结构课程设计的哈夫曼编译码器完整报告文档,适合计算机相关专业本科生的课设参考,也适合需要理解哈夫曼树原理与C++实现的读者。报告基于VC++ 6.0环境,系统阐述哈夫曼编码原理、需求分析、总体设计与详细实现,覆盖初始化、编码、译码、打印代码文件、打印哈夫曼树、退出程序六大功能模块。同时给出字符频率统计、最小堆建树、编码生成及译码还原的完整设计思路,并配有函数列表与主要函数说明,可帮助读者快速搭建同类课设框架。压缩包内共1个doc文档,整体约2.76MB,已有810人学习下载。文档包含华北科技学院课程设计说明书的完整目录结构,从需求分析、总体设计到主要函数介绍层层展开,适合对照写作或直接作为课程设计报告模板。

1. 哈夫曼编译码器:一个能跑通的完整课设,不只是一棵二叉树

数据结构课程设计里最常出现的题目之一就是哈夫曼编译码器,标准玩法是读入一批字符和权值,构造最优二叉树,再做编码和译码。这份课程设计报告把整条链路做成了一个带菜单的完整工具:初始化、编码、译码、打印编码、打印哈夫曼树表、退出六个功能全部落地,连文件读写路径都提前规划好了,不是那种只讲原理的代码片段。

它适合作三类人的参考资料:一是正在做哈夫曼课设、想知道完整工程怎么组织的同学,二是复习数据结构、想对照代码理解最优二叉树原理的开发者,三是想拿现成 C 语言小程序做二次开发、研究文件流和菜单架构的熟手。读懂它,哈夫曼树从建树、编码到译码的完整闭环就基本通了。

2. 总体设计:六个功能模块加一张五域静态链表

2.1 先看整体:五个文件把数据流转串起来

这个编译码器不是单机跑一遍、内存里算完就结束的程序,所有中间结果都落盘到 D 盘根目录,一共五个文件,各管一段。报告里对文件职责写得比较清楚,我整理成一张表。

文件路径存放内容产生时机
D:\ToBeTran.txt待编码的源字符串执行编码(E)时写入
D:\Code.txt源字符串编码后的二进制串编码(E)时写入
D:\Decode.txt译码还原出的原始文本译码(D)时写入
D:\HuffmanTree.txt编译码表与哈夫曼树表初始化(I)与打印树表(T)时写入
D:\CodePrint.txt每行 50 个字符的编码副本打印编码(P)时写入

整个数据流可以描述成一条单向管道:用户从终端输入字符串,原样存到 ToBeTran.txt;编码函数读这个文件,逐字符查编译码表,拼出 01 串写回 Code.txt;译码再从 Code.txt 读编码,按哈夫曼树还原成明文到 Decode.txt。HuffmanTree.txt 是中间契约文件,编码和译码都依赖它重建出来编译码表。

这种文件约定有一个好处:每个功能都可以单独调试。比如编码结果不对,不需要重启程序,直接打开 Code.txt 看输出,跟 HuffmanTree.txt 里的编译码表比对就能定位问题。课设阶段用文本文件而不是二进制文件,牺牲一点空间,换来的是肉眼可见的排障体验。

2.2 节点结构:五个域,数组模拟链式

报告里说哈夫曼树采用链式存储,方便修改替换,实际代码实现却是用数组下标模拟双亲和左右孩子指针,相当于静态三叉链表。每个节点五个域:ch 存字符,weight 存权值,parent、lchild、rchild 分别存双亲、左孩子、右孩子的数组下标。结构体定义大概是这样的:

typedef struct { char ch; /* 叶子节点存储的字符 */ unsigned int weight; /* 权值,即使用频度 */ int parent; /* 双亲节点下标,0 表示无双亲 */ int lchild; /* 左孩子下标 */ int rchild; /* 右孩子下标 */ } HTNode, *HuffmanTree; typedef char **HuffmanCode; /* 编译码表,HC[i] 指向第 i 个字符的编码串 */

这里有个容易被忽略的设计细节:数组下标从 1 开始计数,HT[0] 空着不用。因为 parent 字段用 0 表示“还没有被合并”,如果下标也从 0 开始,那就没法区分“下标为 0 的节点”和“没有双亲的节点”。叶子节点有 n 个,合并过程中每次新增一个内部节点,最终节点总数是 2n - 1,所以树数组申请 (2n) 个元素就够了,下标范围 1 到 2n-1。

编译码表用二级指针实现,本质是一个长度为 n+1 的字符指针数组。每个叶子节点编码时从叶子向上回溯,走到根节点为止,路径上左边记 0、右边记 1,最后把逆序结果复制到 HC[i] 里。这个二级指针结构在后续编码和译码时非常顺手:编码时按下标取 HC[j] 拼接,译码时拿 HC[j] 跟当前编码串前缀做匹配。

2.3 菜单组织:死循环加双写判断,兼容大小写

主函数没用什么黑科技,一个 while 死循环包住菜单打印和功能分支,用户每执行完一个功能自动跳回主菜单,只有输入 q 或 Q 才退出。值得学习的是它兼容了大小写,i/I、e/E、d/D、p/P、t/T、q/Q 都能识别,避免用户开着大写锁直接翻车。

int main() { char choice; while (1) { HCVision(); /* 打印操作菜单 */ choice = getchar(); getchar(); /* 吸收回车符 */ switch (choice) { case 'I': case 'i': Init(); break; case 'E': case 'e': Encoding(); break; case 'D': case 'd': Decoding(); break; case 'P': case 'p': CodePrint(); break; case 'T': case 't': HuffmanTreePrint(); break; case 'Q': case 'q': return 0; default: printf("输入错误,请重新输入\n"); } } return 0; }

菜单处理除了主循环,还有一个容易被忽略的点:getchar() 读完功能字符后必须再调用一次 getchar() 把缓冲区的回车吃掉,否则第 4 章的初始化函数一进来就撞上残留的换行符,导致字符集录入错位。如果不用 C 的 getchar 而改用 scanf 读功能号,同样要处理这个换行残留问题,常见做法是 scanf("%c%*c") 用星号抑制符吃掉回车。

从主循环的分支设计能看出报告作者对用户操作顺序的考虑:每个功能不假设前置状态,编码函数如果没有编译码表,会主动报错并给出处理选项而不是直接崩溃。这种防御式写法从数据结构课设角度看已经合格,后面第 5 章会专门展开这些坑。

3. 核心模块拆解:从建树到译码的完整链路

3.1 初始化:两轮扫描选最小,n-1 次合并建树

初始化是整个系统的地基,主要做三件事:录入字符和权值、调用建树函数构造哈夫曼树、把编译码表写入 HuffmanTree.txt。建树前要先实现一个 Select 函数,它的任务是从当前所有还没有双亲的节点里选出权值最小的两个。我见过不少写法是单循环一次扫出两个最小值,但那种写法在权值相等时容易出 bug,这里用两轮扫描更稳。

void Select(HuffmanTree HT, int n, int *s1, int *s2) { int i; unsigned int min1 = 0xFFFFFFFF, min2 = 0xFFFFFFFF; *s1 = 0; *s2 = 0; /* 第一轮找最小 */ for (i = 1; i <= n; i++) { if (HT[i].parent == 0 && HT[i].weight < min1) { min1 = HT[i].weight; *s1 = i; } } /* 第二轮找次小,排除 s1 本身 */ for (i = 1; i <= n; i++) { if (i != *s1 && HT[i].parent == 0 && HT[i].weight < min2) { min2 = HT[i].weight; *s2 = i; } } }

Select 函数的关键逻辑在两个循环的排除条件上。第一轮找最小值,把下标存在 s1;第二轮用 i != *s1 确保不会把同一个节点选两次,只用 HT[i].parent == 0 筛选还未被合并的节点。这样设计后,即使存在多个相同权值的叶子,也能稳定选出两个不同下标的节点。参数 n 不是总节点数,而是当前待选区域的边界——因为建树是动态的,每合并一次有效节点数就加一,新生成的内部节点也参与下一轮选择。

有了 Select,建树就顺理成章了:

void HuffmanCoding(HuffmanTree *HT, HuffmanCode *HC, int *w, int n) { int m = 2 * n - 1; int i, s1, s2, start, c, f; char *cd; *HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode)); for (i = 1; i <= n; i++) { (*HT)[i].ch = 0; (*HT)[i].weight = w[i]; (*HT)[i].parent = 0; (*HT)[i].lchild = 0; (*HT)[i].rchild = 0; } for (i = n + 1; i <= m; i++) { (*HT)[i].ch = 0; (*HT)[i].weight = 0; (*HT)[i].parent = 0; (*HT)[i].lchild = 0; (*HT)[i].rchild = 0; } /* 反复合并两个最小节点,生成 n-1 个内部节点 */ for (i = n + 1; i <= m; i++) { Select(*HT, i - 1, &s1, &s2); (*HT)[s1].parent = i; (*HT)[s2].parent = i; (*HT)[i].lchild = s1; (*HT)[i].rchild = s2; (*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight; } /* 从每个叶子回溯到根,倒序拼出编码 */ *HC = (HuffmanCode)malloc((n + 1) * sizeof(char *)); cd = (char *)malloc(n * sizeof(char)); cd[n - 1] = '\0'; for (i = 1; i <= n; i++) { start = n - 1; for (c = i, f = (*HT)[i].parent; f != 0; c = f, f = (*HT)[f].parent) { if ((*HT)[f].lchild == c) cd[--start] = '0'; else cd[--start] = '1'; } (*HC)[i] = (char *)malloc((n - start) * sizeof(char)); strcpy((*HC)[i], &cd[start]); } free(cd); }

建树部分的核心是第二个循环。i 从 n+1 走到 2n-1,每次调用 Select 找出两个权值最小的节点,把它们的 parent 指向新建的内部节点 i,再把 i 的左右孩子分别指向 s1 和 s2,权值相加。这个过程重复 n-1 次,最终得到一棵带权路径长度最小的二叉树。

编码部分是另一个容易看懵的细节。不从根向下走,而是从叶子向上走:cd 数组从末尾开始向前填充,每向上一层在 start 位置写入 0 或 1,回溯结束后从 start 到末尾就是该字符的完整编码。因为这个方向是从叶子到根,路径上是反的,所以必须倒着填,最后复制时从 start 取到结尾即可。参数 w 是权值数组,下标从 1 开始,与 Init 函数中用户输入的字符顺序一一对应。

3.2 编码:逐字符查表拼 01 串,未知字符给三个出路

编码函数负责把待压缩的源文本变成 01 串。报告里的实现顺序是:先把用户输入的字符串写到 ToBeTran.txt,再读回来逐字符处理。这样即使程序中途崩溃,源字符串也已经在磁盘上留了底,不至于全部丢失。

void Encoding() { FILE *fpIn, *fpOut, *fpCode; char str[1024]; int i, j; fpIn = fopen("D:\\ToBeTran.txt", "w"); printf("请输入需要编码的字符串:\n"); gets(str); fprintf(fpIn, "%s", str); fclose(fpIn); fpIn = fopen("D:\\ToBeTran.txt", "r"); fpCode = fopen("D:\\Code.txt", "w"); while ((i = fgetc(fpIn)) != EOF) { /* 在字符表里找到对应叶子下标 j */ for (j = 1; j <= n; j++) { if (HT[j].ch == i) { fprintf(fpCode, "%s", HC[j]); /* 查表拼编码 */ break; } } if (j > n) { printf("错误:编译码表中不存在字符 %c\n", i); /* 提供三个选项:重新输入 / 返回主菜单 / 退出 */ } } fclose(fpIn); fclose(fpCode); }

编码本身的逻辑不复杂,读一个字符,在 HT 数组的前 n 个叶子节点里线性查 ch 字段,命中就把 HC[j] 拼到 Code.txt 尾部。这里要注意的是可变长编码的优势:出现频率高的字符编码短,比如 e 可能只有两位,而 z 可能有五六位,整体压缩率来自这种长短搭配。

未知字符处理是这段代码最容易出错的地方。如果用户输入的字符串里有大写字母,而初始化时编译码表只录入了小写字母和空格,循环里的 for 会扫完 n 个叶子都找不到匹配。报告的解法是报错后给出三条路:重新输入字符串、返回主菜单、直接退出程序。这个分支看似简单,实际上把程序从“死板报错退出”提升到了“可恢复错误”的级别,课程设计里能做到这一层的不多。

还有一点值得注意:gets 函数在 VC6 里用得很顺手,但它不检查缓冲区边界,输入超过 1023 字符会越界。课设场景下输入句子不会太长,能跑通即可;如果是二次开发拿去做正式工具,建议换成 fgets 并指定长度。

3.3 译码:前缀匹配与顺序表查找的组合

译码是编码的逆过程,核心思路是利用哈夫曼编码的前缀特性——没有任何一个编码是另一个编码的前缀,所以从编码串开头按位匹配,命中一个就输出字符,然后继续匹配剩下的编码。报告里的实现是把 Code.txt 的 01 串整体读入 h 数组,然后外层循环控制消费进度,内层循环遍历编译码表比对。

void Decoding() { FILE *fpCode, *fpDecode; char h[10240]; /* 从 Code.txt 读出的加密编码串 */ int pos = 0; /* 当前消费到的编码位置 */ int i, len; fpCode = fopen("D:\\Code.txt", "r"); fpDecode = fopen("D:\\Decode.txt", "w"); if (fpCode == NULL) { printf("错误:Code.txt 不存在,请先执行编码功能\n"); return; } fscanf(fpCode, "%s", h); fclose(fpCode); while (h[pos] != '\0') { for (i = 1; i <= n; i++) { len = strlen(HC[i]); if (strncmp(h + pos, HC[i], len) == 0) { fputc(HT[i].ch, fpDecode); /* 命中,写入一个字符 */ pos += len; /* 消费掉这段编码 */ break; } } if (i > n) { printf("错误:编码串与编译码表不匹配,译码终止\n"); break; } } fclose(fpDecode); }

译码循环的核心是 strncmp 配合 h + pos 做前缀匹配。内层 for 从编译码表第 1 个字符开始试,拿 HC[i] 的长度去切编码串当前位置的前缀,匹配成功就输出 HT[i].ch 并前进 len 位。由于哈夫曼编码是前缀码,任何合法的编码串在这个循环里都能唯一匹配,不会出现二义性。

这里有个参数细节值得专门说:strncmp 比较的是前 len 个字符,而 len 是当前字符编码的实际长度,不是全表最长长度。这样做避免了拿短编码去切分长编码串导致误匹配的问题。比如 e 的编码是 01,另一个字符的编码是 0101,如果用整串长度去对齐,就永远对不上了。

程序的健壮性体现在两处:如果 Code.txt 不存在,先判断文件指针再操作,避免空指针解引用;如果内层循环跑完都没匹配,说明编码文件已经被改坏,给出提示后终止。这种优雅降级的处理方式,让译码不只是编码的机械逆过程。

3.4 打印模块:50 字符换行和树表输出

打印编码函数 CodePrint 负责把 Code.txt 里的 01 串按每行 50 个字符重新排版,输出到终端的同时写入 CodePrint.txt。代码思路很朴素:打开源文件和目标文件,逐个字符复制,每写满 50 个就输出一个换行符。

void CodePrint() { FILE *fpCode, *fpPrint; char ch; int cnt = 0; fpCode = fopen("D:\\Code.txt", "r"); fpPrint = fopen("D:\\CodePrint.txt", "w"); if (fpCode == NULL) { printf("错误:Code.txt 不存在,请先执行编码\n"); return; } while ((ch = fgetc(fpCode)) != EOF) { fputc(ch, fpPrint); putchar(ch); cnt++; if (cnt == 50) { fputc('\n', fpPrint); putchar('\n'); cnt = 0; } } fclose(fpCode); fclose(fpPrint); }

这段代码的逻辑很简单,真正要留意的是它只负责排版,不负责改内容。它从 Code.txt 读什么就写什么,唯一增加的是换行符,译码函数如果要重新使用这段编码,直接读 Code.txt 而不是 CodePrint.txt,否则会把分布在各行行尾的换行符也当成编码的一部分。

打印哈夫曼树表函数 HuffmanTreePrint 则完全是另一个用途,它把整棵树的节点信息以表格形式输出,包含节点编号、字符、权值、双亲、左孩子、右孩子六列。初始化时只是把编译码表写入 HuffmanTree.txt,这里的 T 功能才把完整的树结构也补进去,方便人工检查建树过程是否正确。

4. 编译运行与测试:VC++ 6.0 里从建工程到全流程验证

4.1 工程搭建:空控制台工程和源码粘贴

报告标注的开发环境是 VC++ 6.0,这个版本跑哈夫曼课设足够。新建工程的步骤是标准操作:打开 VC6,选 File 菜单 New,Projects 标签页里选 Win32 Console Application,工程名随便起一个,比如 Huffman,路径建议别带中文和空格,然后一直 Next 到 Finish。建完后在 FileView 里找到 Source Files,右键 Add Files to Folder,新建一个 C++ Source File 把附录源码整个粘进去。

编译时最常遇到的是 C++ 和 C 的语法差异。VC6 会把 .cpp 文件当 C++ 编译,malloc 返回值需要显式强转,报告源码里如果写的是 C 风格 malloc 没强转,会报 cannot convert 之类的错误。遇到这种问题,把每个 malloc 前面加上对应的类型强转即可,比如 (HuffmanTree)malloc(...)。另外,如果源码里用了 gets 函数,VC6 会给出 C4996 警告,这只是安全警告不影响链接,课程设计里可以忽略。

还有一类问题是编译选项导致的。工程默认可能是 Unicode 字符集,而源文件里全是窄字符操作,多字节和 Unicode 混用会出链接错误。在 Project Settings 里把字符集改成 Not Set 或多字节字符集,基本就能干净编译通过。这些都处理完后 F7 编译,0 error 0 warning 才算真正进入调试阶段。

4.2 全流程验证:六步闭环跑通

编译通过只是起点,跑通整个闭环才是验证哈夫曼树是否正确的方式。我的建议是准备一组小规模测试数据,比如 8 个字符:空格对应权值 7、a 权值 3、e 权值 4、i 权值 2、o 权值 2、u 权值 1、y 权值 1、句点权值 1。初始化时依次录入,注意字符间用空格隔开,权值用数字输入。

操作顺序是:先按 I 初始化,终端录入字符集后,打开 D:\HuffmanTree.txt 确认编译码表生成正确;再按 E 编码,输入完整句子 You are the only one I am still in love with. 注意这个句子包含大写 Y 和 I 以及句点,如果编译码表没有这些字符,编码函数会立刻报错。要么把编译码表补全,要么测试时把句子改成全小写加句点的版本。

编码完成后立刻按 D 译码,再打开 D:\Decode.txt 和原始的 ToBeTran.txt 做对比,两端内容应该逐字符一致。接下来按 P 打印编码,检查终端输出是否按 50 字符换行。最后按 T 打印哈夫曼树表,对照 HuffmanTree.txt 确认每个节点的双亲和孩子下标关系正确。全部走通后按 Q 退出。

这里最容易翻车的就是字符集覆盖不完整,测试句子里的字符必须在编译码表里全部存在。我建议初始化时把测试句子里出现的所有字符都纳入字符集,包括大写字母、空格和标点,宁可多录几个不常用的字符,也不要让编码函数走到未知字符分支。

4.3 异常分支验证:不按顺序操作时的报错行为

课设验收时老师通常会故意不按顺序操作,比如上来就按 E 编码,或者不初始化直接按 D 译码。报告对这种情况有预设处理:未初始化就编码会提示编译码表可能为空,并提供重新输入、返回主菜单、退出三个选项;未初始化就译码或打印编码,文件不存在时直接提示错误并返回主菜单。

测试时要专门验证这几条异常路径,这比正常路径更能体现程序的完整度。打开程序直接输入 e,观察是否出现友好提示而不是程序崩溃;确认 Code.txt 不存在时输入 d,观察文件指针判断是否生效;没有初始化直接输入 t,看会不会把空的树表打印出来。这些测试记录最后要写进报告的调试与测试章节,作为系统健壮性的证据。

需要注意的一个隐藏陷阱:如果我前面建议用 fscanf 读编码串,那么汉字或者特殊符号会被 fscanf 截断,因为它以空白字符作为分隔符。若测试句子包含中文,编码函数录入到编译码表没问题,但译码时 fscanf 读不回完整串。课设场景下还是老老实实用全英文和标点测试,等改造阶段再考虑二进制读文件。

5. 避坑记录:哈夫曼课设里最常见的几个翻车点

5.1 文件路径与读写侧

第一个翻车点集中在 fopen 上。很多同学直接照抄 D:\ 开头的路径,结果自己电脑 D 盘根目录没有写权限,或者目录根本不存在,fopen 返回 NULL,后续 fputc 全部打在空指针上,程序直接闪退。解决方法是每次 fopen 后立即判断返回值,为空就打印错误并返回主菜单,同时把路径常量抽出来统一改成相对路径,比如 "Code.txt" 直接放工程目录下,这样交报告跑演示时不容易被环境差异绊倒。

第二个坑是文件内容格式的隐式依赖。译码函数用 fscanf("%s", h) 读编码串,如果 Code.txt 里既有编码又有换行和空格,fscanf 会在首个空白处停住,导致只读进前半段编码。编码函数写 Code.txt 时如果用了 fprintf 且格式串里带了 \n,就要确认译码时能完整读回。课设阶段建议统一约定:Code.txt 里只有纯 01 串,不插入任何不可见字符。

5.2 输入输出侧

第三个翻车点几乎每个写 C 课设的同学都踩过:scanf 和 getchar 混用导致字符集录入错位。用户输入完字符个数 n 后按回车,缓冲区的换行符如果不处理,下一个读取字符的语句会直接吃到 '\n',第一个录入的字符就变成了一个看不见的换行。我一般在 scanf("%d", &n) 后面紧跟一句 getchar() 或者 scanf("%*c"),把所有输入缓冲区的回车清掉再继续读字符,这个习惯从那以后一直保留。

第四个坑是字符集与实际输入不匹配。编码函数在编译码表里查不到字符时的报错分支设计得没问题,但不少同学测试时输入了编译码表外的字符,程序提示错误后又不知道错误是来自输入字符串还是编译码表。报告里给出的判断思路很好用:先检查输入字符串是否越界,再检查编译码表是否为空或漏录字符。我在调试时会故意输入一个编译码表确定没有的字符,比如 @,验证报错分支真的能触发。

5.3 状态流转与流程侧

第五个翻车点是不按顺序操作导致的连锁错误。程序虽然做了防御处理,但有些状态是隐藏的,比如执行完编码后,编码函数把 HT 和 HC 都留在全局变量里,此时执行打印哈夫曼树没问题;但如果重新初始化了一组新字符,旧的 HT 被覆盖,而 Code.txt 还是上一次编码的结果,这时再译码就会乱套。解决思路是在每次初始化时清空或重建相关文件,并设计一个简单的状态标志位,记录“当前是否已经初始化”,在所有功能入口处检查这个标志。

还有一个容易被忽略的细节:每行 50 个字符的打印规则只影响 CodePrint,不影响译码。有些同学看到 CodePrint.txt 里换行了,就以为 Code.txt 也换行了,译码时把 \n 当成编码的一部分,结果永远匹配不上。记住一个原则:Code.txt 是唯一合法编码源,CodePrint.txt 只是给人看的排版副本,两边内容一致但格式不同。

6. 二次开发:把课程设计升级成可用的压缩小工具

这个编译码器最明显的短板是权值靠手工录入,不是从文本里统计出来的。真正的压缩工具应该先读一遍源文件,统计每个字符出现次数作为权值,自动建树,再编码。改造方向不复杂:把 Init 函数里手工输入字符和权值的逻辑换成扫描 ToBeTran.txt,用数组做频度统计,然后过滤掉出现次数为 0 的字符,剩下的进哈夫曼树。

第二个值得动手的改造是二进制输出。当前编码结果以字符形式存成 01 串,一个比特占一个字节,压缩效果完全没体现出来。把 01 串每 8 位合成一个字节写入文件,体积直接缩小到原来的八分之一,这才是哈夫曼压缩该有的样子:

unsigned char byte = 0; int bitCnt = 0; for (i = 0; code[i]; i++) { byte = (byte << 1) | (code[i] - '0'); bitCnt++; if (bitCnt == 8) { fwrite(&byte, 1, 1, fpBin); byte = 0; bitCnt = 0; } } if (bitCnt > 0) { byte <<= (8 - bitCnt); /* 末尾不足 8 位补零 */ fwrite(&byte, 1, 1, fpBin); }

这段代码把字符串形式的编码按位移进一个字节,满 8 位就落盘一次,最后不足 8 位时向左移位补零。改造完后,译码端要配套处理最后一个字节的补零问题,方案是在二进制文件头部记录源文本长度或者最后一个有效编码的位数。

第三个改造方向是命令行参数化。现在的菜单交互适合演示,但自动化验证时每跑一次都要手动输入好几个字符和权值,效率太低。参照常规压缩工具的接口设计成 hfm -c source.txt 压缩、hfm -d source.hfm 解压,用不到一百行代码就能把 init、encoding、decoding 串成一条命令链。

我自己的习惯是每次改完都强制走一遍“初始化 → 编码 → 译码 → 字节比对”的闭环,确认还原结果和源文件一字不差,并且随机换几组不同长度的文本跑,看有没有边界情况漏掉。哈夫曼这类课设只有亲手把异常分支也测过一遍,才算真正做完而不是只跑通演示路径。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 1:28:59

低显存怎么跑大模型?AirLLM分层推理原理与实操指南

上周我用一台只有 4GB 显存的旧机器&#xff0c;把一个 7B 参数规模的开源语言模型跑了起来。一开始队友说这不现实&#xff0c;因为单算模型权重&#xff0c;7B 半精度就要占大概 14GB 显存。但实际跑通之后&#xff0c;我发现这件事确实有路可走&#xff0c;而这条路就是我想…

作者头像 李华
网站建设 2026/10/18 1:28:49

30 分钟跑通你的第一个 Hook:LSPosed 模块开发实战

30 分钟跑通你的第一个 Hook&#xff1a;LSPosed 模块开发实战 【免费下载链接】LSPosed_mod My changes to LSPosed 项目地址: https://gitcode.com/GitHub_Trending/ls/LSPosed_mod LSPosed 模块开发的核心&#xff0c;是在目标应用运行时拦截它的方法调用&#xff0c…

作者头像 李华
网站建设 2026/10/18 1:28:41

数据录入辅助工具v3.8:OCR+规则校验+留痕审计闭环实践

简介&#xff1a;打工助手-数据录入辅助工具v3.8是一款面向办公人员、兼职数据员及RPA初学者的轻量级自动化录入支持工具&#xff0c;专为提升重复性表单填写、跨平台数据搬运与结构化信息采集效率而设计。资源包共67个文件&#xff0c;涵盖27个JavaScript核心逻辑脚本&#xf…

作者头像 李华
网站建设 2026/10/18 1:26:51

Status Deck硬件调试全链路指南:从开箱验收到产品级可靠性

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华