# 2023q1 Homework1 (lab0)
contributed by < `csm1735` >
## 開發環境
```shell
$ 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): 4
On-line CPU(s) list: 0-3
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz
CPU family: 6
Model: 158
Thread(s) per core: 1
Core(s) per socket: 4
Socket(s): 1
Stepping: 9
CPU max MHz: 3500.0000
CPU min MHz: 800.0000
BogoMIPS: 4999.90
```
## 改進 `queue.c`
### q_new
```c
struct list_head *q_new()
{
struct list_head *head = malloc(sizeof(*head));
if (!head) {
free(head);
return NULL;
}
INIT_LIST_HEAD(head);
return head;
}
```
### q_free
```c
void q_free(struct list_head *l)
{
if (!l)
return;
element_t *entry, *safe;
list_for_each_entry_safe (entry, safe, l, list) {
q_release_element(entry);
}
free(l);
}
```
釋放已配置的記憶體,考量到刪除節點後,還需要找到下一個節點的位置,因此這邊選擇使用 `list_for_each_entry_safe` 來實作。
### q_insert_head / tail
```c
element_t *new_element(char *s)
{
element_t *element = malloc(sizeof(*element));
if (!element) {
free(element);
return NULL;
}
element->value = strdup(s);
if (!element->value) {
free(element);
return NULL;
}
return element;
}
```
實作後發現 `q_insert_head` 跟 `q_insert_tail` 在程式碼上有大量重複的狀況,考量到精簡度及維護性,因此選擇將重複部分獨立成 `new_element` 來使用。
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *element = new_element(s);
if (!element)
return false;
list_add(&element->list, head);
return true;
}
```
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *element = new_element(s);
if (!element)
return false;
list_add_tail(&element->list, head);
return true;
}
```
`q_insert_head` 跟 `q_insert_tail` 主要差異在於一個使用了 `list_add` 而另一個使用了 `list_add_tail` 。
### q_remove_head
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *tmp = list_first_entry(head, element_t, list);
if (sp) {
strncpy(sp, tmp->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
list_del(&tmp->list);
return tmp;
}
```
一開始沒有看懂說明,以為要對 `sp` 做 `malloc` ,所以產生了問題,後來發現應該直接去檢查 `sp` 是否為 `NULL` , 再來做 `strncpy` 。
### q_remove_tail
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *tmp = list_last_entry(head, element_t, list);
if (sp) {
strncpy(sp, tmp->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
list_del(&tmp->list);
return tmp;
}
```
跟 `q_remove_head` 主要差異在於其使用了 `list_first_entry` 而這邊使用了 `list_last_entry` 。
### q_size
```c
int q_size(struct list_head *head)
{
if (!head || list_empty(head))
return 0;
size_t len = 0;
struct list_head *cur;
list_for_each (cur, head) {
++len;
}
return len;
}
```
目前沒有想到其他更有效率的做法,只能透過走訪所有的節點來計算 `size` 。
### q_delete_mid
```c
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
size_t len = q_size(head), count = -1;
element_t *entry, *safe;
list_for_each_entry_safe (entry, safe, head, list) {
++count;
if (count == len / 2) {
list_del(&entry->list);
q_release_element(entry);
break;
}
}
return true;
}
```
找出中間的節點後,將其移除並釋放記憶體。
### q_delete_dup
```c
bool q_delete_dup(struct list_head *head)
{
if (!head)
return false;
bool flag = 0;
element_t *entry, *safe;
list_for_each_entry_safe (entry, safe, head, list) {
if (&safe->list == head) {
if (flag == 1) {
list_del(&entry->list);
q_release_element(entry);
}
break;
}
if (strcmp(entry->value, safe->value) == 0) {
list_del(&entry->list);
q_release_element(entry);
flag = 1;
} else if (flag == 1) {
list_del(&entry->list);
q_release_element(entry);
flag = 0;
}
}
return true;
}
```
這邊函式需要佇列在排序完的狀況下執行,去刪除具有相同字串的節點,假設這邊有 $N$ 個相同字串的節點,那麼前 $N-1$ 個可以透過 `strcmp` 的判斷來做刪除,第 $N$ 個則是透過 `flag` 的檢查來做刪除 。
:::warning
撰寫更精簡的程式碼。
:notes: jserv
:::
```c
list_for_each_entry_safe (entry, safe, head, list) {
if (&safe->list != head && strcmp(entry->value, safe->value) == 0) {
list_del(&entry->list);
q_release_element(entry);
flag = 1;
} else if (flag == 1) {
list_del(&entry->list);
q_release_element(entry);
flag = 0;
}
}
```
將 `list_for_each_entry_safe` 內調整為更精簡的寫法
### q_swap
```c
void q_swap(struct list_head *head)
{
if (!head || list_empty(head))
return;
q_reverseK(head, 2);
return;
}
```
此函式 `swap` 的實質意義就等同於 `q_reverseK` 在參數 k 值為 2 的時候。
:::warning
是否可據此進一步精簡程式碼?
:notes: jserv
:::
### q_reverse
```c
void q_reverse(struct list_head *head)
{
if (!head || list_empty(head))
return;
struct list_head *node, *safe;
list_for_each_safe (node, safe, head) {
list_move(node, head);
}
return;
}
```
將所有的節點依序搬到頭後就等同於完成 `reverse` , 要注意這邊得使用 `list_for_each_safe` 。
### q_reverseK
```c
void q_reverseK(struct list_head *head, int k)
{
if (!head || list_empty(head))
return;
struct list_head *node, *safe, *head_from = head, tmp;
INIT_LIST_HEAD(&tmp);
size_t count = 0;
list_for_each_safe (node, safe, head) {
++count;
if (count == k) {
list_cut_position(&tmp, head_from, node);
q_reverse(&tmp);
list_splice_init(&tmp, head_from);
head_from = safe->prev;
count = 0;
}
}
return;
}
```
這邊參考了 [komark06](https://hackmd.io/@komark06/SyCUIEYpj#q_reverseK-%E5%AF%A6%E4%BD%9C) 的作法,每 k 個節點就切出來做 `reverse` 的動作,然後再將其連接回去。
### q_sort
:::danger
不用張貼完整程式碼,你應該闡述自己的想法、考量因素,還有分析已有方案的優缺點,並記錄過程中遇到的問題。
:notes: jserv
:::
```c
struct list_head *mergeTwoList(struct list_head *l1, struct list_head *l2)
{
if (!l2)
return l1;
if (!l1)
return l2;
element_t *n1 = list_entry(l1, element_t, list);
element_t *n2 = list_entry(l2, element_t, list);
if (strcmp(n1->value, n2->value) < 0) {
l1->next = mergeTwoList(l1->next, l2);
return l1;
} else {
l2->next = mergeTwoList(l1, l2->next);
return l2;
}
}
```
`mergeTwoList` 原先的遞迴寫法,雖然程式碼較為精簡,但在 `make test` 時效能上無法通過,因此修正為以下的迴圈版本
```c
struct list_head *mergeTwoList(struct list_head *l1, struct list_head *l2)
{
struct list_head *head, *tmp;
element_t *n1 = list_entry(l1, element_t, list);
element_t *n2 = list_entry(l2, element_t, list);
if (strcmp(n1->value, n2->value) < 0) {
tmp = l1;
l1 = l1->next;
} else {
tmp = l2;
l2 = l2->next;
}
head = tmp;
while(l1 && l2) {
n1 = list_entry(l1, element_t, list);
n2 = list_entry(l2, element_t, list);
if(strcmp(n1->value, n2->value) < 0) {
tmp->next = l1;
tmp = tmp->next;
l1 = l1->next;
} else {
tmp->next = l2;
tmp = tmp->next;
l2 = l2->next;
}
}
if(l1) tmp->next = l1;
if(l2) tmp->next = l2;
return head;
}
```
```c
struct list_head *mergeSortList(struct list_head *head)
{
// merge sort
if (!head || !head->next)
return head;
struct list_head *fast = head->next;
struct list_head *slow = head;
// split list
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
fast = slow->next;
slow->next = NULL;
// sort each list
struct list_head *l1 = mergeSortList(head);
struct list_head *l2 = mergeSortList(fast);
// merge sorted l1 and sorted l2
return mergeTwoList(l1, l2);
}
```
以上 `merge sort` 實作參考自 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/)
```c
void q_sort(struct list_head *head)
{
if (!head || list_empty(head))
return;
head->prev->next = NULL;
head->next = mergeSortList(head->next);
struct list_head *cur;
for (cur = head; cur->next; cur = cur->next)
cur->next->prev = cur;
cur->next = head;
head->prev = cur;
return;
}
```
透過 `merge sort` 來完成排序,在實作時一開始忘記將最後一個節點的 next 指向 NULL ,導致程式有無窮迴圈的問題,需要注意,另外在排序完後,記得要將每個節點的 prev 重新連結上,還有將最後一個節點的 next 重新指向 head ,這樣才算完成。
### q_descend
```c
int q_descend(struct list_head *head)
{
if (!head || list_empty(head))
return 0;
element_t *maxNode = list_last_entry(head, element_t, list),
*curNode = NULL;
int count = 0;
for (struct list_head *cur = head->prev, *safe = cur->prev; cur != head;
cur = safe, safe = cur->prev) {
curNode = list_entry(cur, element_t, list);
if (strcmp(maxNode->value, curNode->value) > 0) {
list_del(&curNode->list);
q_release_element(curNode);
} else {
maxNode = curNode;
++count;
}
}
return count;
}
```
這邊的做法是從後往前掃來做檢查, `maxNode` 即為當前節點右側的節點中擁有最大值的那一個,當前節點的值如果小於 `maxNode` 的值則做刪除的動作,反之則更新成 `maxNode` 。
### q_merge
```c
int q_merge(struct list_head *head)
{
queue_contex_t *first = list_first_entry(head, queue_contex_t, chain),
*entry = NULL;
list_for_each_entry (entry, head, chain) {
if (entry->id == first->id) {
continue;
}
list_splice_init(entry->q, first->q);
}
q_sort(first->q);
return q_size(first->q);
}
```
這邊很直接的先將所有的佇列連接在一起,然後再<s>一口氣</s>一次將全部做排序,但這邊並沒有運用到各個佇列已經排序好的性質,可以再思考更好的做法。
:::warning
避免不精準的詞彙,「一口氣」對應的英語是什麼?這對於解析程式有何幫助?
:notes: jserv
> 已修正,謝謝老師
:::
:::info
目前測試分數 95 / 100
複雜度仍須提升
:::
---
## 以 Valgrind 分析記憶體問題
用 `make valgrind` 來測試,發現大部分都有如下的狀況:
```
==32347== 32 bytes in 1 blocks are still reachable in loss record 1 of 6
==32347== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==32347== by 0x10CBE0: do_new (qtest.c:146)
==32347== by 0x10DF3F: interpret_cmda (console.c:181)
==32347== by 0x10E4F4: interpret_cmd (console.c:201)
==32347== by 0x10E8F5: cmd_select (console.c:610)
==32347== by 0x10F1E1: run_console (console.c:705)
==32347== by 0x10D331: main (qtest.c:1274)
```
後來發現在 `qtest.c` 中的 `q_quit` 裡面,第一行就直接做了 `return true;` ,因此趕快將這行拿掉就恢復正常了,而後來發現這個問題在原本的 [lab0](https://github.com/sysprog21/lab0-c) 已經被修正掉了,這也提醒了我要記得同步專案。
:::info
測試結果 100 / 100
:::
---
## 在 qtest 提供新的命令 shuffle
### 實作 shuffle
一開始花了一些時間摸索如何在 `qtest` 中提供新的命令,後來發現要在 qtest.c 中的 `console_init` 加入以下程式碼。
```c
ADD_COMMAND(shuffle, "Shuffle queue", "");
```
並在 qtest.c 中新增 `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 (!current) {
report(3, "Warning: Try to operate null queue");
return false;
}
set_noallocate_mode(true);
if (exception_setup(true))
q_shuffle(current->q);
exception_cancel();
set_noallocate_mode(false);
q_show(3);
return !error_check();
}
```
以下 `q_shuffle` 則是基於 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 演算法的 Modern method 來實作。
```c
void q_shuffle(struct list_head *head)
{
int len = q_size(head), roll = 0;
struct list_head *rollNode = head->next, *lastNode = head->prev;
for (int i = len; i > 1; --i) {
roll = rand() % i;
if (roll == i - 1) {
lastNode = lastNode->prev;
continue;
}
rollNode = head->next;
for (int j = 0; j < roll; ++j)
rollNode = rollNode->next;
// swap value
element_t *A = list_entry(rollNode, element_t, list),
*B = list_entry(lastNode, element_t, list);
char *tmp = A->value;
A->value = B->value;
B->value = tmp;
lastNode = lastNode->prev;
}
return;
}
```
如果隨機抽選到的 `node` 是尚未被選擇的最後一個 `node` (也就是將被交換的那一個),則省略交換的過程,直接做 continue ,反之則針對值來做交換的動作。
### 亂度分析
:::warning
有待分析
:::
## 執行 `option entropy 1`
```
cmd> option entropy 1
cmd> ih RAND 10
l = [jmhfj(21.88%) dlubbo(26.56%) abzjqxrsr(35.94%) uuiye(21.88%)
mxfangfme(32.81%) rdjtz(26.56%) dpjujvuf(29.69%) yvrkshlka(35.94%)
dkgjszx(32.81%) xgvygicw(32.81%)]
```