contributed by < Andrewtangtang
>
完成 list_insert_before
缺少的部分
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;
}
根據該函式的的註解可以得知 item
在經過操作之後會被插入在 before
的前方。
l->head
) 的位址 (&l->head
),假設 before 是 head 則不會進入迴圈,可以直接將 item 設為新的 head ,因此 AAAA
應填入 &l->head
。before
這個節點來做執行插入,因此終止條件應該是 *p== before
,因此 BBBB
應填入 before
。如果還未找到 before
,則將 p
移動到下一個節點的 next
指標位址,也就是 p = &(*p)->next
。*p
會指向 before
節點,因此可以直接將 *p
指向 item
,這樣前一個節點就成功指向了新的節點 item
。最後,將 item->next
設為 before
,確保 item
正確地連接到 before
。這邊要特別注意c語言 precedence of operators
Structure and union member access through pointer->
> indirection (dereference)*
->Address-of&
C Operator Precedence
void remove_free_tree(block_t **root, block_t *target)
{
/* Locate the pointer to the target node in the tree. */
block_t **node_ptr = find_free_tree(root, target);
/* If the target node has two children, we need to find a replacement. */
if ((*node_ptr)->l && (*node_ptr)->r) {
/* Find the in-order predecessor:
* This is the rightmost node in the left subtree.
*/
block_t **pred_ptr = &(*node_ptr)->l;
while (EEEE)
pred_ptr = FFFF;
/* Verify the found predecessor using a helper function (for debugging).
*/
block_t *expected_pred = find_predecessor_free_tree(root, *node_ptr);
assert(expected_pred == *pred_ptr);
/* If the predecessor is the immediate left child. */
if (*pred_ptr == (*node_ptr)->l) {
block_t *old_right = (*node_ptr)->r;
*node_ptr = *pred_ptr; /* Replace target with its left child. */
(*node_ptr)->r = old_right; /* Attach the original right subtree. */
assert(*node_ptr != (*node_ptr)->l);
assert(*node_ptr != (*node_ptr)->r);
} else {
/* The predecessor is deeper in the left subtree. */
block_t *old_left = (*node_ptr)->l;
block_t *old_right = (*node_ptr)->r;
block_t *pred_node = *pred_ptr;
/* Remove the predecessor from its original location. */
remove_free_tree(&old_left, *pred_ptr);
/* Replace the target node with the predecessor. */
*node_ptr = pred_node;
(*node_ptr)->l = old_left;
(*node_ptr)->r = old_right;
assert(*node_ptr != (*node_ptr)->l);
assert(*node_ptr != (*node_ptr)->r);
}
}
/* 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;
}
/* Clear the removed node's child pointers to avoid dangling references. */
target->l = NULL;
target->r = NULL;
}
這段程式碼主要是想找到 空的 Block 將其從樹狀結構中移除,而移除前我們必須找到一個 Block 來填補他的位子,在二元樹中尋找替代 node 的的策略可以分為以下幾個 case。
要刪除的節點是 Leaf Node
要刪除的節點只有一其中一邊有 child
刪除的節點同時有左、右兩個 child
該題的填空部分主要是處理第三種情況,即尋找 Inorder Predecessor 的過程。程式中使用了間接指標(indirect pointer)的方式,最初將 pred_ptr
指向 &(*node_ptr)->l
,也就是待刪除節點的 left
的記憶體位址。接下來的 while
迴圈則負責尋找左子樹中最右邊的節點。在這個過程中,while
迴圈的條件應為 (*pred_ptr)->r
,即當當前節點的右子節點不為空時,代表尚未找到最右邊的節點,應持續往右搜尋。迴圈內的動作則是 pred_ptr = &(*pred_ptr)->r
,將 pred_ptr
更新為指向當前節點的右子節點的記憶體位址,持續深入到最右側。如此,當 while
迴圈結束時,*pred_ptr
就會正確指向左子樹中最大的節點,該節點即為Inorder Predecessor,可以作為替代節點繼續執行後續的替代策略。
該題想撰寫一個基於 Linux 核心風格的鏈結串列來改寫程式碼。首先定義了結構體。
#include "list.h"
typedef struct __node {
long value;
struct list_head list;
} node_t;
以及相關輔助函式
rebuild_list_link(struct list_head *head)
:prev
指針指向前一個節點,並將最後一個節點的 next
指針設為 head
,並將 head->prev
設為最後一個節點(即 prev
),因此 GGGG
應該要填入 head->prev=prev
,使鏈表成為循環鏈表。static void rebuild_list_link(struct list_head *head)
{
if (!head)
return;
struct list_head *node, *prev;
prev = head;
node = head->next;
while (node) {
node->prev = prev;
prev = node;
node = node->next;
}
prev->next = head;
/* GGGG */;
}
struct list_head *list_tail(struct list_head *head)
:next
指針找到 next
為 NULL
的節點,返回非循環鏈表的尾節點。int list_length(struct list_head *left)
:list_for_each
宏遍歷鏈表,計算並返回節點數量,適用於循環鏈表。接著我們來看 QuickSort 的本體實作,為了模擬遞迴,這裡使用了一個名為 begin
的陣列來儲存每個尚未排序子鏈結串列的起始位置。首先在初始化時,因為 QuickSort 最多可能產生約兩倍於 n 個子問題,所以我們將 begin 的大小設定為原鏈結串列長度 n 的兩倍。另外也建立了三個鏈結串列頭指標:left、right 及 result,其中 left 存放小於 pivot 的節點,right 存放大於 pivot 的節點,而 result 則用於逐步連接已排序好的結果串列。
int n = list_length(list);
int value;
int i = 0;
int max_level = 2 * n;
struct list_head *begin[max_level];
struct list_head *result = NULL, *left = NULL, *right = NULL;
begin[0] = list->next;
list->prev->next = NULL;
在完成初始化後,會進入主要排序迴圈,持續執行直到所有子串列都完成排序。迴圈的第一步會檢查 i
是否大於 0,以確認還有尚未完成排序的子串列需要處理。接著會從 begin
陣列中取出當前要排序的子鏈結串列起始點,標記為 pivot
。
接下來是程式 partition 分割,首先先確認 i
是不是大於0判斷確保還有未排序的鍊結串列,接著檢查下 L
是否等於 R
若相同則跳到下方的部分將該單一節點接在 result
的後方,如果不同的話則進入上方的 partition 過程。在做分割的過程中,會先取出 begin
的 top 作為 pivot 來進行比較,因此在此處我們需要將 value
設為list_entry(pivot,node_t,list)->value
用以儲存目前 pivot
的值。接著開始去 traverse 鏈結串列去做切割,假設 n
的值比我們 pivot 的值大,我們就把他加入 right
的鏈結串列,反之加入 left
鏈結串列,因此 IIII
的地方,我們應該要填入 list_entry(n,node_t,list)->value
來取出當前這個節點的數值。
while (p) {
struct list_head *n = p;
p = p->next;
int n_value = /* IIII */;
if (n_value > value) {
n->next = right;
right = n;
} else {
n->next = left;
left = n;
}
}
完成 partition 之後,會把此次分割出的子串列依序存回 begin
陣列內,以模擬遞迴呼叫的特性。在這個實作中,我們會先處理右側的子鏈結串列,因此,存入 begin
陣列的順序應該是:左側鏈結串列 left
、pivot
自身、右側鏈結串列 right
。如此一來,下次迴圈取出子串列來處理時,便會先從 right
開始執行,完成後再繼續處理 pivot
與 left
子鏈結串列。
begin[i] = left;
begin[i + 1] = /* JJJJ */;
begin[i + 2] = /* KKKK */;
left = right = NULL;
i += 2;
{
struct list_head list_less, list_greater;
struct listitem *pivot;
struct listitem *item = NULL, *is = NULL;
if (list_empty(head) || list_is_singular(head))
return;
INIT_LIST_HEAD(&list_less);
INIT_LIST_HEAD(&list_greater);
list_for_each_entry_safe (item, is, head, list) {
if (cmpint(&item->i, &pivot->i) < 0)
list_move_tail(&item->list, &list_less);
else
CCCC(&item->list, &list_greater);
}
list_quicksort(&list_less);
list_quicksort(&list_greater);
DDDD(&pivot->list, head);
EEEE(&list_less, head);
FFFF(&list_greater, head);
}
根據 quicksort 的邏輯,一開始會使用第一個元素當做是 pivot
因此在 AAAA 中我們應該要填入 list_first_entry
來取出第一個鏈結串列中第一個元素。為了能夠將 pivot 作為基準點來劃分鏈結串列,我們必須先將 pivot 從鏈結串列中移除,因此 BBBB
應填入 list_del
,以確保 pivot 不會影響後續的排序操作。
pivot = AAAA(head, struct listitem, list);
BBBB(&pivot->list);
list_move_tail
而非list_move
在 traverse 鏈結串列時,我們會將 pivot 之外的元素與其比較,並根據比較結果將元素分別移動至 list_less
(小於 pivot
)或 list_greater
(大於或等於 pivot
)。由於此實作必須滿足穩定排序(stable sort) 的特性,即相同的元素在排序後仍應維持原有相對順序,因此 CCCC
應選擇 list_move_tail
,確保較晚 traverse 到的元素被放置在對應區段的尾端,若選擇 list_move
則會將比較晚 traverse 到的元素放在鏈結串列的前方,那麼最後在做合併的時候,結果就會變成原本相同元素的順序會因此改變。
list_for_each_entry_safe (item, is, head, list) {
if (cmpint(&item->i, &pivot->i) < 0)
list_move_tail(&item->list, &list_less);
else
CCCC(&item->list, &list_greater);
}
接下來就是將 list_less
與 list_greater
去遞迴呼叫 list_quicksort
直到剩一個元素或者是鏈結串列為空時,而在最後要做合併的操作,因為原本是以 pivot
為中心去做切割的,所以在合併時,我們先將 pivot
加回原本的鏈結串列,接著將已經排序完成的 list_less
接回串列的頭部,而在最後將 list_greater
接在最後完成這個區段的合併,因此 DDDD
應填入 list_add
,EEEE 應填入 list_splice
,FFFF 則是 list_splice_tail
。
DDDD(&pivot->list, head);
EEEE(&list_less, head);
FFFF(&list_greater, head);
static const int mask[] = {0, 8, 12, GGGG};
static const int magic[] = {HHHH, 1, 0, IIII};
unsigned clz2(uint32_t x, int c)
{
if (!x && !c)
return 32;
uint32_t upper = (x >> (16 >> c));
uint32_t lower = (x & (0xFFFF >> mask[c]));
if (c == JJJJ)
return upper ? magic[upper] : KKKK + magic[lower];
return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + LLLL);
}
程式的目的是想計算出要根據該32位元的數字前面有幾個 leading zero,而使用的方法是利用遞回呼叫的方式不停地去將目前關注的位元分成 upper 和 lower 兩個部份來進行檢查直到剩下 2 bit為止。而處理的機制可分為以下兩種方法
upper==0
,則回傳目前檢查的位元數加上遞迴檢查 lower part 的結果upper!=0
,則持續遞迴呼叫檢查 upper 部分直到剩下2 bits 或遇到第一個情況。每次遞迴呼叫時,關注的位元數會減半:第一次關注 16 位、第二次 8 位、第三次 4 位、第四次 2 位。而我們要計算 leading zero 的個數,因此 mask
的作用就是用來去除 lower 中較高的無關位元。從比較的位元數來看,在第四次遞迴時,我們只關注最後 2 位元,因此需要將遮罩設為 14,以確保只保留必要的位元範圍,讓比較過程更有效率。
先針對遞迴的終止的條件來看,當該函式遞迴呼叫三次後 upper 和 lower 會各自剩下 2bits ,因此 JJJJ
應該填入3,這時候假設 upper 不等於0,我們就必須使用 magic
陣列來查表,因為2進位 0、1、2、3 對應的 leading 分別為 2(00)、1(01)、0(10)、0(11),因此觀察期中 leading 的 zero HHHH
填入2、IIII
填入0。
uint64_t sqrti(uint64_t x)
{
uint64_t m, y = 0;
if (x <= 1)
return x;
int total_bits = 64;
/* clz64(x) returns the count of leading zeros in x.
* (total_bits - 1 - clz64(x)) gives the index of the highest set bit.
* Rounding that index down to an even number ensures our starting m is a
* power of 4.
*/
int shift = (total_bits - 1 - clz64(x)) & MMMM;
m = 1ULL << shift;
while (m) {
uint64_t b = y + m;
y >>= NNNN;
if (x >= b) {
x -= b;
y += m;
}
m >>= PPPP;
}
return y;
}
static struct hash_key *find_key(map_t *map, int key)
{
struct hlist_head *head = &(map->ht)[hash(key, AAAA)];
for (struct hlist_node *p = head->first; p; p = p->next) {
struct hash_key *kn = container_of(p, struct hash_key, node);
if (kn->key == key)
return kn;
}
return NULL;
}
在 find_key 這個函式中,我們要使用對應的 key 來找到其在 map 這個struct 中 ht 的位置在哪裡將其記憶體位址賦值給 head
,因此我們在 AAAA
應該要填入 map->bits
,在使用 hash
這個函數查找在 key 在映射到大小為 map->bits
中對應的 bucket
在哪邊。
void map_add(map_t *map, int key, void *data)
{
struct hash_key *kn = find_key(map, key);
if (kn)
return;
kn = malloc(sizeof(struct hash_key));
kn->key = key, kn->data = data;
struct hlist_head *h = &map->ht[hash(key, BBBB)];
struct hlist_node *n = &kn->node, *first = h->first;
n->next = first;
if (first)
CCCC = &n->next;
h->first = n;
DDDD = &h->first;
}
同理在新增操作的地方,假設我們可以在 hash_table
找到該 key
的data 則返回,而假設未找到,則分配一個新 hash_key
struct 給他,同時找到該 key 所屬於的 bucket 的 bucket 並將其插入到鏈結串列的起始點,因此 BBBB
在此處我們要填入的是 map->bits
。在教材Linux 核心的 hash table 實作中可以得知,Linux 核心實作中是使用 pointer to pointer 的技巧來指向前一個節點 next
指標的記憶體位址,這樣在做刪除的動作時,可以不用特別去對開頭的節點做例外的處理。所以 CCCC
要填入 first->pprev
使原本起始點 pprev
的指標可以指向新加入節點 next 的記憶體位址,最後,我們將 bucket 本身的起始節點指標更新為新節點 (h->first = n
),並同時設定新節點的 pprev
指標,使其指向 bucket 中的起始指標 (&h->first
),代表新節點現在是該 bucket 的第一個節點。因此,DDDD
要填入 &h->first;
,維持鏈結串列中 pprev
可以指向自身指標的記憶體位址的原則,以方便在刪除時不用特別處理開頭節點。
void map_deinit(map_t *map)
{
if (!map)
return;
for (int i = 0; i < MAP_HASH_SIZE(map->bits); i++) {
struct hlist_head *head = &map->ht[i];
for (struct hlist_node *p = head->first; p;) {
struct hash_key *kn = container_of(p, struct hash_key, node);
struct hlist_node *n = p;
p = p->next;
if (!n->pprev) /* unhashed */
goto bail;
struct hlist_node *next = n->next, **pprev = EEEE;
*pprev = next;
if (next)
next->pprev = pprev;
n->next = NULL, n->pprev = NULL;
bail:
free(kn->data);
free(kn);
}
}
free(map);
}
在這段程式碼的刪除操作中,需要從鏈結串列中移除一個節點,並確保串列在移除後仍維持正確的結構。當要移除節點 n
時,會先取得它的下一個節點 next,並透過 n->pprev
這個 pointer to pointer
取得前一個節點中指向自己的指標位置,因此 EEEE
應填入 n->pprev
。接著將 *pprev
設為 next
,斷開目前節點與前一節點的連結;若 next
存在,則更新 next->pprev
指向原本的 pprev
,讓其指向前一個節點指向自己這個節點的指標記憶體位址。最後將 n
的指標清空,並釋放該節點,即可正確地將節點從鏈結串列中移除,並保持整體結構的完整。