## 第一周測驗題 ### 測驗 `1` 先閱讀教材 [a pointer to a pointer](https://hackmd.io/@sysprog/c-linked-list) 了解指標的指標是什麼? ```clike #include <stdio.h> int main() { int val = 10; int *pointer_1 = &val; int **pointer_2 = &pointer_1; printf("val 存的值: %d\n", val); printf("val 記憶體位址: %p\n", &val); printf("pointer_1 指向的位址儲存的值: %d\n", *pointer_1); printf("pointer_1 指向的位址: %p\n", pointer_1); printf("pointer_1 記憶體位址: %p\n", &pointer_1); printf("pointer_2 指向的位址所儲存的值(位址): %p\n", *pointer_2); printf("pointer_2 指向的位址所指向的位址儲存的值: %d\n", **pointer_2); printf("pointer_2 指向的位址: %p\n", pointer_2); printf("pointer_2 記憶體位址: %p\n", &pointer_2); return 0; } ``` ``` val 存的值: 10 val 所在的記憶體位址: 0x7fffffffd534 pointer_1 指向的位址儲存的值: 10 pointer_1 指向的位址: 0x7fffffffd534 pointer_1 所在的記憶體位址: 0x7fffffffd538 pointer_2 指向的位址所儲存的值(位址): 0x7fffffffd534 pointer_2 指向的位址所指向的位址儲存的值: 10 pointer_2 指向的位址: 0x7fffffffd538 pointer_2 所在的記憶體位址: 0x7fffffffd540 ``` `lsit_inster_before` 函式的功能為: 負責在鏈結串列 (list_t) 中,在指定的節點 (before) 之前插入新節點 (item)。 AAAA : 是一個指向鏈結串列開頭的變數所以為 &l->head。p 就變成 指向 head 指標的指標 `list_item_t **`,因此 *p 就是 head 本身。 BBBB : 遍歷節點直到找到需要的點 before , *p 代表當前節點,因此 *p != BBBB 代表還沒找到 before。 CCCC : 讓 p 指向下一個 next 指標的位置。 DDDD : *p 是 before 前一個節點的 next,讓 item 成為 before 前一個節點的 next。 最後讓 item->next 連接到 before,確保 item 插入後仍然連接到 before。 ```clike 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; } ``` ` static char *test_list(void) ` 功能為: 測試在各個位置插入元素的情況 正確執行: ![image](https://hackmd.io/_uploads/SJ33nM1hJg.png) #### 在現有的程式碼基礎上,撰寫合併排序操作 在將節點使用 `list_move_tail` 的方法改成使用 `list_insert_before` 替代 ,透過 pointer to pointer 操作 插入正確位置。 ```clike void q_sort(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; struct list_head *slow = head; struct list_head *fast = head->next; while (fast != head && fast->next != head) { slow = slow->next; fast = fast->next->next; } struct list_head first; list_cut_position(&first, head, slow); q_sort(&first); q_sort(head); q_merge_list(head, &first); } ``` ```clike void q_merge_list(struct list_head *left_list, struct list_head *right_list) { if (!left_list || !right_list) return; struct list_head temp; INIT_LIST_HEAD(&temp); while (!list_empty(left_list) && !list_empty(right_list)) { element_t *first_node = list_first_entry(left_list, element_t, list); element_t *second_node = list_first_entry(right_list, element_t, list); bool tag = strcmp(first_node->value, second_node->value) < 0; element_t *add_first = tag ? first_node : second_node; list_insert_before(&temp, temp.next, &add_first->list); } list_splice_tail_init(left_list, &temp); list_splice_tail_init(right_list, &temp); list_splice(&temp, left_list); } ``` ### 測驗 `2` 擁有兩個子節點的情況 * 找到 target 的 predecessor,就是左子樹中最大(最右邊)的節點: 透過 while (EEEE) pred_ptr = FFFF; 來遍歷 target 左子樹中的最右點。 在尋找 target->l 中最右的節點: `EEEE` 表示迴圈繼續的條件,應該是還有右子樹 `*pred_ptr->r`。 `FFFF` 表示更新指標,應該移動到右子數 `&(*pred_ptr)->r`。 * 用這一點來取代 target: 若 pred_ptr 直接是 target->l,直接把 target->r 連接到新節點。 若 pred_ptr 不在 target->l,則將其從原來的位置移除,然後插入 target 的位置。 使用 `find_free_tree(root, target)` 找到 target 的位置。 ```clike block_t **find_free_tree(block_t **root, block_t *target) { while (*root && *root != target) { if (target->size < (*root)->size) root = &(*root)->l; else root = &(*root)->r; } return root; } ``` 使用 `find_predecessor_free_tree(block_t **root, block_t *node)` 找 predecessor ```clike block_t *find_predecessor_free_tree(block_t **root, block_t *node) { block_t *pred = NULL; if (node->l) { pred = node->l; while (pred->r) pred = pred->r; } return pred; } ``` ### 測驗 `3` `HHHH` : 取得 pivot 的值,使用 `list.h` 中的 `list_entry(pivot, node_t, list)->value` 找到 pivot 的值 `IIII` : 取得 n 的值,一樣使用 `list.h` 中的 `list_entry(n, node_t, list)->value` 找到 n 的值 `JJJJ` : pivot `KKKK` : right `GGGG` : list->prev = result ## 第二周測驗題 ### 測驗 `1` 使用快速排序,並且保持 stable sorting 先找到 Pivot * 透過 `list_first_entry` 取得 head 中的第一個元素當作 pivot。 將節點依次分配到 list_less 和 list_greater * `list_less` 儲存小於 Pivot 的元素 * `list_greater` 儲存大於或等於 Pivot 的元素 * 使用 `list_move_tail` 來確認排序是 stable 遞迴處理 `list_less` 和 `list_greater` * 透過 `list_quicksort` 遞迴對 `list_less` 和 `list_greater` 進行排序。 合併結果回 head * 先把 pivot 插入 head `list_add_tail` * 再將 `list_less`、`list_greater` 依序合併回 head `list_splice_tail` `AAAA` : `list_first_entry` ,取得 head 第一個元素作為 pivot。 `BBBB` : `list_del` ,將 pivot 從 head 移除,避免影響 `list_for_each_entry_safe` 。 `CCCC` : `list_move_tail`,確保排序 stable,若 item >= pivot,則將 item 維持原順序放入 `list_greater`。 `DDDD` : `list_add_tail`,把 pivot 放回 head。 `EEEE` : `list_splice_tail`,合併 list_less 到 head,維持順序。 `FFFF` : `list_splice_tail`,合併 list_greater 到 head,維持順序。 `list_move_tail` 會保留原來的相對順序:相同數值的節點不會被重新排序,而是維持它們原始的先後順序。 若改成`list_move` 順序被改變 `list_move(&item->list, &list_less)` 會將 item 插入 `list_less` 的頭部,導致原來的相對順序被顛倒。 `list_move(&item->list, &list_greater)` 會將 item 插入 `list_greater` 的頭部,同樣導致順序改變。 變成 unstable e.g. [3, 2, 2*, 3*] 使用 `list_move_tail`: 2, 2*, 3, 3* (stable) 使用 `list_move`: 2*, 2, 3*, 3 (unstable) ### 測驗 `2` 透過位元運算來實作快速的開平方根運算 clz2 (count leading zeros):計算數值 x 的前導 0 個數 (clz),並藉由遞迴加速。每次遞迴將 x 切成高低兩半: upper = (x >> (16 >> c)) 取得 x 的高位部分 lower = (x & (0xFFFF >> mask[c])) 取得 x 的低位部分 如果 upper 不為 0,則繼續對 upper 遞迴計算 如果 upper 為 0,就對 lower recursive,並加上相應的位數補償 當 c == 3(剩 2 位元)時,直接查表 (magic) 來獲取 clz 值 這樣可以透過少量遞迴快速計算 clz(x),比逐位數檢查快很多。 clz64 (64-bit clz):擴展 clz32 以支援 64 位元運算。 sqrti (integer square root):透過二進位搜尋法計算 sqrt(x)。 `GGGG` : 14,決定 mask 的位移量,用於 mask 陣列,使 lower 取得 4-bit mask。 `HHHH` : 2,magic 陣列的第一個元素,用來在 base case 直接回傳 clz 值。 `IIII` : 3,magic 陣列的最後一個索引。 `JJJJ` : 3,進入 magic 陣列查表的條件,當 c == 3 時表示只剩 2 位元需要處理,可直接查 magic。 `KKKK` : 2,計算 lower 時的 offset,lower 位移調整。 `LLLL` : 1,當 upper == 0 時,clz2 需要對 lower 遞迴並調整 bit-shift,控制 clz2 遞迴深度。 `MMMM` : -2,確保 shift 為偶數,使 m 為 4 的次方。 `NNNN` : 1,y >>= 1,確保二進位運算正確,讓 y 每次右移 1 位。 `PPPP` : 2,m >>= 2,讓 m 每次右移 2 位 (除 4) 目前的 `sqrti(x)` 計算的是 floor(√x),如果 x 不是完全平方數,則 ⌈√x⌉ = floor(√x) + 1。 我們可以判斷 y * y < x 來決定是否要加 1 若 y * y < x,則 y 必須加 1 使用 x >= y * (y + 1) 來避免乘法 當 y * y < x 時,x 可能為y * y ~ (y+1) * (y+1) 之間 若 x >= y * (y + 1),表示 ⌈√x⌉ = y + 1 這樣可以 避免直接計算 y * y (因為 y * (y + 1) 可透過 bit-shift 快速計算) ```clike uint64_t sqrti_ceil(uint64_t x) { uint64_t m, y = 0; if (x <= 1) return x; int total_bits = 64; int shift = (total_bits - 1 - clz64(x)) & -2; m = 1ULL << shift; while (m) { uint64_t b = y + m; y >>= 1; if (x >= b) { x -= b; y += m; } m >>= 2; } if (x > 0 && x >= y * (y + 1)) y++; return y; } ``` ### 測驗 `3` TwoSum 運作流程 建立 hash table (map_t) 遍歷 nums 陣列,對於每個 nums[i]: * 檢查 target - nums[i] 是否已經存在於 map * 若找到匹配值,返回索引 * 若找不到,將 nums[i] 及其索引加入 map free hash table return Time complexity : O(n) `map_get()` : 根據 key 計算 hash value,查找對應的 hlist_head 鏈結。遍歷桶內鏈結串列:若 kn->key == key,return kn->data。若找不到,return NULL。 `map_add()`:插入 Key,避免重複插入 key(若已存在則直接 return)。 插入時,維護 hlist_node 雙向鏈結: * 插入 first 之前 * 更新 first->pprev * 更新 h->first `AAAA` : map->bits ,hash() 需要 bits 來決定 hash table 的大小 `BBBB` : map->bits ,hash() 需要 bits 來尋找 bucket `CCCC` : first->pprev , 將 n->next->pprev 指向 n,維護 hlist 雙向鏈結 `DDDD` : n->pprev , 將 h->first 的前驅指標設為 n->pprev,確保 hlist 正確運作 `EEEE` : &p->pprev, 在 map_deinit 中找到 pprev,並將其指向 next,確保刪除元素時不會破壞鏈結 確保 hash() 的 bits 正確計算 * hash(key, map->bits) 保持一致。 維護 hlist 雙向鏈結 * CCCC = first->pprev,確保 next 指向 n 時,first->pprev 也正確更新。 * DDDD = n->pprev,確保 h->first 被插入 n 後,n->pprev 設為 h->first。 * EEEE = &p->pprev,確保 pprev 連結正確,防止 NULL 存取錯誤。 確保 map_deinit() 正確釋放記憶體 * if (!n->pprev) goto bail; 確保未連結的節點被跳過。 * bail: 確保 kn->data 及 kn 被正確釋放。