霍夫曼编码

霍夫曼编码是将文本或者图像的信号进行编码使其平均长度小于原本的长度来达到压缩效果,其基本原理是贪心算法,不断取两个最小权重最小的点进行合并,这样权重越高的值会在越上层,权重越小的值会在越下层,而霍夫曼编码跟该结点的层次有关,该结点层次越深,编码长度越长,这样,权重小的结点编码就越长,权重高的结点编码就短,来达到获得最短带权长度的目的,以对文章或图像进行压缩。

下面我们选取一段不少于3000字的英文材料,统计各字符出现的次数,实现Huffman编码,以及对编码结果的解码。

1.统计字符出现的频率

    使用的文章

上面为部分截图,包含3000多个单词。

首先获取文章中所有出现的字符,然后计算每个字符在文章中出现的次数,再将其除以文章总字数(总字符个数),得到该字符出现的频率。

代码(matlab):

function h_nodes = get_h_nodes(text)
    % 获取文章中所有出现过的字符
    unique_chars = unique(text);
    % 创建结点结构体
    h_nodes = struct('char', '', 'weight', [], ...
            'lchild', [], 'rchild', [], 'parent', []);
    for i = 1 : numel(unique_chars)
        % 获取当前字符
        h_nodes(i).char = unique_chars(i);
        % 计算当前字符的权重(出现频率)
        h_nodes(i).weight = sum(text == unique_chars(i)) / numel(text);
    end

end

如上述代码,先使用unique函数获取文章中所有信源,也就是文章中所有出现过的字符,将结果保存在数组中,遍历这个数组,取出数组中的所有字符,使用sum函数计算每个字符出现的次数,再将结果除以文章的总字符数得到该字符的频率。

为了方便后续的创建霍夫曼树,我们将字符和字符出现的频率封装在一个结构体中,这个结构体包含属性:字符,权重(频率),左子树,右子树,父节点。左子树、右子树、父节点初值都是空。

将每个字符的霍夫曼编码和出现的次数保存在Huffman.txt文件中作为霍夫曼表以便后续对霍夫曼编码进行解码。

for i = 1 : numel(keys)
    node_num = h_nodes(i).weight * numel(str);
    wpl = wpl + h_nodes(i).weight * numel(cell2mat(values(i)));
    x = h_nodes(i).weight * numel(cell2mat(values(i)));
    disp(wpl)
    fprintf(fp, "%c: %s %d\n", cell2mat(keys(i)), cell2mat(values(i)), node_num);
end

这里从h_codes(h_codes是字典,键是字符,值是字符的霍夫曼编码),中取出其所有出现过的字符及其霍夫曼编码,而其出现的次数等于字符结点的权重(出现的频率)乘以文章总字符数。

这里我还计算了霍夫曼树的加权平均路径长度wpl,在这里wpl的含义是每个字符的加权平均长度,用于衡量文章被压缩的程度(文章未压缩前每个字符的长度为7位,将得到的每个字符的加权平均长度除以7就能得到文章的压缩程度),wpl的计算方法是将该字符的霍夫曼编码长度乘以其权重值(出现频率)。

结果:

如上图最坐标的字符为文章中出现的字符(第一个到第三个分别为\n,\b和空格,所以在文本中仅显示出空白)冒号右边第一个数字为该字符的霍夫曼编码,第二个数字为该字符出现的次数

2.创建霍夫曼树

上一步我们已经获得了所有字符的结点,现在我们通过这些结点创建一个霍夫曼树以便后续获取霍夫曼编码。

首先,我们将所有字符结点按照它们的权重排序,得到两个权重最低的字符,然后我们新建一个结点,将这个结点作为这两个字符结点的父结点,再将这个结点的左子树和右子树分别设为这两个字符结点,最后将这两个字符结点的权重值相加作为父结点的权重。

重复上面的步骤直到霍夫曼树构建完成。

代码:

