contributed by <eric895888>
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ ldd --version
ldd (Ubuntu GLIBC 2.35-0ubuntu3.9) 2.35
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Vendor ID: GenuineIntel
Model name: 11th Gen Intel(R) Core(TM) i5-11300H @ 3.10GHz
CPU family: 6
Model: 140
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 1
CPU max MHz: 4400.0000
CPU min MHz: 400.0000
BogoMIPS: 6220.80
測驗1實作鏈結串列的插入與基本操作,list_insert_before()
用於在指定節點前插入新節點,並透過指標操作維持鏈結串列的結構,test_list()
測試不同插入情境,包括在串列開頭與結尾插入節點,並使用 my_assert()
確保串列的長度與順序正確,list_reset()
重新初始化測試資料,test_suite()
執行所有測試,而 main()
呼叫 test_suite()
並輸出測試結果。
而 list_insert_before()
目的是插入 item 節點於 before 之前,若 before == NULL,則插入到鏈結串列的末端,使用 list_item_t **p 來遍歷鏈結串列,使用指標的指標方式確保能直接修改指向的位置,如果 before 是 head,則 head 直接更新為 item。
AAAA 填入&l->head是遍歷的起點。
BBBB 填入before是搜尋的目標。
CCCC 填入&(*p)->next是遍歷方式。
DDDD 填入item->next讓 item 的 next 指向 before。
static void list_insert_before(list_t *l,list_item_t *before, list_item_t *item)
{
if (!l || !before || !item)
return;
list_item_t **p;
for (p = &l->head; *p != before; p = &(*p)->next)
;
*p = item;
item->next = before;
}
這段程式碼使用合併排序(Merge Sort)來對單向鏈結串列進行排序,首先,findMid()
使用快慢指標找到鏈結串列的中點,將其拆分為兩半,接著,mergeSort()
採用遞迴方式對兩個子串列分別進行排序,直到每個子串列只剩下一個節點;然後根據 你所不知道的 C 語言: linked list 和非連續記憶體 做出mergeTwoLists()
合併兩個已排序的子串列,確保結果仍然是排序好的,最後,mergeSortList()
負責處理 list_t 結構,呼叫 mergeSort()
排序並更新 head,從而完成整個鏈結串列的排序。
list_item_t *mergeTwoLists(list_item_t *left, list_item_t *right) {
list_item_t *head;
list_item_t **ptr = &head;
while (left && right) {
if (left->value < right->value) {
*ptr = left;
left = left->next;
} else {
*ptr = right;
right = right->next;
}
ptr = &(*ptr)->next;
}
*ptr = left ? left : right;
return head;
}
//Fast-Slow Pointer
void findMid(list_item_t *head, list_item_t **left, list_item_t **right) {
if (!head || !head->next) {
*left = head;
*right = NULL;
return;
}
list_item_t *slow = head, *fast = head->next;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
*left = head;
*right = slow->next;
slow->next = NULL;
}
list_item_t *mergeSort(list_item_t *head)
{
if(!head || !head->next)
return head;
list_item_t *left, *right;
findMid(&head,left,right);
left = mergeSort();
right = mergeSort();
return sortedMerge(left,right);
}
測驗2的程式碼實作了一個二元搜尋樹(BST)的節點刪除函式 remove_free_tree()
,用來從 block_t 型態的 BST 中刪除指定的 target 節點。首先,find_free_tree(root, target)
找到指向該節點的指標 node_ptr,接著根據 target 節點的子節點情況進行刪除操作,如果 target 有 兩個子節點,則使用中序(in-order predecessor)的走訪方式,也就是左子樹中最大(最右)節點 pred_ptr,然後以 pred_ptr 替換 target,並將 target 的右子樹連接到 pred_ptr。如果 pred_ptr 不是 target 的直接左子節點,則從原來位置刪除 pred_ptr,並將其作為新節點插入。如果 target 只有一個子節點,則直接讓 target 的父節點指向 target 唯一的子節點,繼續維持 BST 結構。如果 target 沒有子節點,則將其設為 NULL,從樹中刪除。最後,target->l 和 target->r 被設為 NULL。
remove_free_tree()
中如果要被刪除的節點有兩個子樹,使用中序的走訪方式找到左子樹中最右邊的節點。
EEEE 填入 (*pred_ptr)->r 。
FFFF 填入 &(*pred_ptr)->r 則是用來往右走訪的操作。
block_t **pred_ptr = &(*node_ptr)->l;
while ((*pred_ptr)->r)
pred_ptr = &(*pred_ptr)->r;
/* 在 BST 中尋找節點的指標 */
block_t **find_free_tree(block_t **root, block_t *target) {
while (*root) {
if ((*root)->size == target->size)
return root;
else if (target->size < (*root)->size)
root = &((*root)->l);
else
root = &((*root)->r);
}
return NULL;
}
/* 找到左子樹的最大值 */
block_t *find_predecessor_free_tree(block_t **root, block_t *node) {
if (!node || !node->l) return NULL;
block_t *pred = node->l;
while (pred->r) {
pred = pred->r;
}
return pred;
}
/* 插入節點到 BST */
void insert_free_tree(block_t **root, size_t size) {
block_t *new_node = (block_t *)malloc(sizeof(block_t));
new_node->size = size;
new_node->l = new_node->r = NULL;
if (!*root) {
*root = new_node;
return;
}
block_t *parent = NULL, *current = *root;
while (current) {
parent = current;
if (size < current->size)
current = current->l;
else
current = current->r;
}
if (size < parent->size)
parent->l = new_node;
else
parent->r = new_node;
}
/* 印出 BST(中序) */
void print_tree(block_t *root) {
if (!root)
return;
print_tree(root->l);
printf("%zu ", root->size);
print_tree(root->r);
}
int main() {
block_t *root = NULL;
insert_free_tree(&root, 50);
insert_free_tree(&root, 30);
insert_free_tree(&root, 70);
insert_free_tree(&root, 20);
insert_free_tree(&root, 40);
insert_free_tree(&root, 60);
insert_free_tree(&root, 80);
printf("BST 初始化中序走訪: ");
print_tree(root);
printf("\n");
block_t target;
target.size = 50;
printf("刪除節點: %zu\n", target.size);
remove_free_tree(&root, &target);
print_tree(root);
printf("\n");
target.size = 30;
printf("刪除節點: %zu\n", target.size);
remove_free_tree(&root, &target);
print_tree(root);
printf("\n");
target.size = 70;
printf("刪除節點: %zu\n", target.size);
remove_free_tree(&root, &target);
print_tree(root);
printf("\n");
return 0;
}
測驗3的程式碼實作了一個基於單向鏈結串列的快速排序法,不過並不是常見的遞迴方式而是改使用迭代的方式去實作,先建立一個list,並插入隨機順序的 100000 個數字,然後對鏈結串列進行 quick_sort,最後驗證排序結果 (assert(list_is_ordered(list))),並列印排序後的數據。
quick_sort 使用 begin 陣列來管理子鏈表的拆分,並在每層迭代選擇第一個節點作為 pivot,將其後的元素依據數值大小分成 left (小於等於 pivot) 和 right (大於 pivot)處理兩部分,最終將結果合併。
GGGG 填入head->prev = prev;指向最後一個節點。
HHHH 填入list_entry(pivot,node_t,list)->value。
IIII 填入list_entry(n,node_t,list)->value。
JJJJ 填入pivot。
KKKK 填入right。
程式碼使用 struct listitem 定義雙向鏈結串列節點,並透過 testlist 初始化鏈結串列。首先,程式產生隨機數,getnum()
透過三個變數計算亂數,get_unsigned16()
產生 16-bit 無號整數,而 random_shuffle_array()
對 values 陣列進行洗牌,接著,程式遍歷 values 陣列,將其數值存入動態配置的 struct listitem,並插入 testlist 串列中,排序部分使用 list_quicksort()
進行快速排序,選擇 pivot 節點後,將較小與較大的節點分別移動到 list_less 和 list_greater,遞迴處理後重新組合串列,排序完成後,程式先使用 qsort()
排序 values 陣列作為正確結果,然後檢查 testlist 內的節點是否與 values 內容一致,確保排序正確。最後,程式逐一釋放記憶體,並確認 testlist 已清空 (list_empty(&testlist)) 以確保資源管理正確。
AAAA 填入 list_first_entry() 它的作用是獲取鏈結串列的第一個元素。
BBBB 填入 list_del()這是 quicksort 分割(partition)過程的步驟,因為 pivot 不能留在 head 裡,要另外存放,方便後續排序。
CCCC 填入list_move_tail因為需要將大於 pivot 的元素移到 list_greater。
DDDD 填入 list_add(),因為需要把 pivot 放回原本的鏈結串列。
EEEE 填入 list_splice(),因為需要把 pivot 左邊較小的元素併回 head。
FFFF 填入list_splice_tail(),因為需要把 pivot 右邊較大的元素併回 head。
clz2()
若 upper 等於 0,則下一次遞迴呼叫應檢查 lower 部分,並記錄目前已累計的 leading zero,若 upper 不等於 0,則下一次遞迴呼叫應繼續檢查 upper 部分,如果 x == 0 且 c == 0,則回傳 32,表示 32-bit 數字完全沒有 1,全都是 0。
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);
}
GGGG 填入14,因為要lower要保留的位數是2。
HHHH 填入2
IIII 填入0
JJJJ 填入3
KKKK 填入2
LLLL 填入1
MMMM 填入~1
NNNN 填入1
PPPP 填入2
這段程式碼實作 twoSum 演算法,用來尋找陣列 nums 中兩個數字相加等於 target 的索引,其中 map_add 用來插入鍵值對,而 map_get 用來查找特定鍵的值,在 twoSum 函式中,程式遍歷 nums 陣列,對於每個數字 nums[i],計算 target - nums[i] 是否已存在於 hash table 中,若找到則回傳兩個索引,否則將 nums[i] 及其索引存入 hash table 。
AAAA 填入 map->bits
BBBB 填入 map->bits
CCCC 填入 first->pprev
DDDD 填入 n->pprev
EEEE 填入 n->pprev