哈夫曼樹(赫夫曼樹/霍夫曼樹 /最優樹)
若該樹的帶權路徑長度達到最小,稱這樣的二叉樹為最優二叉樹,也稱為哈夫曼樹
應用場景文件壓縮,又叫壓縮算法
現在有3課二叉樹,都有四個節點,分別帶權13,7,8,3
一段字符串中計算每一個字符重復的次數
let a = 'ab cbdal abc' console.log(a.split('').reduce((acc, val) => { acc[val] = (acc[val] || 0) + 1 return acc }, {})) //升級版 const getFreqs = text => [...text] .reduce((acc, val) => (acc[val] ? {...acc, [val]: acc[val] + 1} : {...acc, [val]: 1} ), {}) console.log(getFreqs('abc abx')) //第二種 const cal = str => { let map = {} let i = 0 while (str[i]) { //首先添加str[i]的屬性,因為剛開始沒值,所以復制為1 map[str[i]] ? map[str[i]]++ : map[str[i]] = 1 i++ } return map } console.log(cal('abc ab cc '))
擴充二叉樹
對於一顆已有的二叉樹,如果我們為他添加一系列新結點,使得他原有的所有結點的度都為2,那么我們得到了一顆擴充二叉樹:
其中原有的結點叫做內結點(非葉子結點),新增加的結點叫做葉結點(葉子結點)
外結點數=內結點數+1
總結點數=2*外結點數-1
那什么結點的度?
- 結點的度指的是二叉樹結點的分支數目,如果某個節點沒有孩子結點,即使沒有分支,那么他的度是0,如果有一個孩子的結點,那么他的度數是1,如果既有左孩子也有右孩子,那么這個結點的度是2
赫夫曼樹的外結點和內結點
性質區別: 外結點是攜帶了關鍵數據的節點,二內部節點沒有攜帶這種數據,只作為導向最終的外結點所走的路徑而使用
正因如此,我們的關注點最后是落在赫夫曼樹的外結點上,而不是內結點
帶權路徑長度WPL
如果一個數據結點搜索頻率越高,就讓他分布在離根結點越近的地方,也就是根結點走到該節點經過的路徑長度越短
頻率是個細化的量,這里我們使用一個更加標准的一個詞描述他---"權值"
**我們為擴充二叉樹的外結點(葉子結點)定義兩條屬性:權值(W)和路徑長度(L),同時規定帶權路徑長度(WPL) 為擴充二叉樹的外結點的權值和路徑長度的乘積之和(只是外結點)
外結點的帶權路徑長度(WPL) = T的根到該節點的路徑長度 * 該節點的權值
等長編碼和不等長編碼
前綴編碼
要設計長短不等的編碼,則必須保證:任意一個字符的編碼都不是另一個字符的編碼的前綴,這種編碼叫做前綴編碼
赫夫曼編碼
赫夫曼編碼就是一種前綴編碼,他能解決不等長的譯碼問題,通過他,我們能盡可能減少編碼的長度,同時還能過避免二義性,實現正確編碼
赫夫曼樹的構造
赫夫曼樹是一棵滿二叉樹,樹中只有兩種類型的結點,即葉子節點和度為2的結點,所有樹中任意結點的左子樹和右子樹同時存在
對字符集合按照字符頻率進行升序排序,並構建一顆空樹
若字符頻率不大於根節點頻率,則字符作為根節點的左兄弟,形成一個新的根節點,頻率值為左、右子節點之和;
若字符頻率大於根結點頻率,則字符作為根結點的右兄弟,形成一個新的根結點,頻率值為左右子節點之和
舉例:
1. 做4個葉節點,分別以2,4,5,7為權
2. 從所有入度為0的結點中,最小的兩個節點為2和4,則組成6這個分支
3. 從所有入度為0的結點中,最小的兩個結點為5和6,則組成11這個分支節點
**4. 從所有入度為0的結點中,最小的兩個結點為7和11,則組成18這個分支節點,此時只有一個入度為0的頂點為18,組成了下圖的二叉樹,完成 **
對字符集合按照頻率進行排序,這里使用插入排序
//字符集合為
const contentArr = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']
//對應的頻率
const valueArr = [5, 3, 4, 0, 2, 1, 8, 6, 9, 7]
//使用插入排序
const insertionSort = (valueArr, contentArr) => {
let tmpValue, tmpContent
for (let i = 1; i < valueArr.length; i++) {
tmpValue = valueArr[i]
tmpContent = contentArr[i]
while (i > 0 && tmpValue < valueArr[i - 1]) {
valueArr[i] = valueArr[i - 1]
contentArr[i] = contentArr[i - 1]
i--
}
valueArr[i] = tmpValue
contentArr[i] = tmpContent
}
return contentArr
}
console.log(insertionSort(valueArr, contentArr))
我自己寫的垃圾排序(通過排序的索引對原數組進行重新排序)
let arr = valueArr.slice().sort((a, b) => a - b)
arr.reduce((acc, val) => {
acc.push(contentArr[valueArr.indexOf(val)])
return acc
}, [])
完整步驟前端版
//把字符串重復的轉化成重復次數的對象
const getFreqs = text => [...text]
.reduce((acc, letter) => (acc[letter]
? { ...acc, [letter]: acc[letter] + 1 }
: { ...acc, [letter]: 1 }
), {});
//console.log(getFreqs('abc ab'))
//{ a: 2, b: 2, c: 1, ' ': 1 }
//把對象轉成一個二維數組
const toPairs = freqs => Object.keys(freqs)
.map(letter => [letter, freqs[letter]]);
//給二維數組排序
const sortPairs = pairs => [...pairs]
.sort(([, leftFreq], [, rightFreq]) => leftFreq - rightFreq);
//遞歸創建樹
const getTree = pairs => (pairs.length < 2
? pairs[0]
: getTree(sortPairs([
[pairs.slice(0, 2), pairs[0][1] + pairs[1][1]],
...pairs.slice(2)]))
);
//遞歸編碼
const getCodes = (tree, pfx = '') => (tree[0] instanceof Array
? Object.assign(
getCodes(tree[0][0], `${pfx}0`),
getCodes(tree[0][1], `${pfx}1`),
)
: { [tree[0]]: pfx });
export default (text) => {
const freqs = getFreqs(text);
const freqPairs = toPairs(freqs);
const sortedFreqPairs = sortPairs(freqPairs);
const tree = getTree(sortedFreqPairs);
const codes = getCodes(tree);
return [...text]
.map(letter => codes[letter])
.join('');
};
第二版
//字符串轉對象
const freqs = text => [...text].reduce((acc, val) => (
(acc[val] ? acc[val] = acc[val] + 1 : acc[val] = 1), acc)
, {})
console.log(freqs('abc ab '))
//{ a: 1, b: 1, c: 1, ' ': 1 }
//把對象轉成二維數組
const topaire = freqs => Object.keys(freqs).map(val => [val, freqs[val]])
console.log(topaire(freqs('abc ab ')))
//[ [ 'a', 1 ], [ 'b', 1 ], [ 'c', 1 ], [ ' ', 1 ] ]
//二維數組排序
const sortps = pairs => pairs.sort((a, b) => a[1] - b[1])
console.log(sortps(topaire(freqs('abc ab '))))
//[ [ 'c', 1 ], [ 'a', 2 ], [ 'b', 2 ], [ ' ', 2 ] ]
//構建樹
const tree = ps => {
if (ps.length < 2) {
return ps[0]
}
//拿到最小的兩個,然后求和,然后把剩下的合並起來,進行遞歸創建出樹
return tree(sortps([[ps.slice(0, 2), ps[0][1] + ps[1][1]]].concat(ps.slice(2))))
}
console.log(tree(sortps(topaire(freqs('abc ab ')))))
//編碼
const codes = (tree, pfx = '') => {
if (tree[0] instanceof Array) {
return Object.assign(codes(tree[0][0], pfx + '0'), codes(tree[0][1], pfx + '1'))
}
return ({[tree[0]]: pfx})
}
console.log(codes(tree(sortps(topaire(freqs('abc ab '))))))
//將字符串編碼
const encode = str => {
let output = '編碼'
let a = {c: '00', a: '01', b: '10', ' ': '11'}
for (const item in str) {
output = output + a[str[item]]
}
return output
}
console.log(encode('abc ab '))
//反碼
let c = '011110010010011110101100101101111011111110000000'
let dict = {
k: '00',
c: '0100',
d: '0101',
a: '011',
' ': '10',
b: '110',
f: '111'
}
//第一個參數是編碼的參數,第二個參數是字典
const decodeText = (text, dict) => {
let encode = ''
let decode = ''
let keyArr = Object.keys(dict)
let valueArr = Object.values(dict)
for (let i = 0; i < text.length; i++) {
encode += text.charAt(i)
for (let j = 0; j < valueArr.length; j++) {
if (valueArr[j] == encode) {
decode += keyArr[j]
text.slice(encode.length)
encode = ''
}
}
}
return decode
}
console.log(decodeText(c, dict))
壓縮版
function dictionary(text) {
const freqs = text => [...text].reduce((fs, c) => (fs[c] ? (fs[c] = fs[c] + 1, fs) : (fs[c] = 1, fs)), {});
const topairs = freqs => Object.keys(freqs).map(c => [c, freqs[c]]);
const sortps = pairs => pairs.sort((a, b) => a[1] - b[1]);
const tree = ps => (ps.length < 2 ? ps[0] : tree(sortps([[ps.slice(0, 2), ps[0][1] + ps[1][1]]].concat(ps.slice(2)))));
const codes = (tree, pfx = '') => (tree[0] instanceof Array ? Object.assign(codes(tree[0][0], pfx + '0'), codes(tree[0][1], pfx + '1')) : { [tree[0]]: pfx });
return codes(tree(sortps(topairs(freqs(text)))));
}