# 2020q3 Homework8 (quiz8)
contributed by < `JimmyLiu0530` >
###### tags: `進階電腦系統理論與實作`
## 測驗一: LeetCode [1494. Parallel Courses II](https://leetcode.com/problems/parallel-courses-ii/)
給定一整數 $n$ 表示某所大學裡課程的數目,編號為 $1$ 到 $n$,給定陣列 `dependencies`,使得 $dependencies[i]=[x_i,y_i]$ 表示先修課的關係,亦即課程 $x_i$ 必須在課程 $y_i$ 之前選修並通過。
另外,給予一整數 $k$,表示在一個學期裡頭,最多可同時選修 $k$ 門課,前提是這些課的先修課在之前的學期裡已修過。程式應該回傳上完所有課最少需要多少個學期。題目保證一定存在一種修完所有課的方式。
- [ ] 範例1
給定輸入:
- n = 4 (即 4 門課程)
- dependencies = $[[2,1],[3,1],[1,4]]$
- 即選修 `1` 這門課,應當先修過 `2` 和 `3` 這二門課,又,選修 `4` 之前應當先修過 `1` 這門課
- k = 2 (一個學期中,至多可同時選修 2 門課程)
預期輸出: 3
- 下圖展現可能的修課方式: 第一個學期選修課程 `2` 和課程 `3`,然後第二個學期選修課程 `1` ,第三個學期選修課程 `4`。
```graphviz
digraph{
rankdir=LR
2->1->4
3->1
}
```
- [ ] 範例2
給定輸入:
- n = 5 (即 5 門課程)
- dependencies = $[[2,1],[3,1],[4,1],[1,5]]$
- 即選修 `1` 這門課,應當先修過 `2`,`3` 和 `4` 這三門課,又,選修 `5` 之前應當先修過 `1` 這門課
- k = 2 (一個學期中,至多可同時選修 2 門課程)
預期輸出: 4
- 上圖展現可能的修課方式: 第一學期選修課程 `2` 和 `3` (注意一學期僅能同時選修 `2` 門課程),第二學期選修課程 `4`,第三學期選修課程 `1`,第四學期選修課程 `5`。
```graphviz
digraph{
rankdir=LR
2->1->5
3->1
4->1
}
```
本題的限制:
- $1\leq n\leq 15$
- $1\leq k\leq n$
- $0\leq$ $dependencies.length$ $\leq\dfrac{n×(n-1)}{2}$
- $dependencies[i].length=2$
- $1\leq x_i, y_i\leq n$
- $x_i \neq y_i$
- 所有先修關係都不同,也就是說 $dependencies[i] \neq dependencies[j]$
- 題目輸入的圖是個有向無環圖
注意本題是 [NP 問題](https://en.wikipedia.org/wiki/NP_(complexity)),運用貪婪演算法 (greedy algorithm) 很容易遇到 TLE (time limit exceeded),於是我們可嘗試狀態壓縮和 [dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming) 結合的技巧。狀態壓縮是利用二進位來表達中間的狀態,也就是說,狀態只能是 0 或 1,若用集合來思考,就是「在」或「不在」集合中。此手法適用於資料量不大的案例。先用陣列儲存之前的狀態,再由狀態及其對應的數值,推導出狀態轉移方程式,最終可得適當的解法。
以本題來說,$n$ 至多為 $15$,而且一門課「選」或者「不選」即可運用二進位的位元表示,符合上述的狀態壓縮技巧。演算法如下:
1. 計算每個課程的直接相依課程的 bit mask (位元遮罩),記為 `pre`
2. 令狀態 $f(S)$ 表示完成位元遮罩 $S$ 的課程所需要的最少學期數
3. 在初始階段,$f(0)=0$,其餘為位元遮罩有效範圍以外的數值
4. 狀態移轉時,從某個已修過的課程位元遮罩 $S_0$,嘗試求出 $S_1$,後者表示目前可選擇的新課程(新課程也該滿足相依條件),再從 $S_1$ 逐次找出小於等於 $k$ 門課程,其位元遮罩記作 $S$,移轉關係為 $f(S_0 ∨ S)=min(f(S_0 ∨ S),f(S_0)+1)$
5. 最終解為 $f((1<<n)-1)$
對應的遞迴程式碼如下: (`recursive-nos1.c`)
```c=
#include <string.h>
#include <limits.h>
#define min(a, b) (((a) < (b)) ? (a) : (b))
static void solve(int i, int s, int k, int n, int s0, int s1, int *dp)
{
if ((k == 0) || (i == n)) {
dp[s0 | s] = min(dp[s0 | s], dp[s0] + 1);
return;
}
solve(i + 1, s, k, n, s0, s1, dp);
if ((s1 >> i) & 1)
solve(i + 1, s | 1 << i, k - 1, n, s0, s1, dp);
}
int minNumberOfSemesters(int n,
int **dependencies,
int dependenciesSize,
int *dependenciesColSize,
int k)
{
const size_t cap = 1 << n;
int pre[cap];
memset(pre, 0, sizeof(pre));
/* pre[i]: \\the bit representation of all dependencies of course i */
for (int i = 0; i < dependenciesSize; i++)
pre[dependencies[i][1] - 1] |= 1 << (dependencies[i][0] - 1);
int dp[cap];
for (int i = 1; i < cap; i++)
dp[i] = INT_MAX;
dp[0] = 0;
for (int s0 = 0; s0 < cap; s0++) {
if (dp[s0] == INT_MAX)
continue;
int s1 = 0;
for (int i = 0; i < n; i++)
if (!((s0 >> i) & 1) && ((pre[i] & s0) == pre[i]))
s1 |= 1 << i;
solve(0, 0, k, n, s0, s1, dp);
}
return dp[cap - 1];
}
```
:::info
:angel:`recursive-nos1.c` 在 LeetCode 提交後的執行結果
Runtime: 124 ms
Memory Usage: 6.2 MB
:::
我們可改寫為非遞迴的形式。對應的程式碼如下: (`iterative-nos1.c`)
```c=
#define min(a, b) (((a) < (b)) ? (a) : (b))
int minNumberOfSemesters(int n,
int **dependencies,
int dependenciesSize,
int *dependenciesColSize,
int k)
{
uint16_t prerequisite[n];
memset(prerequisite, 0, sizeof(prerequisite));
for (int i = 0; i < dependenciesSize; ++i)
prerequisite[dependencies[i][1] - 1] |= 1 << (dependencies[i][0] - 1);
const int cap = 1 << n;
uint8_t dp[cap];
memset(dp, 1 << 6, cap);
dp[0] = 0;
for (uint32_t i = 0; i < cap; ++i) {
uint32_t ready_for_take = 0;
for (uint32_t j = 0; j < n; ++j) {
if ((prerequisite[j] & i) == prerequisite[j])
ready_for_take |= 1 << j;
}
ready_for_take &= ~i;
for (uint32_t j = ready_for_take; j; j = ready_for_take & (j - 1)) {
if (__builtin_popcount(j) <= k)
dp[i | j] = min(dp[i | j], dp[i] + 1);
}
}
return dp[cap - 1];
}
```
:::info
:angel:`iterative-nos1.c` 在 LeetCode 提交後的執行結果
Runtime: 80 ms
Memory Usage: 6.1 MB
:::
我們可改良上述的非遞迴的實作,先計算出每個數在二進位表示中有幾個 `1`,亦即選修幾門課,保存於陣列 `count[]` 中,共有 $2^n$ 種狀態。如下: (`iterative-nos2.c`)
```c=
#define min(a, b) (((a) < (b)) ? (a) : (b))
int minNumberOfSemesters(int n,
int **dependencies,
int dependenciesSize,
int *dependenciesColSize,
int k)
{
const size_t cap = 1 << n;
/* pre[i]: the bit representation of all dependencies of course i */
uint16_t pre[cap];
memset(pre, 0, sizeof(pre));
for (int i = 0; i < dependenciesSize; i++)
pre[dependencies[i][1] - 1] |= 1 << (dependencies[i][0] - 1);
/* i is the bit representation of a combination of courses.
* dp[i] is the minimum days to complete all the courses.
*/
uint8_t dp[cap];
memset(dp, INIT, sizeof(dp));
dp[0] = 0;
uint8_t count[cap];
memset(count, 0, sizeof(count));
for (uint32_t i = 0; i < cap; i++) {
for (uint32_t j = 0; j < n; j++)
if (i & (1 << j))
count[i]++;
}
for (uint32_t i = 0; i < cap; i++) {
uint32_t b = 0;
/* figure out "b", the bit representation of the all courses
* that we can study now. Since we have i and pre[j], we know
* course j can be studied if i contains all its prerequisites.
*/
for (uint32_t j = 0; j < n; j++) {
if (COND && (pre[j] & i) == pre[j])
b |= 1 << j;
}
if (count[b] <= k) {
dp[i | b] = min(dp[i | b], dp[i] + 1);
} else {
for (uint32_t j = b; j; j = (j - 1) & b) {
if (count[j] == k)
dp[i | j] = min(dp[i | j], dp[i] + 1);
}
}
}
return dp[cap - 1];
}
```
:::info
:angel:`iterative-nos2.c` 在 LeetCode 提交後的執行結果
Runtime: 44 ms
Memory Usage: 6.1 MB
:::
### 程式說明
這三個程式碼其實都環繞在同一個想法上,只是使用不同的方法實作而已,譬如說利用遞迴或迭代。所以下面的說明一概以`iterative-nos2.c` 為例,至於上面那兩個程式碼,請自行參考對照。
首先,將 `dependencies[]` 內的資料移植到 `pre[]` 中 (line 13-14),其中 `pre[i]` 代表要修課程 `i+1` 之前要先修的課程的位元表示法 ( 即 pre[2] = 3 = $(011)_2$ 代表要修課程 3 之前要先修過課程 1 跟 2 )。
`dp[]` 用來儲存修過某些課程所需的最少學期數。舉例來說,$dp[5=(101)_2]$ = $2$,代表最少需要 **2** 個學期才能修完課程 1 跟 3。 line 19-21 在做 `dp[]` 的宣告以及初始化,要注意的是初始化的值至少要大於總課程數 `n`,又 $1 \leq n \leq 15$,因此,`INIT = 0x5F`。
接著,line 31 的 `for` 迴圈中的 `i` 代表之前修過的課的二進位表示法,透過窮舉去看所有課程
1. 之前是否已經修過 以及
2. 他們的先修課是否已修過
來決定這學期可以修哪些課,並儲存在 `b` (line 37-40)。因此,`COND = !(i & (1 << j))`,用來檢查是否已修過此堂課。
> NOTE: 前兩個程式碼都將此步驟(i.e. 將修過的課移除) 移到 for 迴圈外面去做
最後,列舉所有這學期可以修的課的排列組合,去看總堂數有沒有超過 `k` 堂,若小於等於 `k`,則更新對應的 `dp[]` with
`dp[之前修過 | 這學期修的] = min(dp[之前修過 | 這學期修的], dp[之前修過] + 1)`;
若超過 `k` 堂,則從中選取 `k` 堂,一樣更新 `dp[]`。
:::info
:Notes: 為何 `interative-nos2.c` 會比 `interative-nos1.c` 有更短的執行時間呢?
1. 主要是因為在最後檢查所有這學期可以修的課的排列組合時,`interative-nos2.c` 檢查的比 `interative-nos1.c` 來的精簡。舉例來說對於可以修的課堂數小於等於 `k` 的情況,前者只會更新 "全取" 的情況;而後者卻會更新所有可能性,造成不必要的更新。這是因為當這學期可以修的課堂數小於等於 `k`,那一定是全選會使得所需的總學期數最小。再者,對於這學期可以修的課堂數大於 `k`,前者也只會更新那些加起來課程數為 `k` 的組合而已。
2. 用預先算好的 `count[]` 取代 `__builtin_popcount()`。
:::
### 延伸問題
#### 1. `interative-nos2.c` 可如何改進?
- 可以將 `pre[]` 的大小從 `cap` 下降至 `n` 即可
#### 2. 實作上述不同的程式碼 (限制為 C99/C11 + GNU extensions),應比較遞迴和非遞迴的形式在效能的落差
#### 3. LeetCode [1595. Minimum Cost to Connect Two Groups of Points](https://leetcode.com/problems/minimum-cost-to-connect-two-groups-of-points/)
----------------------------------------
## 測驗二: [Skip List](https://en.wikipedia.org/wiki/Skip_list)
Linked list (鏈結串列) 允許時間複雜度為 $O(1)$ 的隨機插入 (insert) 及隨機移去 (remove) 節點,但搜尋卻需要 $O(n)$ 時間複雜度。假設一個 linked list 內容如下:
$HEAD→1→10→18→38→22→39→47→59→next$
從開頭 (即上方 `HEAD`) 找起,`59` 這個節點需要存取或比對 7 次才能找到,你直覺可能想到二分法,但對於 linked list 來說,隨機存取節點的時間複雜度仍是 $O(n)$,也就是說,二分法無法帶來效益。
我們可在鏈結串列中,增加額外的 “skip” (作用是提供資料存取的捷徑) 節點,如下:
![](https://i.imgur.com/d1U5bEo.png)
如此一來,我們就可依據 “skip” 節點查詢,只需要比對 3 次即可找到上述的 `59`。
至於若想搜尋 `47`,我們先根據 “skip” 節點來比對,於是在節點 `22` 上,它的 “skip” 指標會指向 `59`,後者大於 `47`,因此我們可知 `47` 可能會存在於節點 `22` 和節點 `59` 之間,這時候再根據鏈結串列順序搜尋,一共要比對 5 次。
隨著節點的增多,我們的 “skip” 鏈結串列會變成這樣:
![](https://i.imgur.com/BHPag9n.png)
“skip” 節點的密度約為普通節點的一半,能否再提高密度呢?我們可在這基礎上再加一層新的 “skip” 節點,這層節點的密度為第一層 “skip” 節點的一半。
![](https://i.imgur.com/3McDxXn.png)
換個視角,呈現如下:
![](https://i.imgur.com/EHCbcR6.png)
我們再給予新的限制:每層的 “skip” 節點都由它下一層的 “skip” 節點中產生,最終我們可得類似下圖的 “Skip List”:
![](https://i.imgur.com/GBsz8Ak.png)
於是,我們不再區分每層是原節點還是 “skip” 節點,而將最底下的那層節點通稱為第一層 (level 1) 節點,第一層節點上面為第二層 (level 2) 節點,再來是第三層,以此類推。假設每層的節點數為下一層的一半,於是搜尋的時間複雜度縮減為 $O(log\ n)$。
一般的 linked list 搜尋時必須從第一個開始,按照順序一個一個搜尋,因此不論是搜尋或存取的時間複雜度皆為 $O(n)$,而 Skip list 可將存取、搜尋、新增和刪除等操作的平均時間複雜度降到 $O(log\ n)$
> [Know Thy Complexities!](https://www.bigocheatsheet.com/)
Skip list 是種針對 sorted linked list 的資料結構,透過不同 level 做出不同的 linked list 達到加速搜尋。
舉例來說,當我們想在上方 Skip List 示意圖中搜尋 7,步驟如下:
1. 從 level 4 (圖中左上方) 比對,因 $1 \leq 7$ 得知 level 4 不存在 `7`,但其他 level 可能具備。於是跳到 level 3 繼續找
2. 從 level 3 比對,因 $4 \leq 7$ 且 $6 \leq 7$ 得知 level 3 一樣不存在 `7`,於是跳到 level 2 繼續找
3. 從 level 2 比對,因 $9 \geq 7$ 表示此 level 不存在 `7`,跳去 level 1
4. 在 level 1 比對,發現 `7`
總共比對為 5 次,比直接循序查詢(需要 7 次存取) 還快。
如之前所及,skip list 是具有分層結構的鏈結串列,那麼每層的節點該如何產生呢?
我們可在新增元素時,使用隨機方法 (稱作 “coin flip”,也就是「擲硬幣看正反面結果」) 決定這個元素有幾層節點。設定該元素僅有 1 層節點機率為 $\dfrac{1}{2}$,且有 2 層節點機率為 $\dfrac{1}{4}$,僅有 3 層節點機率為 $\dfrac{1}{8}$,以此類推。
然後觸發隨機事件,當機率為 $\dfrac{1}{2}$的事件發生時,該元素有 1 層節點; 機率為 $\dfrac{1}{4}$的事件發生時,該元素有 2 層節點,以此類推。另外,我們限定一個 “skip” 表格應當具有最大的層數限制。
假設一個 “skip” 表格最大層數限制為4,於是我們可設定一個整數區間為 $[1,2^{4-1}]$,即 $[1,8]$。然後取一個介於 1 到 8 之間的隨機數,當落在 $[5,8]$ 區間時有 1 層節點,落在 $[3,4]$ 區間時有 2 層節點,落在 $[2,2]$ 區間時有 3 層,落在 $[1,1]$ 上時有 4 層。
總結 Skip List 的特徵:
- 第一層包含所有的元素
- 每一層都是個有序的 linked list
- skip list 是隨機產生,所以即使元素完全相同的 linked list ,最終的生成的 skip list 有可能不一樣
- 如果節點 x 出現在第 i 層,則所有比 i 小的層都包含 x
建立 skip list 的流程:
1. 最底層 (level 1) 是包含所有元素的 linked list
2. 保留第一個和最後一個元素,從其他元素中隨機選出一些元素形成新的一層 linked list
3. 為剛選出的每個元素新增指標,指向下一層和自己相等的元素
4. 重複上述 (2) 和 (3) 步驟,直到不再能選擇出除了第一個和最後一個元素以外的元素
下方動畫展示如何在 Skip List 新增節點:
![](https://i.imgur.com/8vFqs56.gif)
Google 的開放原始碼 Key-Value storage (可簡稱 KV) 專案 [LevelDB](https://github.com/google/leveldb) 和 Facebook 延伸 LevelDB 而發展出的 [RocksDB](https://rocksdb.org/),都用到 Skip List 的變形。[Redis](https://redis.io/) 內建一個高效率的 Skip List 實作。
延伸閱讀:
- [Skip 視覺化呈現](https://people.ok.ubc.ca/ylucet/DS/SkipList.html)
- [Skip List](https://www.jianshu.com/p/9d8296562806)
下方程式嘗試實作一個 Skip List,並進行以下變更:
- 將 “coin flip” 改為插入節點前,事先建立
- “coin flip” 原本透過機率,現在取代為特定的雜湊 (hash) 函數,此處選用 [djb2](http://www.cse.yorku.ca/~oz/hash.html)
- Skip List 的高度原本是隨機結果,但透過雜湊函數,得到一個相依於資料分布的實作
此實作以 C99 的前置處理器開發,如下 (`list.h`):
```c=
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#define LIST_MAX_HEIGHT 32
typedef unsigned long custom_t; /* specify user-defined type */
typedef struct __list_node {
uint8_t *key;
size_t key_len;
uint8_t height;
custom_t val;
struct __list_node *next[1];
} list_node_t;
typedef struct {
uint8_t height;
uint32_t bits, reset;
list_node_t *head;
} skiplist;
#define LIST_ALLOC(e, s) \
do { \
e = malloc(s); \
assert(e && "out of memory."); \
memset(e, 0, s); \
} while (0)
#define LIST_HEIGHT(d, str, l, z) \
do { \
bool flip = false; \
for (z = 0; !flip; z++) { \
if (!d->reset) { \
d->bits = 5381; /* djb2 hash */ \
for (size_t i = 0; i < l; i++) \
d->bits = ((d->bits << 5) + d->bits) + str[i]; \
d->reset = LIST_MAX_HEIGHT; \
} \
flip = (bool) d->bits & 1; \
d->bits >>= 1; \
--(d->reset); \
} \
} while (0)
#define LIST_INIT(d) \
do { \
LIST_ALLOC(d, sizeof(skiplist)); \
LIST_ALLOC(d->head, (sizeof(list_node_t) + ((sizeof(list_node_t *)) * \
(LIST_MAX_HEIGHT - 1)))); \
d->height = 0; \
} while (0)
#define LIST_GET(d, k, l, v) LIST_FIND(d, k, l, v, NULL)
#define LIST_FIND(d, k, l, v, u) \
do { \
list_node_t *iterator = d->head, **ud = u; \
for (int i = d->height - 1; i >= 0; --i) { \
while (iterator->next[i] && \
memcmp(k, iterator->next[i]->key, l) > 0) \
iterator = iterator->next[i]; \
if (ud) \
ud[i] = iterator; \
} \
list_node_t *n = iterator->next[0]; \
v = (n && l == n->key_len && !memcmp(k, n->key, l)) ? n->val : 0; \
} while (0)
#define LIST_NODE_INIT(n, k, l, v, h) \
do { \
LIST_ALLOC( \
n, (sizeof(list_node_t) + ((sizeof(list_node_t *)) * (h - 1)))); \
LIST_ALLOC(n->key, l + 1); \
memcpy(n->key, k, l); \
n->val = (custom_t) v, n->height = h, n->key_len = l; \
} while (0)
#define LIST_INSERT(d, k, l, v) \
do { \
custom_t vl; \
list_node_t *u[LIST_MAX_HEIGHT]; \
LIST_FIND(d, k, l, vl, u); \
if (vl) \
break; \
int h; \
LIST_HEIGHT(d, k, l, h); \
if (h > d->height) { \
HHH; \
u[h - 1] = d->head; \
} \
list_node_t *n; \
LIST_NODE_INIT(n, k, l, v, h); \
while (--h >= 0) { \
n->next[h] = u[h]->next[h]; \
u[h]->next[h] = n; \
} \
} while (0)
#define LIST_ITERATE(d, callback) \
do { \
assert(callback); \
list_node_t *iterator = d->head; \
while (iterator->next[0]) { \
III; \
} \
} while (0)
#endif
```
其中 djb2 的作用是產生隨機分佈的的雜湊函數,常見的形式為:
$f(hash)=hash×33+c$
為何選擇 `33` 呢?
1. 乘法易於位移或相加
2. 從位移和加法實作中可見,使用 `33` 可複製雜湊累加器中的大多數輸入位元,並將這些位元相對地分散開來
3. $32=2^5$,32 與位移 5 位元相關,有助於將每個字串的每個位元都用到最終的雜湊數值中
至於為何選用初始的 `5381` 呢?這沒有太大影響,其他質數數也可很好地執行。除了 djb2,可考慮其他雜湊函數,如:
- [MurmurHash](https://en.wikipedia.org/wiki/MurmurHash)
- [xxHash](https://github.com/Cyan4973/xxHash)
針對上述 `list.h` 撰寫的測試程式如下:
```c=
#include <stdio.h>
#include "list.h"
static void print_key(list_node_t *n) {
puts((const char *) n->key);
}
int main()
{
/* initialize the list */
skiplist *d;
LIST_INIT(d);
/* create a key */
char *key = malloc(6);
strncpy(key, "hello", 5);
key[5] = 0;
/* create a value */
char *val = malloc(6);
strncpy(val, "world", 5);
val[5] = 0;
/* insert into the list */
int l = 5;
LIST_INSERT(d, key, l, (custom_t) val);
/* iterate through the list and print */
LIST_ITERATE(d, print_key);
/* retrieve from a list */
custom_t v;
LIST_GET(d, key, l, v);
printf("Hello, %s!\n", (char *) v);
return 0;
}
```
參考執行輸出:
```
hello
Hello, world!
```
請補齊程式碼。
### 程式說明
- LIST_INIT(d)
- 配置一個 `skiplist` 大小的空間給 `d`。
- 配置一個 `list_node_t` 加上 `LIST_MAX_HEIGHT` 個指向 `list_node_t` 指標所需的空間給 `d->head`。
- `d->height` 初始為 0。
- LIST_FIND(d, k, l, v, u)
- 參數說明:
- d: 指向 skiplist 的指標
- k: 鍵值 key
- l: key 的長度
- v: 尋找後的結果。
- u: 指標陣列,紀錄每層中比 `k` 小的最後一個元素,i.e. 待會 `k` 要插入在這個後面
- 從最上層往下找是否已存在鍵值 `k` ,順便更新 `u`。 若有找到鍵值 `k` 相對應的 value,則將之存入 `v`;否則將 0 存入 `v`。
- LIST_HEIGHT(d, k, l, h)
- 利用 `djb2` hash function 去決定 `k` 有多少層節點。
- LIST_NODE_INIT(n, k, l, v, h)
- 建立一個指向新的 `list_node_t` 的指標 `n`,配置空間,並初始化一些變數。
- LIST_INSERT(d, k, l, v)
- 建立一個指標陣列 `u` (與上面提到的 `u` 相同)。
- 呼叫 `LIST_FIND()` 看看鍵值 `k` 是否存在於 skiplist `d` 中。
- 如果存在,則直接結束此次插入。
- 呼叫 `LIST_HEIGHT()` 決定 `k` 有多少層節點。
- 如果決定出來 `k` 的層數 大於 目前 skiplist 擁有的層數,則將 skiplist 向上擴充一層,所以 `HHH 為 h = ++(d->height)` 。此外,還要更新剛擴充這層的 `u`,好讓 `k` 在剛擴充這層找到插入點。
- 呼叫 `LIST_NODE_INIT()` 建立並初始化一個 `list_node_t` 的物件。
- 最後,根據指標陣列 `u` 從最上層往下將含有鍵值 `k` 的物件插入到正確的位置。
- LIST_ITERATE(d, callback)
- 因為各層 `i` 節點的鏈結串列是存在放 `d->head->next[i]` 所指的空間中,因此 `III 為 iterator = iterator->next[0]; callback(iterator)`。
### 延伸問題
#### 1. 探討用 hash 替換 Skip List 裡頭 random 的效益和潛在的議題,並實作 delete 操作
#### 2. 研讀 [Cache-Sensitive Skip List: Efficient Range Queries on modern CPUs](https://www2.informatik.hu-berlin.de/~sprengsz/papers/cssl.pdf),指出考慮到現代處理器架構的 Skip List 實作,有哪些考量和取捨
> - [簡報檔案](http://www.adms-conf.org/2016/cssl-imdm.pdf)
> - 參考實作: [Cache-Sensitive Skip List (CSSL)](https://github.com/flippingbits/cssl)
#### 3. 練習 LeetCode [1206. Design Skiplist](https://leetcode.com/problems/design-skiplist/),除了提交 C 語言的實作並通過自動評分系統,也該提供正確性測試和效能分析
> - 參見 [Skip List](https://github.com/yfernandezgou/skip-list-c)