# 2025q1 Homework2 (quiz1+2) contributed by <`h0w726`> ## W1Q1 ### 解釋程式碼運作原理 一開始先定義 `my_assert` 函式,若 `test` 為 `false` ,則印出 `message` ,用來判斷 list 初始的大小是否為0,分配完的 list 大小為 N ,以及 list 是否正確為遞增或遞減。 在 `test_list` 中,先呼叫 `list_reset` 初始化 i 個節點以及 head 指標。 `list_insert_before` 函式 ```c static inline void list_insert_before(list_t *l, list_item_t *before, list_item_t *item) { list_item_t **p; for (p = &l->head; *p != before; p = &(*p)->next) ; *p = item; item->next = before; } ``` 這裡演示插入開頭的位置的步驟,首先指標的指標 `p` 會指向 `head` 指標, `item` 指標則指向接下來要插入的節點。 ```c for (size_t i = 0; i < N; i++) list_insert_before(&l, l.head, &items[i]); ``` ```graphviz digraph linkedlist{ rankdir=LR p[shape=none,label=p] head[shape=none,label=head] null[shape=none,label=NULL]; null1[shape=none,label=NULL]; before[shape=none,label=before]; i[shape=none,label=item]; a [shape=record,label="{ <data> 4 | <ref> }"]; b [shape=record,label="{ <data> 3 | <ref> }"]; c [shape=record,label="{ <data> 2 | <ref> }"]; d [shape=record,label="{ <data> 1 | <ref> }"]; e [shape=record,label="{ <data> 5 | <ref> }"] { rank = same; p -> head[arrowhead=vee, arrowtail=dot, dir=both,minlen=3] } head -> a e:ref:c->null1[arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; i->e before->a a:ref:c -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; b:ref:c -> c:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; c:ref:c -> d [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; d:ref:c -> null [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; } ``` 接著 `p` 會找到目前 `before` 指向的節點, `*p=item` 讓 `head` 指標指向要插入的節點, `item->next = before;` ,則讓 `item->next` 指向原來的第一個節點。 ```graphviz digraph linkedlist{ rankdir=LR p[shape=none,label=p] head[shape=none,label=head] null[shape=none,label=NULL]; null1[shape=none,label=NULL]; before[shape=none,label=before]; i[shape=none,label=item]; a [shape=record,label="{ <data> 4 | <ref> }"]; b [shape=record,label="{ <data> 3 | <ref> }"]; c [shape=record,label="{ <data> 2 | <ref> }"]; d [shape=record,label="{ <data> 1 | <ref> }"]; e [shape=record,label="{ <data> 5 | <ref> }"] f [shape=record,label="{ <data> 6 | <ref> }"] { rank = same; p -> head[arrowhead=vee, arrowtail=dot, dir=both,minlen=3] } head -> e i->f f:ref:c->null1[arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; before->e e:ref:c -> a:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; a:ref:c -> b:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; b:ref:c -> c:data [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; c:ref:c -> d [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; d:ref:c -> null [arrowhead=vee, arrowtail=dot, dir=both, tailclip=false]; } ``` `list_insert_before` 透過指標的指標 `p` ,找到目前 `before` 指向的節點,在此程式中,每次呼叫 `list_insert_before` `before`都會指向第一個節點,所以每次節點都會插入在鏈結串列的頭部。 ### 延伸問題 ## W1Q2 這裡是基於二元搜尋樹管理空閒記憶體區塊,讓記憶體配置器能更有效率地搜尋到合適的區塊。 Free list 是使用鏈結串列管理記憶體區塊,搜尋合適區塊的時間複雜度為 O(N) ,若使用二元搜尋樹管理記憶體區塊,搜尋合適區塊的時間複雜度可為 O(logN)。 `remove_free_tree` 函式的功能是刪除二元搜尋樹中的某個節點,刪除節點的值就是配置那個大小的記憶體,然而刪除節點會有以下三種情況須討論。 1. `Target` 沒有子節點 這時 `target` 指向 80 ,因為沒有子節點,直接刪除即可。 ```graphviz digraph 1{ node [fontname="Arial"]; 50 -> 30; 50 -> 70; 70 -> 80; t[shape=none,label=target]; t -> 30 null0 [shape=point]; 70 -> null0 } n[shape=none,label=node_ptr]; n -> t ``` 2. `Target` 有一個子節點 這時 `target` 指向 40 ,我們要用45替代到現在40的位置 ```graphviz digraph 2{ node [fontname="Arial"]; 50 -> 30; 30 -> 20; 30 -> 40; t[shape=none,label=target]; t -> 40; 50 -> 70; 70 -> 80; null [shape=point]; 70 -> null; null0 [shape=point]; 40 -> 45; 40 -> null0 } ``` 在這之前我一直以為 `node_ptr` 是指向 `target` 的指標,但看到最後更新的程式碼就一直覺得非常奇怪 ```c /* If the target node has one child (or none), simply splice it out. */ else if ((*node_ptr)->l || (*node_ptr)->r) { block_t *child = ((*node_ptr)->l) ? (*node_ptr)->l : (*node_ptr)->r; *node_ptr = child; } else { /* No children: remove the node. */ *node_ptr = NULL; } ``` 如果 `node_ptr` 指向 `target` 的話,`*node_ptr = NULL` 也只是讓 `target` 指向 `NULL` 而已,根本沒有刪除到節點,後來發現 `find_free_tree` 的註解 ```c /* Locate the pointer to the target node in the tree. */ block_t **node_ptr = find_free_tree(root, target); ``` 只有說到找到指向 target node 的指標,並沒有說一定就是指向 `target` 這個指標,所以 `node_ptr` 是指向 target node 的 父節點中指向 target node 的那個指標,這樣上面程式碼的更新就會顯得合理了。 3. `Target` 有兩個子節點 這是最複雜的情形,二元搜尋樹有以下幾個[規則](https://zh.wikipedia.org/zh-tw/%E4%BA%8C%E5%85%83%E6%90%9C%E5%B0%8B%E6%A8%B9)需要遵守 * 若任意節點的左子樹不空,則左子樹上所有節點的值均小於它的根節點的值 * 若任意節點的右子樹不空,則右子樹上所有節點的值均大於它的根節點的值 * 任意節點的左、右子樹也分別為二元搜尋樹 所以當要刪除的節點有兩個子節點時,必須找到該節點的 in-order predecessor 替代其原本的位置,才不會破壞二元搜尋樹的結構。 透過以下程式尋找 in-order predecessor ```c block_t **pred_ptr = &(*node_ptr)->l; while (*pred_ptr)->r) pred_ptr = &(*pred_ptr)->r; ``` 這段程式碼會找到要刪除的節點的左子樹中,最下方且最右邊的節點,也就是 in-order predecessor。 刪除的節點有兩個子節點可分為下列兩種情況 * in-order predecessor 是要刪除節點的左子節點 ```graphviz digraph 3{ node [fontname="Arial"]; 50 -> 30; 50 -> 70; } ``` 這情況很簡單,只需要先把要刪除節點的右子樹先備份,將要刪除的節點替換成左子節點後,再將右子樹接回即可。 * in-order predecessor 在左子樹的更深層 ```graphviz digraph 4{ node [fontname="Arial"]; 50 -> 30; 50 -> 70; 30 -> 20; 30 -> 40; t[shape=none,label=target]; t -> 50 } ``` 這情況需要先將刪除節點的左子樹和右子樹以及 in-order predecessor 先備份,再將再將 in-order predecessor 從左子樹中刪除,再將 in-order predecessor 替代刪除的節點,並把左右子樹接回。 ```graphviz digraph 5{ node [fontname="Arial"]; 40 -> 30; 40 -> 70; 30 -> 20; } ``` ### 延伸問題