linux2022
目的: 檢驗學員對 linked list 的認知
作答表單: 測驗 1 (針對 Linux 核心「設計」課程)
作答表單: 測驗 2-4 (針對 Linux 核心「實作」課程)
1
LeetCode 編號 1 的題目 Two Sum,貌似簡單,作為 LeetCode 的開篇之題,乃是經典中的經典,正所謂「平生不識 Two Sum,刷盡 LeetCode 也枉然」,就像英語單詞書的第一個單詞總是 Abandon 一樣,很多沒有毅力堅持的人就只能記住這一個單詞,所以通常情況下單詞書就前幾頁有翻動的痕跡,後面都是嶄新如初,道理不需多講,雞湯不必多灌,明白的人自然明白。
以上說法取自 Two Sum 兩數之和
mint condition: "mint" 除了薄荷的意思,還可指鑄幣廠,"mint condition" 裡的 “mint” 就與鑄幣廠有關。有些人收集錢幣會在錢幣剛開始發行時收集,因爲這樣的錢幣看起來很新,他們會用 "mint condition" 來形容這種錢幣的狀況,強調「像剛從鑄幣廠出來」,後來衍伸出「有如新一樣的二手商品」的意涵。
題意是給定一個陣列 nums
和一個目標值 target
,求找到 nums
的 2 個元素相加會等於 target 的索引值。題目確保必為單一解,且回傳索引的順序沒差異。例如給定輸入 nums = [2, 7, 11, 15]
, target = 9
,相加變成 9
的元素僅有 2
及 7
,因此回傳這二個元素的索引值 [0, 1]
考慮以下 C 語言實作:
若用暴力法,時間複雜度為 ,顯然不符合期待。我們可改用 hash table (以下簡稱 HT
) 記錄缺少的那一個值 (即 target - nums[i]
) 和對應的索引。考慮以下案例:
nums =
[2, 11, 7, 15]
:
對應的步驟:
nums[0]
是 2
,HT[2]
不存在,於是建立 HT[9 - 2] = 0
nums[1]
是 11
, HT[11]
不存在,於是建立 HT[9 - 11] = 1
nums[2]
是 7
,HT[7]
存在 (設定於步驟 1
),於是回傳 [2, HT[7]] = [2, 0]
hlist
用於 hash table 的實作,它的資料結構定義在 include/linux/types.h 中:
示意如下:
hlist
的操作與 list
一樣定義於 include/linux/list.h,以 hlist_
開頭。hlist_head
和 hlist_node
用於 hash table 中 bucket 的實作,具有相同 hash value 的節點會放在同一條 hlist
中。 為了節省空間,hlist_head
只使用一個 first
指標指向 hlist_node
,沒有指向串列尾節點的指標。
hash table
主要是由一個 hlist_head
的動態陣列所構成,每個 entry 指向一個由 struct hlist_node
構成的非環狀 doubly linked list ,hash table 長度依照 bits
給定,可知大小為 2 的冪。
而可以發現 struct hlist_head
只有一個 struct hlist_node *
的成員; 而 struct hlist_node
型態卻包含一個 struct hlist_node *
及 struct hlist_node **
,其中一個原因為 hash table 指向的為非環狀的 linked list ,因此只需指向 list 的一端點,若單純使用 struct hlist_node
當作 head ,無用的 "pprev" 會造成大量的記憶體空間浪費,因此將 head 與 node 分開來實做。
而 struct hlist_node
中的 pprev 為何使用「指標的指標」而非「指標」? 回答這個問題前可以先參考 Linux 原始碼中 type.h :
可知在 type.h 中有兩種 list 的結構:
struct list_head
在 Linux 中實作為環狀 doubly-linked list,且可以在行程管理 (process scheduling) 的相關實做上看到,如 sched.h 中有近 20 處使用到此結構,因可快速存取到頭以及尾的節點(時間複雜度 ) 故有較好的效能,適用於行程管理這種對時間要求嚴謹的部分做使用。
引用自 linux sched.h
struct hlist_head
搭配 hlist_node
。在 Linux 核心中專門為了 hash table 而使用,hlist_head
的設計也省去了 list 起始端 pprev
的存放空間、在初始狀態就省去了一半的記憶體容量。而且同時 hash table 不會特別需要存取到 list 的尾端,並且走訪 list 相對沒那麼講求效率(因為 hash 的設計理念就是講求 hash collision rate 要低、因此一個 list 若太長比較需要改進的為 hash function 的設計而非改進整個資料結構)。綜合上述所說單向 list 已滿足 hash table 的需求。
pprev
為何是「指標的指標」?若和 list_head
一樣使用單純的指標( hlist_node *
),則考慮到 list 有方向性,delete node 時需要額外檢查其是否為 list 的 head 或是 NULL 等等,有較冗餘的程式碼必須實做,因此使用 hlist_node **pprev
直接存取上一個 node 所在的位址。Linux 為求程式碼簡潔故以 pointer to pointer 的方式用 pprev
直接指向前一個元素的記憶體位址本身。
以下是引入 hash table 的實作,學習 Linux 核心程式碼風格:
請補完程式碼。
作答區
AAA = ?
(a)
/* no operation */
(b)
n->pprev = first
(c)
n->next = first
(d)
n->pprev = n
BBB = ?
(a)
n->pprev = &h->first
(b)
n->next = h
(c)
n->next = n
(d)
n->next = h->first
(e)
n->next = &h->first
延伸題目:
GOLDEN_RATIO_PRIME
,探討其實作考量2
針對 LeetCode 82. Remove Duplicates from Sorted List II,以下是可能的合法 C 程式實作:
請補完程式碼,注意作答規範:
|
, ||
, &
, &&
作為 logical/bitwise operator 時,應該要跟 operand 有一個空白字元區隔,也就是 A | B
和 C && D
的形式COND1
和 COND2
中「不要」出現小括號,也就是 (
和 )
->
前後不要出現空白,也就是應該寫作 ptr->next
而非 ptr -> next
=
和 ==
前後要有空白,也就是寫作 A == B
和 C = 1
COND1
和 COND2
不包含 ;
, :
, ?
等符號COND1 = ?
COND2 = ?
延伸問題:
3
針對 LeetCode 146. LRU Cache,以下是 Least Recently Used (LRU) 可能的合法 C 程式實作:
請補完程式碼,注意作答規範:
MMM1
, MMM2
, MMM3
, MMM4
都是 Linux 核心風格的 list 巨集,以 list_
開頭延伸問題:
4
針對 LeetCode 128. Longest Consecutive Sequence,以下是可能的合法 C 程式實作:
請補完程式碼
LLL = ?
(a)
left
(b)
left++
(c)
++left
(d)
left--
(e)
--left
RRR = ?
(a)
right
(b)
right++
(c)
++right
(d)
right--
(e)
--right
延伸問題: