# 2022q1 Homework1 (lab0)
contributed by < [Tcc0403](https://github.com/Tcc0403) >
## 實驗環境
```shell
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 48 bits physical, 48 bits virtual
CPU(s): 12
On-line CPU(s) list: 0-11
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
NUMA node(s): 1
Vendor ID: AuthenticAMD
CPU family: 25
Model: 33
Model name: AMD Ryzen 5 5600X 6-Core Processor
Stepping: 0
Frequency boost: enabled
CPU MHz: 2200.000
CPU max MHz: 4650.2920
CPU min MHz: 2200.0000
BogoMIPS: 7385.57
Virtualization: AMD-V
L1d cache: 192 KiB
L1i cache: 192 KiB
L2 cache: 3 MiB
L3 cache: 32 MiB
NUMA node0 CPU(s): 0-11
```
## 針對佇列的操作
### q_new
為佇列建立 `list_head` 結構體後,透過 `INIT_LIST_HEAD` 為其初始化
```c
struct list_head *q_new()
{
struct list_head *q = malloc(sizeof(struct list_head));
if (q == NULL)
return NULL;
INIT_LIST_HEAD(q);
return q;
}
```
### q_free
利用 `list_for_each_entry_safe` 走訪每個 `element_t` ,呼叫 `q_relase_element` 函式釋放空間
```c
void q_free(struct list_head *l)
{
if (l == NULL)
return;
element_t *e, *s;
list_for_each_entry_safe (e, s, l, list) {
q_release_element(e);
}
free(l);
}
```
### q_insert_head
:::warning
[課程文章中題的到 Head](https://hackmd.io/@sysprog/c-linked-list#Linked-list-%E5%9C%A8-Linux-%E6%A0%B8%E5%BF%83%E5%8E%9F%E5%A7%8B%E7%A8%8B%E5%BC%8F%E7%A2%BC) 跟 `q_insert_head` 的 head 是不一樣的東西,這裡所指的是圖片所標記的 Node 0 ,也就是 `head->next`
:::
建立 `element_t` 結構體 `new` ,利用 `strdup` 儲存字串內容,接著將 `element_t` 結構體中包含的 `list_head` 結構體作為參數傳入`list_add` 函式,使其加入佇列
```c=47
bool q_insert_head(struct list_head *head, char *s)
{
if(head == NULL)
return false;
element_t *new = malloc(sizeof(element_t));
if(new == NULL)
return false;
new->value = strdup(s);
if(new->value == NULL){
q_release_element(new);
return false;
}
list_add(&(new->list), head);
return true;
}
```
:::warning
目前無法通過 pre-commit 的靜態分析,顯示第 60 行出錯
```shell
$ git commit
queue.c:60:5: error: Memory leak: new [memleak]
return true;
^
```
:::
把 `&(new->list)` 改成 `&new->list` 就通過了
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (head == NULL)
return false;
element_t *new = malloc(sizeof(element_t));
if (new == NULL)
return false;
new->value = strdup(s);
if (new->value == NULL) {
free(new);
return false;
}
list_add(&new->list, head);
return true;
}
```
### q_insert_tail
與上面操作類似,差別在 `list_add` 改成 `list_add_tail`
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (head == NULL)
return false;
element_t *new = malloc(sizeof(element_t));
if (new == NULL)
return false;
new->value = strdup(s);
if (new->value == NULL) {
free(new);
return false;
}
list_add_tail(&new->list, head);
return true;
}
```
### q_remove_head
利用 `list_del_init` 將第一項 `element_t` 結構體移出佇列
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (head == NULL || list_empty(head))
return NULL;
element_t *ele = list_entry(head->next, element_t, list);
list_del_init(&ele->list);
if (sp != NULL) {
strncpy(sp, ele->value, bufsize - 1);
*(sp + bufsize - 1) = '\0';
}
return ele;
}
```
使用 `list_del_init` 而非 `list_del` 的原因,是擔心未來對回傳的 `element_t` 結構體做額外操作,導致錯誤發生
:::warning
~~時間複雜度測試沒過,待研究~~
發現 [eric88525 筆記](https://hackmd.io/@eric88525/linux2022-lab0#%E6%9C%80%E5%BE%8C%E4%B8%80%E5%80%8B%E6%B8%AC%E8%A9%A6%E7%9A%84bug) 有類似情況,自己上傳後在 GitHub Actions 中也有通過
:::
### q_remove_tail
跟上方作法類似
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (head == NULL || list_empty(head))
return NULL;
element_t *ele = list_entry(head->prev, element_t, list);
list_del_init(&ele->list);
if (sp != NULL) {
strncpy(sp, ele->value, bufsize - 1);
*(sp + bufsize - 1) = '\0';
}
return ele;
}
```
:::warning
同上
:::
### q_size
利用 `list_for_each` 走訪每個節點
```c
int q_size(struct list_head *head)
{
if (head == NULL || list_empty(head))
return 0;
int count = 0;
struct list_head *n;
list_for_each (n, head) {
count++;
}
return count;
}
```
原本以為 `list_for_each` 會走到 `head` 本身,但仔細看才發現是從 `head->next` 開始
```c
#define list_for_each(node, head) \
for (node = (head)->next; node != (head); node = node->next)
```
### q_delete_mid
利用環狀雙向鏈結串列的特性,透過兩個指標反向走訪串列,一個向前、一個向後,當兩個指標相遇或相鄰時, 向前走的指標所指向的節點即是中間節點
```c
bool q_delete_mid(struct list_head *head)
{
if(head == NULL || list_empty(head))
return false;
struct list_head *forward, *backward;
forward = head->next;
backward = head->prev;
while(forward != backward && forward->next != backward)
{
forward = forward->next;
backward = backward->prev;
}
list_del(forward);
q_release_element(list_entry(forward, element_t, list));
return true;
}
```
之後排序會使用到中間節點,因此將取得中間節點寫成函式 `q_get_mid`
```c
struct list_head *q_get_mid(struct list_head *head)
{
if (list_is_singular(head))
return head->next;
struct list_head *forward, *backward;
forward = head->next;
backward = head->prev;
while (forward != backward && forward->next != backward) {
forward = forward->next;
backward = backward->prev;
}
return forward;
}
```
改寫 `q_delete_mid`
```c
bool q_delete_mid(struct list_head *head)
{
if (head == NULL || list_empty(head))
return false;
struct list_head *mid = q_get_mid(head);
list_del(mid);
q_release_element(list_entry(mid, element_t, list));
return true;
}
```
### q_delete_dup
透過 `list_for_each_entry_safe` 來走訪佇列並刪除與相鄰節點有相同值的節點
```c
bool q_delete_dup(struct list_head *head)
{
if (head == NULL)
return false;
element_t *e, *s;
list_for_each_entry_safe (e, s, head, list)
if (strcmp(e->value, s->value) == 0) {
list_del(&e->list);
q_release_element(e);
}
return true;
}
```
:::warning
會發生 Segmentation fault
問題發生在 `list_for_each_entry_safe` 巨集的 `safe` 會走回串列的 `head` 節點,由於該節點並非 `element_t` 結構體,試圖讀取不存在的成員便會出錯
```c
#define list_for_each_entry_safe(entry, safe, head, member) \
for (entry = list_entry((head)->next, __typeof__(*entry), member), \
safe = list_entry(entry->member.next, __typeof__(*entry), member); \
&entry->member != (head); entry = safe, \
safe = list_entry(safe->member.next, __typeof__(*entry), member))
```
:::
修正如下,在比對字串前先確認是 `safe` 是否為串列的 `head`
```c
bool q_delete_dup(struct list_head *head)
{
if (head == NULL)
return false;
element_t *e, *s;
list_for_each_entry_safe (e, s, head, list)
if (&s->list != head && strcmp(e->value, s->value) == 0) {
list_del(&e->list);
q_release_element(e);
}
return true;
}
```
:::warning
近期的 commit [de856da](https://github.com/sysprog21/lab0-c/commit/de856da257eeb42ca1d3dd86424588606692d9f5) 修正 `q_delete_dup` 函式的定義,上述程式碼需要進行修改
:::
修正如下
多宣告一個 `bool` 變數來幫忙紀錄是否重複
```c
bool q_delete_dup(struct list_head *head)
{
if (head == NULL)
return false;
element_t *e, *s;
bool is_dup = false;
list_for_each_entry_safe (e, s, head, list)
if (&s->list != head && strcmp(e->value, s->value) == 0) {
is_dup = true;
list_del(&e->list);
q_release_element(e);
} else if (is_dup) {
is_dup = false;
list_del(&e->list);
q_release_element(e);
}
return true;
}
```
### `q_swap`
直接重新指派兩兩一組節點相連的六個指標
```c
void q_swap(struct list_head *head)
{
struct list_head *node1, *node2;
node1 = head->next;
node2 = node1->next;
while (node1 != head && node2 != head) {
node1->prev->next = node2;
node2->prev = node1->prev;
node2->next->prev = node1;
node1->next = node2->next;
node2->next = node1;
node1->prev = node2;
node1 = node1->next;
node2 = node1->next;
}
}
```
閱讀 [lanser 同學的作法](https://hackmd.io/@laneser/linux2022_lab0#q_swap)後,發現可以將 `q_swap` 視作將奇數節點移除後插入偶數節點之後
List API 中的`list_move` 函式正好是 `list_delete` 和 `list_add` 的連續操作
將以上程式改寫為更精簡易讀的版本
```c
void q_swap(struct list_head *head)
{
struct list_head *node1, *node2;
node1 = head->next;
node2 = node1->next;
while (node1 != head && node2 != head) {
list_move(node1, node2);
node1 = node1->next;
node2 = node1->next;
}
}
```
### q_reverse
利用 `list_for_each_safe` 走訪串列交換 `next` 和 `prev` ,因為不會走到佇列的 `head` ,所以要額外處理
```c
void q_reverse(struct list_head *head)
{
if (head == NULL || list_empty(head))
return;
struct list_head *n, *s;
list_for_each_safe (n, s, head) {
n->next = n->prev;
n->prev = s;
}
struct list_head *tmp;
tmp = head->next;
head->next = head->prev;
head->prev = tmp;
}
```
### q_sort
原本打算參考 [Merge Sort 與它的變化](https://hackmd.io/@lambert-wu/modified-merge-sort#Merge-Sort-%E8%88%87%E5%AE%83%E7%9A%84%E8%AE%8A%E5%8C%96) ,但跟本次實驗的資料結構略為不同,這次實驗是採用[Intrusive linked lists](https://www.data-structures-in-practice.com/intrusive-linked-lists/)
一開始想另外宣告 `list_head` 結構體並存放在 `list_head *` 陣列當中,以便進行上述筆記的[分割方法](https://hackmd.io/@lambert-wu/modified-merge-sort#%E5%88%86%E5%89%B2),但寫完發現本次實驗不允許在 `q_sort` 當中進行 `malloc` 和 `free` 等操作
參考幾位同學的筆記後,決定利用 `LIST_HEAD` 巨集宣告區域變數接收分割出來的串列,維持雙向環狀的結構,以便透過 List API 操作串列
```c
void q_sort(struct list_head *head)
{
if (head == NULL || list_empty(head) || list_is_singular(head))
return;
LIST_HEAD(tmp);
list_cut_position(&tmp, head, q_get_mid(head));
q_sort(head);
q_sort(&tmp);
mergeTwoLists(head, &tmp);
}
```
在 `mergeTwoLists` 函式中,因為兩個引數 `L1`, `L2` 都是用來管理串列的 Head ,可以利用此特性,將兩條串列合併至其中一個 Head 之下
`list_move_tail(node, head)` 函式等價於把 `node` 節點移出原本其所在的串列,再將其插入 `head` 節點之前
```c
void mergeTwoLists(struct list_head *L1, struct list_head *L2) {
struct list_head *node = L1->next;
element_t *E1, *E2;
while(!list_empty(L2) && node != L1){
E1 = list_entry(node, element_t, list);
E2 = list_entry(L2->next, element_t, list);
if(strcmp(E1->value, E2->value) > 0){
list_move_tail(&E2->list, &E1->list);
} else {
node = node->next;
}
}
list_splice_tail(L2, L1);
}
```
### q_shuffle
嘗試撰寫一個 Linux 風格的 List API
```c
struct list_head *list_node_at(struct list_head *head, int index)
{
struct list_head *node = head->next;
while(--index)
node = node->next;
return node;
}
```
依照 [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 演算法實作
因為每次迭代都會縮小選取範圍,直接將選到的節點移至串列最尾端即可
```c
void q_shuffle(struct list_head *head)
{
if (head == NULL || list_empty(head) || list_is_singular(head))
return;
srand(time(NULL));
for(int i = q_size(head); i > 1 ; i--){
list_move_tail(list_node_at(head, rand()%i), head);
}
}
```
在 `qtest.c` 中參考 `do_sort` 函式,實作 `do_shuffle` 函式
```c
static bool do_shuffle(int argc, char *argv[])
{
if (argc != 1) {
report(1, "%s takes no arguments", argv[0]);
return false;
}
if (!l_meta.l)
report(3, "Warning: Calling shuffle on null queue");
error_check();
int cnt = q_size(l_meta.l);
if (cnt < 2)
report(3, "Warning: Calling shuffle on single node");
error_check();
set_noallocate_mode(true);
if (exception_setup(true))
q_shuffle(l_meta.l);
exception_cancel();
set_noallocate_mode(false);
show_queue(3);
return !error_check();
}
```
`console_init` 函式中新增 Shuffle 的相關命令
```c
ADD_COMMAND(shuffle,
" | Perform Fisher-Yates shuffle in queue");
```
透過 `qtest` 進行測試
```shell
$ ./qtest
cmd> new
l = []
cmd> it 1
l = [1]
cmd> it 2
l = [1 2]
cmd> it 3
l = [1 2 3]
cmd> shuffle
l = [3 2 1]
cmd> shuffle
l = [2 1 3]
cmd> it 4
l = [2 1 3 4]
cmd> shuffle
l = [3 2 1 4]
```
git commit 的時候發現不能更動 `queue.h` 和 `list.h`,將所有程式移至 `qtest.c`