# 2018q3 Homework6
contributed by < `TerryShu` > 、 < `ofAlpaca` >
###### tags: `CSIE5006` `HW`
## [Huffman Coding](https://hackmd.io/s/S1azdiUiX#)
此程式碼可以分為兩個部分,第一是**建立 Min Heap** ,第二是**建立 Huffman Tree**。
### 建立 Min Heap
* **Min Heap** : 父節點的值會小於下方 subtree 的所有節點的值,且 root 為所有節點中最小值。
* 步驟大致上可分為 :
1. 計算文件中各字元出現的次數,此紀錄會存在 `dictionary` 中。其中需要注意的是 `dictionary` 結構並非單純的陣列而已,它是**以字元的 ASCII 編碼**來作為 index 存入對應的位置,換句話說,如果讀到 ABC 這三個字元,則會分別會增加 `dictionary[65]` 、 `dictionary[66]` 、 `dictionary[67]` 的 weight ,其餘位置的 weight 則為 0 。
**附註 : `calloc` 會對陣列做初始化,所以沒用到的 node_t 的 weight 會是 0 ,而非 garbage value 。**
![](https://i.imgur.com/yVozVJ1.png)
```clike=
min_heap_t *scan_file(FILE *in) {
node_t *dictionary = calloc(ASCII_SIZE, sizeof(node_t));
min_heap_t *heap = calloc(1, sizeof(min_heap_t));
int ch;
for (;;) {
if ((ch = fgetc(in)) == EOF)
break;
dictionary[ch].weight++;
printf("%d\n", ch);
}
for (ch = 0; ch < ASCII_SIZE; ch++) {
if (dictionary[ch].weight == 0)
continue;
dictionary[ch].data = ch;
add_to_heap(&(dictionary[ch]), heap);
}
rewind(in);
return heap;
}
```
2. 建立好 `dictionary` table 後,會逐一加入 weight 不為 0 的 ASCII 字元來建立 Min Heap 。最後生成的 Min Heap 的 root 為最小值,父節點的值小於子節點的。
* 使用一維陣列來表示 Binary Heap :
![](https://i.imgur.com/afStepN.png)
* 下方迴圈負責**維護 Min Heap 的特性**,每次加入新節點時都去檢查其 weight 是否小於前面節點的,如果是則往前交換,換到有更小的 weight 出現為止,或是到達 root 。
```clike=
static void add_to_heap(node_t *to, min_heap_t *heap) {
int pos = heap->size++;
heap->array[pos] = to;
if (heap->size > 2) { // XXE
while ((heap->array[pos - 1])->weight > (heap->array[pos])->weight) {
swap_node(&(heap->array[pos - 1]), &(heap->array[pos]));
if (--pos == 0)
break;
}
}
}
```
:::info
**問題**:question:
第四行有個 `XXE` 的填空題,答案為 `heap->size > 2` ,看到這裡會想,為什麼 size 為 2 的時候不用檢查,那如果今天只有兩個節點,先加入的 weight 較大,後加入的 weight 較小,如果後者不往前交換不就違背了 Min Heap 定義嗎 ? 如果前後兩個節點的 weight 相差極大的話,還會影響壓縮率。
:::
**實驗** : 比較 `heap->size > XXE` 中的 `XXE` 為 2 或 1 時的壓縮率。
* 測試文件 input 的內容 :
```
AAAAAAAAAAAAAAAAABBCC
```
* 如果 XXE = 2 :
```
$ ./huff input
C : 0
A : 10
B : 11
Compressing...
Achieved 71.428571 % compression.
```
* 圖解 :
![](https://i.imgur.com/9DFxKLq.png)
* 如果 XXE = 1 :
```
$ ./huff input
B : 00
C : 01
A : 1
Compressing...
Achieved 80.952381 % compression.
```
* 圖解 :
![](https://i.imgur.com/z5EfTSy.png)
* 經過測試後發現當 `XXE` 為 1 時壓縮率反而優於 2 ,因為當 `XXE` 為 2 時所求出的 Huffman tree 不正確,照理來說,出現頻率越高的字元應該有更短的 Huffman code 。
---
### 建立 Huffman Tree
* 可分為兩步驟,一是 `build_hufftree` ,二是 `build_pairings` :
1. `build_hufftree` :
* 一開始的 Min Heap 。
![](https://i.imgur.com/qdpLEBJ.png)
* 先將 Min Heap 中最小的前兩個節點合而為一,並再重新加入 Heap 中。這裡的最小前兩個節點是指 `array[0]` 與 `array[1]` ,前者為 lighter node(left node) ,後者為 heavier node(right node) 。
![](https://i.imgur.com/PopDotC.png)
* 為維護 Min Heap ,故往上交換。
![](https://i.imgur.com/H68PnFT.png)
* 因為已經將前兩小的節點結合,所以需要將原本位置的他們移除,故做 `shift_up_2()` 也就是將所有節點往上移兩個單位。
![](https://i.imgur.com/hnofHAA.png)
* `shift_up_2()` 第八行有 `XXC` 的填空題,因為所有節點向上位移兩個單位,所以 size 要減少 2 。
```clike=
static void shift_up_2(min_heap_t *heap) {
int i = 0;
while (i < heap->size) {
(heap->array)[i] = (heap->array)[i + 2];
(heap->array)[i + 1] = (heap->array)[i + 3];
i++;
}
heap->size -= 2; // XXC
}
```
* 重複同樣步驟,最後的 `array[0]` 即是 Huffman tree 的 root 。
![](https://i.imgur.com/KYo0pBD.png)
* 第六行有 `XXD` 的填空題,根據上述可得答案為 `array[0]` 。
* Huffman tree 的建置到此告一段落。
```clike=
static node_t *build_hufftree(min_heap_t *heap) {
while (heap->size > 1) {
add_to_heap(combine_node_ts(heap->array[0], heap->array[1]), heap);
shift_up_2(heap);
}
return heap->array[0]; // XXD
}
```
2. `build_pairings` :
* 這部分負責產生編碼用的 table ,換句話說,就是要找出所有 ASCII 字元符號相對應的 Huffman code 。
* `build_pariings` 函式從 root 開始遞迴呼叫左子樹與右子樹,在進入遞迴前會先記錄進入子樹的方向,往左子樹則紀錄 0 ,往右子樹則紀錄 1 ,這些記錄會寫在陣列 `array` 上, `top` 為此陣列的高度,代表目前節點的 depth。
* 第四、八行有 `XXA` 與 `XXB` 的填空題,答案都為 `build_pairings(root->right, arr, top + 1, pairs)` ,只需要增加陣列的高度 `top` 即可, `arr` 是不用去增加的。
* 當遞迴至左子樹與右子樹都為 `null` 的節點時,代表此節點為葉節點,可以輸出某字元的 Huffman code 至 `pairs` 的陣列中。
* `pairs` 陣列與 `dictionary` 相似,都是以字元的 ASCII 編碼來作為 index 。
* 最後的 `pairs` 會是一個 ASCII 編碼對應 Huffman code 的 table ,接下來做 encode 時只需要將文件中的各字元轉換為 Huffman code 即可。
```clike=
void build_pairings(node_t *root, int arr[], int top, pair_t *pairs) {
if (root->left) {
arr[top] = 0;
build_pairings(root->left, arr, top + 1, pairs); // XXA
}
if (root->right) {
arr[top] = 1;
build_pairings(root->right, arr, top + 1, pairs); // XXB
}
if (!(root->left) && !(root->right)) {
uint8_t index = root->data;
for (int i = 0; i < top; i++)
pairs[index].arr[i] = arr[i];
pairs[index].length = top;
pairs[index].data = root->data;
}
}
```
---
### Encode & Decode
* Encode
* `encode` :
* 一開始先讀入一個 `character`
* 接著在建立好的 ASCII 配對表 `pairs` 中找到其對應的 `Huffman code`
* 並用填一 bit , 左移一位的方式填入 `buffer` 中
* 最後當填完 8 bit (1 byte) 時 ,透過 `stream` 寫入檔案中
* 則最後一位若不足 8 bit 則左移直到填滿 8 bit 為止
```clike=
void encode(FILE *in, FILE *out, pair_t *pairs) {
int curr_size = 0;
unsigned buffer = 0;
for (;;) {
int ch = fgetc(in);
if (ch == EOF)
break;
int i = 0;
while (i++ < pairs[ch].length) {
buffer <<= 1;
buffer += (pairs[ch].arr)[i];
curr_size++;
if (curr_size == 8) {
printf("%d\n",buffer);
fwrite(&buffer, 1, 1, out);
curr_size = 0;
buffer = 0;
}
}
}
while (curr_size < 8) {
buffer <<= 1;
curr_size++;
}
rewind(in);
printf("%d\n",buffer);
fwrite(&buffer, 1, 1, out);
fclose(out);
}
```
:::info
**Bug** :question:
此處提供之 `source code` 應該有誤
第 10 行的判斷條件
```clike
while (i++ < pairs[ch].length)
```
此寫法會先用 `i` 去跟 `pairs[ch].length` 比較是否小於
之後做 `++` 這個動作會導致第一次進入迴圈時 i 的值就是 `1`
則後續取 `Huffman code` 時會出現錯誤
```clike=
buffer += (pairs[ch].arr)[i]; // should start from 0 not 1
```
故將上面程式碼中的 10~19 行的 `while` 改成以下
```clike=
while (i < pairs[ch].length) {
buffer <<= 1;
buffer += (pairs[ch].arr)[i];
curr_size++;
if (curr_size == 8) {
printf("%d\n",buffer);
fwrite(&buffer, 1, 1, out);
curr_size = 0;
buffer = 0;
}
i++;
}
```
:::
* Decode
* `decode` : 利用走訪 MinHeap 達到 Decode 要求
* 一開始從要解密檔案的 `stream` 內讀入一個 `byte`
* 接著將 `byte` 轉換為 `bit` 遵循以下規則走訪 `MinHeap`
1. 當 `bit` 為 `1` 時往右 `0` 時往左
2. 若走到的節點為 `leaf node` 則為結果
* 當解碼出來的字是 `EOF` 時表示完成解碼
```clike
void decode(FILE *in, min_heap_t *heap) {
unsigned char buffer;
long filelen;
fseek(in, 0, SEEK_END); // Jump to the end of the file
filelen = ftell(in); // Get the current byte offset in the file
rewind(in); // Jump back to the beginning of the file
node_t *root = heap->array[0];
node_t *temp = root;
bool stop = false;
printf("Decoding...\nPlaintext:\n");
for (int i = 0; i < filelen; i++) {
fread(&buffer, 1, 1, in); // Read One Byte
for (int j = 0; j < 8; j++) {
unsigned first_bit = buffer >> 7;
buffer <<= 1;
if (first_bit)
temp = temp->right;
else
temp = temp->left;
if (((int)temp->data)) {
if (((int)temp->data) == ASCII_EOF) {
stop = true;
break;
}
printf("%c", temp->data);
temp = root;
}
}
if (stop) {
printf("\n");
break;
}
}
}
```
:::info
**問題** :question:
一開始的 `Encode` 並無加入一些控制字元如 `EOF` 則有可能出現以下狀況
* 輸入內容 :
```
AAAABBCC
```
* 經過編碼後
```
編碼表 :
A : 1
B : 00
C : 01
編碼 :
11110000 0101(0000)
↑
補0
```
* 解碼
```
11110000 01010000
AAAABBCCBB
```
會發現最後面的 補0 會導致錯誤
所以解決方法是補上一個 `EOF` 符號
一起去建立 `Huffman code` 則可解決問題
修改後
* 輸入內容
```
AAAABBCC(EOF)
```
* 經過編碼後
```
編碼表 :
A : 1
B : 011
C : 00
EOF : 010
編碼 :
11110110 11000001 0(0000000)
↑
補0
```
* 解碼
```
11110110 11000001 00000000
AAAABBCC(EOF) 當 EOF 出現後後面就不用再進行解碼
```
經過加入一個 EOF 去處理結尾的問題後發現有可能會因為新增了一個新的 node 導致壓縮率變差但若不加則無法保證正確性
:::
---
## 延伸題目
1. 回顧 dict 作業,提出資料壓縮的方案;
* 深入理解 dict 中所建構的 ternary tree 後可以發現其建立的 tree 並非如 [Ternary Search Tree Visualization](https://www.cs.usfca.edu/~galles/visualization/TST.html) 裡頭所顯示的那樣,以綠色標記出一個單字的結尾, **[dict](https://hackmd.io/s/B1Bq5Jat7) 是以空白的節點來當作單字的結尾**,比較如下圖 : (輸入順序 `CODE` -> `CODES`)
* 原本所認知的 Ternary tree :
![](https://i.imgur.com/hnf5JkR.png)
* dict 中所產生的 Ternary tree :
* `S` 節點為何會連向上個結尾節點的 right child ? 那是因為當 `CODES` 比較到 `S` 時是和空白節點比 key value ,因為 `S` 的 key value 大於空白,所以會往 `high child` 移動。
![](https://i.imgur.com/EBms1YM.png)
* 但是更好玩的還在後頭,如果改變插入順序是先 `CODES` 再 `CODE` ,就會變成下圖。當 `CODE` 單字在 tree 中找完後會停留在 `S` ,接著由空白字元與 `S` 節點的 key value 做比較,想當然空白字元會較小,所以接下來才會往 `left child` 移動。
![](https://i.imgur.com/FT6ANeW.png)
* 順帶一提,每個結尾節點的 `middle child` 還會指向當時 input 的那個單字,所以整體的資料結構應該如下。
![](https://i.imgur.com/KWIkjTG.png)
* 壓縮方法 :
由上圖可以知道總共花了 7 個節點來建立整棵樹,如果把 `C` 到 `E` 的部分合成一個節點,那就只需要 4 個節點就可以建立整棵樹,藉此達到壓縮的目的。
![](https://i.imgur.com/lIfmi3J.png)
* 實作方面需要先從資料結構做修改,將原本的 key value 改為 pointer to character ,好讓節點的 key value 可以儲存超過一個的 key 。
```clike=
typedef struct tst_node {
char *key;
unsigned refcnt;
struct tst_node *lokid; /* ternary low child pointer */
struct tst_node *eqkid; /* ternary equal child pointer */
struct tst_node *hikid; /* ternary high child pointer */
} tst_node;
```
* 接下來開始對建構出來的 ternary tree 做壓縮,其步驟可分為下 :
1. 由 `root` 開始往下移動直到**有分岔或是結尾的節點**出現 :
* 有分岔的節點意思是其 `left child` 與 `right child` 不為 `NULL` ,這代表這些單字的最長 prefix 就此結束,所以可壓縮節點到此為止。
* 結尾節點出現代表說這些單字的 prefix 也是一個單字,所以此單字可以壓縮成一個節點。
2. 將這些節點的 key value 取出並寫入至第一個節點的 key value ,如此一來在壓縮 tree 後才能保有原本的 key value 。
![](https://i.imgur.com/ac4OnnQ.png)
3. 把除了第一個節點外的節點刪除,並將第一個節點的 `middle child` 指向分岔或結尾的節點。
![](https://i.imgur.com/0sMmaOo.png)
4. 就此,已經壓縮完從 `root` 到分岔或結尾前的節點,接下來從分岔或結尾的節點的 child 開始做 DFS 遍歷,重複上述步驟,遍歷完整棵樹後即停止。如果遍歷途中遇到結尾節點則不走訪其 `middle child` ,因為結尾節點的 `middle child` 都是指向一個字串。
![](https://i.imgur.com/G4c362a.png)
* 較多節點的例子 ,從原本的 15 個節點壓縮成 11 個節點。
![](https://i.imgur.com/Bh9jihj.png)
```c=
void tst_compress(tst_node **p)
{
if (!(*p))
return;
char buffer[BUFFER_SIZE];
memset(buffer, '\0', BUFFER_SIZE);
int i = 0, free_cnt = 0;
tst_node *root = *p;
tst_node *curp = *p;
// find the longest prefix and collect it
while ((curp->hikid == NULL) && (curp->lokid == NULL) && *(curp->key)) {
buffer[i++] = curp->key[0];
tst_node *free_ptr = curp;
curp = curp->eqkid;
if (root != free_ptr) {
free(free_ptr->key);
free(free_ptr);
free_cnt++;
}
}
// No released node,then no need to concat key value
if (free_cnt > 0) {
root->eqkid = curp;
free(root->key);
root->key = strndup(buffer, i);
}
tst_compress(&(curp->lokid));
if (curp->key[0])
tst_compress(&(curp->eqkid));
tst_compress(&(curp->hikid));
}
```
---
2. 實作程式正確驗證和效能分析機制;
* **完整性驗證** : 經過壓縮後的樹是否有遺失或增加單字
* 驗證方法 : 使用 DFS 尋訪原本的樹與壓縮後的樹,一旦找到了「單字」,就將其寫入各自的檔案,最後使用 md5 hash function 去驗證兩檔案是否擁有一樣的 hash value ,有則表示壓縮後的樹是完整的。
* 此函式會透過 DFS 尋訪各節點,找到「單字」的結尾節點後就寫入檔案。
```c=
void tst_traverse_seq(const tst_node *p, char arr[], int top, FILE *fp)
{
if (!p)
return;
tst_traverse_seq(p->lokid, arr, top, fp);
if (p->key[0]) {
int size = strlen(p->key);
strncpy(arr + top, p->key, size);
tst_traverse_seq(p->eqkid, arr, top + size, fp);
} else {
arr[top] = '\n';
fwrite(arr, top + 1, 1, fp);
}
tst_traverse_seq(p->hikid, arr, top, fp);
}
```
* 執行 `test_tst` 後會產生 `raw_data.txt` 紀錄著原本樹的走訪順序,與 `compress_data.txt` 紀錄壓縮後樹的走訪順序,比較兩者經過 md5 的 hash value 即可得知是否有遺失或增加任何單字。
* 就結果而言,兩者的 hash value 皆為 `99cfdb4c44ea28808cdbc41eccc1d2ea` ,故壓縮後的樹應該是完整的。
```
$ md5sum raw_data.txt
99cfdb4c44ea28808cdbc41eccc1d2ea raw_data.txt
$ md5sum compress_data.txt
99cfdb4c44ea28808cdbc41eccc1d2ea compress_data.txt
```
* **壓縮率** :
* 計算方法 : 建制原本的 Ternary tree 與壓縮後的樹,計算各自花費的節點與其內部 key value 所 malloc 的空間,以下為計算方式:
$Compress\ ratio = 100 - ((\dfrac{compressed\ tree\ size} {original\ tree\ size})\cdot100)$
* 結果 :
計算結果為壓縮了 `51.763%` ,但尚未找到能正確算出耗費記憶體多寡的方法。
```
$ ./test_tst
ternary_tree, loaded 259112 words in 0.151419 sec
Compressing
ternary_tree, compressed tree in 0.026532 sec
Compress ratio : 51.762904 %
```
* **CPY 與 REF 差異**
* 前面的結果都是使用 `test_ref.c` 的方式來配置記憶體( memory pool ),以下測試為比較使用一般 `malloc` 與 memory pool 的差別。
* 使用 memory pool :
```
$ perf stat --repeat 100 -e cache-misses,cache-references,instructions,cycles ./test_tst -s
Performance counter stats for './test_tst -s' (100 runs):
5,550,240 cache-misses # 58.325 % of all cache refs ( +- 0.12% )
9,516,017 cache-references ( +- 0.06% )
988,553,351 instructions # 0.86 insn per cycle ( +- 0.00% )
1,142,911,602 cycles ( +- 0.08% )
0.310643358 seconds time elapsed ( +- 0.09% )
```
* 使用一般 `malloc`
```
$ perf stat --repeat 100 -e cache-misses,cache-references,instructions,cycles ./test_tst_cpy -s
Performance counter stats for './test_tst_cpy -s' (100 runs):
5,935,431 cache-misses # 61.311 % of all cache refs ( +- 0.43% )
9,680,913 cache-references ( +- 0.24% )
1,016,060,042 instructions # 0.84 insn per cycle ( +- 0.01% )
1,207,350,409 cycles ( +- 0.34% )
0.332096963 seconds time elapsed ( +- 0.43% )
```
* 由上圖可見,使用 memory pool 在各方面都比使用一般 `malloc` 的稍微好一點,原因應該如 [dict](https://hackmd.io/s/r1eSxvo5m) 中所述的和 data locality 有關。
---
## 參考資料
[Huffman Encoding and Data Compression](https://web.stanford.edu/class/archive/cs/cs106b/cs106b.1126/handouts/220%20Huffman%20Encoding.pdf)
[ASCII WIKI](https://en.wikipedia.org/wiki/ASCII)