javascript實現數據結構: 樹和二叉樹的應用--最優二叉樹(赫夫曼樹),回溯法與樹的遍歷--求集合冪集及八皇后問題

赫夫曼樹及其應用node

赫夫曼(Huffman)樹又稱最優樹,是一類帶權路徑長度最短的樹,有着普遍的應用。算法

 

最優二叉樹(Huffman樹)數組

1 基本概念數據結構

① 結點路徑:從樹中一個結點到另外一個結點的之間的分支構成這兩個結點之間的路徑。ide

② 路徑長度:結點路徑上的分支數目稱爲路徑長度。函數

③ 樹的路徑長度:從樹根到每個結點的路徑長度之和。佈局

如下圖爲例:ui

A到F :結點路徑 AEF ;this

路徑長度(即邊的數目) 2 ;編碼

樹的路徑長度:3*1+5*2+2*3=19;

 

④ 結點的帶權路徑長度:從該結點的到樹的根結點之間的路徑長度與結點的權(值)的乘積。

權(值):各類開銷、代價、頻度等的抽象稱呼。

⑤ 樹的帶權路徑長度:樹中全部葉子結點的帶權路徑長度之和,記作:

WPL=w1*l1+w2*l2+⋯+wn*ln=∑wi*li (i=1,2,⋯,n)

其中:n爲葉子結點的個數;wi爲第i個結點的權值; li爲第i個結點的路徑長度。

⑥ Huffman樹:具備n個葉子結點(每一個結點的權值爲wi) 的二叉樹不止一棵,但在全部的這些二叉樹中,一定存在一棵WPL值最小的樹,稱這棵樹爲Huffman樹(或稱最優樹) 。

 

在許多斷定問題時,利用Huffman樹能夠獲得最佳判斷算法。

下圖是權值分別爲二、三、六、7,具備4個葉子結點的二叉樹,它們的帶權路徑長度分別爲:

(a) WPL=2*2+3*2+6*2+7*2=36 ;

(b) WPL=2*1+3*2+6*3+7*3=47 ;

(c) WPL=7*1+6*2+2*3+3*3=34 。

其中(c)的 WPL值最小,能夠證實是Huffman樹。

 

 

2  Huffman樹的構造

① 根據n個權值{w1, w2, ⋯,wn},構形成n棵二叉樹的集合F={T1, T2, ⋯,Tn},其中每棵二叉樹只有一個權值爲wi的根結點,沒有左、右子樹;

②  在F中選取兩棵根結點權值最小的樹做爲左、右子樹構造一棵新的二叉樹,且新的二叉樹根結點權值爲其左、右子樹根結點的權值之和;

③  在F中刪除這兩棵樹,同時將新獲得的樹加入F中;

④ 重複②、③,直到F只含一棵樹爲止。

構造Huffman樹時,爲了規範,規定F={T1,T2, ⋯,Tn}中權值小的二叉樹做爲新構造的二叉樹的左子樹,權值大的二叉樹做爲新構造的二叉樹的右子樹;在取值相等時,深度小的二叉樹做爲新構造的二叉樹的左子樹,深度大的二叉樹做爲新構造的二叉樹的右子樹。  

 

下圖是權值集合W={8, 3, 4, 6, 5, 5}構造Huffman樹的過程。所構造的Huffman樹的WPL是: WPL=6*2+3*3+4*3+8*2+5*3+5*3 =79

 

 

赫夫曼編碼及其算法

1  Huffman編碼

在電報收發等數據通信中,常須要將傳送的文字轉換成由二進制字符0、1組成的字符串來傳輸。爲了使收發的速度提升,就要求電文編碼要儘量地短。此外,要設計長短不等的編碼,還必須保證任意字符的編碼都不是另外一個字符編碼的前綴,這種編碼稱爲前綴編碼。

Huffman樹能夠用來構造編碼長度不等且譯碼不產生二義性的編碼。

