---
# System prepended metadata

title: 2025q1 Homework1 (lab0)

---

# 2025q1 Homework1 (lab0)
contributed by <`DrXiao`>

{%hackmd NrmQUGbRQWemgwPfhzXj6g %}

## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.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):                   24
On-line CPU(s) list:      0-23
Vendor ID:                GenuineIntel
Model name:               13th Gen Intel(R) Core(TM) i7-13700F
CPU family:               6
Model:                    183
Thread(s) per core:       2
Core(s) per socket:       16
Socket(s):                1
Stepping:                 1
CPU(s) scaling MHz:       23%
CPU max MHz:              5200.0000
CPU min MHz:              800.0000
BogoMIPS:                 4224.00  
L1d:                      640 KiB (16 instances)
L1i:                      768 KiB (16 instances)
L2:                       24 MiB (10 instances)
L3:                       30 MiB (1 instance)                     
NUMA node(s):           1
NUMA node0 CPU(s):      0-23
```

<s>
* GitHub 檔案庫：[DrXiao/lab0-c](https://github.com/DrXiao/lab0-c)
</s>

:::danger
不用在此提及 repository 資訊，你該聚焦在細節。

注意 markdown 的項目編號，也就是內文該用 `##` 和 `###`，避免過深的縮排
:::

## 開發前處理
在開發 labo-c 時，因為 sysprog21/lab0-c 本身後來又有不少 pull request 被合併，故每當有新的修改，會隨時同步到自己的檔案庫後，才繼續開發下去。