function h_nodes = create_h_tree(h_nodes)
    % 获取结点个数
    num_nodes = numel(h_nodes);
    % 总结点数等于叶子结点数乘2-1
    for i = num_nodes + 1 : 2 * num_nodes - 1
        % 设置初始x1,x2,和最小两个权重w1,w2,两个权重初始设为极大值
        x1 = -1; x2 = -1; w1 = 1.1; w2 = 1.1;
        % 先添加这个父结点,否则下面遍历到这个下标时会出现越界错误
        h_nodes(i) = struct('char', '', 'weight', 1.2, ...
            'lchild', [], 'rchild', [], 'parent', []);
        for j = 1 : i
            % 如果没有父结点且权重值小于最小或次小权重值,将其设为最小或次小结点
            if isempty(h_nodes(j).parent) && h_nodes(j).weight < w1
                w2 = w1; x2 = x1;
                w1 = h_nodes(j).weight; x1 = j;
            elseif isempty(h_nodes(j).parent) && h_nodes(j).weight < w2
                w2 = h_nodes(j).weight;
                x2 = j;
            end
        end

        % 将最小和次小结点设为新建的结点的左右子树,再将新建的结点设为它们两个的父结点
        h_nodes(i).lchild = x1; h_nodes(i).rchild = x2;
        h_nodes(x1).parent = i; h_nodes(x2).parent = i;
        h_nodes(i).weight = h_nodes(x1).weight + h_nodes(x2).weight;
    end
end

如上述代码,先获取字符结点的个数,然后根据字符结点的个数算出总结点的个数。因为霍夫曼树是最优二叉树,总结点数等于2*叶子结点数-1,而叶子结点数就是字符结点数,因为初始的字符结点只可能是叶子结点,不会是其他结点父结点,如果不知道这个知识点,我们也可以自己算,霍夫曼树是每次选择两个结点合并,合并到最后只剩一个结点,每次合并总结点数减一,要减到只剩1则需要n-1次,即需要合并n-1次,每次合并会出现一个新的结点,一共出现n-1个新结点,则总共会有2*n-1个结点。

知道总结点数后,我们就可以从n+1开始遍历2n-1(n为初始字符结点个数),总共n-1次,每次创建一个新的结点,每次循环执行如下操作:

1.遍历当前的所有结点(包括字符结点和由字符结点合并得来的新结点):

        1.1.如果当前结点没有父结点并且权重值小于最小值(初始权重值为极大值,意味着第一个结点一开始一定会是最小权重的结点,也可以直接将权重值初始化为第一个结点的权重值),将这个结点的权重值设为最小权重值,这个结点的下标设为最小权重结点的下标,将原来的最小权重结点设为次小的结点。

        1.2.如果当前结点没有父结点并且权重小于次小值大于最小值,则将其设为次小结点(具体操作同上)。

2.将新建结点的左子树设为最小权值结点,右子树设为次小权重结点(顺序相反也可以),再将新建结点的权重设为左右两子树权重的和。

3.将最小和次小权值的结点的父结点设为当前新建的结点。

3.获取霍夫曼编码

获得霍夫曼树后,我们就可以进行霍夫曼编码了,将每个字符向上不断寻找其父结点,直到霍夫曼树的根结点,这样就会得到一个路径,对于路径的每一步,如果是从左子树寻上去的则为0,右子树则为1(左子树1,右子树0也可以),这样就可以得到一个二进制的编码,这个编码就是该字符的霍夫曼码了。

代码:

function h_codes = create_h_code(h_nodes)
    num_nodes = numel(h_nodes);
    h_codes = containers.Map();
    for i = 1 : num_nodes / 2 + 1
        f = h_nodes(i).parent;
        c = i; j = 0;
        code = '';
        while ~isempty(f)
            if h_nodes(f).lchild == c
                code = [code '0']; j = j + 1;
            else
                code = [code '1']; j = j + 1;
            end
            c = f;
            f = h_nodes(f).parent;
        end

        % 逆序
        code = flip(code);
        h_codes(h_nodes(i).char) = code;
    end
end