設電文中的字符集C={c1,c2, ⋯,ci, ⋯,cn},各個字符出現的次數或頻度集W={w1,w2, ⋯,wi, ⋯,wn}。

 

Huffman編碼方法

以字符集C做爲葉子結點,次數或頻度集W做爲結點的權值來構造 Huffman樹。規定Huffman樹中左分支表明「0」,右分支表明「1」 。

從根結點到每一個葉子結點所經歷的路徑分支上的「0」或「1」所組成的字符串,爲該結點所對應的編碼,稱之爲Huffman編碼。

因爲每一個字符都是葉子結點,不可能出如今根結點到其它字符結點的路徑上,因此一個字符的Huffman編碼不多是另外一個字符的Huffman編碼的前綴。

若字符集C={a, b, c, d, e, f}所對應的權值集合爲W={8, 3, 4, 6, 5, 5},如上圖所示,則字符a,b, c,d, e,f所對應的Huffman編碼分別是:10,010,011,00 ,110,111。

 

2  Huffman編碼算法實現

(1)  數據結構設計

Huffman樹中沒有度爲1的結點棵有n個葉子結點的Huffman樹共有2n-1個結點,則可存儲在大小爲2n-1的一維數組中。

緣由:

◆ 求編碼需從葉子結點出發走一條從葉子到根的路徑;

◆ 譯碼需從根結點出發走一條到葉子結點的路徑。

1 // 赫夫曼樹和赫夫曼編碼的存儲結構
2 function HuffmanNode(weight, parent, leftChild, rightChild) {
3     this.weight = weight || 0;
4     this.parent = parent || 0;
5     this.leftChild = leftChild || 0;
6     this.rightChild = rightChild || 0;
7 }

Weight:權值域;

Parent:雙親結點下標

leftChild, rightChild:分別標識左、右子樹的下標

 

(2) Huffman樹的生成

 1 // 建立一棵葉子結點數爲n的Huffman樹
 2 function buildHuffmanTree(weights, n) {
 3     n = n || weights.length;
 4     var m = 2 * n - 1;
 5     var huffmanTree = [];
 6 
 7     // 初始化
 8     for (var i = 0; i < n; i++)
 9         huffmanTree[i] = new HuffmanNode(weights[i], 0, 0, 0);
10     for (; i < m; i++)
11         huffmanTree[i] = new HuffmanNode(0, 0, 0, 0);
12 
13     for (i = n; i < m; i++) {
14         // 在HT[1..i-1]選擇parent爲0且weight最小的兩個結點,返回其序號爲[s1, s2]
15         var ret = select(huffmanTree, i);
16         var s1 = ret[0];
17         var s2 = ret[1];
18         huffmanTree[s1].parent = i;
19         huffmanTree[s2].parent = i;
20         huffmanTree[i].leftChild = s1;
21         huffmanTree[i].rightChild = s2;
22         huffmanTree[i].weight = huffmanTree[s1].weight + huffmanTree[s2].weight;
23     }
24 
25     return huffmanTree;
26 }
27 
28 function select(huffmanTree, len) {
29     var ret = [];
30     for (var i = 0; i < len; i++) {
31         var node = huffmanTree[i];
32         if (node.parent !== 0) continue;
33 
34         if (ret.length < 2) {
35             ret.push(i);
36         } else {
37             var index = huffmanTree[ret[0]].weight > huffmanTree[ret[1]].weight
38                 ? 0 : 1;
39 
40             if (node.weight < huffmanTree[ret[index]].weight)
41                 ret[index] = i;
42         }
43     }
44 
45     if (ret[0] > ret[1]) {
46         var temp = ret[0];
47         ret[0] = ret[1];
48         ret[1] = temp;
49     }
50 
51     return ret;
52 }

 

說明:生成Huffman樹後,樹的根結點的下標是2n-1 ,即m-1 。

 

(3) Huffman編碼算法

