前言
哈夫曼樹是數據壓縮編碼算法的基礎,本文使用JavaScript語言實現了該算法。
算法流程:輸入待編碼的字符串,算法去構造哈夫曼樹,從而實現對字符串的二進制壓縮編碼。
對於哈夫曼樹理論的學習,可去參見其他文章。本文僅包含實現的代碼以及註釋。
註釋比較豐富,相信不難理解。
算法實現
樹節點
既然是樹數據結構,就要有樹節點,下面是樹節點定義
class Node {
constructor(value, char, left, right) {
this.val = value; // 字符出現次數
this.char = char; // 待編碼字符
this.left = left;
this.right = right;
}
}
一般來說,節點只需要val,left,right即可,這裏加了一個char字段,表示該節點代表待編碼字符串裏面的哪個字符,當前節點是葉子節點的時候,會賦值這個字段。
核心代碼
構造函數
class huffmanTree{
constructor(str){
// 第一步,統計字符出現頻率
let hash = {};
for(let i = 0; i < str.length; i++){
hash[str[i]] = ~~hash[str[i]] + 1;
}
this.hash = hash;
// 第二步,構造哈夫曼樹
this.huffmanTree = this.getHuffmanTree();
// 第三步,遍歷哈夫曼樹,得到編碼表
let map = this.getHuffmanCode(this.huffmanTree);
// 查看編碼表,即每個字符的二進制編碼是什麼
console.log(map);
// 第四部,根據編碼對照表,返回最終的二進制編碼
this.binaryStr = this.getBinaryStr(map, str);
}
}
下面我們逐一的看一下,(1)構造哈夫曼樹的過程、(2)遍歷哈弗曼樹取得編碼表的過程 以及 (3)返回最終二進制串的過程。
構造哈夫曼樹
// 構造哈夫曼樹
getHuffmanTree(){
// 以各個字符出現次數爲node.val, 構造森林
let forest = []
for(let char in this.hash){
let node = new Node(this.hash[char], char);
forest.push(node);
}
let allNodes = []; // 存放被合併的節點,因爲不能真的刪除森林中任何一個節點,否則.left .right就找不到節點了
// 等到森林只剩一個節點時,表示合併過程結束,樹就生成了
while(forest.length !== 1){
// 從森林中找到兩個最小的樹,合併之
forest.sort((a, b) => {
return a.val - b.val;
});
let node = new Node(forest[0].val + forest[1].val, '');
allNodes.push(forest[0]);
allNodes.push(forest[1]);
node.left = allNodes[allNodes.length - 2]; // 左子樹放置詞頻低的
node.right = allNodes[allNodes.length - 1]; // 右子樹放置詞頻高的
// 刪除最小的兩棵樹
forest = forest.slice(2);
// 新增的樹加入
forest.push(node);
}
// 生成的哈夫曼樹,僅剩一個節點,即整棵樹的根節點
return forest[0];
}
遍歷哈夫曼樹,返回編碼表
// 遍歷哈夫曼樹,返回一個 原始字符 和 二進制編碼 的對照表
getHuffmanCode(tree){
let hash = {}; // 對照表
let traversal = (node, curPath) => {
if (!node.length && !node.right) return;
if (node.left && !node.left.left && !node.left.right){
hash[node.left.char] = curPath + '0';
}
if (node.right && !node.right.left && !node.right.right){
hash[node.right.char] = curPath + '1';
}
// 往左遍歷,路徑加0
if(node.left){
traversal(node.left, curPath + '0');
}
// 往右遍歷,路徑加1
if(node.right){
traversal(node.right, curPath + '1');
}
};
traversal(tree, '');
return hash;
}
返回編碼串
// 返回最終的壓縮後的二進制串
getBinaryStr(map, originStr){
let result = '';
for(let i = 0; i < originStr.length; i++){
result += map[originStr[i]];
}
return result;
}
代碼彙總
// 哈弗曼編碼是將一個 字符串序列 用 二進制表示 的壓縮算法
class huffmanTree{
constructor(str){
// 第一步,統計字符出現頻率
let hash = {};
for(let i = 0; i < str.length; i++){
hash[str[i]] = ~~hash[str[i]] + 1;
}
this.hash = hash;
// 構造哈夫曼樹
this.huffmanTree = this.getHuffmanTree();
let map = this.getHuffmanCode(this.huffmanTree);
// 查看對照表,即每個字符的二進制編碼是什麼
console.log(map);
// 最終的二進制編碼
this.binaryStr = this.getBinaryStr(map, str);
}
// 構造哈夫曼樹
getHuffmanTree(){
// 以各個字符出現次數爲node.val, 構造森林
let forest = []
for(let char in this.hash){
let node = new Node(this.hash[char], char);
forest.push(node);
}
// 等到森林只剩一個節點時,表示合併過程結束,樹就生成了
let allNodes = []; // 存放被合併的節點,因爲不能真的刪除森林中任何一個節點,否則.left .right就找不到節點了
while(forest.length !== 1){
// 從森林中找到兩個最小的樹,合併之
forest.sort((a, b) => {
return a.val - b.val;
});
let node = new Node(forest[0].val + forest[1].val, '');
allNodes.push(forest[0]);
allNodes.push(forest[1]);
node.left = allNodes[allNodes.length - 2]; // 左子樹放置詞頻低的
node.right = allNodes[allNodes.length - 1]; // 右子樹放置詞頻高的
// 刪除最小的兩棵樹
forest = forest.slice(2);
// 新增的樹加入
forest.push(node);
}
// 生成的哈夫曼樹
return forest[0];
}
// 遍歷哈夫曼樹,返回一個 原始字符 和 二進制編碼 的對照表
getHuffmanCode(tree){
let hash = {}; // 對照表
let traversal = (node, curPath) => {
if (!node.length && !node.right) return;
if (node.left && !node.left.left && !node.left.right){
hash[node.left.char] = curPath + '0';
}
if (node.right && !node.right.left && !node.right.right){
hash[node.right.char] = curPath + '1';
}
// 往左遍歷,路徑加0
if(node.left){
traversal(node.left, curPath + '0');
}
// 往右遍歷,路徑加1
if(node.right){
traversal(node.right, curPath + '1');
}
};
traversal(tree, '');
return hash;
}
// 返回最終的壓縮後的二進制串
getBinaryStr(map, originStr){
let result = '';
for(let i = 0; i < originStr.length; i++){
result += map[originStr[i]];
}
return result;
}
}
測試代碼
let tree = new huffmanTree('ABBCCCDDDDEEEEE')
console.log(tree)
對照表:{ C: ‘00’, A: ‘010’, B: ‘011’, D: ‘10’, E: ‘11’ }
最終編碼結果:010011011000000101010101111111111
結語
前端算法庫:https://github.com/cunzaizhuyi/js-leetcode
這裏記錄了我刷過的近500道LeetCode的題解,
希望對前端同行找工作面試、修煉算法內功有幫助。