目录
1.路径:从一个结点到另一个结点之间的分支序列
2.结点之间的路径长度:从一个结点到另一个结点之间的分支数目
3.树的路径长度(用PL表示):从树的根到每一个结点的路径长度之和 → 深度之和
PL = 0+1+1+2+2+2+2+3=13
WPL=7*2+5*2+2*2+4*2=36
以数据{5,6,7,8,15}为例
注意观察特征:原数据5\6\7\8\15均只会出现在叶子结点(自下而上构建新的节点)
①文件 :
有损压缩 无损压缩(Huffman为无损压缩)
png 有损压缩
9个像素合为一个像素
②例子:
"abbbbcccccccccddddddddddddddfffffffffffffffffffffff"
'a' 1 'b' 4 'c' 9 'd' 14 'f' 23
原来:54字节---->Huffman编码后:24*1 + 14*2 + 9*3 + 4*4 + 1*4= 13字节不到
③压缩过程:
1. 分析待压缩文件
(获取文件中出现过的字节 和 每个出现过的字节的出现次数 组合成字典(索引))
2. 根据字典组建哈夫曼树,获取每个字节对应的哈夫曼编码
3. 创建压缩后文件,把字典写入压缩后文件中
然后 一个一个字节去读待压缩文件的内容 把它转换成对应的编码
把编码写入压缩后文件中
④解压过程:
1. 从压缩后文件中读取字典
2. 根据字典生成哈夫曼树
3. 循环从压缩后文件中读取字节
3.1 一个个比特位获取 并 根据比特位的值去哈夫曼树中找
3.2 找到一个叶子节点就解压出一个字节
3.3 都没有孩子了还往右 说明结束了 或者 读取完毕 结束
1.函数声明以及main函数.
#include #include #include #define TESTHUFFMANCODE 0 #define TESTWRITECHAR 0 //哈夫曼编码长度 注意 文件中字符种类越多 对应哈夫曼编码长度越长 #define HFM_CODE_LEN 10 struct ZiFu { unsigned char zf; //字符 unsigned int count; //字符出现次数 char code[HFM_CODE_LEN]; //对应哈夫曼编码 int idx; //字符数组下标 }; //字典 struct allZifu { int zf_count; //出现过的字符的个数 ZiFu zf_arr[256]; //存储每个字符的信息 }; //哈夫曼树节点类型 struct treeNode { ZiFu data; treeNode* pLeft; treeNode* pRight; treeNode* pParent; }; //创建 哈夫曼树节点 treeNode* createNode(ZiFu* pZf); //分析文件 得到字典(出现过的字符的个数 字符 字符出现次数) void analysisFile(char* srcFile, allZifu* zd); //创建哈夫曼树并返回 treeNode* createHaffmanTree(allZifu* zd); //根据哈夫曼树 创建哈夫曼编码 并写入字典中 void createHaffmanCode(treeNode* root, allZifu* zd); //判断当前节点是否叶子节点,是返回true 否则返回false bool isLeaf(treeNode* root); //设置哈夫曼编码到字典中 void setHaffmanCode(treeNode* root, char* pCodeStr); //创建压缩文件并把字典和对应字符的编码写入到压缩后文件中 void writeCompressFile(char* src, char* dst, allZifu* zd); //从字典中获取某个字符对应的haffman编码 void getHaffmanCode(char c, char* haffmanCode, allZifu* zd); int main() { char srcFile[256] = "1.txt";//待压缩文件 char dstFile[256] = "2.txt";//压缩后文件 allZifu zd = { 0 };//字典 analysisFile(srcFile, &zd); treeNode* root = NULL; root = createHaffmanTree(&zd); createHaffmanCode(root, &zd); writeCompressFile(srcFile, dstFile, &zd); printf("压缩完毕!\n"); system("pause"); //while (1); return 0; }
2.分析文件 analysisFile
正常统计是否出现过or出现过几次
//分析文件 得到字典(出现过的字符的个数 字符 字符出现次数) void analysisFile(char* srcFile, allZifu* zd) { //1 打开文件 FILE* fp = fopen(srcFile, "rb"); if (NULL == fp) { printf("analysisFile函数中 打开待压缩文件:%s 失败!\n",srcFile); return; } //2 读取文件内容 unsigned char c;//存储读到的字节 int r;//fread 返回值:读取到的字节数 bool is;//标记是否出现过 int i; while (1) {//循环读取 //读到一个字节 读不到 循环结束 r = fread(&c, 1, 1, fp); if (1 != r) break; //判断是否出现过 is = false;//默认没有出现过 for (i = 0; i < zd->zf_count; i++) { if (c == zd->zf_arr[i].zf) { is = true; break; } } if (!is) {//如果没有出现过 zd->zf_count++;//字符个数增加 zd->zf_arr[i].zf = c;//记录字符 } zd->zf_arr[i].count++; } //3 关闭 fclose(fp); }
3.创建结点Node与哈夫曼树 tree
//创建 哈夫曼树节点 treeNode* createNode(ZiFu* pZf) { treeNode* pNew = (treeNode*)malloc(sizeof(treeNode)); if (NULL == pNew) return NULL; memset(pNew, 0, sizeof(treeNode)); memcpy(pNew->data.code, pZf->code, HFM_CODE_LEN); pNew->data.count = pZf->count; pNew->data.zf = pZf->zf; pNew->data.idx = pZf->idx; return pNew; }创建Huffman树的步骤:
①准备一个指针数组,存储所有节点(依次将字典中的字符创建成结点放入arr)
②寻找count-1次minIDX和secIdx,制作父节点数据,双向连接即可。
//创建哈夫曼树并返回 treeNode* createHaffmanTree(allZifu* zd) { //1 准备一个指针数组 存储所有节点 treeNode** pArray = (treeNode**)malloc(sizeof(treeNode*)* (zd->zf_count)); for (int i = 0; i < zd->zf_count; i++) { pArray[i] = createNode(&(zd->zf_arr[i]));//将字典里存放的一个个字符转化成一个个treeNode pArray[i]->data.idx = i;//下标 } //vectorbuff; int minIdx, secMinIdx; int j; treeNode* pTemp = NULL; //循环找最小的和第二小的 循环 zd->zf_count - 1 次(每次删两个,加一个-----共需要n-1次组建,最后一次剩下中间结点data=0) for (int i = 0; i < zd->zf_count - 1; i++) { //找出最小的 j = 0; while (pArray[j] == NULL) j++;//让j是第一个的下标(删除是直接置空or新赋值结点) minIdx = j; for (j = 0; j < zd->zf_count; j++) {//利用短路效应(pArray[j]可能为nullptr) if (pArray[j] &&pArray[j]->data.count < pArray[minIdx]->data.count) minIdx = j; } //找出第二小的 j = 0; while (pArray[j] == NULL || j==minIdx) j++;//避开最小下标(下同) secMinIdx = j; for (j = 0; j < zd->zf_count; j++) { if (pArray[j] && j!=minIdx &&pArray[j]->data.count < pArray[secMinIdx]->data.count) secMinIdx = j; } //组件成树 //1.1制作父节点的数据 ZiFu tempZf = { 0, pArray[minIdx]->data.count + pArray[secMinIdx]->data.count }; tempZf.idx = -1;//保险 pTemp = createNode(&tempZf); //双向设置 pTemp->pLeft = pArray[minIdx]; pTemp->pRight = pArray[secMinIdx]; pArray[minIdx]->pParent = pTemp; pArray[secMinIdx]->pParent = pTemp; //1.2数组中添加(最小的和第二小的 的 父) 删掉(最小的和第二小的) pArray[minIdx] = pTemp; pArray[secMinIdx] = NULL; } return pTemp; }
4.设置哈夫曼编码到字典中&将整个树完成对应的设置&从字典中获取字符c对应的Huffman编码
①设置Huffman编码到字典中,传入的root是叶子结点,所以在此向上追溯,左1右2(不用0是因为初始化默认为0,以示区分)---->最终记得将结果进行翻转
//设置哈夫曼编码到字典中 void setHaffmanCode(treeNode* root, char* pCodeStr) {//args:pCodeStr是传入对应字符的code成员的。(直接赋值) treeNode* pTemp = root; char buff[HFM_CODE_LEN] = { 0 }; int buffIdx = 0; while (pTemp->pParent) {//一路往上 if (pTemp == pTemp->pParent->pLeft) { buff[buffIdx] = 1; } else{ buff[buffIdx] = 2; } buffIdx++; pTemp = pTemp->pParent; } //逆转存储到参数数组中 char temp; for (int i = 0; i < buffIdx / 2; i++) { temp = buff[i]; buff[i] = buff[buffIdx - i - 1]; buff[buffIdx - i - 1] = temp; } //赋值 strcpy(pCodeStr, buff); }②将整个Huffman的原始元素进行相应的赋值,原始元素是会是在叶子节点上!! --->递归搜索判断是否符合,
//根据哈夫曼树 创建哈夫曼编码 并写入字典中 void createHaffmanCode(treeNode* root, allZifu* zd) { int idx; if (root) { //终止条件 if (isLeaf(root)) //只有在叶子结点需要创建好哈弗曼编码(元素均在叶子结点) { idx = root->data.idx; setHaffmanCode(root, zd->zf_arr[idx].code); } else//递归调用 { createHaffmanCode(root->pLeft, zd); createHaffmanCode(root->pRight, zd); } } }
- //从字典中获取某个字符对应的haffman编码
- void getHaffmanCode(char c, char* haffmanCode, allZifu* zd)
- {
- for (int i = 0; i < zd->zf_count; i++)
- {
- if (c == zd->zf_arr[i].zf)
- {
- strcpy(haffmanCode, zd->zf_arr[i].code);
- break;
- }
- }
- }
5.判断叶子结点&创建压缩文件并写入对应的编码
//判断当前节点是否叶子节点,是返回true 否则返回false bool isLeaf(treeNode* root) { if (root && NULL == root->pLeft && NULL == root->pRight) return true; return false; }写编码到文件中的关键:
①charForwrite有8个bit位,依次由对应的每个字符对应而来。当Huffman编码读到0的时候,表明当前的char已经读完,需要再读入一个字符和对应编码。当读到的是2,对应0。当读到是1,对应的是1.
②每满8个bit位就写入这个charForWrite
③1---->0000 0001 然后对它进行左移操作,结合& ~ |等位运算,可以很容易的将charForWrite的二进制的bit进行需要的修改。0x80也行---->1000 0000
//创建压缩文件并把字典和对应字符的编码写入到压缩后文件中 void writeCompressFile(char* src, char* dst, allZifu* zd) { //1. 创建压缩后文件 打开 待压缩文件 FILE* fpSrc = fopen(src, "rb"); FILE* fpDst = fopen(dst, "wb"); if (NULL == fpSrc || NULL == fpDst) { printf("writeCompressFile函数中 打开文件失败!\n"); return; } //2. 优先把字典写入压缩后文件中 fwrite(zd, 1, sizeof(allZifu), fpDst); //3. 一个个字节读待压缩文件 对照字典中的哈夫曼编码 得到对应二进制 每拼成一个字节(8bit) 写入到压缩后文件中 读完 写完 循环结束 char c;//存储读到的字节 int r;//读到的字节数 char charForWrite;//用来存储写入 压缩后文件中的 字节 (二进制位拼接而来) int idxForWrite;//charForWrite 的 二进制位 索引 char haffmanCode[HFM_CODE_LEN] = { 0 };//临时存储哈夫曼编码 int haffmanCodeIdx=0;//haffmanCode 数组下标 bool isEnd = false; while (1) { if (0 == haffmanCode[haffmanCodeIdx]) { r = fread(&c, 1, 1, fpSrc); if (1 != r) break;//读不不到了 getHaffmanCode(c, haffmanCode, zd);//获得字符c对应的哈弗曼编码 haffmanCodeIdx = 0; } #if TESTHUFFMANCODE printf("%c=================", c); for (int i = 0; i < HFM_CODE_LEN; i++) { printf("%d", haffmanCode[i]); } printf("\n"); #endif //把haffman编码转成二进制并存入charForWrite里 注意八个bit位 写入fpDst一次 idxForWrite = 0; charForWrite = 0; while (idxForWrite < 8)//8个bit位写一次 { if (2 == haffmanCode[haffmanCodeIdx]) {//设置对应的bit位为0 charForWrite &= ~(1 << (7 - idxForWrite));//把对应位置零 其他 维持不变 haffmanCodeIdx++; idxForWrite++; } else if (1 == haffmanCode[haffmanCodeIdx]) {//设置对应的bit位为1 charForWrite |= 1 << (7 - idxForWrite);//把对应位置1 其他 维持不变 haffmanCodeIdx++; idxForWrite++; } else {//当前这个字符对应的haffman编码操作完毕 读取下一个字符的haffman编码 或者 结束 r = fread(&c, 1, 1, fpSrc); if (1 != r) {//读完了 isEnd = true; break;//结束 while (idxForWrite < 8) 循环 } getHaffmanCode(c, haffmanCode, zd); haffmanCodeIdx = 0; #if TESTHUFFMANCODE printf("%c=================", c); for (int i = 0; i < HFM_CODE_LEN; i++) { printf("%d", haffmanCode[i]); } printf("\n"); #endif } } fwrite(&charForWrite, 1, 1, fpDst);//写入压缩后文件中 #if TESTWRITECHAR for (int i = 0; i < 8; i++) { if ((charForWrite & 0x80) == 0x80) {//这边&的优先级是低于==,所以要加上一个括号 printf("1"); } else { printf("0"); } charForWrite <<= 1; } printf("\n"); #endif if (isEnd) break; } //4. 关闭保存 fclose(fpSrc); fclose(fpDst); }
关键:如何将放在一起的0101进行区分成相应的字符
将压缩后的文件,读出字典---->根据字典生成相应的Huffman树---->读一个字节,依次看每一个比特位,1就往左孩子走一个,0往右边走一个,实时判断是否走到了叶子结点->当走到了叶子结点,结束,读取走到当前p节点的data存放的字符,写入文件即可.
- int main()
- {
-
- char srcFile[256] = "2.txt";//压缩后文件
- char dstFile[256] = "3.txt";//解压后文件
-
- allZifu zd = { 0 };//字典
- // 1 读取字典
- FILE* fpSrc = fopen(srcFile, "rb");
- fread(&zd, 1, sizeof(allZifu), fpSrc);
-
- FILE* fpDst = fopen(dstFile, "wb");
- //2 根据字典生成haffman树
- treeNode* root = createHaffmanTree(&zd);
-
- //3 循环读取
- //3.0 统计字节数
- int allCount = root->data.count;//根节点就保存了总的字节数
- printf("字节数为:%d\n", allCount);
-
- int myCount = 0;
- char temp;
-
- int bit_idx;//比特位的索引
- treeNode* tempNode = root;
- bool isOver = false;
- while (1 == fread(&temp,1,1,fpSrc))
- {
- bit_idx = 0;
- while (bit_idx < 8)
- {/*0x80是 1000 0000*/
- if ((0x80 >> bit_idx) == (temp&((0x80 >> bit_idx))))
- {//当前位为 1
- tempNode = tempNode->pLeft;
- }
- else
- {
- tempNode = tempNode->pRight;
- }
- if (isLeaf(tempNode))//若走到了叶子结点√---->就写一个字符(tempNode结点存储的字符即可)
- {
- fwrite(&(tempNode->data.zf), 1, 1, fpDst);
- myCount++;
- if (myCount >= allCount)
- {
- isOver = true;
- break;
- }
- tempNode = root;//继续下一个字符
- }
- bit_idx++;
- }
- if (isOver) break;
- }
-
- fclose(fpSrc);
- fclose(fpDst);
- printf("解压缩完毕!\n");
- system("pause");
- return 0;
- }
数据结构 --- 栈和堆实现哈夫曼树_https://blog.csdn.net/weixin_60569662/article/details/123376155