# 2024q1 Homework1 (lab0)
contributed by < [ranvd](https://github.com/ranvd) >
### Reviewed by Shawn531
* 開發紀錄中可以加入 commit 連結以便追蹤。
* 善用 `diff`
### Reviewed by `ShawnXuanc`
可以減少完整程式碼的張貼
程式碼有善用 list 巨集以及將重複的程式碼再利用
### Reviewed by `Shiang1212`
有幾份 commit 沒有撰寫 commit message。即便該 commit 只有簡單的程式更動,仍應該在 commit message 中描述程式碼更動的重點,加速其他開發人員理解這份程式更動的脈絡,進而提高專案的可維護性。
## 開發環境
```shell
$ uname -r
5.15.133.1-microsoft-standard-WSL2
$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 20
On-line CPU(s) list: 0-19
Vendor ID: GenuineIntel
Model name: 12th Gen Intel(R) Core(TM) i7-12700H
CPU family: 6
Model: 154
Thread(s) per core: 2
Core(s) per socket: 10
Socket(s): 1
Stepping: 3
BogoMIPS: 5376.00
```
## 針對指定佇列操作的實作
### `q_new`
:::danger
改進你的漢語表達。
:::
一開始我沒有檢查 `malloc` 的回傳結果,因此當 `malloc` 失敗時回傳的 NULL 會導致在執行 `INIT_LIST_HEAD` <s>巨集</s> 內的程式碼時對 null pointer 取值,進而引發 segmentation fault。因此增加了以下程式碼檢查 `malloc` 是否成功。
:::danger
`INIT_LIST_HEAD` 不是巨集。
:::
```diff
struct list_head *q_new()
{
struct list_head *new = malloc(sizeof(struct list_head));
INIT_LIST_HEAD(new);
+ if (!new)
+ return NULL;
return new;
}
```
### `q_free`
因為 head 有可能是 null pointer,因此在執行 q_free 動作前應該先檢查 head 是否為 NULL,否則會觸發 segmentation fault。
```diff
void q_free(struct list_head *head)
{
element_t *e, *s;
+ if (!head)
+ return;
list_for_each_entry_safe (e, s, head, list) {
q_release_element(e);
}
free(head);
}
```
### `q_insert_head` 與 `q_insert_tail`
由於一開始沒注意到 `strdup` 也透過巨集的方式替換成專案內的實作,在 `make test` 時一直想不透為什麼檢查了 `malloc` 的回傳值後 `q_insert_head` 還會觸發 segmentation fault。在發現 `test_strdup` 內也使用到 `test_malloc` 後就增加下列程式碼,透過檢查 `new->value` 是否為 null pointer 來判斷 `strdup` 是否有執行成功。(`q_insert_tail` 也有相同的更動)
```diff
bool q_insert_head(struct list_head *head, char *s)
{
element_t *new = malloc(sizeof(element_t));
if (!new)
return false;
new->value = strdup(s);
+ if (!new->value) {
+ free(new);
+ return false;
+ }
list_add(&new->list, head);
return true;
}
```
閱讀 [alanjian85](https://hackmd.io/@alanjian85/lab0-2023) 時,看到老師在多處提醒應該盡量重複使用程式碼,因此以下將 `q_insert_tail` 改寫,重複使用 `q_insert_head` 中的程式碼。
```c
bool q_insert_tail(struct list_head *head, char *s)
{
return q_insert_head(head->prev, s);
}
```
### `q_remove_head` 與 `q_remove_tail`
因為 strncpy 會將字串中的前 n 個字元複製到指定的空間,因此當要複製的字串超過 bufsize 時,會使得指定指定的空間中的字串沒有 `'\0'`,因此後續如果在執行 printf, strcpy 等函式的時候會出現錯誤。
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
struct list_head *rm = head->next;
list_del(rm);
if (sp){
strncpy(sp, list_entry(rm, element_t, list)->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
return list_entry(rm, element_t, list);
}
```
也在閱讀 [alanjian85](https://hackmd.io/@alanjian85/lab0-2023) 時,看到老師在多處提醒應該盡量重複使用程式碼,因此以下將 `q_remove_tail` 改寫,重複使用 `q_remove_head` 中的程式碼。
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
return q_remove_head(head->prev->prev, sp, bufsize);
}
```
### `q_delete_mid`
使用快慢指標來找出佇列中的中間點。在重新閱讀自己的實作後,發現 `head == head->next` 應該使用 list.h 內的 `list_empty` 替代,以提高程式碼的可讀性。
```diff
bool q_delete_mid(struct list_head *head)
{
- if (!head || head == head->next)
+ if (!head || list_empty(head))
return false;
struct list_head *slow, *fast;
fast = head;
list_for_each (slow, head) {
if (fast->next == head || fast->next->next == head)
break;
fast = fast->next->next;
}
list_del(slow);
q_release_element(list_entry(slow, element_t, list));
return true;
}
```
### `q_reverse`
透過 list.h 提供的介面 `list_move` 與 `list_for_each_safe` 可以很方便的實作 q_reverse 的功能。
```c
void q_reverse(struct list_head *head)
{
struct list_head *li, *sa;
list_for_each_safe (li, sa, head)
list_move(li, head);
}
```
### `q_reverseK`
由於 q_reverseK 函式要求在佇列中每 K 個節點要進行反轉,因此我將每 k 佇列中的節點放到暫時的佇列 tmp 中,並透過上面提到的 q_reverse 函式將佇列反轉。反轉過後的佇列可以使用 list.h 中提供的介面 list_splice_tail 放到新的佇列 result 中,這樣可以避免一個一個加入節點。
```c
void q_reverseK(struct list_head *head, int k)
{
if (k <= 0)
return;
struct list_head result, tmp;
INIT_LIST_HEAD(&result);
INIT_LIST_HEAD(&tmp);
while (!list_empty(head)) {
struct list_head *p = head->next;
for (int i = 1; i < k && p != head; i++) {
p = p->next;
}
list_cut_position(&tmp, head, (p == head) ? p->prev : p);
if (p != head)
q_reverse(&tmp);
list_splice_tail(&tmp, &result);
}
list_splice(&result, head);
}
```
### `q_swap`
在閱讀 [yanjiew](https://hackmd.io/@yanjiew/linux2023q1-lab0#q_swap) 時,看到 yanjiew 的 `q_swap` 的實作後,發現 `q_swap` 要求每兩個節點相互交換位置,因此可以視為 `q_reverseK(head, 2)`,因此將實作改成以下方式。
```c
void q_swap(struct list_head *head)
{
q_reverseK(head, 2);
}
```
### `q_descend` 與 `q_ascend`
`q_descend` 與 `q_ascend` 要求剩下在佇列中的節點要符合嚴格的遞減/遞增序列,以下以 q_descend 作範例。由於 `q_descend` 要求佇列符合嚴格的遞減序列,因此可以得知,對於任意一個在佇列中的節點,其前面的節點必定要大於自己,且其後面的節點必定小於自己。因此可以從佇列的後面開始遍歷整個佇列,在過程中用指標 max 紀錄目前遇到的最大值的位置。如果在遍歷的過程中 max 有被更動即代表上個 max 與現在 max 的節點符合嚴格遞減序列的要求;反之,如果再遍歷的過程中 max 沒有被更動即代表 max 與現在走訪到的節點不符合嚴格遞減序列,因此要將目前走訪到的節點從佇列中移除。
```c
int q_descend(struct list_head *head)
{
struct list_head *n, *safe, *max = head->prev;
int size = 1;
for (n = max->prev, safe = n->prev; n != head;
n = safe, safe = safe->prev) {
if (strcmp(list_entry(n, element_t, list)->value,
list_entry(max, element_t, list)->value) > 0) {
size++;
max = n;
} else {
list_del(n);
q_release_element(list_entry(n, element_t, list));
}
}
return size;
}
```
`q_ascend` 與 `q_descend` 很像,只需將判斷式從 > 0 改成 < 0 即可,目前想到的重複利用程式碼的方式為使用巨集來做代換。不過這樣只有看起來程式碼比較少,實際上在巨集展開後是一樣的,因此目前沒有實作。
### `element_t_cmp`
因為後續 `q_sort` 與 `q_merge` 需要考慮到不同的排序方式(由 descned 變數控制),因此實作一個比較函式來統一回傳的介面對後續的實作比較方便,程式也可以保持較高的可讀性。
根據 [man 3 strcmp](https://man7.org/linux/man-pages/man3/strcmp.3.html)
> The strcmp() and strncmp() functions return an integer less than, equal to, or greater than zero if s1 (or the first n bytes thereof) is found, respectively, to be less than, to match, or be greater than s2.
由敘述只能得知當兩個不同的字串 s1 與 s2 放到 strcmp 的結果可能是大於 0 或小於 0,並不能確定是否為 1 或 -1,因此此函式也會遵循這個規定。
首先分析可能發生的情況
* a > b 且 descend = 1:回傳一個大於 0 的數值
* a > b 且 descend = 0:回傳一個小於 0 的數值
* a == b:回傳 0
* a < b 且 descend = 1:回傳一個小於 0 的數值
* a < b 且 descend = 0:回傳一個大於 0 的數值
因此可以透過將 `descend << 31` 與 `strcmp` 的結果做 XOR 運算後做 `-` 運算來取得正確的會傳結果。例如 a < b 且 descned=1 時,回傳結果應該要小於 0,透過 `strcmp(a, b) ^ descend << 31`,使得最高位元因為 XOR 運算被設為 0,此時結果大於 0,最後加上 `-` 運算就可使結果小於 0。
```c
static int element_t_cmp(struct list_head *a, struct list_head *b, bool descend)
{
char *a_value = list_entry(a, element_t, list)->value;
char *b_value = list_entry(b, element_t, list)->value;
return -(strcmp(a_value, b_value) ^ (descend << 31));
}
```
### `q_sort`
在 `q_sort` 實作中採用部分 timsort 的想法與 buttom-up 的 merge sort 的合併方式,一開始會將佇列分成多個 run,每個 run 之間使用 prev 指標來連接,形成堆疊的結構,如下圖。每個 run 皆會符合 `descend` 變數中的要求,如果 `descend==1` 則 run 以遞減方式排列;反之,以遞增方式排列。當將所有佇列的節點分成不同的 run 後就會進行 run 之間的合併,合併的方式為每兩個連續的 run 一組,並以組為單位合併,以此方式不斷迭代直到堆疊中只剩下一個 run。
> 假設有 n 個 run,則在第一次迭代後會剩下 $\lceil {n\over 2}\rceil$ 個 run,第二次迭代剩下 $\lceil {n\over 4}\rceil$,以此類推直到剩下一個 run 在堆疊中。
```graphviz
digraph structs{
rankdir=BT
node[shape=record]
run1 [label="<p> run_1|..."];
run2 [label="<p> run_2|..."];
run3 [label="<p> ..."];
run4 [label="<p> run_n|..."];
run4:p -> run3;
run3 -> run2:p;
run2:p -> run1:p;
}
```
```c
void q_sort(struct list_head *head, bool descend)
{
if (!head || list_empty(head))
return;
/* convert circular-linked list into linear-linked list */
struct list_head *list = head->next, *stk_ptr = NULL, *tmp;
list_del_init(head);
list->prev = list->prev->next = NULL;
int stk_size = 0;
while (list) {
tmp = find_run(&list, descend);
stk_size++;
tmp->prev = stk_ptr;
stk_ptr = tmp;
}
list = merge_force_collapse(stk_ptr, descend);
for (struct list_head *safe = list->next;;) {
list_add_tail(list, head);
list = safe;
if (!list)
break;
safe = safe->next;
}
}
```
在合併堆疊中的 run 的過程中,除了需要紀錄堆疊的開頭外,也需要將合併後的 run 放回堆疊中的正確位置,為了省去特殊情況的判斷,在連接 run 與紀錄堆疊的開頭時可以使用指標的指標來實作。
```c
static struct list_head *merge_force_collapse(struct list_head *stk,
bool descend)
{
while (stk && stk->prev) {
struct list_head **iptr = &stk;
while ((*iptr) && (*iptr)->prev) {
(*iptr) = merge_at(*iptr, descend);
iptr = &(*iptr)->prev;
}
}
return stk;
}
```
由於在開發 q_sort 過程中一直觸發 segmentation fault,因此使用 GDB 進行除錯。在過程中一直遇到 `ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient` 的問題。透過 GDB 的 `bt`、`up`、`down` 追蹤程式碼後發現是因為執行時間過長,導致系統發出 `SIGALRM` 的訊號。從 [GDB 5.4 Signals](https://sourceware.org/gdb/current/onlinedocs/gdb.html/Signals.html) 的手冊可以看到,GDB 可以針對不同的 Signal 做出不同的應對方式,在將 GDB 對 `SIGALRM` 的行為設定為 ignore 後就可以正常除錯。
不過在後續閱讀 [README.md](https://github.com/ranvd/lab0-c/blob/master/README.md) 的時候發現,文件已經告知使用 `./script/debug -d` 就可以直接正常的除錯。
### merge_list
用來合併兩個環狀鏈結串列,使用兩層迴圈,外層走訪鏈結串列 `from`,內層走訪鏈結串列 `to`,因為呼叫 `merge_list` 的前提是兩個鏈結串列是已經排序好的,因此內層的迴圈會透過先前提到的 `element_t_cmp` 函式來判斷 `cmp_ptr` 是否指向第一個 `list` 可以插入的位置,等到找到位置後將 `list` 從 `from` 中移出並加入插入至 `cmp_ptr` 前。當兩個迴圈都走訪完後,如果 `from` 還有節點沒有被放入 `to`,則將剩下的節點全部放入 to 的尾端。
```c
static void merge_list(struct list_head *to,
struct list_head *from,
bool descend)
{
struct list_head *list, *safe, *cmp_ptr = to->next;
if (list_empty(from))
return;
list_for_each_safe (list, safe, from) {
while (cmp_ptr != to && element_t_cmp(cmp_ptr, list, descend) > 0) {
cmp_ptr = cmp_ptr->next;
}
if (cmp_ptr == to)
break;
list_del(list);
list_add_tail(list, cmp_ptr);
}
list_splice_tail_init(from, to);
}
```
### q_merge
這裡的 merge 因為要求需要將所有的鏈結串列合併至第一個,因此這個實作並沒有太多的考量,而是以實作上方便做優先,直接將除了第一個以外的鏈結串列合併至第一個鏈結串列。
```c
int q_merge(struct list_head *head, bool descend)
{
// https://leetcode.com/problems/merge-k-sorted-lists/
struct list_head *f = head->next, *m = f->next;
for (; f != head && m != head; m = m->next) {
queue_contex_t *to = list_entry(f, queue_contex_t, chain);
queue_contex_t *from = list_entry(m, queue_contex_t, chain);
merge_list(to->q, from->q, descend);
}
return q_size(list_entry(f, queue_contex_t, chain)->q);
}
```
:::danger
既然你選擇合併排序演算法,你要如何確保輸入的資料涵蓋合併排序演算法的最差狀況呢?
:::
## 確保 linenoise 套件在 web 啟動時正常執行
> 目前只有使用 firefox 瀏覽器測試成功,使用 Chrome 會出現非預期錯誤,整個程式會卡在 web_recv,變成只剩下 web 功能,目前沒辦法解決。
原本在啟動 web 功能後 linenoise 的功能會因為 `use_linenoise` 被設定為 `false` 而關閉,因此我的想法是直接忽略這項變數,在 linenoise 內使用 [select](https://man7.org/linux/man-pages/man2/select.2.html) 系統呼叫同時監控 socket file descriptor 與 stdin file descriptor,就如同[作業中的提示](https://hackmd.io/@sysprog/linux2024-lab0/%2F%40sysprog%2Flinux2024-lab0-f)。
為了能夠在不同的檔案中存取到 socket file descriptor,應將 web_fd 以一般變數的方式宣告。並且在執行 `interpret_cmd` 後應將建立的連線 (`web_connfd`) 關閉,防止 client 端無盡的等待。
```diff
-static int web_fd;
+int web_fd = -1;
bool run_console(char *infile_name)
{
... 略
if (!has_infile) {
char *cmdline;
- while (use_linenoise && (cmdline = linenoise(prompt))) {
+ while (cmdline = linenoise(prompt)) {
interpret_cmd(cmdline);
line_history_add(cmdline); /* Add to the history. */
line_history_save(HISTORY_FILE); /* Save the history on disk. */
line_free(cmdline);
while (buf_stack && buf_stack->fd != STDIN_FILENO)
cmd_select(0, NULL, NULL, NULL, NULL);
has_infile = false;
+ if (web_connfd){
+ close(web_connfd);
+ web_connfd = 0;
+ }
}
- if (!use_linenoise) {
- while (!cmd_done())
- cmd_select(0, NULL, NULL, NULL, NULL);
- }
} else {
while (!cmd_done())
cmd_select(0, NULL, NULL, NULL, NULL);
}
}
```
接著是修改 linenoise 套件的內容,因為 stdin 與 socket 這兩邊的 file descriptor 隨時有可能有輸入,因此選擇使用 `select` 來等待可執行相應動作的 file descriptor。不過這兩個 file descriptor 的輸入行為有很大的區別,socket 這邊的輸入會是完整的<s>指令</s> 請求,不存在<s>指令</s> 命令只打到一半情況,例如使用 web 的方式執行 `new`,傳送過去的<s>指令</s> 命令就只能是 `new`,如果只傳了 `ne` 則會因為沒有辦再修改而直接跳出錯誤;反之,stdin 這邊的輸入可以在任何情況下針對目前還沒傳送出去的<s>指令</s> 命令做修改,且由於需要支援自動補齊,所以每當 stdin 有任何輸入時程式都應該做出相對應的反應。
:::danger
「指令」?
> 更正:command 的中文翻譯是命令
:::
因此我在 linenoise 中宣告了兩個新變數,第一個是 `struct line_state` 靜態全域結構 `g`,用來備份 stdin 輸入時的狀態;第二個是靜態 `bool` 變數 `need_swap` 用來紀錄上一次處理的資料是否是來自 socket 端,如果是的話 need_swap = 1。
```c
static struct line_state g;
static bool need_swap = 0;
```
如果當 stdin 輸入到一半時 socket 要求建立連線,就可以先將目前的狀態存至 `g` 中,並處理 socket 的請求。如果目前處理的是 socket 連線,則 `need_swap = 1`,這代表下一次在做 select 之前需要先將在 g 的資料寫回 l 作為目前的狀況。
```c
} else if (FD_ISSET(web_fd, &read_fds)) {
/* Copy l into g which is used for store current state*/
line_state_write(&g, &l);
need_swap = 1;
FD_CLR(web_fd, &read_fds);
struct sockaddr_in clientaddr;
socklen_t clientlen = sizeof(clientaddr);
...建立連線
```
在處理完 socket 的請求後,依據 `need_swap` 內的數值選擇是否要將 `g` 的資料寫回 `l`,即恢復被中斷前的狀態,且應該要將 `l.buf` 內的資料輸出至畫面中,讓使用者知道自己剛才打的指令內容,這樣即使輸入到一半被中斷,stdin 端的功能不會受影響。
```c
if (need_swap) {
need_swap = 0;
line_state_write(&l, &g);
if (write(l.ofd, l.buf, l.len) == -1)
;
}
...略
int result = select(nfds, &read_fds, NULL, NULL, NULL);
```
執行結果會如下面所展示。當使用者輸入 `my input buffer` 時,有一個 socket 端的請求,會先保存目前的狀態,並處理 socket 的執行內容。接著,恢復剛才 socket 進入前的狀態,並將 `l.buf` 的內容輸出至介面 (即 `my input buffer`)。
```shell
cmd> web
listen on port 9999, fd is 3
cmd> my input buffer
Warning: Calling insert head on null queue
l = NULL
cmd> my input buffer
```
:::danger
文字訊息避免用圖片來表示,否則不好搜尋和分類
> 收到
:::
<s>
![image](https://hackmd.io/_uploads/S1ZHTUS0T.png)
</s>
:::danger
重新張貼「關鍵」程式碼。
:::
### 目前已知此實作的缺陷
除了在一開始提到程式無法在 chrome 瀏覽器上正常的執行外,當按下 tab 鍵使用自動補齊功能時,為了維持功能的正常運作,在 `complete_line` 函式中會再次讀取一個字元,作為後續動作的執行依據。因此當按下 tab 鍵時,網頁端的請求會被 block 住,要等到 stdin 再次按下除了 tab 以外的鍵才可以。
```c
static int complete_line(struct line_state *ls)
{
line_completions_t lc = {0, NULL};
char c = 0;
... 略
int nread = read(ls->ifd, &c, 1);
if (nread <= 0) {
free_completions(&lc);
return -1;
}
switch (c) {
case 9: /* tab */
i = (i + 1) % (lc.len + 1);
if (i == lc.len)
line_beep();
break;
case 27: /* escape */
/* Re-show original buffer */
if (i < lc.len)
refresh_line(ls);
stop = true;
break;
default:
/* Update buffer and return */
if (i < lc.len) {
int nwritten =
snprintf(ls->buf, ls->buflen, "%s", lc.cvec[i]);
ls->len = ls->pos = nwritten;
}
stop = true;
break;
... 略
}
```
## 研讀 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c)
> 以下當變數不為陣列時,使用 `count[k]` 表示第 k 個 bit;`count[k-1:0]` 表示第 k-1 到第 0 個 bits。
從以下註解可以得知 `list_sort` 為 2:1 balance merge sort。即意,在等待合併的鏈結串列中,有兩個長度為 $2^k$ 的鏈結串列時,且在這鏈結串列之後的資料個數總和如果等於 $2^k$ 時就將這兩個鏈結串列進行合併成長度為 $2^{k+1}$ 的鏈結串列。
```
* This mergesort is as eager as possible while always performing at least
* 2:1 balanced merges. Given two pending sublists of size 2^k, they are
* merged to a size-2^(k+1) list as soon as we have 2^k following elements.
```
`count` 為 `list_sort` 中用來記錄目前已經放入等待合併的資料個數。根據以下註解可以得知,當 `count` 數量增加,導致 `count[k]==1` 且 `count[k-1:0]==0` 時,代表有兩個長度為 $2^k$ 的鏈結串列可以被合併成 $2^{k+1}$。除了 `count[k]` 第一次等於 1 時例外 (即 `count` 為 2 的冪時)。
```
* Each time we increment "count", we set one bit (bit k) and clear
* bits k-1 .. 0. Each time this happens (except the very first time
* for each bit, when count increments to 2^k), we merge two lists of
* size 2^k into one list of size 2^(k+1).
```
註解中提到,需要透過 `count[k-1]` 與 `count[n:k+1]` 的資訊得知目前有幾個長度為 $2^k$ 的鏈結串列在等待合併。因此可以分為六種不同的情況,以下 `x` 代表任意的數值;`y` 代表不為 0 的數值,且如果 `k==0` 時 -1 bit 當作是 1。其中第五種情況 (`y01x`) 最為重要,因為代表有兩個長度為 $2^k$ 的鏈結串列在等待合併,當發生上面提到的 `count[k]==1` 且 `count[k-1:0]==0` 時就將兩個 $2^k$ 鏈結串列合併。考慮 `count` 為 (10101)~2~ 時,等待合併的鏈結串列為 8+8+2+2+1,此時同時在 k=3 與 k=1 的位置皆出現第五種 `y01x` 的情況,但再將 `count+1` 後 `count` 為 (10110)~2~ 這時只有 `k=1` 的位置發生了 `count[1]==1` 且 `count[0:0]==0` 的情況,因此將 $2^1$ 合併,等待合併的鏈結串列變成 8+8+(4)+1+1。
```
* The number of pending lists of size 2^k is determined by the
* state of bit k of "count" plus two extra pieces of information:
*
* - The state of bit k-1 (when k == 0, consider bit -1 always set), and
* - Whether the higher-order bits are zero or non-zero (i.e.
* is count >= 2^(k+1)).
*
* There are six states we distinguish. "x" represents some arbitrary
* bits, and "y" represents some arbitrary non-zero bits:
* 0: 00x: 0 pending of size 2^k; x pending of sizes < 2^k
* 1: 01x: 0 pending of size 2^k; 2^(k-1) + x pending of sizes < 2^k
* 2: x10x: 0 pending of size 2^k; 2^k + x pending of sizes < 2^k
* 3: x11x: 1 pending of size 2^k; 2^(k-1) + x pending of sizes < 2^k
* 4: y00x: 1 pending of size 2^k; 2^k + x pending of sizes < 2^k
* 5: y01x: 2 pending of size 2^k; 2^(k-1) + x pending of sizes < 2^k
```
由上面的例子中可以知道,當有同時有兩個不同的數字 $k$ 與 $k'$ 使得正在等待合併的鏈結串列中分別有兩個 $2^k$ 與 $2^{k'}$ 時,會從小的數字開始合併。可以看到,list_sort 中透過 for 迴圈將最低位的 0 找出來後,就去判斷 bits 是否為 0,如果不為 0 就代表符合 `y01x` 的情況,因為 `count` 會在 while 迴圈最後時加一,使得 `count[k]==1` 且 `count[k-1:0]==0`,因此這邊可以先進行合併。
```c
do {
size_t bits;
struct list_head **tail = &pending;
/* Find the least-significant clear bit in count */
for (bits = count; bits & 1; bits >>= 1)
tail = &(*tail)->prev;
/* Do the indicated merge */
if (likely(bits)) {
struct list_head *a = *tail, *b = a->prev;
a = merge(priv, cmp, b, a);
/* Install the merged result in place of the inputs */
a->prev = b->prev;
*tail = a;
}
/* Move one element from input list to pending */
list->prev = pending;
pending = list;
list = list->next;
pending->next = NULL;
count++;
} while (list);
```
當所有的節點都已經從 list 中拿出,代表所有的節點都已經在 pending 中等待合併。因此這時候需要將在 pending 上的多個鏈結串列合併成一個鏈結串列。這時會從長度最小的開始合併,同時因為一開始的限制 (當 $2^k$ 合併成 $2^{k+1}$ 時,合併與未合併的鏈結串列長度總和必須要有 $3\times2^k$),使得在最差的情況下只會是兩個長度為 2:1 的鏈結串列做合併。
```c
/* End of input; merge together all the pending lists. */
list = pending;
pending = pending->prev;
for (;;) {
struct list_head *next = pending->prev;
if (!next)
break;
list = merge(priv, cmp, pending, list);
pending = next;
}
```
### 論文閱讀
在 [Queue-Mergesort](https://www.sciencedirect.com/science/article/pii/002001909390088Q?via%3Dihub) 中利用 Huffman encoding 的方式證明了在最差的情況下,Queue-Mergesort 是 Optimal 的,且 Top-down mergesort 也是。接著在 [The Cost Distribution of Queue-Mergesort, Optimal Mergesorts, and Power-of-2 Rules](https://www.sciencedirect.com/science/article/pii/S0196677498909865?via%3Dihub) 中提到不同的 mergesort 的比較次數可以寫成 $f_n=f_{\tau(n)}+f_{n-\tau(n)}+g_n$ 的遞迴關係式,其中 $g_n$ 代表合併時所需的比較次數。根據不同的實作可以將 $\tau(n)$ 分成三種
- $\tau(x)=\lfloor{n\over2}\rfloor$:Top-down mergesort
- $\tau(x)=2^{\lceil log_2{n\over2}\rceil}$:Buttom-up mergesort
- $\tau(x)=2^{\lfloor log_2{2n\over3}\rfloor}$:Queue-mergesort
在論文中提到這個 $2^{\lfloor log_22n/3 \rfloor}$ 是一個特別的數字,可以使最差的情況下要合併的兩邊資料長度為 2:1,如果使用其他的數字就可能導致 $f_n$ 在分割時兩邊的差距過大。
> Note that $2^{\lfloor log_22n/3 \rfloor}$ is the unique power of two lying between $n/3$ and $2n/3$ and that the choice of rationals other than $2/3$ will not be more balanced. For example, if $2^{\lfloor log_2n/2 \rfloor}$ or $2^{\lfloor log_25n/9 \rfloor}$ then the sizes of two subproblems are $(2,5)$ for $n = 7$ while the balanced power-of-two gives $(3,4)$
因為 Queue-mergesort 可以表示成 Huffman Tree,因此在 [On the Optimality of Huffman Trees](https://epubs.siam.org/doi/10.1137/0131030) 的內容可以套用至 Queue-mergesort,論文中說明 Huffman Tree 的遞迴關係中的 k 是 $2^{\lfloor log_2{2n\over3}\rfloor}$ 而不是其他數字或 $\lfloor2n/3\rfloor$。
> $$F(n)=1-q^n+\min_{1\le k\le n-1}{F(k)+F(n-k)}$$ Since the function $f(n)=1-q^n$ is concave nondecreasing, the "power of 2" rule applies, and the optimal $k$ is the power of 2 satisfying $$n/3\le k\lt 2n/3$$
最後透過 Geogebra 觀察函式在不同 n 時的數值,下圖中 $f$ 代表 $2^{\lfloor log_22n/3 \rfloor}$,$h$ 與 $g$ 分別代表 $2n/3$ 與 $n/3$。可以看到確實 $f$ 會在 $h$ 與 $g$ 之間。這讓 mergesort 在合併兩個鏈結串列時可以維持在最差長度為 2:1 的情況。
![image](https://hackmd.io/_uploads/Syiz4qL0p.png)
不過論文中很多部分還不能理解,例如為什麼 Buttom-up mergesort 的數值是 $2^{\lceil log_2{n\over2}\rceil}$。還有許多推倒的過程不太能理解,只能記住結論而已。
###
:::danger
HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」,不是用來「假裝自己有付出」
:::