根據出現頻度(權值)Weight,對葉子結點的Huffman編碼有兩種方式:

① 從葉子結點到根逆向處理,求得每一個葉子結點對應字符的Huffman編碼。

② 從根結點開始遍歷整棵二叉樹,求得每一個葉子結點對應字符的Huffman編碼。

 

由Huffman樹的生成知,n個葉子結點的樹共有2n-1個結點,葉子結點存儲在數組HT中的下標值爲1∽n。

①  編碼是葉子結點的編碼,只需對數組HT[1…n]的n個權值進行編碼;

②  每一個字符的編碼不一樣,但編碼的最大長度是n。

 

算法實現:

 1 function huffManCoding(weights) {
 2     var n = weights.length;
 3     if (n < 1) return;
 4 
 5     var huffmanTree = buildHuffmanTree(weights, n);
 6 
 7     // 從葉子到根逆向求每一個字符的赫夫曼編碼
 8     var hc = calcHuffmanCode(huffmanTree, n);
 9 
10     return [huffmanTree, hc];
11 }
12 
13 function calcHuffmanCode(huffmanTree, n) {
14     // 從葉子到根逆向求每一個字符的赫夫曼編碼
15     var hc = [];
16     var cd = [];
17     for (var i = 0; i < n; i++) {
18         var start = n - 1;
19         for (var c = i, f = huffmanTree[i].parent; f != 0; c = f, f = huffmanTree[f].parent) {
20             if (huffmanTree[f].leftChild == c) cd[--start] = '0';
21             else cd[--start] = '1';
22         }
23 
24         hc[i] = strCopy(cd, start);
25     }
26 
27     return hc;
28 }
29 
30 function strCopy(str, start) {
31     var s = '';
32     for (; str[start]; start++) {
33         s += str[start];
34     }
35     return s;
36 }

 

 

所有代碼實現:

 1 // 赫夫曼樹和赫夫曼編碼的存儲結構
 2 function HuffmanNode(weight, parent, leftChild, rightChild) {
 3     this.weight = weight || 0;
 4     this.parent = parent || 0;
 5     this.leftChild = leftChild || 0;
 6     this.rightChild = rightChild || 0;
 7 }
 8 function huffManCoding(weights) {
 9     var n = weights.length;
10     if (n < 1) return;
11 
12     var huffmanTree = buildHuffmanTree(weights, n);
13 
14     // 從葉子到根逆向求每一個字符的赫夫曼編碼
15     var hc = calcHuffmanCode(huffmanTree, n);
16 
17     return [huffmanTree, hc];
18 }
19 
20 function calcHuffmanCode(huffmanTree, n) {
21     // 從葉子到根逆向求每一個字符的赫夫曼編碼
22     var hc = [];
23     var cd = [];
24     for (var i = 0; i < n; i++) {
25         var start = n - 1;
26         for (var c = i, f = huffmanTree[i].parent; f != 0; c = f, f = huffmanTree[f].parent) {
27             if (huffmanTree[f].leftChild == c) cd[--start] = '0';
28             else cd[--start] = '1';
29         }
30 
31         hc[i] = strCopy(cd, start);
32     }
33 
34     return hc;
35 }
36 
37 // 建立一棵葉子結點數爲n的Huffman樹
38 function buildHuffmanTree(weights, n) {
39     n = n || weights.length;
40     var m = 2 * n - 1;
41     var huffmanTree = [];
42 
43     // 初始化
44     for (var i = 0; i < n; i++)
45         huffmanTree[i] = new HuffmanNode(weights[i], 0, 0, 0);
46     for (; i < m; i++)
47         huffmanTree[i] = new HuffmanNode(0, 0, 0, 0);
48 
49     for (i = n; i < m; i++) {
50         // 在HT[1..i-1]選擇parent爲0且weight最小的兩個結點,返回其序號爲[s1, s2]
51         var ret = select(huffmanTree, i);
52         var s1 = ret[0];
53         var s2 = ret[1];
54         huffmanTree[s1].parent = i;
55         huffmanTree[s2].parent = i;
56         huffmanTree[i].leftChild = s1;
57         huffmanTree[i].rightChild = s2;
58         huffmanTree[i].weight = huffmanTree[s1].weight + huffmanTree[s2].weight;
59     }
60 
61     return huffmanTree;
62 }
63 
64 function strCopy(str, start) {
65     var s = '';
66     for (; str[start]; start++) {
67         s += str[start];
68     }
69     return s;
70 }
71 
72 function select(huffmanTree, len) {
73     var ret = [];
74     for (var i = 0; i < len; i++) {
75         var node = huffmanTree[i];
76         if (node.parent !== 0) continue;
77 
78         if (ret.length < 2) {
79             ret.push(i);
80         } else {
81             var index = huffmanTree[ret[0]].weight > huffmanTree[ret[1]].weight
82                 ? 0 : 1;
83 
84             if (node.weight < huffmanTree[ret[index]].weight)
85                 ret[index] = i;
86         }
87     }
88 
89     if (ret[0] > ret[1]) {
90         var temp = ret[0];
91         ret[0] = ret[1];
92         ret[1] = temp;
93     }
94 
95     return ret;
96 }
97 
98 console.log('-------huffman coding :------');
99 console.log(huffManCoding([5, 29, 7, 8, 14, 23, 3, 11]));
View Code

 

 

