# 2024q1 Homework2 (quiz1+2) contributed by[<Terry7Wei7>](https://github.com/Terry7Wei7) ## 第一週測驗題 ### 測驗一 ### 解析 [Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/) #### 運作原理 選擇樞軸(Pivot): 一開始,從待排序的數組中選擇一個元素作為樞軸。這個枢軸的選擇可以是隨機的、固定的,或者使用一些特殊的選擇方法。在這個實現中,它選擇了待排序數組的第一個元素。 分割(Partition): 將數組中的元素分為兩部分,使得樞軸左邊的元素都小於或等於樞軸,右邊的元素都大於或等於樞軸。這是通過左右兩個指針(L和R)的移動實現的,左指針找到一個大於樞軸的元素,右指針找到一個小於樞軸的元素,然後這兩個元素進行交換,重複這個過程直到左右指針相遇。 遞迴: 接下來,分別對樞軸左右兩邊的子數組遞迴地應用相同的排序過程。這就是快速排序的分治策略,將原始問題分解為兩個較小的子問題,然後遞迴解決它們。 結束條件: 遞迴過程中,如果子數組的大小變得足夠小(通常是只有一個元素或沒有元素),則遞迴結束。這時子數組視為已經排序。 繼續遞迴: 遞迴完成後,將所有子數組的結果合併起來,即可得到完全排序的數組。 這個實現中使用了一個額外的數組(beg 和 end)來跟踪每個子數組的起始和結束位置,而不是使用遞迴調用。這樣可以避免函數調用的開銷,並且沒有使用堆疊,這是一個非常基本但高效的實現方式。總的來說,快速排序是一種高效的排序算法,平均時間複雜度為O(n log n)。 #### 改進方法 將節點添加到左邊或右邊時,需要確定左右子鏈表的尾節點。 :::spoiler 程式碼 ``` c code void quick_sort(node_t **list) { int n = list_length(list); int value; int i = 0; int max_level = 2 * n; node_t *begin[max_level], *end[max_level]; node_t *result = NULL, *left = NULL, *right = NULL; begin[0] = *list; end[0] = list_tail(list); while (i >= 0) { node_t *L = begin[i], *R = end[i]; if (L != R) { node_t *pivot = L; value = pivot->value; node_t *p = pivot->next; pivot->next = NULL; while (p) { node_t *n = p; p = n->next; // 補全部分 list_add(n->value > value ? &right : &left, n); } // 找到左右子鏈表的尾節點 node_t *left_tail = list_tail(&left); node_t *right_tail = list_tail(&right); begin[i] = left; end[i] = left_tail; // 更新左子鏈表的尾節點 begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; end[i + 2] = right_tail; // 更新右子鏈表的尾節點 left = right = NULL; i += 2; } else { if (L) list_add(&result, L); i--; } } *list = result; } ``` ::: ### 使用 Linux 核心風格的 List API 改寫快速排序 隨機選擇 pivot。 使用三值中值分割法(median-of-three partitioning),即在每次分割前,從子序列的頭、尾和中間選擇三個元素,並選擇其中值作為 pivot,以確保 pivot 能夠接近序列的中間值,從而更好地分割序列。 :::spoiler 程式碼 ``` c code // 隨機選擇 pivot static inline struct node *random_pivot(struct list_head *head) { int length = 0; struct list_head *pos; list_for_each(pos, head) { length++; } int pivot_index = rand() % length; struct node *pivot_node = NULL; int i = 0; list_for_each(pos, head) { if (i == pivot_index) { pivot_node = list_entry(pos, struct node, list); break; } i++; } return pivot_node; } // 三值中值分割法選擇 pivot static inline struct node *median_of_three(struct list_head *head) { struct node *first = list_first_entry(head, struct node, list); struct node *last = list_last_entry(head, struct node, list); int size = list_length(head); int middle_index = size / 2; struct node *middle = NULL; struct node *middle_node = NULL; struct list_head *pos; int i = 0; list_for_each(pos, head) { if (i == middle_index) { middle_node = list_entry(pos, struct node, list); middle = middle_node; break; } i++; } if (first->value <= middle->value && middle->value <= last->value) return middle_node; else if (first->value <= last->value && last->value <= middle->value) return last; else return first; } // 帶有改進策略的快速排序實現 void improved_quick_sort(struct list_head *head) { if (list_empty(head) || list_is_singular(head)) // 如果鏈結串列為空或只有一個節點,則已經排序好了 return; // 隨機選擇 pivot struct node *pivot_node = random_pivot(head); // 三值中值分割法選擇 pivot // struct node *pivot_node = median_of_three(head); int pivot_value = pivot_node->value; // 初始化左右子串列 struct list_head left, right; INIT_LIST_HEAD(&left); INIT_LIST_HEAD(&right); // 分割鏈結串列為左右兩部分 struct list_head *pos, *tmp; list_for_each_safe(pos, tmp, head) { struct node *current_node = list_entry(pos, struct node, list); if (current_node->value < pivot_value) list_move_tail(pos, &left); // 將節點移動到左子串列 else list_move_tail(pos, &right); // 將節點移動到右子串列 } // 遞迴地對左右子串列進行快速排序 improved_quick_sort(&left); improved_quick_sort(&right); // 將左子串列、pivot 節點和右子串列連接起來 list_splice_tail(&right, head); list_splice_tail(&left, head); } ``` ::: ### 測驗二 ### 解析[Timsort](https://en.wikipedia.org/wiki/Timsort) #### 運作原理 #### 改進方法 --- ## 第二週測驗題 ### 測驗一 ### 解析[LeetCode 105 Construct Binary Tree from Preorder and Inorder Traversal](https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) #### 原理解釋 struct hlist_node:這是一個雜湊表節點結構,它包含指向下一個節點的指標 next,以及指向前一個節點的指標 pprev。 struct hlist_head:這是一個雜湊表頭結構,它包含指向第一個節點的指標 first。 INIT_HLIST_HEAD 函數用於初始化雜湊表頭結構,將 first 指標設置為 NULL,表示鏈表為空。 hlist_add_head 函數用於將節點添加到雜湊表中。它將新節點插入到雜湊表的頭部,即 first 指標指向新節點,並更新新節點的 next 和 pprev 指標,以及原來頭部節點的 pprev 指標。 find 函數用於在給定雜湊表中查找特定值的節點索引。它使用雜湊函數計算值的雜湊值,並遍歷對應雜湊槽中的節點,直到找到值相等的節點,然後返回該節點的索引。如果未找到,則返回 -1。 dfs 函數是一個遞歸函數,用於根據前序遍歷和中序遍歷序列建立二元樹。它首先創建當前根節點,然後根據根節點在中序遍歷序列中的位置,遞歸建立左子樹和右子樹,並將其連接到根節點上。 node_add 函數用於創建並添加一個新節點到雜湊表中。它首先分配內存以存儲新節點,然後計算節點值的雜湊值,並將新節點添加到雜湊表的對應槽中。 buildTree 函數是入口函數,用於初始化雜湊表並調用深度優先搜索函數 dfs 來建立二元樹。 #### 實作測試程式 使用指標而不是索引:在哈希表中查找值時,可以將哈希表的頭指標直接傳遞給 find 函數,而不是傳遞整個哈希表。這樣可以減少參數傳遞的開銷。 釋放節點內存:在構建樹的過程中,應該在適當的時候釋放節點的內存,避免內存泄漏。 錯誤處理:在節點添加到哈希表時,應該檢查內存分配是否成功,並在失敗時進行適當的錯誤處理。 簡化節點結構:可以簡化 order_node 結構,不需要存儲值的索引,因為在構建樹的過程中索引已經不再需要 :::spoiler 程式碼 ``` c code #include <stdio.h> #include <stdlib.h> #define container_of(ptr, type, member) \ ((type *) ((char *) (ptr) - (size_t) & (((type *) 0)->member))) #define list_entry(ptr, type, member) container_of(ptr, type, member) #define hlist_for_each(pos, head) \ for (pos = (head)->first; pos; pos = pos->next) struct hlist_node { struct hlist_node *next, **pprev; }; struct hlist_head { struct hlist_node *first; }; static inline void INIT_HLIST_HEAD(struct hlist_head *h) { h->first = NULL; } static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) { if (h->first) h->first->pprev = &n->next; n->next = h->first; n->pprev = &h->first; h->first = n; } struct TreeNode { int val; struct TreeNode *left, *right; }; struct order_node { struct hlist_node node; int val; }; static struct order_node *find(int num, struct hlist_head *heads, int size) { struct hlist_node *p; int hash = (num < 0 ? -num : num) % size; hlist_for_each(p, &heads[hash]) { struct order_node *on = container_of(p, struct order_node, node); if (num == on->val) return on; } return NULL; } static struct TreeNode *dfs(int *preorder, int pre_low, int pre_high, int *inorder, int in_low, int in_high, struct hlist_head *in_heads, int size) { if (in_low > in_high || pre_low > pre_high) return NULL; struct TreeNode *tn = malloc(sizeof(*tn)); tn->val = preorder[pre_low]; struct order_node *on = find(preorder[pre_low], in_heads, size); int idx = on->val; tn->left = dfs(preorder, pre_low + 1, pre_low + (idx - in_low), inorder, in_low, idx - 1, in_heads, size); tn->right = dfs(preorder, pre_high - (in_high - idx - 1), pre_high, inorder, idx + 1, in_high, in_heads, size); return tn; } static inline void node_add(int val, struct hlist_head *heads, int size) { struct order_node *on = malloc(sizeof(*on)); if (!on) { perror("Memory allocation failed"); exit(EXIT_FAILURE); } on->val = val; int hash = (val < 0 ? -val : val) % size; hlist_add_head(&on->node, &heads[hash]); } static struct TreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) { struct hlist_head *in_heads = malloc(inorderSize * sizeof(*in_heads)); if (!in_heads) { perror("Memory allocation failed"); exit(EXIT_FAILURE); } for (int i = 0; i < inorderSize; i++) INIT_HLIST_HEAD(&in_heads[i]); for (int i = 0; i < inorderSize; i++) node_add(inorder[i], in_heads, inorderSize); struct TreeNode *root = dfs(preorder, 0, preorderSize - 1, inorder, 0, inorderSize - 1, in_heads, inorderSize); // 釋放哈希表分配的內存 free(in_heads); return root; } void inorderTraversal(struct TreeNode *root) { if (root == NULL) return; inorderTraversal(root->left); printf("%d ", root->val); inorderTraversal(root->right); } int main() { int preorder[] = {3, 9, 20, 15, 7}; int inorder[] = {9, 3, 15, 20, 7}; int preorderSize = sizeof(preorder) / sizeof(preorder[0]); int inorderSize = sizeof(inorder) / sizeof(inorder[0]); struct TreeNode *root = buildTree(preorder, preorderSize, inorder, inorderSize); printf("Inorder Traversal: "); inorderTraversal(root); printf("\n"); return 0; } ``` ::: ### 測驗二 ### [LeetCode 146. LRU Cache](https://leetcode.com/problems/lru-cache/description/) #### 原理解釋 結構定義: struct hlist_node 和 struct hlist_head:這些結構定義了雜湊表的節點和表頭。 struct list_head:這是一個雙向鏈表的結構。 初始化函數: INIT_HLIST_HEAD 和 INIT_LIST_HEAD 函數用於初始化雜湊表和鏈表的表頭。 添加函數: hlist_add_head 函數用於將節點添加到雜湊表中。 list_add 函數用於將節點添加到鏈表中。 刪除函數: hlist_del 函數用於從雜湊表中刪除節點。 list_del 函數用於從鏈表中刪除節點。 獲取函數: lRUCacheGet 函數用於從快取中獲取特定鍵的值。它首先計算鍵的雜湊值,然後遍歷對應的雜湊槽,找到對應的節點,並將其移動到鏈表頭部,以表示最近使用。如果找到鍵,則返回其值,否則返回 -1。 放置函數: lRUCachePut 函數用於將鍵值對放入快取中。它首先計算鍵的雜湊值,然後遍歷對應的雜湊槽,查找是否存在相同的鍵。如果存在,則將其移動到鏈表頭部;否則,如果快取已滿,則淘汰最近最少使用的項目,並添加新的節點;如果快取未滿,則直接添加新的節點。最後,更新鍵對應的值。 釋放函數: lRUCacheFree 函數用於釋放整個快取的內存。它遍歷快取中的每個節點,從鏈表中刪除節點並釋放內存。 這樣,通過使用雜湊表來實現快取的查找功能,並使用雙向鏈表來實現最近最少使用的淘汰策略,從而實現了 LRU 快取的功能。 #### 改進方法並實作 釋放函數的改進:在 lRUCacheFree 函數中,釋放每個節點的內存後,將應該將節點從雜湊表中刪除。否則,快取的雜湊表可能會保留對已經釋放的節點的引用,這可能導致未定義的行為或記憶體泄漏。 更好的錯誤處理:在 lRUCacheCreate 函數中,應該檢查是否成功分配了快取結構的內存。如果分配失敗,應該進行適當的錯誤處理。 :::spoiler 程式碼 ``` c dode void lRUCacheFree(LRUCache *obj) { struct list_head *pos, *n; int i; list_for_each_safe(pos, n, &obj->dhead) { LRUNode *cache = list_entry(pos, LRUNode, link); list_del(&cache->link); hlist_del(&cache->node); free(cache); } free(obj); } LRUCache *lRUCacheCreate(int capacity) { LRUCache *cache = malloc(sizeof(LRUCache) + capacity * sizeof(struct hlist_head)); if (!cache) { perror("Memory allocation failed"); exit(EXIT_FAILURE); } cache->capacity = capacity; cache->count = 0; INIT_LIST_HEAD(&cache->dhead); for (int i = 0; i < capacity; i++) INIT_HLIST_HEAD(&cache->hhead[i]); return cache; } ``` ::: ### 測驗三 ### [實作 find_nth_bit 函數](https://hackmd.io/@sysprog/linux2024-quiz2#%E6%B8%AC%E9%A9%97-3) #### 原理解釋 find_nth_bit 函數是這段程式碼的主體。該函數接受三個參數:記憶體空間的起始地址 addr、記憶體空間的大小 size 和要找到的第 N 個設定的位元位置 n。 如果 n 大於或等於 size,則表示要找的位元超出了記憶體空間的範圍,因此直接返回 size。 如果 size 是一個小於等於 BITS_PER_LONG 的常數,則使用位元掩碼 GENMASK 取出相關的位元組,然後調用 fns 函數在這些位元組中尋找第 N 個設定的位元位置。 如果 size 大於 BITS_PER_LONG,則使用迴圈遍歷記憶體空間中的每個字,計算該字中設定的位元數量,並與要找的位元位置 n 進行比較,以確定要找的位元位於哪個字中。接著,使用 fns 函數在該字中尋找第 N 個設定的位元位置。 最終,返回第 N 個設定的位元位置,如果找不到,則返回 size。 內部輔助函數 __ffs 和 __clear_bit: __ffs 函數用於在一個字中尋找第一個設定的位元位置,即從右邊數起第一個為 1 的位元的位置。 __clear_bit 函數用於清除指定位元的值,將其設置為 0。