2024q1 Homework2 (quiz1+2)
---
contributed by < LULser0204 >
### 第一週測驗題
#### 測驗1
##### 前言
參考 [Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/),實作非遞迴 (non-recursive; iterative) 的快速排序法。作者是用了一個明確的堆疊( beg[] 和 end[] )取代了遞迴呼叫使用,減少了函式調用的開銷。
除此之外,作者在每一輪中只通過 `swap` 交換一次軸心點,避免了重複的移動開銷,減少了元素與自身交換的情況。
##### 分析此題鍊結串列的運作
###### 結構
自定義一個鍊結串列結構體:
```
typedef struct __node {
struct __node *left, *right;
struct __node *next;
long value;
} node_t;
```
在 `node_t` 結構中,他有一個指標 `next` 指向下一個節點,而 `left` 和 `right` 並沒有在 `quicksort`中特別著墨,猜測此為單向鍊結串列。
:::spoiler quick sort 程式碼
```
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 = p->next;
list_add(n->value > value ? &right : &left, n);
}
begin[i] = left;
end[i] = list_tail(&left);
begin[i + 1] = pivot;
end[i + 1] = pivot;
begin[i + 2] = right;
end[i + 2] = list_tail(&right);
left = right = NULL;
i += 2;
} else {
if (L)
list_add(&result, L);
i--;
}
}
*list = result;
}
```
:::
在這個函式的迴圈中,有幾個重要的步驟:
1.選擇軸心點 (pivot) 並將其從鍊表中分離。
2.遍歷練表,根據截點值與軸心點值得比較結果,將截點添加到 `left` 或 `right` 鍊表中
3.更新 `begin` 、 `end` 陣列以反映新的子鍊表界線。
4.重設 `left` 和 `right` 為 `NULL` 並增加`i` 以處理下一層
過程如下圖:
##### 可改進之處
我認為可行的改進之處:
1.由於 `quick sort` 的效率很大程度依賴於軸心點的選擇。使用第一個作為軸心點在 `worst case` 下會導致很差的性能。
> 可以考慮使用"三數取中"策略,即從鍊表的(首部、中部和尾部)選擇三個元素,然後將這三個元素的中間值作為軸心點
2.
#### 測驗2
##### 前言
Tim Peter 他結合合併排序和插入排序的特點,提出 Timsort。在現實世界中,許多資料往往是由一些已經部分排序的序列組合而成,於是他的策略是首先識別出資料中已排序的子序列 (這些子序列稱為 run),然後透過不斷合併這些 run 來達成全資料範圍的排序。
##### 分析此題鍊結串列的運作
###### 結構
雙向鏈結串列
```
struct list_head {
struct list_head *prev;
struct list_head *next;
};
```
鏈結串列元素
```
typedef struct {
struct list_head list;
int val;
int seq;
} element_t;
```
##### 完成程式碼
程式碼
* `AAAA` 應該要填 `&head` 。因為要初始化 `tail` 指標的位址,使其指向鍊結串列的 `head` 位址。這樣 `*tail` 就可以被賦值為第一個節點,即合併後鍊節串節的頭節點。
* `BBBB` 和 `CCCC` 應該要填 `&(a->next)` 和 `&(b->next)` 從上面可以得知 `*tail` 要放入的是下一個節點,故要更新 `tail` 值使其指向下一個節點的 `next` 。這麼做是為了在下一次跌代中,將下一個節點插入鍊結串列。
* `DDDD` 和 `EEEE` 應該要填 `head->prev` 和 `tail->next` 完成雙向鍊結串列的循環。
* `FFFF` 應該填 `1` ,因為如果大於1代表鍊結串列還可以被合併
##### 運作原理
`run_size` :根據鍊結串列的頭節點計算當前運行的大小。
`merge` :合併兩個已排序的鍊結串列。
`build_prev_link` :重新連接鍊結串列的 `prev` 指針。
`find_run` :在鍊結串列中找到一個遞增或是遞減的序列,並且翻轉遞減的序列使其成為遞增序列。
`merge_at` 、 `merge_force_collapse` 、 `merge_collapse` :這些函式負責串列的合併操作。
`merge_final` :所有的 runs 都通過上面的 merge 函式合併後,負責將最後兩個 runs 合併成一個完全有序的串列,以及重建雙向鍊結。
而主函式 timsort:
1.初始化和準備:將雙向鍊結串列轉換為以 `NULL` 為結尾的單向鍊結串列。
2.尋找並合併 `runs` :通過 `find_run` 尋找 `runs`,然後利用 `merge_collapse` 或 `merge_force_collapse` 根據特定規則來合併runs。後者是在排序過程接近完成時使用。
3.最終合併:當輸入的鍊結串列完全轉化為 runs 後,使用 `merge_force_collapse` 強制合併剩餘的所有 runs ,最後通過 `merge_final` 或 `build_prev_link` 重建整個串列的雙向鍊結。
##### 改進方案
> 待補...
---
### 第二週測驗題
#### 測驗1
##### 前言
[LeetCode 105 Construct Binary Tree from Preorder and Inorder Traversal](https://leetcode.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/description/) 題意:
> 給定由二個由整數構成陣列的前序 (preorder) 和中序 (inorder) 形式,分別是對同一棵二元樹的前序及中序走訪結果,藉此重建此二元樹。
```
Example 1:
Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]
```
##### 完成程式碼
先觀察hash table 實作中,用以處理 hash 數值碰撞的 hlist_node,注意他的 pprev 是 pointer to pointer
```
struct hlist_node {
struct hlist_node *next, **pprev;
};
```
示意圖![hash_table](https://hackmd.io/_uploads/ByS47D66a.png)
* `AAAA` 應該填 `h->first` 。因為要將新節點 n 插入 hash table 的頭部, n->next 應該指向原鍊結串列的第一個節點,即目前的 h->first
* `BBBB` 應該填 `&heads[hash]` 。使用 `hlist_for_each` 遍歷特定的 hlist 。而 `&heads[hash]` 是該 hlist 的頭。
* `CCCC` 應該填 `list_entry` 。從括號內的參數剛好對應 list_entry 的 (ptr, type, member) 推測。
* `DDDD` 應該填 `&heads[hash]` 。在 node_add 函式中,其功能為將一個新的節點加入到 hash table 。 DDDD 應該指向我們想要增加新節點的鍊節串列的頭。
##### 說明特定函式的功能
* hlist_add_head :將一個新節點 n 加到指定的 hash table 頭部 h
* find :根據給定的數字 `num` 和 hash table 大小 `size` ,在 hash table 中尋找對應的 `order_node` ,返回找到的節點其索引值。
* dfs :首先通過 `preorder` 結果找到當節的根節點,然後在 `inorder` 結果中找到該節點,從而分隔出左右子樹,然後遞迴構建左右子樹。
* node_add :將中序遍歷中的一個節點和索引加入到 hash table 中 dfs 函式快速找到任何節點在中序遍歷中的位置。
* buildTree :使用前序和中序遍歷的結果來重建二元樹。首先使用 `in_heads` 紀錄中序遍歷中每個元素的 hlist 頭部。接下來,在迴圈內使用 `node_add` 進行中序遍歷,並且將其中的每個元素和對應的索引加到 hash table 。最後通過使用 dfs 函式構建二元樹,並返回構建好的二元樹根節點
##### 運作原理
1. 構建一個 hash table ,儲存 inorder 遍歷結果中每個元素,方便查詢以及之後建造二元樹
2. 使用 dfs 函式深度優先方式尋找根節點,利用 find 函式回傳索引值(根節點位置)。
3. 以根節點為中心,分為左右子樹,在遞迴乎叫 dfs 函式完成整棵二元樹。
##### 可改進之處
> 待補...
#### 測驗2
##### 前言
針對 [LeetCode 146. LRU Cache](https://leetcode.com/problems/lru-cache/description/)
> 題目說明:有一個 LRU Cache 其有一定的容量 (capcity) ,然後使用 **put** 放入 Cache 裡面,如果放入時 Cache 已滿,則把 LRU (Least Recently Used) key 拋棄,並把新的 key 放入 Cache ;而 **get** 則是看當下 Cache 裡面是否有對應的 key ,有則返回其值,並將對應的 key **更新** ,沒有則返回 -1 。
##### 說明&完成程式碼
結構:
LRUCache :LRU 主要結構,包含容量 (capcity) 、當前數據量 (count) 、雙向鏈結的頭用於實現 LRU 功能 (dhead) 、hash table 陣列 (hhead) 用於快速查詢
LRUNode :存放在 LRUCache 中的節點結構,包含 key、value、hash table 節點、雙向鏈結串列節點 link
主要函式:
lRUCacheCreate :創建一件新的 LRU Cache 物件,初始化其 capcity、count、dhead、hhead
lRUCacheFree :釋放LRU Cache 中所占用的記憶體空間,包括所有 LRUNode 節點
lRUCacheGet: 從 Cache 中獲取一個元素的值。如果找到,將其移至雙向鏈結串列的頭,代表最近被使用。
lRUCachePut: 往 Cache 中放入一個新元素或更新現有元素的值。如果 Cache 已滿,則先移除 LRU 元素,然後在鏈結串列頭部放入新元素
EEEE :在 hlist_del 函式中,應該填入" next->pprev "。舉例:假設現在有 A->B->C 的串列,現在要刪除節點 B。
刪除前:
* A 的 next 指向 B , B 的 pprev 指向 A 的 next
* B 的 next 指向 C , C 的 pprev 指向 B 的 next
執行刪除動作:
* 將 B 的 pprev (即 A 的 next )更新為 B 的 next (即 C ),此時 A 直接指向 C
* 如果 B 的 next (即 C )存在,更新 C 的 pprev 為指向 A 的 next
刪除後:
* A 的 next 現在指向 C ,C 的 pprev 指向 A 的 next (從串列中移除 B )
FFFF :要填入 link ,因為這是 LRUNode 結構中連接到 list_head 的 member 名。
GGGG :要填入 &cache->link 。這裡要傳遞一個指向 list_head 的 pointer 到 list_del 函式中刪除節點。
HHHH :要填入 node ,因為 lRUCacheGet 函式是基於 hlist_node 所指向的物件。
IIII :要填入 &cache->link 指向 list_head 結構的 pointer 。
JJJJ :要填入 node ,其原因一樣是基於 hlist_node 所指向的物件。
KKKK :要填入 &c->link ,在 lRUCachePut 函式的功能就是將剛使用到的節點移到鏈結串列的頭部。
##### 運作流程
1. 由 lRUCacheGet 進行查詢的動作,通過鍵的 hash table 值定位到位置,如果找到,則將對應的節點移到頭部,並返回值;反之,找不到則返回 -1 。
2. 由 lRUCachePut 進行更新的動作,通樣先定位,檢查元素是否存在。如果存在,更新其值並移至頭部。如果不存在,檢查 Cache 是否已滿。滿了,去除掉 LRU 元素,增加新的元素在串列頭部;沒滿,直接增加新元素在串列頭部。
##### 延伸問題
> 待補...
#### 測驗3
##### 前言
在指定的記憶體空間找出第 N 個設定的位元(即 1
##### 解釋程式碼&完成程式碼
主要函式:
FIND_NTH_BIT :找到第 n 個 bit的位置。
_ffs : Find First Set 找到第一個 bit 位置。返回給定的無號整數最小有效位置的 index 。
__clear_bit :清除再給定位址中特定位。
fns :在一個常整數中找到第n個設置位的位置。通常迴圈使用 __ffs 來找到並清除最低設置位,直到找到第 n 個為止。
AAAA : 要填 0xffffffff 。根據下面判斷式中( 0x1、0x3、0xf...)的規律推導出來的
BBBB : 要填 ~mask 。 __clear_bit 函式主要功能是對於特定 bit 進行清除。要清除一個位元需要通過 AND 操作。
CCCC : 要填 % 。用於尋找第 n 個 bit 為 1 的索引。
##### 運作主要原理
find_nth_bit 是整個核心。通過遍歷數組中每個字,使用 hweight_long 快速跳過完全為 0 的字,直到找到包含目標位置 bit 。然後再使用 fns 函式定位出確切的位置。
##### 延伸問題
> 待補