回溯法與樹的遍歷

 

在程序設計中,有至關一類求一組解,或求所有解或求最優解的問題,例如八皇后問題等,不是根據某種肯定的計算法則,而是利用試探和回溯(backtracking)的搜索技術求解。回溯法也是設計遞歸過程的一種重要方法,它的求解過程實質上是一個選序遍歷一棵「狀態樹」的過程,只是這棵樹不是遍歷前預先創建的,而是隱含在遍歷過程當中。

 

求含n個元素的集合的冪集:
集合A的冪集是由集合A的全部子集所組成的集合。如:A = {1,2,3},則A的冪集p(A) = {{1,2,3}, {1,2}, {1,3}, {1}, {2,3}, {2}, {2}, 空集}
求冪集p(A)的元素的過程可當作是依次對集合A中元素進行「取」或「舍」的過程,而且可用一棵二叉樹來表示過程當中冪集元素的狀態變化情況。
樹中根結點表示冪集元素的初始狀態(爲空集);
葉子結點表示它的終結狀態;
而第i(i=2,3,...,n-1)層的分支結點,則表示已對集合A中前i-1個元素進行了取/舍處理的當前狀態(左分支表示「取」,右分支表示「舍」)。

 以下圖所示:

 1 // 求含集合aList的冪集
 2 // 進入函數時已對A中前i-1個元素作了取捨處理
 3 function getPowerSet(i, aList) {
 4     var bList = [];
 5 
 6     void function recurse(i, aList) {
 7         if (i > aList.length - 1) console.log('set: ' + bList);
 8         else {
 9             var x = aList[i];
10             bList.push(x);
11             recurse(i + 1, aList);
12             bList.pop();
13             recurse(i + 1, aList);
14         }
15     }(i, aList);
16 
17     return bList;
18 }
19 
20 console.log('getPowerSet:');
21 var list = [1, 2, 3];
22 console.log('list: ' + getPowerSet(0, list));

 

求八皇后問題的全部合法佈局:

同理,咱們能夠用回溯法解決八皇后問題。
首先,咱們把問題簡化爲四皇后問題。
在求解過程,棋盤的狀態變化就像一棵四叉樹,樹上每一個結點表示一個局部佈局或一個完整佈局。根結點表示棋盤的初始狀態:棋盤上無任何棋子。每一個(皇后)棋子都有4個可選擇的位置,但在任什麼時候刻,棋盤的合法佈局都必須知足任何兩個棋子都不佔據棋盤上同一行,或者同一列,或者同一對角線。
求全部合法佈局的過程即爲上述約束條件下先根遍歷狀態樹的過程。

