contributed by < komark06
>
q_new
: 建立新的「空」佇列;q_free
: 釋放佇列所佔用的記憶體;q_insert_head
: 在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則);q_insert_tail
: 在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則);q_remove_head
: 在佇列開頭 (head) 移去 (remove) 給定的節點;q_remove_tail
: 在佇列尾端 (tail) 移去 (remove) 給定的節點;q_release_element
: 釋放特定節點的記憶體空間;q_size
: 計算目前佇列的節點總量;q_delete_mid
: 移走佇列中間的節點,詳見 LeetCode 2095. Delete the Middle Node of a Linked Listq_delete_dup
: 在已經排序的狀況,移走佇列中具備重複內容的節點,詳見 LeetCode 82. Remove Duplicates from Sorted List IIq_swap
: 交換佇列中鄰近的節點,詳見 LeetCode 24. Swap Nodes in Pairsq_reverse
: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點;q_sort
: 以遞增順序來排序鏈結串列的節點,可參閱 Linked List Sort 得知實作手法;queue.c
實作q_new
實作q_new
: 建立新的「空」佇列。
根據linux2022-lab0
的定義: 「空」(empty) 的佇列是指向有效結構,但開頭 (head) 的值為 NULL
定義都有了,那就先看 queue.h
的函式宣告:
由於函式不需要參數,個人偏好以下寫法:
加入 void
後,可以避免不必要的參數傳遞。如果傳遞了參數,編譯器也可以偵測並報錯。但由於無法變更 queue.h
,所以維持原樣。
q_new
實作:
以上實作透過 qtest
裡的 new
檢查後,會出現錯誤。
說明佇列必須要是雙向環狀鏈結串列。改為以下程式碼:
INIT_LIST_HEAD
函式位於 list.h
,是 linux/list.h 簡化版的實作。差異在於 WRITE_ONCE
的使用(詳見: Linux 核心原始程式碼)。以下是 list.h
的 INIT_LIST_HEAD
:
初始化 list_head
,將 head
的成員 next
和 prev
都指向 head
。
再一次用 qtest
測試,成功通過測試。
q_free
實作在進入如何實作 q_free
前,必須先確定要如何建立 element_t
型態的物件 (佇列的元素),根據 queue.h
,element_t
包含一個字串和 list_head
結構體。
直觀的做法,會先為 element_t
配置記憶體,再為字串配置記憶體。
以下情況不考慮 malloc
失敗的情況:
但是參考過 Redis 的字串函式庫 SDS 後,決定只呼叫 malloc
一次,便能分配 element_t
結構體與字串。這樣的好處是,增加 cache hit 的機會。改成以下程式碼:
TODO: 設計實驗來說明 cache 效益的討論
知道如何建立 element_t
型態的物件後,就可以著手 q_free
了。
list_for_each_entry_safe
,是為了避免釋放 obj
後,無法走向下一個元素。而多使用了 save
保存下一個元素的位置。
q_release_element
free invalid pointer比較 e_new
和 q_release_element
,可預期 free(e->value)
會失敗,因為沒有在 e->value
配置 malloc
。使用 qtest
測試 q_remove_head
後,錯誤確實出現了。
free invalid pointer 可以透過以下程式碼解決:
但在 queue.c
註解提到不能更動 q_release_element
,所以將 e_new
和 q_free
改成兩次 malloc
的版本。此外,考量輸入字串不是 Null-terminated string 的情況。將 strlen
改為 strnlen
(老師建議)。
q_insert_head
和 q_insert_tail
實作有了 e_new
後,要插入元素就變得簡單許多,只要考量插入的位置,並使用對應的函式即可。
list_add
將元素插入佇列的開頭,需要注意,用 &
取出 list_head
的地址
與 q_insert_head
雷同,換成 list_add_tail
即可。
q_remove_head
和 q_remove_tail
實作注意到 q_remove_head
只要 remove 就好,所以不用額外 free
。使用 list_first_entry
和 list_last_entry
取代 container_of
,增加程式碼可讀性。
q_size
實作在 作業 就有提供程式碼了,稍微改一下變數名字。
q_delete_mid
實作透過指標前後走訪,就能找到中間節點。
q_delete_dup
實作透過兩個節點的比較,來刪除重複的節點。最後還會剩下一個節點並未刪除。所以加入 del
來刪除最後一個節點。
參考 blueskyson 的作法,加入一個指標指向前一個字串作比較。減少 branch 的時候,程式碼也更簡潔了。
q_swap
實作透過 list_move
實作, list_move
本質上就是先 list_del
再 list_add
。需要注意的地方,是 cur
只需要移動一次就好,因為 cur
已經透過list_move
移動過一次了。
經過 list_move
和 cur = cur->next
:
q_reverse
實作走訪每一個節點,將連結反向串連。
q_sort
實作使用 merge sort 實作,先將串列對半,直到剩下一個節點後,再合併。
透過快慢指標,將串列切成一半,分割的方法參考 jserv。
如果想將串列切成三分之一,只要讓快指標一次走三步即可。
接下來就是合併了,這裡使用 strncmp
,是延續 e_new
使用 strnlen
,讓 srrcmp
不會因為非 Null-terminated string ,進入無限迴圈。這部分參考 jserv。
因此轉型成 uintptr_t
前,應該先轉型成 void*
,相對的,從 uintptr_t
轉型成原本型態,也要先轉型成 void*
,故上面的程式碼應該改成:
mergesort
和 mergeTwoLists
將串列當作單向串列,是由於 mergesort 完成前,大多數節點都會再次合併,與其花費時間維持環狀鏈結串列,不如等合併完,再補上環狀鏈結串列的特徵。這部分在 q_sort
實現。
自己的電腦上跑測試沒問題,不過在 Github Action 上顯示 q_insert_tail
錯誤。
透過修改 traces/trace-17-complexity.cmd
分別測試 q_insert_tail
, q_insert_head
, q_remove_tail
, q_remove_head
。發現 q_insert_tail
其實沒問題,但 q_remove_tail
有時候無法通過測試,那就著手來改。
原先的 q_remove_tail
:
將第7行的 bufsize
去除,原先的用意是,避免無號整數取負值,造成不符預期的狀況。例如-1變成18446744073709551615(依照平台不同會有不同數值)。
成功召喚出皮卡丘了!
額外測試原版程式碼,竟然通過了。此外,拿一些同學通過 Github Action 的程式碼來測試,也會有錯誤產生。這部份的原因還有待商榷。
研讀 Linux 核心原始程式碼的 lib/list_sort.c(未完成)