contributed by < hankluo6
>
add_entry
new_node
加入到 linked list 的尾端,使用 pointer to pointer 來指向要加入的節點位置的「地址」。AA1
(a)
assert(new_node)
先使用 assert
來檢驗 malloc
是否成功。
AA2
(b)
*indirect = new_node
當找到位置後,將該位址的值設為 new_node
。
indirect
設定為 head
的 addresswhile
中持續尋找有 NULL
值的位置,indirect
為 &node->next
,也就是存有 node 值的 addressNULL
值的位置indirect
為 NULL
位置的 addressnew_node
設定為該 address 的內容,完成插入在 linked list 尾端如果 malloc
無法分配記憶體,malloc
會回傳 NULL
。NULL
的定義在規格書中有寫道:
An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.
得知 NULL
定義為 0
或為 (void*)0
。對 NULL
取其成員,應為未定義行為。
透過程式碼驗證
出現 segmentation fault,與預期的一樣。
再看看 assert
的作用,從 man page 中寫道:
If expression is false (i.e., compares equal to zero), assert()
prints an error message to standard error and terminates the program
by calling abort(3).
可推知當 expression 為 NULL (0)
時,會呼叫 abort(3)
。
如果這邊 assert
的作用是用來檢驗 malloc
是否成功,我認為應放在 new_node->value
前比較合適,否則沒有作用,new_node
為 NULL 時,new_node->value
就會丟出 segmentation fault。
find_entry
find_entry
函式用來尋找指定 value 的節點
remove_entry
remove_entry
使用 pointer to pointer,可以減少刪除 head
時需要的判斷式
while
迴圈找到 entry
的位置indirect
為 &head->next
,所以直接將該 address 上的值更新為 node2
即可swap_pair
BB1
(e)
node = &(*node)->next->next
可以先把 type 不合的選項刪掉,(a), (d), (f)
的 pointer type 不合先移除。接著分析迴圈作用,從 *node && (*node)->next
可猜測每次迴圈應為移動兩個 node。注意迴圈內的程式碼,可看出我們沒設定 *node
前節點的 next
,這是因為我們 node
是以之前 node->next
的 address 來更新,所以答案為 (e)
這邊需要兩次 next
的原因是因為 *node
已被設定到 tmp
的前面
BB2
(c)
*node = (*node)->next
我們需要將 node
往後移動一個節點,型態吻合的只有 (b), (c)
兩個選項。
深入討論 (b)
選項,先執行 node_t *tmp = *node
,tmp
目前為第一個節點,假如執行 node = &(*node)->next
的話,node
現在為第一個節點的 next
的 address,再來, tmp->next = (*node)->next
這行會讓第一個節點 tmp
的 next
指向 *node
的 next
。但 &tmp->next == node
,對 tmp->next
的改動會影響到 *node
的值,所以 tmp->next = (*node)->next
事實上與 *node = (*node)->next
等效。造成迴圈的 node = &(*node)->next->next
有不正確的結果。
故答案應為 (c)
*tmp
指向 *node
,tmp
為要 swap 的第一個節點*node = (*node)->next
將 node 位址的值前進一個節點,現在 *node
為要 swap 的第二個節點tmp
的 next
指向 *node
的後方,也就是將 swap 的第一個 node 的 next 指向第三個節點(*node)->next
要指回 tmp
,實現 swap 的部分*node
與 (*node)->next
已經完成 swap,將 node
更新為下兩個節點的位置,根據 loop invariant,可以兩兩 swap 每個節點用 gdb 來檢驗上述 BB2 選項的分析是否正確
假設將 BB2 設為 node = &(*node)->next
可以看到 node 的值在 tmp->next = (*node)->next
後被更新為下個節點,與推測符合,所以該選項為非。
reverse
CCC
(b)
head->next = cursor; cursor = head
從現有程式碼可以看到,node_t *next = head->next;
是將 next
移動到 head
後方,而 head = next
則是將 head
向後移動。所以缺少的部分應為將 next
反轉的部分。反轉的目標從 linked list 的頭之 prev
開始,隨著 head
移動而跟著移動。可以推測出反轉的目標應為 cursor
。再從選項中找有執行上面步驟的程式碼 (b)
。
cursor
為 NULL
表示 head
的前一個節點為 NULL
next
指標用來指向 head->next
,用來在 head
反轉後,還能知道原本 head->next
的值head->next
反轉,指向為 cursor
,這邊 cursor
為 head
前節點 NULL
cursor
需移動到 head
,紀錄下次反轉需指向的節點
head
往後移動,準備反轉第二個節點 b
,而 a
已經反轉完畢,根據 loop invariant,最後將反轉整個 linked listswap_pair
與 reverse
只需將傳進去的參數 head
改為 pointer to pointer 的形式就行了,因為本身 **node
就已經在 &head
上操作,所以其餘部分皆不用改動。
將 head
改為 pointer to pointer,迴圈內每個步驟只會改變 *head
的值,並固定真正的記憶體位置,就不需要回傳 head
。須注意迴圈的判斷式為 *head
是否為 NULL
,故跳出迴圈後,*head == NULL
,需將 *head
設定為最後一個節點 cursor
。
reverse
遞迴的方式很簡單,每次進入 rev_recursive
時,保證 prev
前的 node 皆已經 reverse
,此時再將 head->next = prev
就能讓 head
以前皆完成 reverse
,在繼續遞迴下去。終止條件為 head
為空,也就表示 prev
以前已經完成 reverse
。
參考 wiki 上的 pseudocode
紀錄要交換的兩個 node 的 node->prev
與 node
,再互相 swap 就行了。
既然有 node->prev
的話,我們可以不用特別紀錄需要交換的 node
的值,只要找要交換的 node
的 prev
就行了。
增加 dummy
節點來減少 target
選到 head
的時的判斷式,程式的邏輯變得很簡單,找到要交換的兩節點之 prev
後,swap 目前兩節點之 next
,再將節點往後移動,並再一次 swap 兩邊的 next
。
current
與 target
設置為 dummy
,一開始要交換的 node 為 a
,只是我們的 current
目前指向要交換的節點之 prev
,target
也一樣指向為要交換的節點之 prev
target
選到 b
,所以 a
與 c
需要交換a->prev
的 next
與 c->prev
的 next
互換target
與 current
至它們的 next
節點a
的 next
與 c
的 next
互換使用 pointer to pointer 來取代 dummy
head,減少所需記憶體。
current
與 target
指向 head
的 addresstarget
選到 node2
,指向 node2
的 adresshead
與 node2
交換,注意這邊交換的是節點,而 current
與 target
指向的位置不變target->next
與 current->next
交換,完成 swapcurrent
往後移動,進入下輪迴圈使用 valgrind 來比較三種版本的記憶體
測試程式
使用 massif 分析,記得開啟 stacks 紀錄堆疊區塊
為了版面整潔,只列出程式結束前幾個 snapshots 的記憶體情形
malloc
一個 dummy
node,所以比其他兩種版本多了 16 bytes 的 heap。最理想的版本為 version 3,使用到的 total memory 最小,為 2,400,360 bytes。
linux2020