---
tags: linked list, insertion sort, quiz, linux internal, linux2020quiz
---
# [2020q1 第一週測驗題](https://hackmd.io/@sysprog/linux2020-quiz1) 解題思路
contributed by [chses9440611](https://github.com/chses9440611)
* [related code](https://github.com/chses9440611/2020_linux_internal_test)
```cpp
list *sort(list *start) {
if (!start || !start->next)
return start;
list *left = start;
list *right = left->next;
left->next = NULL; // LL0
left = sort(left);
right = sort(right);
for(list *merge = NULL; left || right;) {
if (!right || (left && left->data < right->data)) {
if (!merge) {
start = merge = left; // LL1
} else {
merge->next = left; // LL2
merge = merge->next;
}
left = left->next; // LL3
} else {
if (!merge) {
start = merge = right; // LL4
} else {
merge->next = right; // LL5
merge = merge->next;
}
right = right->next; // LL6
}
}
return start;
}
```
## Solving thinking
* 首先,先來考慮如何決定 LL0
LL0 有四個選項<br>
```
(a) left->next = NULL
(b) right->next = NULL
(c) left = left->next
(d) left = right->next
```
由第8列和第9列可以知道,排序採取的策略應該跟 **"Divide and Conquer"** 有關 ,而 `right = sort(right)` 會使right 指向一排序好的串列,而 (a) 選項,可以將一 linked list 分成兩個部分。至於 sort 要回傳一個由小到大排好的串列。
若用 \(b\) 選項 `right->next = NULL` 則會在`left = sort(left)` 產生迴圈式的函式呼叫(infinite loop)。
\(c\) 會使left 和 right 指向同一個節點,且 sort(left) 會和 sort(right) 做同樣的事,故不考慮。
\(d\) 會使 left 和 right 因為 sort(left) 和 sort(right)分別指向 n-1 節點中次小和最小的節點,而 start 會指向第 n 個節點,但是觀察後面的程式碼,並沒有 start->data 的比較敘述。故不選擇 \(d\) 選項
* 由於 LL0 為 `left->next = NULL`。則對於一個有 n 個節點的 串列,left 和 right 會分別指到最左邊的節點跟一個有 n-1 節點的 串列,而 sort(right) 會回傳一個由小到大排好的串列。
則接下會有
1. left->data < right->data
2. left->data > right->data
這兩種情形。
附圖是進入迴圈前會有的樣子
![link list](https://i.imgur.com/4edbXCg.jpg)
由於 merge 的功用應該是找出要執行合併的節點的位置。
因此在第一個 case 中 merge 要跟 left 指向同一個節點。即 LL1 應為
```
(c) merge = left
或
(e) start = merge = left
```
```
start = merge = left -> a -> NULL
right -> b -> c -> ... -> d -> e -> NULL
```
* 接下來,case 1 會執行 LL3,而在 case 1 中,只剩下要怎麼組合 merge 指向的節點和 right 所指向的串列。又 for 迴圈的中止條件為 left 和 right 都要指向 NULL。當下只有 left 可以更動,且 left->next = NULL,所以 LL3 為 `left = left->next`。
```
left = NULL
start = merge -> a -> NULL
right -> b -> c -> ... -> d -> e -> NULL
```
* 繼續執行下去,因為 `left = NULL`,會去執行 LL5,從選項可以知道,LL5負責合併,故選擇`merge->next = right`,至此,case 1 基本上已經完成,接著只要讓 right 指到 NULL 即可完成。
```cpp=23
else {
merge->next = right; // LL5
merge = merge->next;
}
LL6
```
```
left = NULL
start = a -> b -> c -> ... -> d -> e -> NULL
^
merge,right
```
* 因此 LL6 只有 `right = right->next` 可選。因為left 已經指到 NULL 所以不會是 `right = left->next`。
* 到這邊只剩下 LL4 了,接著考慮 case 2的情況,若我們假設 case2 的情況如下:
```
start = left->3
right->1->2->4->5
```
那麼在迴圈中會進入到 LL4的部份,這邊要重新將 start 指向 right 所指向的節點。而且要 merge 的節點會在 right 所指向的串列之中。故 LL4 應為 `start = merge = right`。
```
left -> 3
start = merge = right -> 1 -> 2 -> 4 -> 5
```
* 至此,除了 LL1 的blank 應該都填好了,可以看得出來這是一個串列遞迴版的 [Insertion Sort](https://en.wikipedia.org/wiki/Insertion_sort)
* 繼續,用上述的例子來驗證之前的選擇是否能夠正常運行。
在第二次迴圈前
```
left -> 3
start = merge -> 1 -> 2 -> 4 -> 5
^
right
```
* 接著因為 `left -> data > right -> data` 且 `merge != NULL`,所以進入 line 23-26 的部份。
```cpp=23
else{
merge -> next = right;
merge = merge ->next;
}
right = right -> next;
```
執行完後
```
left -> 3
start -> 1 -> 2 -> 4 -> 5
^ ^
merge right
```
* 進入迴圈之後,因為 `left ->data < right -> data`,會跳到 line 15-18 的地方來執行
```cpp=15
else {
merge->next = left; // LL2
merge = merge->next;
}
left = left->next;
```
執行完後
```
left -> NULL
start -> 1 -> 2 -> 3 -> NULL
^
merge
right -> 4 -> 5 -> NULL
```
* 之後進入後會跳到去執行 line 23-26
```cpp=23
else{
merge -> next = right;
merge = merge ->next;
}
right = right -> next;
```
執行完後是
```
left -> NULL
start -> 1 -> 2 -> 3 -> 4 -> 5 -> NULL
^ ^
merge right
```
* 至此,排序已經完成,進入迴圈再次執行 line 23-26 直到 right 指向 NULL
```cpp=23
else{
merge -> next = right;
merge = merge ->next;
}
right = right -> next;
```
執行完後會是
```
left -> NULL
start -> 1 -> 2 -> 3 -> 4 -> 5 -> NULL
^ ^
merge right
```
* 最後因為 left 和 right 都指向 NULL,不會再進入迴圈,驗證正確。
## Explanation
遞迴式的Insertion Sort
1. 將 n 個結點的串列分成一個節點和一 n-1 個節點的串列,分別用 left 和 right 去指向
2. right = sort(right) 回傳一個已排序好的串列
3. 比較 left 和 right 所指的節點的大小,較小的節點為 start ,並將 left 所指向的節點用 Insertion Sort 的方式插入到正確的位置
## LL1 的選擇
* LL1有兩種可能選擇,`(c) merge = left` 或是 `(e) start = merge = left`,而答案給的是後者,但是在一開始,start 跟 left 是指向同一個節點。而start 只需要在回傳的 right 所指向的節點小於 left 所指向的節點才需要重新指向 right。所以兩者的答案都不會改變結果。
* 除了是避免在運行過程中 start 因不可預期的錯誤導致指到 undefined 的地方,那 \(e\) 會是比 \(c\)好的選項。
* 所以我也用不同的選項分別去執行來看出是否有所不同
```cpp=
if (version == 0)
start = merge = left;
else
merge = left;
```
* 利用產生 100 組 10 個亂數的數字串列,每組分別有兩個一樣的串列 head 和 head2,分別去做不同選項的排序。
所得結果如下:
```
...
-----------------------
The array numbers are: 37 34 4 12 39 92 7 87 97 72
Queue1 after sort: 4 7 12 34 37 39 72 87 92 97
Queue2 after sort: 4 7 12 34 37 39 72 87 92 97
Are two queues same?
Same queues
-----------------------
The array numbers are: 16 36 34 73 24 70 23 63 95 20
Queue1 after sort: 16 20 23 24 34 36 63 70 73 95
Queue2 after sort: 16 20 23 24 34 36 63 70 73 95
Are two queues same?
Same queues
-----------------------
The array numbers are: 36 81 5 54 41 18 72 8 72 82
Queue1 after sort: 5 8 18 36 41 54 72 72 81 82
Queue2 after sort: 5 8 18 36 41 54 72 72 81 82
Are two queues same?
Same queues
-----------------------
The total scores: 100/100
```
由此可見,LL1 應有兩個答案。
## Usage of cppcheck
使用 cppcheck 來做靜態檢查
`cppcheck --enable=all main.c qsort.c qsort.h`
輸出如下:
```
Checking main.c ...
main.c:15:7: style: The scope of the variable 'numbers' can be reduced. [variableScope]
int *numbers, i, scores;
^
main.c:16:8: style: The scope of the variable 'q_head' can be reduced. [variableScope]
list *q_head, *q_tail, *q_head2, *q_tail2;
^
main.c:16:17: style: The scope of the variable 'q_tail' can be reduced. [variableScope]
list *q_head, *q_tail, *q_head2, *q_tail2;
^
main.c:16:26: style: The scope of the variable 'q_head2' can be reduced. [variableScope]
list *q_head, *q_tail, *q_head2, *q_tail2;
^
main.c:16:36: style: The scope of the variable 'q_tail2' can be reduced. [variableScope]
list *q_head, *q_tail, *q_head2, *q_tail2;
^
1/3 files checked 47% done
Checking qsort.c ...
qsort.c:23:8: style: The scope of the variable 'tmp' can be reduced. [variableScope]
list* tmp;
^
2/3 files checked 92% done
Checking qsort.h ...
3/3 files checked 100% done
qsort.c:43:0: style: The function 'q_compare' is never used. [unusedFunction]
^
nofile:0:0: information: Cppcheck cannot find all the include files (use --check-config for details) [missingIncludeSystem]
```
* 大都是將變數宣告跟賦值分開寫的問題,所以會出現 variable scope 的 reduced 建議
* qsort.c 有一個 q_compare function ,這個是用來檢測 sort() 回傳的串列的排序和經過 bubble_sort 的整數陣列是否一致,後來驗證過後,把 main.c 中關於 q_compare 的敘述拿掉了。所以出現 unusedFunction 的回報。
* 使用 --supress=missingIncludeSystem 來避免找不到 include files 的訊息
經修正後使用`cppcheck --enable=all --supress=missingIncludeSystem *.[ch]`
結果如下:
```
Checking main.c ...
1/3 files checked 48% done
Checking qsort.c ...
2/3 files checked 93% done
Checking qsort.h ...
3/3 files checked 100% done
```
## Usage of [infer](https://fbinfer.com)
* 使用 infer 發現能夠找出 cppcheck 不一定會回報的問題。
```
infer run -- gcc -c main.c qsort.c qsort.h
```
然後出現以下錯誤和一堆紅字:
```
Capturing in make/cc mode...
main.c:22:25: error: use of undeclared identifier 'q_tail2'; did you mean 'q_tail'?
list *q_tail = *q_tail2 = NULL;
^~~~~~~
q_tail
main.c:22:15: note: 'q_tail' declared here
list *q_tail = *q_tail2 = NULL;
^
main.c:22:33: error: assigning to 'list' (aka 'struct __list') from incompatible type 'void *'
list *q_tail = *q_tail2 = NULL;
^ ~~~~
main.c:24:38: error: use of undeclared identifier 'q_tail2'; did you mean 'q_tail'?
list *q_head2 = q_insert_lot(q_tail2, numbers, LENGTH);
^~~~~~~
q_tail
main.c:22:15: note: 'q_tail' declared here
list *q_tail = *q_tail2 = NULL;
^
3 errors generated.
Error: the following clang command did not run successfully:
...
```
看了之後,發現是 q_tail2 宣告的問題,這種寫法是無效的。將其改為
```
list *q_tail, *q2_tail;
q_tail = q2_tail = NULL;
```
也改正其他位置的錯誤。
* 再次執行 infer 的檢查
得到如下訊息
```
Found 2 issues
qsort.c:8: error: NULL_DEREFERENCE
pointer `tmp` last assigned on line 7 could be null and is dereferenced at line 8, column 5.
6. {
7. list *tmp = malloc(sizeof(list));
8. > tmp->data = d;
9. tmp->next = NULL;
10. return tmp;
main.c:66: error: NULL_DEREFERENCE
pointer `tmp` last assigned on line 63 could be null and is dereferenced at line 66, column 9.
64. int i;
65. for (i = 0; i < size; i++)
66. > tmp[i] = rand() % 100;
67.
68. return tmp;
Summary of the reports
```
這邊是因為 tmp 有可能有 NULL_DEREFERENCE 的問題,我這邊透過增加 if 敘述來處理,即因為 malloc 失敗的話就會回傳 NULL。較好的方式應該是寫例外處理的函式來進行。
* 修正到沒有 NULL deference 的問題後,可得如下
```
Capturing in make/cc mode...
Found 3 source files to analyze in /home/jason/Project/2020_Linux_Internal/ttt/2020_linux_internal_test/infer-out
Analysis finished in 4.517ss
```
* 使用 infer 會在目前目錄中產生 infre-out 的資料夾,會將檢查的報告在 bugs.txt 這個檔案中。
## Feedback
若有想要問的或是哪裡的敘述寫得不足或是不清楚的地方,可以在下面直接編輯。
ex:
- [ ] 延伸問題勒
延伸問題
===
1. 解釋上述程式運作原理;
2. 指出程式改進空間,特別是考慮到 Optimizing merge sort;
3. 將上述 singly-linked list 擴充為 circular doubly-linked list 並重新實作對應的 sort;
4. 依循 Linux 核心 include/linux/list.h 程式碼的方式,改寫上述排序程式;
5. 嘗試將原本遞迴的程式改寫為 iterative 版本;