contributed by < Ahsen-lab >
queue.c 僅提供介面 (interface) 但尚未有完整程式實作,需要由學員補完並逐步精進,包含以下:
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 List。q_delete_dup
: 在已經排序的狀況,移走佇列中具備重複內容的節點,詳見 LeetCode 82. Remove Duplicates from Sorted List II。q_swap
: 交換佇列中鄰近的節點,詳見 LeetCode 24. Swap Nodes in Pairs。q_reverse
: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點。q_sort
: 以遞增順序來排序鏈結串列的節點,可參閱 Linked List Sort 得知實作手法。建立新的「空」佇列,使用malloc
分配記憶體空間給head
,再用INIT_LIST_HEAD
function來初始化head
,需要注意malloc
有可能會 fail,若分配記憶體空間失敗,則回傳NULL
。
釋放佇列所佔用的記憶體
一開始對 Linux List Management API 還不是很熟,所以使用上方這種比較多此一舉的方式來實作,用 list_for_each_safe
走訪每個 node
並用 list_entry
取出對應的 element
,然後釋放 element
所佔的記憶體空間。
後來重新讀了 list.h 檔裡的 API 說明,以及參考 laneser 的設計,改為使用 list_for_each_entry_safe
API 來實作,程式碼如下,變得簡潔許多。
在佇列開頭 (head) 插入 (insert) 給定的新節點
我的作法是分配一段記憶體空間給一個新的 element
,再分配一段空間給 element
的成員變數 char *value
用來儲存字串,一開始我是使用 calloc
來分配空間給字串,原因是 calloc
分配的空間是一個 array
,而且會自動將 array
中的每個 bits
都初始化為 0
,在字元中 0
與 NULL
相等,只要分配 strlen(str) + 1 長度的 array
,再將字串複製進去,就不用手動在字串尾端加上 NULL
。
- C99 [7.20.3.1] The calloc function
- The calloc function allocates space for an array of nmemb objects, each of whose size is size. The space is initialized to all bits zero.
但在實作過程中有遇到一個問題,若使用 calloc
來分配空間給 char *value
,在 q_free
function 中 free char *value
會出現 error :
Error: Attempted to free unallocated block
但若是使用 malloc
則不會有這個問題,所以後來我改成下方那種寫法。
TODO : 造成 malloc
和 calloc
會導致不同結果的原因目前還在追蹤中。
在佇列尾端 (tail) 插入 (insert) 給定的新節點
實作概念與 q_insert_head
相同,只是將 list_add
換成 list_add_tail
。
在佇列開頭 (head) 移去 (remove) 給定的節點
在佇列尾端 (tail) 移去 (remove) 給定的節點
釋放特定節點的記憶體空間,包含節點裡儲存的 value
字串也一並釋放掉
計算目前佇列的節點總量,設定一個變數 len
,初始值為 0
,再使用 list_for_each
來遍歷佇列中的所有節點,每經過一個節點 len
就加一,最後回傳 len
的值,若佇列為 NULL
或 empty
則返回 0
,
移走佇列中間的節點,作法參考
你所不知道的 C 語言: linked list 和非連續記憶體 案例探討 : Leetcode 2095. Delete the Middle Node of a Linked List
原本的案例探討是針對單向的 linked list,我將其改為雙向環狀 linked list 的版本,若傳入的佇列為 NULL
或是空佇列,則回傳 false
。
使用指標的指標,搭配快慢指標的技巧來找出中間的節點,首先設定一個指標的指標 indir
,以及一個指標 fast
,兩個指標都會指向 head->next
,然後使用迴圈走訪節點,fast
每次會前進兩個節點,而 indir
每次前進一個,當 fast
本身等於 head
或是 fast->next
等於 head
時,代表已走訪完佇列,這時 indir
會指向佇列中間的節點。
找到中間節點後,使用 list_entry
來取得相對應的 element_t
,然後用 list_del
及 q_release_element
來移除節點並釋放記憶體空間,最後回傳 true
。
在已經排序的狀況,移走佇列中具備重複內容的節點,若傳入的佇列為 NULL
,回傳 false
。
以下實作有參考 laneser 同學的作法。
設定兩個 element_t *
變數 cur
、nex
,以及兩個 struct list_head *
變數 curr
、next
,初始化 curr = head->next
、next = head->next->next
,使用迴圈來走訪節點,在走訪的過程中使用 list_entry
來取得 curr
、next
所對應的 entry cur
和 nex
。
另外,在走訪節點的過程中需要考慮兩個情況:
cur->value == nex->value
:
cur
,並將 needDel
設成 true
,表示所有與 cur
有相同字串的 entry 都是需要刪除的節點。cur->value != nex->value
:
needDel == true
表示 cur
也是需要刪除的 entry。needDel == false
表示 cur
無須刪除。在走訪完所有節點後 cur
會指向佇列的最後一個節點,若此時 needDel
依然為 true
,則代表佇列最後的兩個節點所包含的字串是相同的,兩個節點都需要刪除,而迴圈過程中會刪除倒數第二個節點,最後一個節點則需透過刪除 cur
來刪除。
交換佇列中鄰近的節點,若傳入的佇列為 NULL
,則不做任何動作。
這個 function
中我主要是用 list_move
來完成實作:
list_move
會將 node
移到佇列的開頭,也就是會讓 head->next
指向 node
,利用這個功能,搭配迴圈走訪節點,一開始設定一個指標 h
指向 head
。
每次迴圈 h
會前進兩個節點 h = h->next->next
,將 h->next->next
當作新的 head
代入 list_move
中,就可達到交換佇列中鄰近的節點的目的。
以反向順序重新排列鏈結串列
以遞增順序來排序鏈結串列的節點