contributed by < TonyLinX >
1
這段程式碼定義單向 linklist 的 node
以及串列整個 linklist
的 list_t
:
測試函式 list_insert_before
這個函式的功能是在指定的 before
節點前插入新的 node
,並且會遍歷 linklist
來找到 before
節點的位置。
先來討論另個想法,實際上如果只是要遍歷整個 linklist
不需要 **p
,只需要 *p
,因為只要創建一個指向 list_item_t
的指標並讓它指向 l->head
,並一路透過 next
即可往後遍歷。
那為什麼我們需要雙重指標 (**p
),因為我們要修改結構,如果說是單指標就會在 p = item
的地方出問題。因為 p
本身是局部變數,p = item
只是讓 p
去指向另個位址,但是整個 linklist
結構並不會改變,所以說我們需要 **p
。
這裡其實可以想像成,如果傳送過去的指標是要被改變去指向另一個位址,那就必須要用雙重指標,因為你需要去改變這個指標的值,但是如果你只是要透過指標操作它所指向的內容,而不需要改變它本身的值(指向位址),那只需要單指標就夠了。
假設鏈結串列是:head -> A -> B -> C -> NULL
,而 before
是 B
,你想在 B
前插入 item
,目標是讓鏈結串列變成:head -> A -> item -> B -> C -> NULL
。
list_insert_before
的流程會先透過 for loop
尋找到 before
前面的節點,所以先初始化 p
為 l->head
的地址,然後比對 (*p) 是否為 before
,如果不是 before
就往下一個節點移動, p = &(*p)->next
中的 *p
代表的是指向某一個 node
的指標,所以我們可以用他的成員 next
進行移動。 當遇到 before
時,p為 A->next
的地址,所以 *p
就是 A->next
,那我們就可以透過 *p = item
插入 item
,最後再讓 item
的 next
指向 before
就可完成這個 linklist
。
p = l->head
,所以 p
指向 A
(地址 0x1000
)。*p != before(0x1000 != 0x2000)
,p = &((*p)->next)
:
*p
是 A(0x1000)
,(*p)->next
是 A->next
,也就是 B(0x2000)
。&((*p)->next)
是 A->next
的地址(假設是 0x1004
,因為 next
是 A
結構中的成員)。 p
更新為 &A->next(0x1004)
。*p == before(0x2000 == 0x2000)
,條件不成立,離開迴圈。p
指向 A->next
的地址(0x1004)
。*p = item
:
p
是 &A->next(0x1004)
,*p
是 A->next
的值(原本是 0x2000
,指向 B
)。*p = item
把 A->next
修改成 item
的地址(0x4000
)。l->head -> A -> item
,鏈結串列結構改變了!item->next = before
:
item->next
被設為 B(0x2000)
。l->head -> A -> item -> B -> C
,插入成功!2b418c3
首先,設計 merge sort
的 test case
,這邊的設計方式透過 list_insert_before
創建一個 999 -> 998 -> ... -> 0
的 linklist
,期望再透過 merge sort
之後可以變成 0 -> 1 -> ... -> 999
。而 merge sort
的方式採用 top-down
的方式實現,也就是遞迴的方式。
2
這段 code 主要是在做,當某個 free block 被 malloc() 拿去用了,要將 BST 上的 free block 移除。
由於要維護 BST 的順序,在刪除上會有三種 case,每一種 case 要做的事情都不同,分成以以下三種:
if ((*node_ptr)->l && (*node_ptr)->r)
=>
所以說 EEEE
為 (*pred_ptr)->r
, FFFF
為 &(*pred_ptr)->r
else if ((*node_ptr)->l || (*node_ptr)->r) {}
=> 直接把左子樹或右子樹補上去NULL
下面這段 code 主要的設計是考慮到 pred_ptr
的位置,如果說 pred_ptr
是目標節點左子樹的根,那可以直接替換上去,但如果是在非常深處的 leaf ,由於沒辦法得知 pred_ptr
的父節點,所以只能透過遞迴的方式把它找出來並且移除,然後再把它拿去替換。
3
這個題目主要是在做 QuickSort,常見的 QuickSort 是會透過 swap 以及 recursive 的方式實現排序。在這裡不會使用以往 recursive 的方法,而是改成 stack 來模擬原本的遞迴呼叫。
具體來說,他會先找一個 pivot
,並取比較剩下的 node_t
的 value,如果比 pivot
還小就接在left
的尾部,如果比較大就分給 right
的尾部,所以當處理完所有的 node_t
,你就會得到三組分割 left
,pivot
, right
。接下來,這三組分別對應 i
,i+1
,i+2
,各自將自己的第一個 node_t
加入到 begin[i]
, begin[i+1]
, begin[i+2]
,並也將自己的最後一個 node_t
加入到 end[i]
, end[i+1]
, end[i+2]
。 最後,就從最後面的部分開始排序。
這段 code 主要是當不只一個 node_t
,必須根據 pivot
將所有的 node_t
分成 left
以及 right
,
value
代表的是 pivot
的值,由於 pivot
是 list_head
,所以要先用 list_entry
找到 node_t
,才可以得到 value
,所以 HHHH
= list_entry(pivot,node_t,list)->value
。n_value
代表的是 n
的值,所以 IIII
= list_entry(n,node_t,list)->value
begin[i]
,begin[i + 1]
,begin[i + 2]
,對應到的是三組分割 left
,pivot
,right
,所以 JJJJ
= pivot
, KKKK
= right
。這段 code 主要是維護環狀的 double-linklsit。
GGGG = head->prev=prev
1
這段 code 主要是在對 values
進行賦值以及打亂。j
代表是 index
這個部分是實作 quicksort
,這裡的 code 跟第一周測驗三有點雷同,因為都會切成三塊,這裡的 less
, pivot
, greater
對應到那第一周測驗三的 left
, pivot
, right
,整體流程為:
pivot
的 listietm
,並將 pivot
分割出來。pivot
將小於的點接在 list_less
後面,而大於的點接在 list_greater
後面。list_less
以及 list_greater
進行遞迴排序。listitem
做為pivot
,所以 AAAA
= list_first_entry
。pivot
分割出來,所以 BBBB
= list_del
。pivot
的 listitem
接在 list_greater
後面,所以 CCCC
= list_move_tail
。pivot
是一個點,所以一定是 list_add
,所以 DDDD
= list_add
(但我覺得 list_add_tail
也可以)。list_less
是比 pivot
還小的值,且可能為多個點,所以 EEEE
= list_splice
。list_greater
是比 pivot
還大的值,且可能為多個點,所以 FFFF
= list_splice_tail
。延伸問題
2
clz2() 這支函式用遞迴的方式計算 32 bit 無號整數 x 的前導零個數:它把輸入逐層一分為二,c = 0 代表先把 32 bit 切成高 16 bit (upper) 與低 16 bit (lower),若 upper 全為 0,就可以直接把 16 累加到答案並改遞迴處理 lower;反之,若 upper 非 0,表示那 16 bit 內尚未遇到 1,就只遞迴 upper 繼續細分成 8/4/2 bit 區段。這個過程隨 c 增大而重複,區段長度依序是 16, 8, 4, 2 bit(所以 c 最大值為 3);到最底層 (c == 3) 時,upper 只剩 2 bit,可能是 00, 01, 10, 11,對應要加的前導零個數可透過 magic[] 查表一次取得,同時若 upper 為 0 則還需把 magic[lower] 加進去。mask[] 是遮罩,是為了從 x 取出正確長度的 lower;magic[] 是當 lower 只剩下 2 bit 用來查表,由於只會是 00, 01, 10, 11,剛好可以對應為 {2, 1, 0, 0}。如果最一開始 x == 0 且 c == 0,代表 32 bit 全零,直接回傳 32。
lower
最後只需要 2 bit,所以 GGGG = 14
。HHHH = 2
, IIII = 0
。JJJJ = 3
。upper
以及 lower
為 2 bit 的時候,在 upper == 0
,才會觸發 KKKK + magic[lower];
,所以 KKKK = 2
。c != 3
,不管 upper
是不是 0,c 都要加 1,繼續計算 clz
,所以 LLLL = 1
。這支 sqrti()
函式透過「二進位長除法」計算 64 bit 整數平方根。流程如下:先用 clz64(x)
找出最高的 1 所在位元索引 h = 63 - clz64(x)
,再把 LSB 清 0(shift = h & ~1ULL
),確保起始位階是偶數,因為開平方一次處理的是 2 bit ,所以起始位置一定要是偶數。
在迴圈中,每回合先計算出除數 b = y + m
,接著把 y
右移 1(從 2 r 還原為 r);若 x ≥ b
就說明這個 bit 應設 1 ,把 x -= b
並將 y += m
;無論成敗,移到下一組 2 bit,繼續判斷。當 m
減到 0,y
即為 ⌊√x⌋。
MMMM
= ~1
y
代表的是 2r,要變成 r ,所以 NNNN
= 1
。PPPP
= 2
。延伸問題
3
測驗 3 是要用 hash table 的方式完成 Two sum 這個題目。
這段 code 是在建立一個 hash table,假設 bits
= ,代表這個表會有 = 個 buckets。每一個 bucket 對應一個 struct hlist_head
,它的結構中包含一個 struct hlist_node *first
,這是該 bucket 所屬 double linklist 的開頭指標,用來儲存碰撞時的多個 key-value 節點。我們透過 malloc(sizeof(struct hlist_head) * MAP_HASH_SIZE(bits))
一次配置出整塊 bucket 陣列的空間。雖然 map->ht
是一個指標,但它實際上指向的是一段連續的記憶體空間,因此可以使用陣列方式操作(例如 map->ht[i]
)。接下來透過迴圈逐一初始化每個 bucket 的 first
成員為 NULL
,表示目前所有 buckets 都是空的、尚未儲存任何資料。
map_deinit()
是用來釋放整個雜湊表 map_t
所使用的記憶體資源,其核心流程如下:
struct hlist_head
,其中的 first
成員指向該 bucket 的鏈結串列起點。MAP_HASH_SIZE(bits)
計算出雜湊表大小(實際上是 個 buckets)。first == NULL
),代表此 bucket 沒有資料,會被自動略過。container_of()
從 hlist_node
取回對應的 struct hash_key
。hlist
中移除:這透過 next
與 pprev
指標操作來實現。n->pprev == NULL
,代表此節點尚未被插入(unhashed),會直接跳過斷鏈處理。data
與節點本身都會被 free()
掉。map
本身的記憶體這段 code 主要是去搜尋 hash table 中有沒有該 key
值。
有了 key
值,先透過 hash function 計算出是哪一個 bucket,得到 bucket 的 index 後,用 for 迴圈去走訪整個 hlist,這個 hlist 是由一堆 hash_key
組成,不是 hlist_node
,hlist_node
是 hash_key
的成員,用來串接所有 hash_key
,所以為了知道該 hash_key
中的 key
值,必須先透過 container_of
得到該 hash_key
。若有找到就回傳該 hash_key
,沒有就回傳 NULL
。
這個 hash function 的設計方式是將 key 值乘上黃金比例常數,藉此打亂其位元分布、減少碰撞,這是來自 Donald Knuth 在《The Art of Computer Programming》中的建議。bits
則用來控制雜湊表的大小,決定 bucket 的數量。例如當 bits = 10
時,雜湊表會有 個 buckets。整個 hash 的結果會將 key 乘上黃金比例後,再將 32-bit 結果右移 位,也就是右移 22 位,讓 index 落在 [0, 1023]
的範圍內。
如果沒有在 hash table 中找到,在 hash table 中建立該 key 值的 hash_key。
AAAA = map->bits
以及 BBBB = map->bits
。CCCC = first->pprev
以及 DDDD = n->pprev
。n
,要先取得 n
的前一個以及下一個 hash_key
,EEEE = n->pprev
。延伸問題