## 為所有佇列操作介面提供初步實作
* 對應到 commit [aea1a33](https://github.com/DrXiao/lab0-c/commit/aea1a332dde9a694cac43bbbe9bb9b3e591b8af5)

在 GitHub 上產生 lab0-c 的檔案庫後，第一個想法是先為所有的佇列操作撰寫可以運作的程式碼，先了解每個操作函式應該要達成什麼目的，爾後尋找改進的機會，接著會一一說明目前的實作原理

### `q_new()`
以 lab0-c 設計的佇列操作介面來說，會設想開發者會以 Linux 核心風格的 `list_head` 結構體，以該結構體的指標去維護整個佇列，因此 `q_new()` 的目的即是適當地建立一空佇列，動態配置記憶體後，將佇列本身的指標作為該函式的回傳值，爾後讓開發者以其他介面操作佇列。

故這裡的步驟很簡單：
* 動態配置一個 `list_head` 的物件，協助初始化該物件的指標後，就回傳該物件的記憶體位置。
* 但若遭遇動態配置失敗，則回傳 `NULL`。（爾後使用佇列的開發者要自行注意對應的處理）

因此目前 `queue.c` 中，即可見到上面步驟的實作

### `q_free()`
因為是以動態配置記憶體的方式，提供開發者使用佇列，自然也要有對應的釋放記憶體的操作，讓開發者釋放不再使用的佇列。

而這裡做的事情，**自然是逐一走訪一個佇列中仍存在的節點，並對每一個節點進行記憶體的釋放，最後再釋放 `q_new()` 而來的 `list_head` 物件**（即佇列的開頭指標）。

而這裡的實作步驟如下：
* 以 `list_for_each_safe()` 巨集，配合兩個 `list_head` 指標，分別命名為 `curr` 、 `safe`，走訪所有節點
* 使用 `element_t` 的指標、`curr` 指標再加上 `list_entry()` 巨集，即可取得節點的開頭位址，爾後
    * 對每一個節點進行移除操作，將前一節點、下一節點互相鏈結起來
    * 先釋放節點維護的資料，即 `element_t` 的 `value` 的記憶體，再釋放節點本身的記憶體
* 走訪完所有節點以後，最後釋放 `head`


<s>warning</s>

:::danger
不要濫用 `warning` 標示
:::


但這裡其實最初實作時，是使用 `list_for_each_entry_safe` 處理，但在要 git commit 時，Git hook（或是說 `cppcheck` ）會產生下面的訊息：
```
Following files need to be cleaned up:
queue.c
Running static analysis...
queue.c:35:5: style: Label 'int' is not used. [unusedLabel]
    list_for_each_entry_safe (curr_elem, safe_elem, head, list) {
    ^
queue.c:272:5: style: Label 'int' is not used. [unusedLabel]
    list_for_each_entry (curr_elem, head, list) {
    ^
queue.c:303:5: style: Label 'int' is not used. [unusedLabel]
    list_for_each_entry (curr_elem, head, list) {
    ^

Fail to pass static analysis.
```

似乎是 `cppcheck` 認為我定義了一個名為 `int` 的 jump label（可用 `goto` 進行無條件跳躍），但沒有被使用從而輸出這道訊息，類似的情形也發生在 `list_for_each_entry` 上。

目前這裡不確定是我當時候使用上真的有問題，還是 Git hook 撰寫的規則有潛在瑕疵，或許後續再擇時來釐清。

### `q_insert_head()` 、 `q_insert_tail()`
這兩個函式都是為佇列增加節點的操作，共通點都是會給定「佇列的開頭指標」、「新節點的資料」，唯獨差別在於前者要將新節點安插在佇列的第一個位置，而後者則要將新節點插入在佇列的最尾端。

故這裡的步驟如下：

1. 先動態配置 `element_t` 物件
2. 再利用 `element_t` 的 `value` 指標，配置記憶體。
    * 因為此處佇列每一節點的資料即是一道字串，故使用 `strdup()`，直接完成動態配置及複製字串的操作
3. 將新節點安插至適當位置後，便回傳 `true`
    * 以 `q_insert_head()` 來說，就要使用 `list_add()`，即可將節點插入在第一個位置
    * 相對地，`q_insert_tail()` 就是使用 `list_add_tail()`，將節點放在最後一個位置
4. 若遭遇動態配置失敗，釋放已配置的記憶體後，就回傳 `false`

### `q_remove_head()` 、 `q_remove_tail()`
類似於前面討論增加節點，這兩個函式都是移除節點，差別僅在於要操作的佇列的開頭節點還是尾端的節點。

然而，以這兩個函式的設計來說，若開發者有給定合法的字元陣列及其大小的話，就要將移除節點的資料，複製到該字元陣列裡面。最後移除成功後，要將所移除的節點的記憶體位址作為回傳值，回傳給呼叫者

處理步驟：
1. 若佇列開頭指標是 `NULL`，或是佇列本身是空佇列，就直接回傳 `NULL`
2. 若有節點可移除，就利用 `element_t` 指標、佇列的開頭指標、`list_entry()`，取得某個節點的開頭位址
    * `q_remove_head` 要使用 `head->next` ，配合巨集來取得第一個節點的開頭位址
    * `q_remove_tail` 則要用 `head->prev` ，配合巨集取得最後一個節點的開頭位址
3. 若有給定合法的字元指標（`sp` 不為 `NULL`），就利用 `strncpy()` 、 `bufsize` ，將移除節點的資料，複製到 `sp` 裡。
4. 最後利用 `list_del()` 斷開移除節點的鏈結，並回傳移除節點的開頭位址
    * `q_remove_head` 使用 `list_del()` 時要帶入 `head->next`。
    * `q_remove_tail` 則帶入 `head->prev`。

不過**目前的實作存在一些缺失，導致 `trace-07-string` 的測試項目失敗**，後續會釐清並改進
### `q_size()`
這個函式是用來計算並回傳佇列所包含的節點數量，實作上只需要透過一個 `list_head` 指標一個計數器，搭配 `list_for_each()` 巨集，逐一走訪每一個節點並對計數器的值加 1 即可。

而當然地，若給定的佇列開頭指標不合法或是給定一空佇列，直接回傳 0 即可。

### `q_delete_mid()`
這個函式的用意是，刪除佇列的正中間的那個節點，這裡以兩個例子舉例

* **包含奇數個節點的佇列**

    若給定一個奇數個節點（假設數量是 $n$）的佇列，要刪除的就是第 $\frac{n+1}{2}$ 個節點。

    假設 $n$ 是 5 為例，要刪除的就是第 3 個節點（正中間的節點）
    ![delete-mid-n5](https://hackmd.io/_uploads/S10W0HJjyl.png)
* **包含偶數個節點的佇列**

    給定一個奇數個節點（假設數量是 $n$）的佇列，要刪除的就是第 $\frac{n}{2}$ 個節點。

    假設 $n$ 是 4 為例，要刪除的就是第 2 個節點。因為偶數的關係，對於第 2 與第 3 個節點來說，都可說是「正中間」，但此處定義要刪除的是最靠近 `head` 的那個中間節點
    ![delete-mid-n4](https://hackmd.io/_uploads/S1CwCB1oyg.png)

---
因為佇列在 `q_new()` 時，僅使用 `list_head` 指標維護整個佇列，除此之外，不會儲存其他資訊。因此最直接的作法，可以先走訪整個陣列一次，得知佇列節點的數量後，再走訪到正中間的節點並移除。

不過在指標操作上，可以使用**快慢指標**的技巧，準備兩個 `list_head`，分別命名為 `slow`、`fast`，都先指向第一個節點，爾後利用迴圈走訪佇列時：
* `slow`: 每次走訪一個節點後，前往下一個節點，即 `slow->next`
* `fast`: 每次走訪一個節點後，前往下下一個節點，即 `fast->next->next`

這裡將兩個指標的移動以走路的步數來比喻，因為每次移動時，`fast` 會比 `slow` 還要快一步，故當 `slow` 走了 $n$ 步時，`fast` 就走了 $2n$ 步。基於這樣的特性也就代表，**若 `fast` 已經抵達佇列的尾端節點，`slow` 剛好走到整個佇列的一半，也就是正中間的節點**。如此，透過快慢指標，只要用一個迴圈讓兩個指標走訪，`fast` 走訪完整個佇列的那一刻，`slow` 所指向的節點就是正中間的節點。

因此在 `q_delete_mid()` 的實作裡，就使用前述的兩個指標、一個 `element_t` 的指標（`del_elem`），在 `fast` 走訪完佇列後，讓 `del_elem` 利用 `list_entry()` 與 `slow`，取得正中間的節點本體，最後再進行刪除即可。

:::danger
考慮到 List API 針對「環狀」且「雙向」的鏈結串列，改寫上述的 fast-slow pointer 手法並比較。
:::


而最後再說明一些細節處理：
* 一個佇列包含 n 個節點，
    * 若 $n$ 是偶數，`fast` 指標最後會順利地回到佇列的開頭指標（`head`），並且 `slow` 也找到適當的正中間節點
    * 若 $n$ 是奇數，則要小心處理，因為每次走兩步的關係，會有一次剛好走到第 $n$ 個節點（即最後一個節點），此時若再次移動兩步，又會移動到第一個節點，從而讓迴圈繼續下去。最終 `slow` 會得到非預期的節點。
    * 因此迴圈進行時，要同時判斷 `fast->next`、`fast->next->next` 是否都不會回到 `head`
* 這個函式明確是要進行**刪除**而非移除，故斷開正中間節點的鏈結後，也要釋放該節點所配置的記憶體

### `q_delete_dup()`
這個函式的用意是若**給定已排序的佇列**，若有重複出現的資料，就要將帶有相同資料的節點全部刪除，舉例如下：

![delete-dup-before](https://hackmd.io/_uploads/HyqjfWls1e.png)

上圖可見，佇列當中，有三個節點都擁有字串 `"alpha"`，以 `q_delete_dup()` 的行為來說，最終應刪除這三個節點，最後使佇列留下那些資料僅出現一次的那些節點：

![delete-dup-after](https://hackmd.io/_uploads/BkYN7ZgiJg.png)

故最終，整個佇列會留下兩個節點（分別包含 `"beta"` 、 `"gamma"`）

---

因為該函式認定佇列是已經排序過的，所以若有重複的字串出現，它們所在的節點必定是相鄰的，故可以用兩個指標（以下以 `curr`、`iter` 代稱）走訪整個佇列處理，具體手法下：

1. 利用 `list_for_each()` + `curr` 開始走訪節點，而`iter` 在每次執行迴圈內容的一開始，都指向 `curr` 的下一個節點。
2. 適當利用 `element_t` 指標及 `list_entry()` 後，比較 `curr` 與 `iter` 所在節點的字串
    * 若相同，以一個布林變數標記為 `true`，代表有重複的字串出現，爾後 `iter` 則移動至下一個節點，且重複第 2 步
    * 若不同，前進至第 3 步
3. 利用布林變數得知重複的狀況
    * 若有發現有重複的字串出現，則開始移動 `curr`，將 `curr` 所在的節點開始，到 `iter` 的前一個節點，通通進行刪除。
    * 若沒有重複的字串，不需要做其他動作

這邊接續前面的舉例，配合示意圖輔助說明上述步驟：
![delete-dup-steps-rm-alpha](https://hackmd.io/_uploads/B1bHUHlokg.png)

上圖可見，`iter` 會如同前述步驟所述，因為節點資料與 `curr` 相同，故不斷往下走，直至走到帶有 `"beta"` 的節點而停止，而顯而易見地，`curr` 到 `iter->prev` 之前的節點，都帶有 `"alpha"` 字串，故這些節點都要刪除，而接下來會如下圖所示：
![delete-dup-steps-no-rm](https://hackmd.io/_uploads/rJ-O_Sxoyl.png)

因為前面遭遇重複資料的節點， `curr` 逐一走訪這些節點並刪除後，就會移動到 `iter` 所在的節點。爾後 `iter` 再度指向 `curr` 的下一個節點，重複前述步驟。
* `curr` 指向 `"beta"` 的節點時，`iter` 會指向 `"gamma"` 的節點，這時發現沒有重複出現的字串，不需要刪除節點。故 `curr` 直接因為 `list_for_each()` 直接走向下一個節點（即是 `iter` 所在的節點）
* `curr` 指向 `gamma` 的節點時，因為 `iter` 已經回到佇列的開頭指標，仍未發現有重複的資料出現，故不須刪除節點

最後，`curr` 會再次往下走一步，同樣地回到佇列的開頭指標，此時即完成了該函式的操作。

### `q_swap()`
這個函式的用意是，將佇列的所有節點，每兩個相鄰節點組成一對，爾後將每一對節點的位置互相交換。（若遭遇奇數個節點，則最後一個節點不做處理）

針對這個操作，只需要如下處理：
* 依序走訪第 1、3、5 ... 個位置的節點
* 透過 `list_head` 的 `next` 成員，存取這些奇數位置節點的下一個節點，即第 2、4、6 ... 個位置的節點
* 直接將成對的節點，以 `list_move()`，互換成對節點的位置即可

故實作上可見，用了 `list_for_each()` 及 `list_head` 指標（命名為 `curr`）走訪節點，並以 `list_move()` 將 `curr->next` 移動到 `curr` 之前，這樣成對的節點便可互換位置。

最後這邊的細節是：
1. `curr` 原先指向奇數位置的節點，進行位置互換後，`curr` 本來所指向節點，其位置索引值自然會變成偶數，直接透過 `list_for_each()` 隱含的 `curr = curr->next`，即可前往下一個奇數位置的節點。也就是說 **`list_move()` 完成後，不需要思考 `curr` 是否要另外處理移動的問題。**
2. `list_move()` 的第一個參數確實是帶入 `curr->next`，表示要移動偶數位置的節點，但第二個參數須注意的是，要帶入 `curr->prev`，**代表說 `curr->next` 指向的節點要移動到 `curr->prev` 所指向的節點的後面**。

### `q_reverse()`
這個操作是反轉整個佇列，也就是說
* 第一個節點變成最後一個節點，最後一個節點變成第一個節點
* 第二個節點變成倒數第二個節點，倒數第二個節點變成第二個節點
* ...

這裡的處理只需要依序走訪所有節點，將每一個節點的 `list_head`，`prev` 與 `next` 成員所儲存的記憶體位址互換即可。然而佇列的開頭指標（`head`）因為本質上也是儲存一個 `list_head` 物件，`next` 儲存第一個節點的位址、`prev` 儲存最後一個節點的位址，因此「交換 `next` 與 `prev` 儲存的內容」的動作，同樣地也要作用在 `head` 所指向的 `list_head` 物件上。

如此歸納後，可以更簡單的說，只要依序走訪佇列當中所有的 `list_head` 物件，對每個物件交換其兩個指標的內容，即可完成佇列反轉。

### `q_reverseK()`
這個函式與 `q_reverse()` 類似，目的是為佇列進行反轉動作，不同的是，是每一定數量的節點進行局部的反轉。

開發者可在該函式當中，用第二個參數 `k`，指定每 `k` 個節點進行反轉，舉例如下：
![reverseK](https://hackmd.io/_uploads/rkLUlFxjJl.png)

上圖舉例一個包含 6 個節點的佇列，在指定 `k` 為 3 並執行 `reverseK()` 以後，
* 會將前 3 個節點 `"alpha" <--> "gamma" <--> "beta"` 視作一組，反轉後即得到 `"beta" <--> "gamma" <--> "alpha"`
* 而後 3 個節點也是相似的道理，如上圖所示，最後會變成 `"bbb" <--> "ttt" <--> "ppp"` 的順序。

---

實作上，以 `list_for_each()` 與 `list_head` 指標（命名為 `curr`）來走訪佇列，爾後準備另一個 `list_head` 指標（命名為 `iter`），先指向 `curr` 所指向的節點，爾後不斷往下一個節點前進且邊計數，直至數了 k 個節點，就對 `curr` 與 `iter` 之間的節點，開始實施反轉。

接下來接續前述的例子（6 個節點的佇列、將 `k` 指定為 3 進行 `q_reverseK()`），搭配數張圖片探討處理方式：

1. 最一開始，`curr` 與 `iter` 皆會指向同一個節點
    ![reverseK-steps-1](https://hackmd.io/_uploads/ByOuNcxoJg.png)
2. `iter` 會邊往下一個節點前進邊計數，直至計數累積到 3，爾後可見 `iter` 會抵達第 4 個節點
    ![reverseK-steps-2](https://hackmd.io/_uploads/rkZJr9xoyg.png)
    這時候即是要把 `curr` 到 `iter->prev` 之間的所有節點（包含 `curr` 與 `iter->prev` 指向的節點），視作同一組，並實施反轉處理。
3. 這時候在開始反轉前，先透過另外 3 個指標 `prev` 、 `new_head` 與 `new_tail`（後續可得知這些指標的用途），分別指向 `curr->prev`、`iter->prev` 與 `curr`。
    ![reverseK-steps-3](https://hackmd.io/_uploads/HJrGwqgsJx.png)
    爾後與 `q_reverse()` 雷同，逐步移動 `curr` 指標，直到抵達 `iter` 所指向的節點，將該組每一個節點的 `list_head`，`next` 與 `prev` 所儲存的記憶體位址互換。
4. 當 `curr` 逐步移動、逐步處理 `list_head` 的兩個指標，抵達 `iter` 指向的節點後，中間各個指標的指向，一度會如下圖所示（多留意前 3 個節點的 `next` 與 `prev` 指標的指向）：
    ![reverseK-steps-4](https://hackmd.io/_uploads/S1b1T9lokx.png)
    觀摩上圖可見，某些指標的指向會指向不對的節點，故這裡要額外處理下列事項：
    * `prev` 指向的 `list_head` 物件（這裡只是剛好是佇列的開頭指標 `head`）， `prev->next` 應變成指向 `new_head` 所指向的節點。
    * 剛剛以「分組」的概念，來說明要進行反轉的那些節點們，我們可以將每一個分組視作一個「子」佇列（或是說「子」鏈結串列），`new_head` 的含意就是子佇列的反轉後，子佇列真正的第一個節點，而明顯地，`new_head->prev` 應該要指向 `prev` 所指向的節點。
    * `new_tail` 就是指反轉後，子佇列的最後一個節點，而 `new_tail->next` 應要指向
`curr` 所指向的節點。
    * `curr` 此時是反轉前，子佇列最後一個節點（第 3 個節點）的下一個節點，此時也要修正 `curr->prev` 的指向，應該指到 `new_tail` 所指向的節點。
    
5. 到此，一旦完成第 4 點最後提及的 4 個修改指標指向的操作，最後就完成了一組節點的反轉了：
    ![reverseK-steps-5](https://hackmd.io/_uploads/H1acljxo1l.png)
    該圖的上半部是完成修改指標後的樣貌，下半部的圖示則是依據上半部，適當調整前 3 個節點在圖中的相對位置而已。
    
    如此可見，前 3 個節點已經適當地反轉了，該修改的指標指向也都修正到。

最終，只要透過如前述的處理方式，適當地找出要反轉的節點區間，反轉子佇列的所有節點，並適當修正 4 個指標的指向，不斷分組處理後，即可完成對整個佇列的局部反轉。

而最後只要稍加注意，若分組分到最後，剩餘的節點不足 `k` 個的話，就不對這些剩餘的節點處理

### `q_sort()`
該函式了行為是為佇列的所有元素進行排序（升序或是降序），原先測試的要求，是必須以時間複雜度 $O(n\log{n})$ 的排序演算法，完成佇列的排序，這樣才能通過 `trace-15-perf` 的測試項目。

不過此處暫時先以選擇排序法處理，後者實際上是時間複雜度為 $O(n^2)$ 的排序法，這樣做主要是為了先了解 `qtest` 及相關的測試項目，使用佇列操作的情況，所以先以一個簡單的演算法代替。

後續會再改寫為合併排序法，改進這個函式的效能。

### `q_ascend()`
`q_ascend()` 的目的是讓整個佇列變成一個升序排序的佇列，但不是進行排序，而是針對每一個節點而言，在它之後有某個節點的字串比前者要小，就把後者自記憶體中刪除。

也就是說，透過刪除節點的方式，僅留下部份節點，使這些節點的資料是升序的。實作上目前也是採取簡單明瞭的方法，先走訪一個節點，爾後自這個節點的下一個節點往後找，若找到某些節點的字串比前者小，就把這些節點刪除。

### `q_descend()`
該函式與 `q_descend` 雷同，但是要將佇列變成一個降序排序的佇列，這個操作同樣可能會刪除某些節點。

實作上，是從佇列的最後一個節點開始，逐步走訪節點並逐步往前一個節點移動，直至回到佇列的開頭。每走訪一個節點時，就掃描這個節點的前面，是否有其他節點的字串小於當前的節點，若有找到某些節點使前述的情況成立，就刪除這些節點。

### `q_merge()`
`q_merge()` 是可以給定多個已排序的佇列，將這些佇列合併成一個仍保持排序的佇列。

而給定兩個佇列要合併時，實作時只需要透過兩個 `list_head` 指標，分別走訪兩個佇列的節點
* 第 2 個佇列的指標（這裡以 `q2_curr` 代稱）指向某個節點後，會先固定不動，第 1 個佇列的指標（以 `q1_curr` 代稱）則會逐步移動。
* `q2_curr` 即是要被合併到第 1 個佇列的某個節點，為了找到適當的插入位置，`q1_curr` 就是拿來逐步在第一個佇列走訪，爾後找到某個適當的節點，`q1_curr` 可拿來協助將 `q2_curr` 安插在適當的位置上。
* `q1_curr` 與 `q2_curr` 各自走訪到某個節點時，會用 `list_entry()` + `element_t` 指標，爾後取得字串並比較。
    * 若發現 `q1_curr` 須排在 `q2_curr` 之後，就把 `q2_curr` 自第 2 個佇列移除，並插入在 `q1_curr` 之前。爾後 `q2_curr` 指標指向第 2 個佇列的新的第一個節點，也就是原先移動前，`q2_curr` 的下一個節點 `q2_curr->next`。
    * 反之，`q1_curr` 直接往下一個節點前進。
* 可能會有兩種情況發生
    * `q1_curr` 已經走回第 1 個佇列的開頭指標 `head`，若第 2 個佇列還有剩餘的節點，直接將這些節點移動至第 1 個佇列的尾端，隨後即可結束合併流程。
    * 第 2 個佇列已經成為一個空佇列，表示所有節點都被合併至第 1 個佇列，此時不用再繼續做下去，可直接結束合併流程

這樣一來，`q1_curr` 與 `q2_curr` 會各自走訪一個佇列，透過上述方式，即可完成兩個佇列合併。

不過 `q_merge()` 是給定多個佇列，進行合併，目前的手法是會將第 2, 3, 4... 個佇列，一一取出，透過前述手法合併回第 1 個佇列。

### 額外實作的 `cmp_ascend()` 、 `cmp_descend()` 函式
因為 `q_sort()` 本身的第二個參數，可以決定要讓佇列以**升序**或是**降序**的方式排序，故準備了這兩個全域的靜態比較函式，這兩個函式皆是給定 2 個字串，進行比較後便回傳適當的布林值。

這兩個靜態函式的參數都命名為 `str1` 與 `str2`，排序演算法須將兩個不同的元素代入靜態函式內，若回傳值得到 `true`，則排序演算法要將那兩個元素的位置交換。其中須注意代入兩個元素時，兩個元素的位置必定是相對地，一個在前一個在後，而位在前面的元素要代入 `str1`，後面的元素則代入 `str2` 當中。

這兩個比較函式，本質上會呼叫 `strcmp()` 進行字串比較
* 若是 `cmp_ascend()`，會回傳 「`strcmp()` 的回傳值是否大於 0」。
    * 這樣的含意是若 `str1` 比 `str2` 還要大，那麼以升序方式來講，這兩個字串就應該要交換。
    * 反之若 `str1` 小於或等於 `str2`，則代表兩個元素的相對位置正確，是升序的，不需要交換
* 若是 `cmp_descend()`，則會回傳 「`strcmp()` 的回傳值是否小於 0」。
    * 道理與 `cmp_ascend()` 相同，只是判斷的是某兩個元素之間是否是降序的。

如此，`q_sort()` 再準備一個函式指標陣列，準備兩個指標分別指向上述兩個比較函式，
，最後只須透過 `descend` 參數，就可以直接呼叫對應的比較函式。這樣不需要用分支判斷要使用升序還是降序的比較方式。

而這兩個比較函式，也會拿去輔助 `q_merge()` 的實作。

### 額外定義的 `q_is_empty()` 巨集
因為實作前述的函式時，有很多時候需要判斷佇列是否是一個空佇列，而判斷的方式自然是使用佇列的開頭指標 `head`，判斷其所維護 `list_head` 物件的 `next` 是否指向 `head` 本身。

在 `list.h` 中，原先已經有 `list_empty()` 函式，裡頭已經定義前述所需的判斷式，不過因為是要判斷「佇列是否是空佇列」，因此還是定義了 `q_is_empty()` 巨集，給定 `head` 後，會再協助代換成 `list_empty()` 函式，最終仍回傳為 `head->next == head`。

## 改進初步實作
### 修正 `q_remove_head()` 、 `q_remove_tail()` 的錯誤
當呼叫移除節點的函式的時候，若有給定合法的 `sp` 指標，這些移除節點的函式會將移除節點的字串，協助複製到 `sp` 指標所指向的字元陣列當中（其中陣列大小可由 `bufsize` 得知）。

原先實作上使用 `strncpy()`，這樣即可依據 `bufsize` 複製適當的字元數量到 `sp` 當中，而考量要容納 `'\0'` 字元，`strncpy()` 最後一個參數會代入 `bufsize - 1`，爾後進行複製。

後來發現 `strncpy()` 的複製處理，有可能剛好複製 `bufsize - 1` 個字元，但沒有附加 `'\0'` 字元，從而導致 `sp` 所最終儲存的字串，後續使用時會發生錯誤（如 `printf()` 輸出的字串，長度超過 `bufsize - 1` 個字元）

因此針對這兩個移除節點的函式，只要在呼叫 `strncpy()` 以後，主動為 `sp` 附加一個 `\0`，即可解決前述的缺陷：

* 實施修改後，對應到 commit [4809388](https://github.com/DrXiao/lab0-c/commit/4809388b88946ac549181e1cbe40048c435e37e2)

### 將 `q_sort()` 改為使用合併排序法
原先 `q_sort()` 使用選擇排序法去處理佇列的排序，後來透過 `qtest` 進行測試後，已知佇列的排序除了注意比較大小的方式外，還要**特別注意各個指標的指向是否正確**，後者會是一個要特別注意的點。

爾後為了滿足測試項目的要求，已將該函式改為合併排序法進行排序，前述的修改對應到 commit [ac55cdf](https://github.com/DrXiao/lab0-c/commit/ac55cdf2ab4d6600e701535d4d6e0283fcba1021)。

---
因為 lab0-c 的佇列本質上是用鏈結串列實作，接下來會以「鏈結串列」為主要術語，說明 `q_sort()` 的合併排序法是如何進行的。

對鏈結串列實施合併排序法，觀念與對陣列排序相同，都是先將所有元素視為個體，先兩兩合併，成為一個較小、已排序的子串列/子陣列後，再將相鄰的子串列/子陣列不斷地兩兩合併並排序，直至合併到只剩下一組串列/陣列後，即是排序完畢了。

以下先不論原先 lab0-c 的設計，先以一個儲存數個整數的鏈結串列，搭配數張圖片說明：

![mergesort-0](https://hackmd.io/_uploads/Bkcc31fsJg.png)

首先上圖可見一個鏈結佇列中，包含了數個整數（1 ~ 8），若要實施合併排序法，首先須如下圖所示意，先將整個鏈結串列分割成 8 個子鏈結串列（每個子串列僅包含一個節點）
![mergesort-0-1](https://hackmd.io/_uploads/H1yfRJGo1e.png)

上圖下半部以虛線示意，將鏈結串列分割成了 8 個子串列，這樣便完成了第一步。接著要依序將所有子串列兩兩合併並排序，不斷進行合併且排序後，即可完成整個鏈結串列的排序，前面這一段描述，可以用下面這張圖示意：
![mergesort-1-4](https://hackmd.io/_uploads/HyA5AJMs1e.png)

這裡搭配圖中，由上至下逐步解釋其含意
* 第 1 步完成分割子串列後，接下來先將 8 個子串列兩兩進行合併，爾後整個串列的樣貌如第 2 步所示
* 第 2 步此時會有 4 個已排序的子串列，因仍包含諸多子串列，故再次進行兩兩合併，爾後前進至第 3 步
* 第 3 步此時已經剩下 2 個已排序的子串列，再度合併後，即是第 4 步呈現的樣貌
* 第 4 步此時發現剩下 1 個已排序的子串列，後者事實上就是整個已排序完畢的鏈結串列，故此時排序完成。

以上，便是鏈結串列實施合併排序法的原理，與陣列的差異，僅僅只是在記憶體中，節點之間是離散的分佈，故要用指標走訪這些節點而已。

---
然而，lab0-c 模仿 Linux 核心，建立「雙向」且「環狀」的鏈結串列，並且連維護串列的方式，都與 Linux 核心相同，接下來將以實作的角度（但串列節點的資料仍假設儲存整數），講解應注意的細節：

首先這裡先沿用前述鏈結串列的例子，但示意圖畫出 `list_head` 物件在各個節點的樣貌：
![mergesort-details](https://hackmd.io/_uploads/Sk7uFgMi1e.png)

爾後要注意的細節列舉及說明如下：
1. **如何分割串列為多個子串列？**

因為鏈結串列的節點在記憶體是離散的，無法像陣列一樣用索引值（或是已知的陣列大小）快速地進行分割，而對應的作法是，設法用 `list_head` 指標，找到正中間的節點：
![mergesort-details-1](https://hackmd.io/_uploads/rJKpAd4ikg.png)

這樣一來，就可以用這個指向正中間節點的指標（對應圖中的 `mid_ptr`），做第一次的分割，換言之，排序整個鏈結串列，相當於先排序 `head->next` ~ `mid_ptr` 這個子串列以及 `mid_ptr->next` ~ `head->prev` 這個子串列，最後再合併兩個子串列即可。

當然對於子串列的排序，同樣是用指標找到子串列的中間節點，不斷遞迴下去，直至無法再分割時便開始將更小的子串列，不斷排序並合併回原本的串列。

而前述也提及了關鍵字「遞迴」，所以在實作上，有先定義了名為 `q_sort_interval()` 的靜態函式，這裡僅列出做出關鍵行為的程式碼：

```c
static int q_sort_interval(struct list_head *head, 
                           struct list_head *tail,
                           bool (*cmp)(const char *, const char *))
{
    /* Handle the simplest cases */
    if (head->next == tail) {
        ...
        return 1;
    }
    ...
    ...
    
    /* Split the lists into two sub-lists and sort them. */
    q_sort_interval(head, mid_ptr, cmp);
    ...
    q_sort_interval(mid_ptr->next, tail, cmp);
    
    /* Merge two sub-lists. */
    ...
    ...
}

static void q_sort(struct list_head *head, bool descend)
{
    bool (*cmp[2])(const char *, const char *) = {cmp_ascend, cmp_descend};

    if (!head || q_is_empty(head))
        return;

    q_sort_interval(head->next, head->prev, cmp[descend]);
}
```

透過 `q_sort_interval()`，後者可給定鏈結串列的第一個節點指標、最後一個節點的指標以及排序的比較方式（給定比較函式的指標），即會實施排序。其中須注意 `q_sort_interval()` 不可以給定空串列。

* 若遭遇某些最簡單的排序案例，就會直接將串列排序後並結束執行
    * 最簡單的案例，即是 `q_sort_interval()` 發現該串列只有一個節點或是兩個節點，若是後兩者其一的話，直接視情況交換兩個元素的位置或是直接結束執行即可。
* 反之若串列的節點數量夠多，則會分割成兩個串列後，遞迴地呼叫自己兩次，排序兩個子串列。排序完畢後，就會將兩個已排序的子串列進行合併。

如此，`q_sort()` 本身只需要呼叫 `q_sort_interval()`，給定真正的第一個節點與最後一個節點的記憶體位址（即 `head->next` 與 `head->prev`），這樣即可完成整個串列的排序。


2. **`list_head` 物件的兩個指標處理**

前面第 1 點已經提及可用遞迴搭配指標的方式，不過這裡要提及第二個細節，就是實作上若僅用**指標**是不夠的，要用到**指標的指標**（或稱**間接指標**）的操作技巧，具體原因進一步說明如下：

![mergesort-details-bug](https://hackmd.io/_uploads/S1ewrY4j1x.png)

原先會預期 `q_sort_interval()` 代入開頭節點與結尾節點的指標並完成串列的排序，但因為前述並沒有提到 `head` 所指向的 `list_head` 物件，其 `next` 與 `prev` 指標要如何適當調整，故若處理不當，即便排序完畢，`head` 所維護的 `list_head` 也會指向錯誤的節點（如上圖的下半部所呈現）。

再來看一個例子，因為會不斷分割成小的串列並排序，所以也有這樣的例子：
![mergesort-details-bug2](https://hackmd.io/_uploads/rkYTvFVo1g.png)

從前面的例子不難觀察到，原先的串列不斷地分割完後，會先合併 `4` 與 `1` 的節點並排序，再來合併 `7` 與 `3` 的節點並排序。上圖假設 `4` 與 `1` 的節點已排序變成先 `1` 後 `4`，再來處理 `7` 與 `3` 的節點。

而處理帶有 `3` 與 `7` 的子串列時，會遭遇到與剛剛類似的理由，因為 `q_sort_interval()` 僅代入開頭節點與結尾節點，所以 `7` 的節點與 `3` 的節點確實可以排序到，**但這個子串列前一與後一節點的指標沒有被調整到**（即上圖下半部 `4` 的節點的 `next` 指標、`5` 的節點的 `prev` 指標），這樣走訪串列時仍會得到錯誤的順序。

---
如此可以這樣歸納說明：「`q_sort_interval()` 若參數僅用指標，無法處理（子）串列前一個 `list_head` 物件與後一個 `list_head` 物件的指標。」

因此，可以用間接指標的技巧，

```c
static int q_sort_interval(struct list_head **head, 
                           struct list_head **tail,
                           bool (*cmp)(const char *, const char *))
{
    /* Handle the simplest cases */
    if (head->next == tail) {
        ...
        return 1;
    }
    ...
    ...
    
    /* Split the lists into two sub-lists and sort them. */
    q_sort_interval(head, &mid_ptr, cmp);
    ...
    q_sort_interval(&mid_ptr->next, tail, cmp);
    
    /* Merge two sub-lists. */
    ...
    ...
}

static void q_sort(struct list_head *head, bool descend)
{
    bool (*cmp[2])(const char *, const char *) = {cmp_ascend, cmp_descend};

    if (!head || q_is_empty(head))
        return;

    q_sort_interval(&head->next, &head->prev, cmp[descend]);
}
```

將參數改寫為 `struct list_head **` 的型態後，就可以解決前面提及的問題，讓子串列被排序後，在子串列前後的 `list_head` 物件的指標也被調整到。

雖說光靠 `struct list_head *` 也可以存取子串列前後的 `list_head` 物件並修改，但這樣就要在程式碼中加入「若子串列排序後，開頭/結尾節點有改變，就要調整子串列前後 `list_head` 物件的指標」的處理。

在 [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E5%BE%9E-Linux-%E6%A0%B8%E5%BF%83%E7%9A%84%E8%97%9D%E8%A1%93%E8%AB%87%E8%B5%B7) 教材中，有一段「移除鏈結串列的節點」程式碼，如果僅用指標，就要做出額外的特例處理，但用指標的指標，就可以省去特例處理，從而讓程式碼變得簡潔。所以 `q_sort_interval()` 參數型態改用指標的指標，也是類似的道理。

如此，透過前述手法，`q_sort_interval()` 即可適當地完成串列的排序，從而順利完成 `q_sort` 的實作，滿足時間複雜度的要求。

## 修改 `q_delete_mid()` 的手法並比較
原先 `q_delete_mid()` 採用快慢指標的手法，找到正中間的節點並刪除之，但考慮 List API 建立雙向且環狀的鏈結串列，還有另一個實作思維：

以一個圓形的操場來比喻，若兩人自操場的同一點，背對背行進行走操場，以速率相同但行進方向相反的方式繞著操場走，交會的那一刻，兩人都剛好走了操場圓周長的一半。故針對環狀且雙向的鏈結串列，可以利用兩個指標，一個從開頭節點往結尾節點的方向移動，另一個節點，從結尾節點開始往開頭節點的方向走，兩個指標在迴圈中不斷地移動（僅移動一步），當兩個指標交會的那一刻，也可以找到正中間的節點。

依循上述的手法進行修改後，對應到 `improve-q_delete-mid` 分支的 commit [c837f44
](https://github.com/DrXiao/lab0-c/commit/c837f4495199f75a29df5ccf1a6eb28c74d3da4f)

為了比較前後的執行時間差異，找出真正最佳的那個方法，後來有追加一些程式，做了一些前置準備後，利用一些手法產生測量數據並比較。

### 追加 `measure` 命令
在 `qtest` 內建的命令列，雖然已經有 `time` 命令可以測量某個命令的執行時間，但這個命令的輸出時間，精確度只能到毫秒等級，而且一次只能執行一次命令。

為了可以「下一次命令，執行多次動作」，後續自行追加了 `measure` 命令，在執行 `qtest` 後，可用 `help` 命令見到說明如下：
```
cmd> help
..
..
  measure     n cmd arg .. | Using clock_gettime to measure the time for each execution
..
..
```

也就是說，我新做的 `measure` 命令與 `time` 命令類似，但前者使用 `clock_gettime()` 來測量執行時間（精確度達到奈秒等級），並且第一個參數 `n` 即是可執行給定的命令（`cmd arg`）n 次，並用標準輸出（或者說利用 `report()` 函式），將每一次執行的時間呈現在命令介面上。

舉例來說，可以以下列方式執行命令：
```
cmd> new
l = []
cmd> measure 5 it RAND
l = [jceids]
elapsed time: 22777
l = [jceids gebwxleul]
elapsed time: 16983
l = [jceids gebwxleul pynqvwtc]
elapsed time: 17329
l = [jceids gebwxleul pynqvwtc vfvdaby]
elapsed time: 14588
l = [jceids gebwxleul pynqvwtc vfvdaby iwllwfjk]
elapsed time: 18163
```
上面這段 `qtest` 的操作，是先新增一個空佇列後，爾後執行 `measure 5 it RAND`，意思就是 `it RAND` 這個操作被執行 5 次，而每執行完一次後，會輸出 `elapsed time: ...` 的訊息。這樣一來，開發者若要測量某個佇列操作的執行時間，可以透過 `measure` 命令，讓某個操作被執行多次並輸出每一次的執行時間，接著可以進一步做的事情，就是收集這多次的時間，進行分析後，得知該操作的平均執行時間（注意是在自己環境上的平均執行時間）。

上述追加 `measure` 命令的修改，對應到 commit [ef57f26](ef57f26a48b2c7209b3e9feb72d3a0da34b838ca)

### 設計 `q_delete_mid()` 的測試實驗
1. 設計初步的測試命令序列

有了自己撰寫的 `measure` 命令後，我初步便設計了以下的命令序列（建立 `testcase.cmd` 檔案）

* `testcase.cmd`
```
new
it RAND 10000
log result-mytest
measure 30 dm
free
```
這樣一來，即可測試 10000 個節點下，移除 30 次正中間節點的執行時間。不過因為是直接移除 30 個節點，會輸出 30 次 `elapsed time: ...` 的訊息，並且將輸出訊息也儲存到 `result-mytest` 檔案中

這裡要注意：

* 第一次的 `elapsed time: ...` 訊息：是自 10000 個節點中，移除正中間節點的執行時間
* 第二次的 `elapsed time: ...` 訊息：是自 **9999** 個節點中，移除正中間節點的執行時間
* 第三次的 `elapsed time: ...` 訊息：是自 **9998** 個節點中，移除正中間節點的執行時間
* ...

亦即那 30 次執行移除正中間節點時，遭遇的節點總數量不同，故要注意這樣的區別。

爾後在終端機，可以用下列 shell 命令，使用 `qtest` 來測試 `testcase.cmd`
```
cat testcase.cmd | ./qtest > /dev/null
```
這樣就可以用 `cat` 將 `testcase.cmd` 欲執行的所有操作，透過 shell 的管線（pipe）一次性地送到 `qtest` 內執行。

而假設想執行 100 次 `qtest` 及設計好的測試命令組合，並取得平均時間，就要執行上述的 shell 命令 100 次，產出 100 個 `result-mytest` 檔案後，自行取出裡面的內容，再計算平均執行時間。（具體的計算方式，會在第 3 點論述）

為了可以自動化執行前面的過程、計算平均執行時間、產出測試圖表，於是接下來就設計了一系列地測試工具。

---
2. 建立自動化測試的腳本

```shell=
#!/bin/sh

if [ "$1" = "" ] || [ "$2" = "" ] || [ "$3" = "" ]; then
	echo "Usage: ./test_script.sh <n> <init_node> <rm_node"
	echo "run a testcase n times and produce analysis figure."
	exit 1
fi

testcase="
new
it RAND $2
log result-mytest
measure $3 dm
free
"

mkdir -p result-mytest-dir

git checkout master
make
for i in $(seq 1 $1); do
	echo "$testcase" | ./qtest > /dev/null
	mv result-mytest result-mytest-dir/result-mytest$i
done
mv result-mytest-dir/result-mytest* measurement/mytest-master || exit 1

git checkout improve-q_delete_mid
make
for i in $(seq 1 $1); do
	echo "$testcase" | ./qtest > /dev/null
	mv result-mytest result-mytest-dir/result-mytest$i
done
mv result-mytest-dir/result-mytest* measurement/mytest-improve || exit 1

cd measurement
python3 measure.py $1 $2
```

這個 shell 腳本會進行數項操作，這邊配合行號並描述上述的執行過程
* L1 - L7：提示命令執行方式為 `./test-script.sh <n> <init_node> <rm_node>`
    * 因為前面提及的 `testcase.cmd` 固定是初始有 10000 個節點，並移除 30 次節點，所以 `<init_node>` 可以指定初始節點、`<rm_node>` 可以指定要移除的次數，增加測試的彈性
    * 而 `<n>` 可指定使用 `qtest` 執行命令序列 $n$ 次，產出 $n$ 個 `result-mytest` 的測試結果檔案
* L9 - L14：這裡用名為 `testcase` 的變數，儲存稍後欲執行的命令序列，而實際上就是前面提及的 `testcase.cmd` 所包含的命令，不過將 `10000` 置換為 `$2`、30 置換為 `$3`，這樣可以透過在 shell 下命令時，彈性地修改初始節點數量、移除正中間節點的次數
* L17：因為會產出多個 `result-mytest` 檔案，所以會用 `mkdir` 產生一個目錄，後續會將所有 `result-mytest` 檔案存放至 `result-mytest-dir` 目錄下
* L19 - L33：
    * 針對 `q_delete_mid()` 的實作，在 `master` 支線採用快慢指標的作法，`improve-q_delete_mid` 採用新的作法（兩個指標向正中間移動的作法），接著必須先後切換至兩個分支上，編譯並執行 `qtest` 並執行 `testcase` 的命令，爾後產出 `result-mytest` 並進行後續分析
    * 所以這裡會自動執行 `git checkout`，
        * 先切換到 `master` 支線，以 `make` 編譯 `qtest` 並透過 `for` 迴圈執行 `qtest` $n$ 次，並將每一次產生的 `result-mytest` 移動到 `result-mytest-dir` 目錄，且在檔案名稱最後面附加號碼。執行完後，會將 `result-mytest-dir` 下多個 `result-mytest` 檔案，存放到 `measurement/mytest-master` 目錄下
        * 爾後再次 `git checkout`，切換到 `improve-q_delete_mid`，並做一樣的事情，但最後 `result-mytest-dir` 下的所有檔案，會通通移動到 `measurement/mytest-improve` 目錄底下
* L35 - L36：
    * 根據我自己的設計來講，執行 `test_script.sh` 以後，最後會得到類似下列的目錄及檔案：
        ```
        .
        ├── console.c
        ...
        ├── measurement
        │   ├── measure.py
        │   ├── mytest-master
        │   │   ├── result-mytest1
        │   │   ├── result-mytest2
        │   │   ...
        │   └── mytest-improve
        │       ├── result-mytest1
        │       ├── result-mytest2
        │       ...
        │
        ├── qtest
        ...
        ```
        也就是說，`master` 與 `improve-q_delete_mid` 兩個分支各自的測試結果，會分別存放至 `measurement/mytest-master` 及 `measurement/mytest-improve` 底下。
（其中 `measurement` 是自行預先建立好的目錄、`measure.py` 後續會說明其用意）
    * 接著就是透過 `cd` 切換工作目錄至 `measurement` 內，爾後啟用 `measure.py`，透過 Python 程式進行分析

---
3. 透過 Python 分析、計算平均執行時間、產出圖表

經過前面的 shell 腳本，最終會得到許多的 `result-mytest<n>` 檔案（`<n>` 是 shell 腳本最終給定的編號），亦即多次執行結果，如果觀察 `result-mytest<n>`，不難發現我們可以將這多次的執行結果，自行想像並展開成一個表格

假設執行 `./test_script.sh 50 1000 100` 的話，亦即 `mytest-master` 與 `mytest-improve` 目錄，最終都應可以得到各自的 `result-mytest1` ~ `result-mytest50`，爾後可以像下方這樣展開成兩個表格

```
mytest-master
+--------------------+--------+--------+               +---------+
|           \ sample |        |        |               |         |
| total nodes \      |   1    |   2    | . . . . . . . |   50    |
+--------------------+--------+--------+               +---------+
| 1000               | 170012 | 160020 |               | 1611116 |
+--------------------+--------+--------+               +---------+
| 999                | 166611 | 165432 |                     .
+--------------------+--------+--------+                     .   
| 998                | 165555 | 164353 | . . . . . . .       . 
+--------------------+--------+--------+
.                                         .
. . . . . . . . . . . .. . . . . . . . .     .
.                                                .
+--------------------+                                 +---------+
| 901                | . . . . . . . . . . . . . . . . | 1443232 |
+--------------------+                                 +---------+
```

```
mytest-improve
+--------------------+--------+--------+               +---------+
|           \ sample |        |        |               |         |
| total nodes \      |   1    |   2    | . . . . . . . |   50    |
+--------------------+--------+--------+               +---------+
| 1000               | 171012 | 164420 |               | 1613216 |
+--------------------+--------+--------+               +---------+
| 999                | 164611 | 165432 |                     .
+--------------------+--------+--------+                     .   
| 998                | 165555 | 157353 | . . . . . . .       . 
+--------------------+--------+--------+
.                                         .
. . . . . . . . . . . .. . . . . . . . .     .
.                                                .
+--------------------+                                 +---------+
| 901                | . . . . . . . . . . . . . . . . | 1333232 |
+--------------------+                                 +---------+
```
* 如第一點所述，因為移除多個節點的過程中，節點的總數量是同時遞減，故勢必要以總節點數量區分收集到的多次執行時間，所以上述表格就有了 `total nodes`，表示在對應的總節點數量下，移除節點的執行時間。
* 接著因為透過 `test_script` 執行了 **50** 次 `qtest`，所以會有縱軸的 `sample`，這樣可以清晰地見到 `total nodes` 的每一個項目，都有 50 次執行時間的數據。

如果可以理解前述的說明，就可以知道接下來要這樣處理：
```
mytest-master
+--------------------+--------+
|           \ average|        |
| total nodes \      |        |
+--------------------+--------+
| 1000               | 160612 |
+--------------------+--------+
| 999                | 160011 |
+--------------------+--------+
| 998                | 163355 |
+--------------------+--------+
.              .              . 
.              .              .
.              .              .   
+--------------------+--------+
| 901                | 141212 |
+--------------------+--------+
```
```
mytest-improve
+--------------------+--------+
|           \ average|        |
| total nodes \      |        |
+--------------------+--------+
| 1000               | 157312 |
+--------------------+--------+
| 999                | 156611 |
+--------------------+--------+
| 998                | 154555 |
+--------------------+--------+
.              .              . 
.              .              .
.              .              .  
+--------------------+--------+
| 901                | 142002 |
+--------------------+--------+
```
因為原先的表格，每一列都有 50 次數據，所以可以取平均值，得知特定總節點數量下，移除正中間節點的平均執行時間，最終算完平均值的表格，就可以將其繪製曲線圖，從而比較 `master` 與 `improve-q_delete-mid` 的實作，從視覺化的圖表中，得知執行時間的差異並找出最佳的實作

因此，為了將所有的 `result-mytest<n>`，轉換成表格、計算每一項目的平均值、並畫出曲線圖，這裡採用撰寫 Python 腳本的策略，完成前述要做的事情，而 Python 腳本的實作如下：

```python
import matplotlib.pyplot as plt
import numpy as np
import sys

base_name = "result-mytest"

def outlier_filter(data, threshold = 2):
    data = np.array([y for y in data])
    z = np.abs((data - data.mean()) / data.std())
    return data[z < threshold]

def parse_data(path, cases, init_node):
    all_data = None
    for i in range(cases):
        j = 0
        data = []
        with open(path + "/" + base_name + "%d"%(i + 1), "r") as f:
            for line in f:
                if line.startswith("elapsed time:"):
                    exec_time = int(line.split(":")[1].strip())
                    data.append([init_node - j, exec_time])
                    j += 1
            if all_data:
                for i in range(len(all_data)):
                    all_data[i].append(data[i][1])
            else:
                all_data = data
    
    for i in range(len(all_data)):
        filtered_data = outlier_filter(all_data[i][1:]).mean()
        all_data[i] = [all_data[i][0], filtered_data]

    return all_data
    

def main():
    if len(sys.argv) < 3:
        print("Usage: python3 measure.py <num_of_data> <init_node>")
        exit(1)

    num = int(sys.argv[1])
    init_node = int(sys.argv[2])
    plt.figure(figsize=(8, 6))

    master_data = parse_data("mytest-master", num, init_node)
    improve_data = parse_data("mytest-improve", num, init_node)

    x_values, y_values = zip(*master_data)
    plt.plot(x_values, y_values, marker='o', linestyle='-',
             color='b', label="fast-slow pointer")

    x_values, y_values = zip(*improve_data)
    plt.plot(x_values, y_values, marker='o', linestyle='-',
             color='g', label="Two pointers move toward to each other")

    plt.legend()
    plt.show()

if __name__ == "__main__":
    main()
```
因為上述的程式碼並未上傳到 GitHub 上，故呈現在此並解說：

* 執行方式：`python3 measure.py <n> <init_node>`
    * 透過 shell 的參數，告知 `result-mytest<n>` 的 `<n>` 最高是多少
    * 因為 `result-mytest<n>` 的內容並沒有初始總節點的資訊，所以透過 `<init_node>` 告知這個腳本，每一次測試的初始總節點數量
* `parse_data()` 即是給定 `mytest-master` 或是 `mytest-improve` 字串後，會去指定的目錄下讀取 `result-mytest1` ~ `result-mytest<n>`，並轉成 Python 的 `list` 物件（就是將收集到的數據做成表格），爾後會對每一列的數據取平均值，變成算完平均值的表格
    *  考量數據可能會有極端值的存在，故有參考 2022 年 - [K04: fibdrv](https://hackmd.io/@sysprog/linux2022-fibdrv#%E7%94%A8%E7%B5%B1%E8%A8%88%E6%89%8B%E6%B3%95%E5%8E%BB%E9%99%A4%E6%A5%B5%E7%AB%AF%E5%80%BC) 的說明，可以用統計手法去除極端值，所以中間還有設計 `outlier_filter()` 函式，讓 `parse_data()` 先除去極端值後再計算平均值。
* `main` 函式就是呼叫 `parse_data()` 後，得到兩種表格，分別對應 `master` 與 `improve-q_delete_mid` 的執行時間數據。接著呼叫 `matplotlib` 的繪圖功能，將兩條曲線畫出來。

---

綜合前面的說明，執行 `test_script.sh`，指定取樣次數、初始總節點數量、要移除正中間節點的次數以後，接著腳本會協助到兩個分支上測試並產出測試數據，最後 Python 腳本程式可以協助產出類似下方的圖表：

![Figure_1](https://hackmd.io/_uploads/B1573u2jyx.png)

圖中的橫軸即是總節點數量、縱軸則是執行時間（單位是奈秒），所以**圖中的每一個點，都代表對應的總節點數量下，移除正中間節點的平均時間**。

### 修改 `q_delete_mid` 的小結
透過 [修改 q_delete_mid() 的手法並比較](#設計-q_delete_mid-的測試實驗) 的說明，我原先設想可以收集多次執行時間的數據後，得知 `q_delete_mid()` 的平均執行時間並繪製圖表後，就可以比較這個函式修改前後的差異。

不過後來有多用 `test_script` 執行數次，有發現到以下的現象

* 執行 `./test_script.sh 50 10000 100` 兩次
    * 第一次可見是新方法比較快
        ![Figure_1](https://hackmd.io/_uploads/Hkl_Jtno1x.png)
    * 但第二次竟然顯示舊方法比較塊
        ![Figure_2](https://hackmd.io/_uploads/rJ4O1F2oyl.png)

後來嘗試修改初始節點數量，也會有類似的情形

* 執行 `./test_script.sh 50 30000 100` 兩次
![Figure_3](https://hackmd.io/_uploads/ryZn1t2s1l.png)
![Figure_4](https://hackmd.io/_uploads/ryH3kKns1x.png)

* 執行 `./test_script.sh 50 50000 100` 兩次
![Figure_5](https://hackmd.io/_uploads/rJk6Jthske.png)
![Figure_6](https://hackmd.io/_uploads/Hkeza1Y3iyg.png)

原先預期不論怎麼執行 `test_script.sh`，都必定會顯示某一種方法比較快，但從前面的描述可以得知，`test_script.sh` 會告訴我有時候是新方法比較快，有時則是舊方法比較快，這樣無從判斷哪一個方法是最佳的實作。

或許是我設想的實驗方式，有未考量到的影響因素，從而導致有時是新方法比較快，有時則是舊方法比較快，後續或許要更周全地考量實驗方式與測試環境，才能更正確地分析並找出最佳的實作。