数据结构之----哈夫曼树

179 阅读3分钟

路径长度

  • 路径 从树中的一个节点到另一个节点之间的分支构成这两点之间的路径
  • 路径长度 路径上的分支条数,树的路径长度是从树的根节点到每一个节点的路径长度之和

下面这张图里有两棵树

图a中 WPL = 2*(2+4+5+7) = 36

图b中 WPL = 2 + 4*2 + 3*(5+7) = 46
图c中 WPL = 7 + 5*2 + 3*(2+4) = 35

带权路径长度最小的二叉树就是哈夫曼树,节点权值越大,距离根节点就越近

2.哈夫曼编码

在通信领域,经过哈夫曼编码的的信息小于大量冗余数据,提高传输效率,是重要的数据压缩方法。

一段信息由 a b c d e 5个字符组成,各自出现的概率是0.12 0.4 0.15 0.08 0.25 ,把这几个字符串编码成二进制的0,1 序列,有两种方法,一种是定长编码,另一种是变长编码,如下图所示

符号

概率

定长编码

变长编码

a

0.12

000

1111

b

0.4

001

0

c

0.15

010

110

d

0.08

011

1110

e

0.25

100

10

如果采用定长编码,平均编码长度为 (0.12 + 0.4 + 0.15 + 0.08 + 0.25)*3 = 3
采用变长编码,平均编码长度为 0.12*4 + 0.4*1 + 0.15*3 + 0.08*4 + 0.25* 2 = 2.15

显然,采用变长编码,更加高效,变长编码的二叉树表示如下图

2.1 代码实现

1.3 中的哈夫曼算法,每次都要从F中找出权值最小的两个根节点,构造出新的树以后,删除这两个根节点并加入新构造的树,这个过程用最小堆来实现正合适。

先将森林里的根节点加入到一个最小堆中,循环n-1次,每次循环,先连续两次删除最小堆的堆顶,利用这两个堆顶,构造新的树并将新的树的根节点放入到最小堆中,循环结束后,堆顶就是哈夫曼树的根节点。

2.1.1 定义存储编码和概率的类

// 编码
var CodeNode = function(code, rate){
    this.code = code;     // 字符
    this.rate = rate;     // 概率
};

2.1.2 定义树节点

var TreeNode = function(data){
    this.data = data;
    this.leftChild = null;
    this.rightChild = null;
    this.parent = null;
};

2.1.3 准备数据

// 准备数据
var code_dict = {
    "a": 0.12,
    "b": 0.4,
    "c": 0.15,
    "d": 0.08,
    "e": 0.25
};
var forest = [];

for(var key in code_dict){
    var item = new CodeNode(key, code_dict[key]);
    forest.push(new TreeNode(item));
}

2.1.4 哈夫曼树类定义

function HuffmanTree(){
    var root = null;

    this.init_tree = function(arr){
        var min_heap = new MinHeap();
        min_heap.init(arr);
        for(var i = 0;i < arr.length - 1; i++){
            var first = min_heap.remove_min();
            var second = min_heap.remove_min();

            var new_item = new CodeNode("", first.data.rate + second.data.rate);
            var new_node = new TreeNode(new_item);
            min_heap.insert(new_node);

            new_node.leftChild = first;
            new_node.rightChild = second;
            first.parent = new_node;
            second.parent = new_node;

            root = new_node;
        }
    };

    var get_code_from_tree = function(node, dict, code_str){
        if(!node.leftChild && !node.rightChild){
            // 页节点
            dict[node.data.code] = code_str;
            return;
        }

        if(node.leftChild){
            get_code_from_tree(node.leftChild, dict, code_str+"0");
        }
        if(node.rightChild){
            get_code_from_tree(node.rightChild, dict, code_str+"1");
        }
    };

    this.get_code = function(){
        // 获得最终的变长编码
        var code_dict = {};
        get_code_from_tree(root, code_dict, "");
        return code_dict;
    };

    this.print = function(){
        console.log(root);
    };
};

2.1.5 最小堆

上一讲的最小堆里的实现,存入的数据都是整数,此次需要存入的是树节点,类型为TreeNode, 关键码是节点里的data的rate属性,而由于javascript里不支持比较运算符的重载,因此,根据这种实际情况,对最小堆代码稍作修改

function MinHeap(size){
    var heap = new Array(size);
    var curr_size = 0;
    var max_size = size;


    var shif_down = function(start, m){
        // 从start这个位置开始,向下下滑调整
        var parent_index = start;                       // start就是当前这个局部的父节点
        var min_child_index = parent_index*2 + 1;       // 一定有左孩子,先让min_child_index等于左孩子的索引

        while(min_child_index <= m){
            // min_child_index+1 是左孩子的索引, 左孩子大于右孩子
            if(min_child_index < m && heap[min_child_index].data.rate > heap[min_child_index+1].data.rate){
                min_child_index = min_child_index+1;  // min_child_index永远指向值小的那个孩子
            }

            // 父节点的值小于等于两个孩子的最小值
            if(heap[parent_index].data.rate <= heap[min_child_index].data.rate){
                break;   // 循环结束,不需要再调整了
            }else{
                // 父节点和子节点的值互换
                var tmp = heap[parent_index];
                heap[parent_index] = heap[min_child_index];
                heap[min_child_index] = tmp;
                parent_index = min_child_index;
                min_child_index = 2*min_child_index + 1;
            }
        }

    };

    // 传入一个数组,然后调整为最小堆
    this.init = function(arr){
        max_size = arr.length;
        curr_size = max_size;
        heap = new Array(arr.length);
        // 填充heap, 目前还不是一个堆
        for(var i =0; i<curr_size;i++){
            heap[i] = arr[i];
        }

        var curr_pos = Math.floor(curr_size/2);      // 这是堆的最后一个分支节点
        while(curr_pos >= 0){
            shif_down(curr_pos, curr_size-1);        // 局部自上向下下滑调整
            curr_pos -= 1;                           // 调整下一个分支节点
        }
    };

    var shif_up = function(start){
        var child_index = start;         // 当前节点是叶节点
        var parent_index = Math.floor((child_index-1)/2);   // 找到父节点


        while(child_index > 0){
            // 父节点更小,就不用调整了
            if(heap[parent_index].data.rate <= heap[child_index].data.rate){
                break;
            }else{
                // 父节点和子节点的值互换
                var tmp = heap[child_index];
                heap[child_index] = heap[parent_index];
                heap[parent_index] = tmp;
                child_index = parent_index;
                parent_index = Math.floor((parent_index-1)/2);
            }
        }
    };

    this.insert = function(item){
        // 插入一个新的元素
        // 堆满了,不能再放元素
        if(curr_size == max_size){
            return false;
        }

        heap[curr_size] = item;
        shif_up(curr_size);
        curr_size++;
        return true;
    };

    //删除最小值
    this.remove_min = function(){
        if(curr_size <= 0){
            return null;
        }
        var min_value = heap[0];
        heap[0] = heap[curr_size-1];
        curr_size--;
        shif_down(0, curr_size-1);
        return min_value;
    };

    this.size = function(){
        return curr_size;
    };

    this.print = function(){
        console.log(heap);
    };
};

2.1.6 最终使用

var huffman_tree = new HuffmanTree();
huffman_tree.init_tree(forest);
console.log(huffman_tree.get_code());

程序最终输出结果为

{ b: '0', e: '10', c: '110', d: '1110', a: '1111' }