owned this note
owned this note
Published
Linked with GitHub
2018q1 Homework3 (list)
===
contributed by < `TingL7` >
## 生日悖論
### 如何透過近似的運算式,估算上述機率計算呢?附上有效的 LaTeX 算式
生日悖論是指 23 人以上,出現兩個人生日同一天的機率高於 50 %
* 參考 [Birthday problem](https://en.wikipedia.org/wiki/Birthday_problem)
* 假設
* 所有人的生日都是隨機分佈
* 一年 365 天
* P(n) : n 人中至少兩人生日是同一天
* $n 人中生日都不同天 = \dfrac{365}{365} \cdot \dfrac{364}{365} \cdot \dfrac{363}{365} \cdot ...\dfrac{366-n}{365} = \dfrac{365!}{365^{n} \cdot (365-n)!}$
* $P(n) = 1 - n 人中生日都不同天 = 1 - \dfrac{365!}{365^{n} \cdot (365-n)!}$
* $P(23) = 1 - \dfrac{365!}{365^{23} \cdot 342!}\\
\approx 1 - 0.49270276567 = 0.50729723432$
#### 參考資料
* [HackMD – LaTeX 語法與示範](https://hackmd.io/s/B1RwlM85Z#)
### [Birthday problem](https://en.wikipedia.org/wiki/Birthday_problem) 對資訊安全的影響在哪些層面?
* 主要的影響在於 hash function 的應用
* hash function 的碰撞,利用窮舉法製作出具有相同 hash value 的檔案或合同,取代真正的檔案以進行欺騙
* 有一長度 n 的字串 t1 透過 hash function 得到 f(t1)
* 要找到字串 t2 ,使得 f(t1) = f(t2) ,可以透過大量製造內容不同但意義相同(如,增減標點或空白)的字串
* 從生日悖論中,可以知道只要有 2^n/2^ 個字串就會有很大的機率發生碰撞
* 因此,只要製造出 2^n/2^ 個字串就有機會找到具有相同雜湊值的字串
* 應對方法 : 增加字串長度
* 參考[Birthday attack](https://en.wikipedia.org/wiki/Birthday_attack) 、 [生日攻击是什么,有什么用?](https://www.zhihu.com/question/54307104) 、 [hash碰撞,数字签名,生日攻击](http://blog.renren.com/share/250153535/16381267487/0)
### 像是 [Random Number Generator](http://stattrek.com/statistics/random-number-generator.aspx) 這樣的產生器,是否能產生「夠亂」的亂數呢?
* 產生器產生的亂數,是透過 seed 來產生亂數表,因此 seed 相同的話,得到的亂數表都是相同的。
* 如果每次的亂數種子都設不一樣就可以得到不同的亂數表,進而達到看起來像亂數的效果。
* 像是 c 語言中常用的 `srand(time(NULL));` ,以時間作為亂數種子,使得每次亂數種子都不同,得到不同亂數表,進而得到「亂」數
* 而 `rand()` 測試幾次,會發現每次得到的亂數是相同的,因為他的種子每次都相同
## Linux 風格的 linked list
### 為何 Linux 採用 macro 來實作 linked list?一般的 function call 有何成本?
* 參考 [What is the difference between a macro and a function in C?](https://stackoverflow.com/questions/4990362/what-is-the-difference-between-a-macro-and-a-function-in-c) 中提到,`The basic difference is that function is compiled and macro is preprocessed.`
* macro 在前置處理的過程中,就完成程式碼的替換。
* function 在被編譯後執行,每次呼叫時都需要時間進行 stack 的 pop 、 push,比較耗費時間。
* 因此,採用 macro 來實作 linked list 可以避免過多的 function call 時間成本
### Linux 應用 linked list 在哪些場合?舉三個案例並附上對應程式碼,需要解說,以及揣摩對應的考量
### GNU extension 的 typeof 有何作用?在程式碼中扮演什麼角色?
* 參考 [GCC typeof在kernel中的使用](http://deltamaster.is-programmer.com/posts/37253.html) 、 [Referring to a Type with typeof](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html)
* `int a;typeof(a) b;` 宣告與變數 `a` 相同 type 的變數 `b`
* 可以達到 c++ 中「多型」的效果
* 可以根據函式傳入不同的 type 而產生不同的行為
e.g.
```c
#define max(a,b) \
({ typeof (a) _a = (a); \
typeof (b) _b = (b); \
_a > _b ? _a : _b; })
```
### 解釋以下巨集的原理
```c
#define container_of(ptr, type, member) \
__extension__({ \
const __typeof__(((type *) 0)->member) *__pmember = (ptr); \
(type *) ((char *) __pmember - offsetof(type, member)); \
})
```
* 在 [Alternate Keywords ](https://gcc.gnu.org/onlinedocs/gcc-3.0.1/gcc_5.html#SEC109) 中,提到編譯參數 `-traditional` 、 `-ansi` 、 `-std=c99` 會使一些關鍵字失效,如 `typeof` ,在GNU C 中,在字的前後加上 `__` 可以解決關鍵字失效的問題
* 定義 `container_of(ptr, type, member)` 以 `__extension__()` 取代。
* `const __typeof__(((type *) 0)->member) *__pmember = (ptr);`
* `(type *) 0` : 型態轉成 type * 的 0
* `((type *) 0)->member` : type * 的 0 指向 member
* 由 `->` 可以猜測 type 是 structure
* `__typeof__(((type *) 0)->member)` : type 中 member 的型態
* 0 可以替換成其他數字,只是為了知道 member 的 type 而使用
* `const __typeof__(((type *) 0)->member) *` : 型態為 const type 中 member 的型態 指標
* `const __typeof__(((type *) 0)->member) *__pmember` : 宣告變數__pmember,型態為 const type 中 member 的型態 指標
* `const __typeof__(((type *) 0)->member) *__pmember = (ptr);` : 宣告變數__pmember,型態為 const type 中 member 的型態 指標,指向 ptr 指的位置
* `(type *) ((char *) __pmember - offsetof(type, member));`
* `(char *) __pmember` : 型態轉換成 char * 的 __pmember
* `offsetof(type, member)` : type 中 member 的位址大小
* `#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)` 計算出 struct 和其 member 的距離
* `((char *) __pmember - offsetof(type, member))` : 指向 ptr 也是 struct typr 中的 member的位址 減去 struct 開頭與 member 的距離,也就是 struct 開頭的位址
* `(type *) ((char *) __pmember - offsetof(type, member));` : 以(type *) 儲存 type 開頭的位址
### 除了你熟悉的 add 和 delete 操作,list.h 還定義一系列操作,為什麼呢?這些有什麼益處?
* 除了一般的 add 和 delete ,還定義這些操作
* list_add_tail
* list_del_init : 在刪除節點之前,先將其初始化,這樣可以避免錯誤的操作
* list_empty
* list_is_singular : 是否只有一個節點
* list_splice : 把 list 的一些節點 插入到另一個 list 開頭
* list_splice_tail
* list_splice_init
* list_cut_position : 將 list 開頭移到另一個 list
* list_move : 把指定的節點移到 list 開頭
* list_move_tail
* 由於這是環狀 doublely-linked list ,會有針對開頭或尾端的操作,這樣能夠更方便的使用,也能提升效能
### LIST_POISONING 這樣的設計有何意義?
* [list poisoning](https://en.wikipedia.org/wiki/List_poisoning) 是指 mail list 出現無效的郵件地址導致資源的消耗
* LIST_POISONING 被定義在 [linux-list/include/list.h](https://github.com/TingL7/linux-list/blob/master/include/list.h)中
```clike=119
/**
* list_del() - Remove a list node from the list
* @node: pointer to the node
*
* The node is only removed from the list. Neither the memory of the removed
* node nor the memory of the entry containing the node is free'd. The node
* has to be handled like an uninitialized node. Accessing the next or prev
* pointer of the node is not safe.
*
* Unlinked, initialized nodes are also uninitialized after list_del.
*
* LIST_POISONING can be enabled during build-time to provoke an invalid memory
* access when the memory behind the next/prev pointer is used after a list_del.
* This only works on systems which prohibit access to the predefined memory
* addresses.
*/
static inline void list_del(struct list_head *node)
{
struct list_head *next = node->next;
struct list_head *prev = node->prev;
next->prev = prev;
prev->next = next;
#ifdef LIST_POISONING
node->prev = (struct list_head *) (0x00100100);
node->next = (struct list_head *) (0x00200200);
#endif
}
```
* 第 130 行的註解有提到, LIST_POISONING 的設計是因為刪除節點時,只有刪除 list 中的節點,並沒有真正的釋放記憶體,因此在重新使用這塊記憶體時,可能會去使用到不該用的 next / prev
### linked list 採用環狀是基於哪些考量?
* 在使用上效能比較好,例如要使用到 linked list 尾端,直接由 head 就可以找到,不必再使用 loop 去找到 `node->next = NULL;`
### list_for_each_safe 和 list_for_each 的差異在哪?“safe” 在執行時期的影響為何?
* [linux-list/include/list.h](https://github.com/TingL7/linux-list/blob/master/include/list.h)中
```clike=401
/**
* list_for_each_safe - iterate over list nodes and allow deletes
* @node: list_head pointer used as iterator
* @safe: list_head pointer used to store info for next entry in list
* @head: pointer to the head of the list
*
* The current node (iterator) is allowed to be removed from the list. Any
* other modifications to the the list will cause undefined behavior.
*/
#define list_for_each_safe(node, safe, head) \
for (node = (head)->next, safe = node->next; node != (head); \
node = safe, safe = node->next)
```
```clike=370
/**
* list_for_each - iterate over list nodes
* @node: list_head pointer used as iterator
* @head: pointer to the head of the list
*
* The nodes and the head of the list must must be kept unmodified while
* iterating through it. Any modifications to the the list will cause undefined
* behavior.
*/
#define list_for_each(node, head) \
for (node = (head)->next; node != (head); node = node->next)
```
* 從程式碼來看最大的差異在於 safe 版本多了一個變數 safe 儲存了當下節點的 next
* 多了一個 safe 的好處在於,可以對當下節點進行刪除的動作,也不會影響整個迴圈的進行,因為 safe 會事先儲存下一個節點的位址,不會使 list 斷掉
### for_each 風格的開發方式對程式開發者的影響為何?
* 與 c 語言中 for 的寫法相比, foreach 簡潔許多,可讀性也較高
* foreach 可以簡單的走訪過整個資料結構
* Perl foreach 用法
```
foreach my $i (@array){;}
```
* 可以發現,參數僅有控制變數 i 跟要操作的 array ,而在 c 的 for 的寫法中可以自訂變數初始動作、條件、每次內容做完後的動作,彈性比較大,但相對的寫法就比較複雜
### 程式註解裡頭大量存在 @ 符號,這有何意義?你能否應用在後續的程式開發呢?
* [Doxygen](https://en.wikipedia.org/wiki/Doxygen) 是產生參考文件的工具,會抓取程式內容與註解來自動產生說明文件。因此,只要按照 Doxygen 的格式撰寫註解,就可以不用另外寫說明文件,可以由 Doxygen 自動產生
* [Doxygen 說明文件](https://www.stack.nl/~dimitri/doxygen/manual/commands.html#cmda) 中提到,`@` 符號表示註解的開頭,而 `\` 對於 Doxygen 來說,與 `@` 意義相同
* `@` 會用在參數的開頭,或是一些關鍵字的開頭 (如 `@struct`)
### tests/ 目錄底下的 unit test 的作用為何?就軟體工程來說的精神為何?
* tests/ 目錄底下有許多測試 function 的檔案,可以先確認每個 function 的功能正確無誤,再應用到程式中,使得 debug 更容易
* 就軟體工程來說,希望將開發流程、大程式切割成許多小部份,可以實做一些就測試分析一些,在維護、找錯、團隊合作上都會進行的更容易、更順利
### tests/ 目錄的 unit test 可如何持續精進和改善呢?
## 針對 linked list 的排序演算法
### 對 linked list 進行排序的應用場合為何?
* linked list 的優點在於插入、刪除、共用節點,而且需要的記憶體空間是動態的
* 需要以不同的優先權依據重新排列
* 排程的管理
* 需要以優先順序排列(優先權依據可能是必須馬上做的或者比較重要的)
* 有新的事項進來要插入容易
* 事項的數量不固定
### 考慮到 linked list 在某些場合幾乎都是幾乎排序完成,這樣貿然套用 quick sort 會有什麼問題?如何確保在平均和最差的時間複雜度呢?
* 參考 [快速排序(Quick Sort) – 寫點科普,請給指教。](https://hellolynn.hpd.io/2017/08/03/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F-quick-sort/)
* quick sort 不適用於幾乎排序好的資料
* quick sort 在使用時,會選擇最後一個當分割的 pivot ,如果是幾乎排序好的每次在挑 pivot 時,容易選到最大值,以此做分割會造成沒有分割成兩組數的效果
* 因此,對於幾乎排序完成的資料,在使用 quick sort 的時間複雜度通常是 worst case O(n^2^)
* 可能的解法 : Middle-of-Three 方法
* 由資料的最左值、最右值、中間值三個中挑出中間值作為 pivot
* 可以避免取到最大值造成分割沒有效果的情況,甚至接近 best case ,因為是幾乎排序好的資料,容易選到中間值,分割出數量相等的兩組數
### 能否找出 Linux 核心原始程式碼裡頭,對 linked list 排序的案例?
* 利用 [搜尋工具](https://livegrep.com/search/linux) ,搜尋關鍵字 `list_sort`
* [/lib/list_sort.c](https://github.com/torvalds/linux/blob/v4.16/lib/list_sort.c#L1)
* 以 merge sort 來實作 list sort
* [/drivers/gpu/drm/drm_modes.c](https://github.com/torvalds/linux/blob/v4.16/drivers/gpu/drm/drm_modes.c#L34)
* mode 的 list 需要以 favorability 的順序排序,因此用到 list sort
### 透過 [skiplist](https://en.wikipedia.org/wiki/Skip_list) 這樣的變形,能否加速排序?
![](https://i.imgur.com/rGiwDyt.png)
* skip list 是一種針對 sorted linked list 的資料結構,透過不同 level 做出不同的 linked list 達到加速搜尋的效果
e.g. 搜尋 `7`
1. 從 level 4 比對, 1 <= 7 , null 跳 level 3
2. level 3 比對 4 <= 7 , 6 <= 7, null 跳 level 2
3. level 2 比對 9 >= 7 跳 level 1
4. level 1 比對 7 == 7
比對 5 次,比直接查詢 (7 次) 還要快
* 對於 insertion sort ,這樣子的資料結構,能夠加速,因為每次插入時都需要搜尋插入的位置,這時候 skip list 就派上用場了
* 對於其他的排序法就沒有明顯的幫助
### 研讀 [How to find the kth largest element in an unsorted array of length n in O(n)?](https://stackoverflow.com/questions/251781/how-to-find-the-kth-largest-element-in-an-unsorted-array-of-length-n-in-on),比照上方連結,是否能設計出針對 linked list 有效找出第 k 大 (或小) 元素的演算法?
* 參考 連結中[ hoder ](https://stackoverflow.com/users/2525478/hoder) 的回答,試著設計找出第 k 小元素的 pseudocode:
```
QUICKSELECT(list, k)
建立 l1, l2 兩個空的 linled list
r = 從 list 中隨機挑出一個節點中的 val
foreach (node, head)
if noed->val < r
then add(node, l1)
if noed->val > r
then add(node, l2)
if k <= len(l1)
return QUICKSORT(l1, k)
else if k > len(list)-len(l2)
return QUICKSORT(l2, k - (len(list) - len(l2)))
else
return r
```
### [linux-list](https://github.com/sysprog21/linux-list) 裡頭 examples 裡頭有兩種排序實作 (insertion 和 quick sort),請簡述其原理
* insertion : 分成兩部份 list_insert_sorted 及 list_insertsort
* 原理: 將未排序好的 list 中所有節點,一個個插入到已排序的 list 中適合的位置
* list_insertsort
```clike
static void list_insertsort(struct list_head *head)
{
struct list_head list_unsorted;
struct listitem *item = NULL, *is = NULL;
INIT_LIST_HEAD(&list_unsorted);
list_splice_init(head, &list_unsorted);
list_for_each_entry_safe (item, is, &list_unsorted, list) {
list_del(&item->list);
list_insert_sorted(item, head);
}
}
```
這個函式主要做兩件事
1. 將 list 移到另一個 list (list_unsorted)
2. 先將節點從 list 中刪除,再將節點插入排序好的 list 中
* list_insert_sorted
```clike
static void list_insert_sorted(struct listitem *entry, struct list_head *head)
{
struct listitem *item = NULL;
if (list_empty(head)) {
list_add(&entry->list, head);
return;
}
list_for_each_entry (item, head, list) {
if (cmpint(&entry->i, &item->i) < 0) {
list_add_tail(&entry->list, &item->list);
return;
}
}
list_add_tail(&entry->list, head);
}
```
要將節點插入排序好的 list 中,分 3 種情況
1. 第一個: 當 list 是空的話, item 直接放進 list 中
2. 中間: 找到比 item 小的節點,將 item 放到它後面
3. 最後一個: list 中最小的元素
* quick sort
* 原理: 設定一個 pivot ,將 list 以 pivot 為刀切割成兩部份,一部分都比 pivot 大,一部分比 pivot 小,在將切割出來的部份遞迴做 quick sort 直到切出來的部份為空
* list_qsort
```clike=
static void list_qsort(struct list_head *head)
{
struct list_head list_less, list_greater;
struct listitem *pivot;
struct listitem *item = NULL, *is = NULL;
if (list_empty(head) || list_is_singular(head))
return;
INIT_LIST_HEAD(&list_less);
INIT_LIST_HEAD(&list_greater);
pivot = list_first_entry(head, struct listitem, list);
list_del(&pivot->list);
list_for_each_entry_safe (item, is, head, list) {
if (cmpint(&item->i, &pivot->i) < 0)
list_move_tail(&item->list, &list_less);
else
list_move(&item->list, &list_greater);
}
list_qsort(&list_less);
list_qsort(&list_greater);
list_add(&pivot->list, head);
list_splice(&list_less, head);
list_splice_tail(&list_greater, head);
}
```
由第 13 行可以知道, pivot 挑選第一個 entry
第 26~28 行是將切割過的 list 重新收集、排好