# 2024q1 Homework2 (quiz1+2) contributed by < `yan112388` > ## 2024q1 第 1 週測驗題 [題目](https://hackmd.io/@sysprog/linux2024-quiz1) ### 測驗一 **基本結構**:考慮 struct __node 為節點結構,有一個 int 儲存資料,及一個 left pointer, right pointer 和 next pointer ```c typedef struct __node { struct __node *left, *right; struct __node *next; long value; } node_ ``` 本題實作了一個非遞迴 (non-recursive iterative) 的快速排序演算法,其中: 在 `quicksort` 中,可見以下程式碼初始化 begin 和 end。 ```c begin[0] = *list; end[0] = list_tail(list); ``` 由此可知 `list_tail` 如其名,目的為找到鏈結串列的尾端節點 ```c node_t *list_tail(node_t **left) { while ((*left) && (*left)->next) left = &(AAAA); return *left; } ```` AAAA 應填入 `(*left)->next` ,使 left持續往下個節點走訪,直到最後一個節點為止 ```c int list_length(node_t **left) { int n = 0; while (*left) { ++n; left = &(BBBB); } return n; } ```  BBBB 應填入 `(*left)->next` ,與前者作用相似,直到無節點為止 **本題quicksort原理** * 利用 begin 和 end 作為堆疊,以利將數值分成不同區段來做排序。 首先會選擇區段的第一個節點作為基準點 (pivot),接著走訪此區段內的每個節點, 將小於基準點之值的節點串接到 left,大於基準點之值的節點串接 right。 在每次循環時, 如果從 begin 與 end 拿出的值相同,則表示此節點的排列位置正確, 可將此加入最終的鏈結串列 result 中。 ```c while (p) { node_t *n = p; p = CCCC; list_add(n->value > value ? &right : &left, n); } ``` CCCC 應填入 `p->next`,用來拜訪區段內的每個節點 ```c begin[i] = left; end[i] = DDDD; begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; end[i + 2] = EEEE; ``` DDDD 填入 `list_tail(&left)` ,取得 left 的尾節點 EEEE 填入 `list_tail(&right)` ,取得 right 的尾節點 **改進方案** * `max_level` 不須開到 `2*n` ? `n` 即可 ### 測驗二 [Timsort](https://en.wikipedia.org/wiki/Timsort): 該排序法考量現實世界中的資料大多為已排序的,首先將序列分成多個 runs,使用插入排序法做排序,再以合併排序法將 runs 合併,最後排序完成。 * `find_run()` :找到每個 run ,如果該 run 為降序排列,則反轉為升序 * `merge_force_collapse()`:強制合併剩餘的 runs ,直到剩餘 runs 的數量小於 3 個,過程中呼叫 `merge_at()` * `merge_collapse()`:根據剩餘的 runs 數量決定要合併哪兩個 runs,過程中呼叫 `merge_at()` * `merge_at()`:合併位於 at 處的兩個相鄰 runs * `merge_final()`:將最後兩個 runs 合併,並呼叫 `build_prev_link()` * `build_prev_link()`:將單向鏈結串列轉換為雙向循環鏈結串列 ## 2024q2 第 2 週測驗題 [題目](https://hackmd.io/@sysprog/linux2024-quiz2) ### 測驗一 **以 Linux 核心的 hash table 架構實作** [LeetCode 105 Construct Binary Tree from Preorder and Inorder Traversal](https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/description/) * 前序會先拜訪每個子樹的根節點,中序則將每個子樹的左右子樹分開來,因此能建構出唯一二元樹 * hlist_node 架構示意圖 ![image](https://hackmd.io/_uploads/SyO_BYna6.png) 其中,next 指向相鄰的節點本身,而 pprev 宣告為指標的指標,指向指著自身節點的指標。 ``` c struct hlist_node { struct hlist_node *next, **pprev; }; ``` **Q:為甚麼要使用 hash table**? 建構過程,需要根據前序,在中序中找到對應的索引來確定左右子樹的範圍。直接在中序內線性查找的時間複雜度為 O(n) ,對於大規模的數據會很費時。因此,使用 hash table 能加快尋找速度。 * `find`:計算 hash 值,在中序中查找指定元素的索引 * `node_add`:將新的節點加到 hash table 中,使用 Division method 作為 hash function * `dfs`: 1. 找到根節點在中序中的索引 `idx` 2. 根據索引 `idx` 將中序分成左子樹和右子樹 3. 根據前序的順序,遞迴建構左子樹和右子樹 ```c 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]; int idx = find(preorder[pre_low], size, in_heads); 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; } ``` 參考資料:[Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable)、[内核基础设施——hlist_head/hlist_node结构解析](https://linux.laoqinren.net/kernel/hlist/) **測試程式** 根據給予的前序和中序,建構出一顆根為 root 的二元樹 ```c struct TreeNode *root = buildTree(preorder, preorderSize, inorder, inorderSize); ``` 以下兩個函式能根據傳入的根結點,印出二元樹的前序與中序 ```c void printPreorder(struct TreeNode *root) { if (!root) return; printf("%d ", root->val); printPreorder(root->left); printPreorder(root->right); } ``` ```c void printInorder(struct TreeNode *root) { if (!root) return; printInorder(root->left); printf("%d ", root->val); printInorder(root->right); } ``` ### 測驗二 實作一個 LRU Cache * `lRUCacheCreate` : 建立一個新的 LRU Cache,指定其容量大小為 capacity * `lRUCacheFree` : 釋放 LRU Cache 所佔用的記憶體空間 * `lRUCacheGet` : 從快取中獲取特定 key 對應的值,如果該值存在,則將該節點移動到鏈結串列的頭部 * `lRUCachePut` : 將指定值依據 key 放進 Cache,如果 Cache 已滿,則移除最久未被存取的節點。如果 key 已存在,則更新其值並將該節點移動到鏈結串列的頭部 ### 測驗三 在指定的記憶體空間找出第 N 個設定的位元 * 有三個參數會傳入 `find_nth_bit` `*addr` :指向要查找的記憶體區域起始位置的指標 `size` :要查找的記憶體區域的位元大小 `n` :要查找的是第幾個設定的位元(從 0 開始數) ```c static inline unsigned long find_nth_bit(const unsigned long *addr, unsigned long size, unsigned long n) { if (n >= size) return size; if (small_const_nbits(size)) { unsigned long val = *addr & GENMASK(size - 1, 0); return val ? fns(val, n) : size; } return FIND_NTH_BIT(addr[idx], size, n); } ``` * 如果 `n` 大於或等於 `size`,則直接回傳 `size`,因為 `n` 已經超出了給定的範圍 * 如果 `size` 是一個很小的常數值且小於等於 `BITS_PER_LONG(64)` 並大於0,則只需檢查第一個 `unsigned long` 值 * 如果以上皆非,則利用巨集 `FIND_NTH_BIT` * `GENMASK`:依據給定的範圍,產生連續的 bitmask * `h` 和 `l` 是需要設置為 1 的最高有效位和最低有效位的位置 * 例:`GENMASK(5, 0)` 會生成 00000000000000000000000000111111(二進位) * `FIND_NTH_BIT`:逐一檢查每個 `unsigned long` 值中設定的位元數量。如果設定的位元數量大於` n` ,則進入 `found` 標籤,使用 `fns` 函數在該值中找到第 `n` 個設定的位元。 * `__ffs`:回傳該數值中最右邊值為1的位元的位置 >延伸問題皆待補充