# 2024q1 Homework2 (quiz1+2) contributed by < [allenliao666](https://github.com/allenliao666) > ## 2024quiz1 ### 測驗1 #### 想法及思考 ```c typedef struct __node { struct __node *left, *right; struct __node *next; long value; } node_t; ``` ```graphviz digraph "__node" { node [shape="record"]; rankdir = "LR" subgraph cluster_a { label = "node_t"; node1 [label="<head>value|*next|{<left> *left |<right> *right}"]; } } ``` `node_t` 為佇列中的節點結構,由 `node_t` 指標 *left 、 *right 和 *next 還有 `long` value 所組成。 其中 *next 用於連接下一個 `node_t` , value 為該節點的值。 #### 所需函式 - void list_add(node_t **list, node_t *node_t) - node_t *list_tail(node_t **left) - int list_length(node_t **left) - node_t *list_construct(node_t *list, int n) - void list_free(node_t **list) 以此串列為例: ```graphviz digraph "eg" { node [shape="record"]; rankdir = "LR" a[label = 5] b[label = 3] c[label = 2] d[label = 7] e[label = 8] a -> b -> c -> d -> e } ``` 在測驗一的快速排序非遞迴實作中,首先 pivot 指向串列第一個節點,並且把該節點的 value 設定為比較對象。並把 L 和 R 指標分別指向待排序串列兩側的節點。 ```graphviz digraph "eg" { node [shape="record"]; pivot[label = pivot] L[label = L] R[label = R] rankdir = "LR" a[label = 5] b[label = 3] c[label = 2] d[label = 7] e[label = 8] pivot -> a -> b -> c -> d -> e L -> a R-> e } ``` 接著依序和串列中的其他節點比較大小,將 value 小於 piovt 的節點加入 left 指向的串列,反之加入 right 指向的串列。最後使用 begin[] 和 end[] 兩個指標陣列紀錄 left 和 right 指向的兩個串列的開頭和尾端節點,其中 begin[i] 指向 left 的串列的開頭節點,end[i] 指向 left 的串列的尾端節點,begin[i + 2] 指向 right 的串列的開頭節點,end[i + 2] 指向 right 的串列的尾端節點。 ```graphviz digraph "eg" { node [shape="record"]; pivot[label = pivot] rankdir = "LR" b1[label="begin[0]"] b2[label="begin[2]"] e1[label="end[0]"] e2[label="end[2]"] a[label = 5] b[label = 3] c[label = 2] d[label = 7] e[label = 8] pivot -> a c -> b e ->d b1 -> c b2 -> e e1 -> b e2 -> d } ``` 接著會針對 right 串列中重複執行上述的步驟,持續分割排序串列,並把分割排序後的串列的頭尾紀錄在對應的 begin[] 和 end[] 中,直到待排序的串列只有一個節點後開始合併。 #### 改進空間 - 原程式碼中,先宣告 begin[] 和 end[] 的大小為串列長度的兩倍,然而實際執行時,最多僅會將串列分割為 n 個子串列,故 begin[] 和 end[] 僅須為串列即可。 - end[i] 的功能為記錄子串列的尾端節點,故可用 list_tail(&begin[i]) 取代。 - (實驗待補)據 quick sort 性質,其最差狀況是 pivot 為最小值,導致該次迴圈的比較無意義,最差的時間複雜度為 $O(n^2)$ 。 ### 測驗2 #### 想法及思考 Timsort 結合合併排序及插入排序的優點。 Timsort 執行過程如下: 1. 首先使用 `find_run()` 尋找資料中已排序的子序列(以下稱為 run ),以避免浪費時間排序資料中已排序的部分。在尋找 run 的過程中,若遇到反向排序( value 由大到小)的部分時會將其反向排序成為 run 。 2. 將 run 透過 `pair` 結構紀錄,其中 `head` 指向 run 的第一個節點, `next` 指向剩餘串列的第一個節點。 ```c struct pair { struct list_head *head, *next; }; ``` 3. 透過堆疊儲存每一個 run ,以 `tp` ˋ指標指向最新的 run ,並用 `result.head` 的 prev 連接上一個加入堆疊的 run 。 4. 透過 `merge_collapse` 確保堆疊頂端的3個 run 符合長度規則,若不符合則合併相鄰的2個 run ,藉此維持堆疊中 run 長度的平衡。 #### 改進空間 ## 2024quiz2 ### 測驗1 #### 想法及思考 題意:給定二元樹的前序及中序表示法,藉此重建二元樹。 ### 測驗2 #### 想法及思考 ### 測驗3 #### 想法及思考 題意:尋找記憶體位置中的第 n 個設定位元。 因為對 bitwise 操作一竅不通,故依序理解巨集。 **GENMASK(h, l)** ```c #define GENMASK(h, l) \ (((~0UL) - (1UL << (l)) + 1) & (~0UL >> (BITS_PER_LONG - 1 - (h)))) ``` 其中`((~0UL) - (1UL << (l)) + 1)` 即把右邊 `l - 1`個位元設為0,`(~0UL >> (BITS_PER_LONG - 1 - (h)))` 把左邊 `h + 1`個位元設為0。其手法類似 linux 中 [bitops.h](https://elixir.bootlin.com/linux/v4.0/source/include/linux/bitops.h) 的程式碼。 ```c #define GENMASK(h, l) \ (((~0UL) << (l)) & (~0UL >> (BITS_PER_LONG - 1 - (h)))) #define GENMASK_ULL(h, l) \ (((~0ULL) << (l)) & (~0ULL >> (BITS_PER_LONG_LONG - 1 - (h)))) ``` 其目的皆為設定 `l` 到 `h` 位元為1,其餘皆為0。 **BIT_WORD(nr)** ```c #define BIT_WORD(nr) ((nr) / BITS_PER_LONG) ``` 換算 nr 個位元的長度為多少個 word。 **#define BITS_TO_LONGS(nr)** ```c #define BITS_TO_LONGS(nr) DIV_ROUND_UP(nr, BITS_PER_TYPE(long)) ``` 使用 `DIV_ROUND_UP(n, d)` 和 `BITS_PER_TYPE(long)` 兩個巨集, `DIV_ROUND_UP` 的目的為將 n / d 的結果向上取至整數,例如42/5=8.4 代入 `DIV_ROUND_UP` 就會得到9。`BITS_PER_TYPE(long)` 則是計算 long 的位元數。故`BITS_TO_LONGS(nr)` 表示至多需要多少個 long 儲存 nr 。 **__const_hweight8(w)** ```c #__const_hweight8(w) \ ((unsigned int) ((!!((w) & (1ULL << 0))) + (!!((w) & (1ULL << 1))) + \ (!!((w) & (1ULL << 2))) + (!!((w) & (1ULL << 3))) + \ (!!((w) & (1ULL << 4))) + (!!((w) & (1ULL << 5))) + \ (!!((w) & (1ULL << 6))) + (!!((w) & (1ULL << 7))))) ``` 計算8個位元中共有多少個位元被設定(即為1)。 **BITMAP_LAST_WORD_MASK(nbits)** ```c #define BITMAP_LAST_WORD_MASK(nbits) (~0UL >> (-(nbits) & (BITS_PER_LONG - 1))) ``` 這個巨集會用在當 nbit 不整除於 BITS_PER_LONG 時(即 nbit 非64的倍數),必須把多餘的部份湊成64位元,所以使用遮罩保護右邊 nbit % BITS_PER_LONG 的值。 **FIND_NTH_BIT(FETCH, size, num)** 這個巨集較複雜,故先從參數開始介紹: * FETCH : 記憶體位置,並從該位置開始尋找設定的位元 * size(sz) : 尋找的範圍 * num : 尋找的目標 該巨集在 size 範圍內從 FETCH 開始尋找第 num 個設定的位元。 idx 為 word 的索引。 分為以下幾種狀況: 1. idx * BITS_PER_LONG + nr >= sz : 當 idx * BITS_PER_LONG 長度超過 sz ,表示尋找範圍已經超過 size ,所以 goto out 並回傳 sz 。 2. w > nr : w 為該 word 中設定的位元數,當 w 大於 nr 表示目標就在這個 word 中,故 goto found 利用 fns 巨集和 word 偏移地址即可得到目標。 ```c #define FIND_NTH_BIT(FETCH, size, num) \ ({ \ unsigned long sz = (size), nr = (num), idx, w, tmp; \ \ for (idx = 0; (idx + 1) * BITS_PER_LONG <= sz; idx++) { \ if (idx * BITS_PER_LONG + nr >= sz) \ goto out; \ \ tmp = (FETCH); \ w = hweight_long(tmp); \ if (w > nr) \ goto found; \ \ nr -= w; \ } \ \ if (sz CCCC BITS_PER_LONG) \ tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz); \ found: \ sz = min(idx * BITS_PER_LONG + fns(tmp, nr), sz); \ out: \ sz; \ }) ```