如上述代码,首先获取总结点数,将其除2加1得到叶子结点数,循环遍历n次(n为叶子结点数),每次取一个结点,获取该结点的父节点,判断其父节点是否为空,为空则说明是根结点,无需进行操作。如果不为空则判断该结点是其父结点的左子树还是右子树,左子树则将code设为0,右子树设为1,然后再从该结点的父节点继续向上寻找父节点的父节点,直到根结点结束(也就是父节点为空),此时就能得到一个编码了,但这并不是真正的结果,因为编码是逆序向上的,而霍夫曼编码是顺序从根结点向下的,需要将得到的编码进行逆序操作,得到其逆序编码,此编码就是该结点的编码了,循环n次后就能将每个结点的霍夫曼编码计算出来。

4.对文章进行霍夫曼编码

上面我们已经得到了每个字符的霍夫曼编码了,现在我们只需要遍历整篇文章,将每篇文章的每个字符转换成对应的霍夫曼编码即可。

代码:

function encoded_text = encode_text(text, h_codes)
    encoded_text = '';
    num_text = numel(text);
    for i = 1 : num_text
        code = h_codes(text(i));
        encoded_text = [encoded_text code];
    end

end

上述代码首先定义一个空的文本字符串用以存储文章的霍夫曼编码,接着在进行n次循环(n为文章的长度),每次取文章的第i个字符,在霍夫曼编码表中查找该字符的霍夫曼编码(这里霍夫曼编码表是以字典形式存储的,只需要通过字符作为键就能直接获取该字符的霍夫曼编码),再将这个编码加到encoded_text中。这里每个字符的霍夫曼编码直接不需要间隔,因为霍夫曼树的特性是所有初始结点都是叶子结点,不会有子结点,再根据霍夫曼编码的过程我们可以得知不会有字符的霍夫曼编码是另一个字符的霍夫曼编码的前缀,因此不用担心解码时会出错,只需一个个编码进行匹配找到对应的字符即可。

对文章编码后我们将文章的霍夫曼编码以二进制形式存储进code.dat文件中(注意一定要是以二进制形式存储进文件中,否则编码的内容将以ASCII码形式存储,原本1位0和1会变为7位的ASCII码,不仅达不到压缩的效果,反而会使文件大小变大几倍。

fp = fopen('code.dat', 'wb');
fwrite(fp, encoded_text, 'ubit1');
fclose(fp);

效果:

可以看到文章被压缩到了原来的3/5,压缩效果还是很好的。

5.霍夫曼解码

压缩后的霍夫曼编码是看不出任何信息的,在传输后我们需要将其解码才能得到其包含的信息。要解码霍夫曼编码,我们需要两个数据,一个是文章的霍夫曼编码,另一个是霍夫曼编码表(存储每个字符对应的霍夫曼编码),在上面我们已经将霍夫曼编码和霍夫曼编码表存储在文件中了,在传输时需要将两个文件同时传输,解码时根据两个文件来进行解码得到需要的信息。

霍夫曼解码的方式是将依次遍历霍夫曼编码的每一位,如果没找到对应的字符,则继续向下一位,将上一位连接下一位作为一个子串,以这个字串在霍夫曼表中寻找对应的字符,这是一个很典型的字符串匹配操作,

function decoded_text = decode_text(encoded_text, h_codes)
    decoded_text = '';
    t_code = '';
    keys = h_codes.keys;
    values = h_codes.values;
    for i = 1 : numel(encoded_text)
        t_code = [t_code encoded_text(i)];
        for j = 1 : numel(keys)
            if strcmp(values(j), t_code)
                decoded_text = [decoded_text keys(j)];
                t_code = '';
            end
        end
    end
    decoded_text = cell2mat(decoded_text);

end

    结果:

    解码前(部分截图):

    解码后(部分内容截图):

可以看出两篇文章是一样的,再使用iseuqal函数比较两个文本字符串是否相同进行进一步校验。

disp(isequal(decoded_text, str))

str是原文本字符串,decoded_text是解码后的文本字符串。

输出结果为1,表示两个字符串相同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值