遍歷中訪問結點的操做爲,判別棋盤上是否已獲得一個完整的佈局(即棋盤上是否已有4個棋子)。
如果,則輸出該佈局;
不然依次先根遍歷知足約束條件的各棵子樹,即首先判斷該子樹根的佈局是否合法;
若合法,則先根遍歷該子樹;
不然剪去該子樹分支。

 

爲了通用性,我將代碼實現成n皇后問題:

  1 // 求n皇后問題的全部合法佈局
  2 function Queen(n) {
  3     var board = [];
  4     var count = 0;
  5 
  6     this.init = function(){
  7         for(var i = 0; i < n; i++){
  8             board[i] = [];
  9             for(var j = 0; j < n; j++){
 10                 board[i][j] = 0;
 11             }
 12         }
 13     };
 14 
 15     this.init();
 16 
 17     this.getSolutionsCount = function(){
 18         return count;
 19     };
 20 
 21     this.printCurrentLayout = function () {
 22         ++count;
 23         console.log(board);
 24     };
 25 
 26     this.addPoint = function (i, j) {
 27         if(board[i][j] === 0){
 28             board[i][j] = 1;
 29             return true;
 30         } else {
 31             console.log('already occupated!');
 32             return false;
 33         }
 34     };
 35 
 36     this.isCurrentLayoutLegal = function (i, j) {
 37         return checkHorizontal(i, j) && checkVertical(i, j) && checkLeftTop2RightBottom(i, j) && checkRightTop2LeftBottom(i, j);
 38     };
 39 
 40     function checkHorizontal(x, y){
 41         for(var i = 0; i < y; i++){
 42             if(board[x][i] === 1) return false;
 43         }
 44         for(i = y + 1; i < n; i++){
 45             if(board[x][i] === 1) return false;
 46         }
 47         return true;
 48     }
 49 
 50     function checkVertical(x, y){
 51         for(var i = 0; i < x; i++){
 52             if(board[i][y] === 1) return false;
 53         }
 54         for(i = x + 1; i < n; i++){
 55             if(board[i][y] === 1) return false;
 56         }
 57         return true;
 58     }
 59 
 60     function checkLeftTop2RightBottom(x, y){
 61         for(var i = 1; x - i >= 0 && y - i >= 0; i++){
 62             if(board[x - i][y - i] === 1) return false;
 63         }
 64         for(i = 1; x + i < n && y + i < n; i++){
 65             if(board[x + i][y + i] === 1) return false;
 66         }
 67         return true;
 68     }
 69 
 70     function checkRightTop2LeftBottom(x, y){
 71         for(var i = 1; x - i >= 0 && y + i < n; i++){
 72             if(board[x - i][y + i] === 1) return false;
 73         }
 74         for(i = 1; x + i < n && y - i >= 0; i++){
 75             if(board[x + i][y - i] === 1) return false;
 76         }
 77         return true;
 78     }
 79 
 80     this.removePoint = function (i, j) {
 81         if(board[i][j] === 1){
 82             board[i][j] = 0;
 83         }
 84     };
 85 
 86     var me = this;
 87     this.trial = function trial(i) {
 88         i = i || 0;
 89         if (i > n - 1) {
 90             me.printCurrentLayout();
 91         } else {
 92             for (var j = 0; j < n; j++) {
 93                 if(me.addPoint(i, j)){
 94                     if (me.isCurrentLayoutLegal(i, j)) trial(i + 1);
 95                     me.removePoint(i, j);
 96                 }
 97             }
 98         }
 99     };
100 }
101 
102 var test = new Queen(8);
103 test.trial();
104 console.log('getSolutionsCount: ' + test.getSolutionsCount());
相關文章
相關標籤/搜索