---
tags: linux2023
---
# 2023q1 Homework1 (quiz1)
contributed by < `brianlin314` >
> [第一周測驗題](https://hackmd.io/@sysprog/linux2023-quiz1)
### 測驗一
:::danger
不用抄題目,開發紀錄著重於你的想法和過程中遭遇到的問題,紀錄和討論才是主體,程式碼僅作搭配說明的用途。
:notes: jserv
> 學生收到
:+1:
:::
遞迴版 `quick sort` ,使用 `Divide and Conquer` 的概念。
`quick sort` 實做流程:
1. 在資料列中找出一個基準值 `pivot`
2. 將小於 `pivot` 的資料放在左邊,大於 `pivot` 的資料放在右邊
3. 左右兩邊資料分別重複1~2步驟,直到剩下1筆資料
4. 合併
```c
if (list_empty(head) || list_is_singular(head))
return;
```
首先,會先判斷 `list` 是否有節點存在,抑或是只存在一個節點。
```c
struct list_head list_less, list_greater;
INIT_LIST_HEAD(&list_less);
INIT_LIST_HEAD(&list_greater);
```
`list_less`, `list_greater` 分別代表兩個不同 `list` 的 `head`,前者放入比 `Pivot` 小的節點,後者,反之。
```c
struct item *pivot = list_first_entry(head, item, list);
```
`pivot` 設為該 `list` 的第一個節點。
所以使用 `list_first_entry` 來取得 `list` 的第一個節點的 `entry`。
:::warning
不用提及行號,可斟酌列舉相關程式碼來解說。避免「舉燭」,你應該思考:要是我來實作這樣的程式碼,我會怎麼做。
:notes: jserv
:::
```c
struct item *itm = NULL, *is = NULL;
list_for_each_entry_safe (itm, is, head, list) {
if (cmpint(&itm->i, &pivot->i) < 0)
list_move_tail (&itm->list, &list_less);
else
list_move_tail (&itm->list, &list_greater);
}
```
使用 `list_for_each_entry_safe` 來走訪整個 `list`,接著使用比較函式 `cmpint` 來判斷節點數值跟 `pivot` 的大小
- 若節點的值小於 `pivot`,用 `list_move_tail` 將該節點從原 list 移動到 `list_less` 的尾端。
- 反之若大於等於 `pivot`,用 `list_move_tail` 將改節點移動到 `list_greater` 的尾端。
```c
list_add(&pivot->list, head);
list_splice(&list_less, head);
list_splice_tail(&list_greater, head);
```
接著持續不斷對 `&list_less` 與 `&list_greater` 進行遞迴,直到無法切割為止。
最後,將 `&list_less` 先串接到 `head`,再將 `&list_greater` 接到該 `list` 的尾端。
#### 可改進之處
因為上述程式碼每次皆選取 `list` 的第一個節點作為 `pivot`,這會遇到一個問題,就是當該 `list` 已經是由大到小或是由小到大排序時,會導致每次選到的 `pivot` 都剛好是最大值或最小值,而沒辦法均分 `list_less` 和 `list_greater`,若遇到該極端狀況,上述程式碼的時間複雜度就是 $O(n^2)$。
因此可以透過更改選擇 `pivot` 的方法,去改進上述的程式碼,大概分為以下三種方法:
1. Middle-of-Three
(1) 令 $middle = \dfrac{left + right}{2}$
(2) 比較 list[left]、list[middle] 與 list[right] 這三筆資料,排出中間值,並將此中間值再與 list[left] 做交換
(3) 讓現在新的 list[left] 作為 pivot
2. Randomized pivot
亂數選取的方式,隨機挑 list 中的一個值作為 pivot,但 worst case 還是 $O(n^2)$
3. Median-of-Medians
詳細流程可參考 [Median of medians](https://en.wikipedia.org/wiki/Median_of_medians)
---
### 測驗二
```c
struct list_head stack[MAX_DEPTH];
for (int i = 0; i < MAX_DEPTH; i++)
INIT_LIST_HEAD(&stack[i]);
int top = -1;
list_splice_init(head, &stack[++top]);
struct list_head partition;
INIT_LIST_HEAD(&partition);
```
一開始即宣告一個 `stack` ,代表之後的實作皆與之有關,並且依照其定義的大小,使用 `INIT_LIST_HEAD` 初始化,接著將整條未排列的 `list` 放到 `stack` 裡。
再宣告一個 `partition` ,作用是每次迴圈中都會存放 `stack` top 中的 `list`。
需特別注意 `++top` 與 `top++` 的差別,因為自己習慣用 `i++` 了,所以常常忘記區分這兩種 case。
```c
while (top >= 0) {
INIT_LIST_HEAD(&partition);
list_splice_init(&stack[top--], &partition);
...
}
```
每次進入迴圈時,都會將 `stack` top 中的 `list` 放到 `partition` 中,並進行以下 if-else 判斷。
```c
if (!list_empty(&partition) && !list_is_singular(&partition))
```
若 `partition` 有大於等於兩個節點,則通過該條件式,並且與 `測驗一` 的實作方法相同,`pivot` 選為該 `list` 的第一個節點,使用 `list_for_each_entry_safe` 走訪整個 `list` 並將比 `pivot` 小的節點放到 `list_less` 中,反之,則放到 `list_greater` 。
```c
list_move_tail (&pivot->list, &list_less);
if (!list_empty(&list_greater))
list_splice_tail(&list_greater, &stack[++top]);
if (!list_empty(&list_less))
list_splice_tail(&list_less, &stack[++top]);
```
先將 `pivot` 放到 `list_less` 的尾端,因為 `list_less` 裡面的節點值皆比 `pivot` 小。
再來要將 `list_less` 和 `list_greater` 再次放到 `stack` 中,由於是要加入到 `stack` 中,所以要先放入 `list_greater`,再放入 `list_less`,這樣才符合 FILO,等等拿出來的順序才是由小到大。
```c
else {
top++;
list_splice_tail(&partition, &stack[top]);
while (top >= 0 && list_is_singular(&stack[top])) {
struct item *tmp =
list_first_entry(&stack[top], struct item, list);
list_del(&tmp->list);
INIT_LIST_HEAD(&stack[top--]);
list_add_tail(&tmp->list, head);
}
}
```
若 `partition` 只有一個節點的話,會將 `partition` 放回 `stack` 中。
`while` 迴圈會不斷從 `stack` 中 pop `list` 出來,並且插入到 `head` 的尾端,直到該 `list` 不是只有一個節點存在或該 `stack` 為空為止。
---
### 測驗三