contributed by < jimmylu890303
>
測驗是參考 Optimized QuickSort — C Implementation (Non-Recursive) ,實作非遞迴 (non-recursive; iterative) 的快速排序法。 再使用
list_add
在這個函式中,它會插入一顆新的節點到原本串列的前方,他傳入一個 node_t ** list
的參數為 pointer to pointer
, 因為 C 語言的在函數中傳入的參數為一個副本
,所以要將原本 head
指標改指向到新的節點上,就需要使用 pointer to pointer 的方式去更改原本的 head 指標。
圖片取自於你所不知道的 C 語言:指標篇,在函數傳入參數時,會複製一份副本 p
,這個指標會指向原本的 head 指標。
透過更改 head
指標所指向的位置可以將 ptrA 的指標更改成下個位置。
list_tail
在這個函式中會去尋找著串列中的最後一個節點位置並回傳該位置,它傳入一個 node_t ** left
的參數,但是這邊使用方式為 indirect pointer
,為了可以能夠更精簡程式碼,可以省略一些條件判斷的行數。
但是在這個函式中,使用 node_t *left
中也是可以的,並不會增加程式碼的行數。
list_length
在這個函式中會計算串列中的長度,但是傳入的參數為 node_t **left
, 使用方式為 indirect pointer
,所以在迴圈內 left 要指向的位置必須為 node_t *
的指標,所以 left 在迴圈內操作會指向 (*left)->next
。
quick_sort
在 quick_sort
中一開始宣告 max_level = 2 * n
為堆疊深度,為了模仿遞迴中堆疊的行為。 begin 負責儲存串列的頭元素位置, end 負責儲存串列的尾元素位置, i 負責模擬堆疊中 pop 操作。
在 while 迴圈負責控制 stack 的行為,每次迴圈內,從 bgein 和 end 取出串列的頭部及尾部節點位置。如果 L != R 則代表該串列元素大於一個,反之串列元素剩下一個,在將該元素加入到 result 的結果串列中。
p 指標一開始會為 pivot 的下個元素, 在 while § 迴圈內會將此串列分割成 left 和 right 兩個串列。 left 串列為元素值小於 pivot, right 串列為元素值大於 pivot。
將串列分成 left 和 right 串列後,就需要將他們存入到 begin 和 end 中,在這個部分因為 list_add
是將新的元素插入到串列的第一個,所以堆疊的順序是高位為 right 。
以上程式碼在事先宣告兩個大小為 max_level 的 begin 和 end 陣列太耗費記憶體空間了,假設在 n = 且處理器架構為32位元的情況,一個 begin 陣列就需要高達 Bytes, 所以這個部分我覺得可以使用 dynamic array
的方式去實作這個陣列。
我將原先的 begin 和 end 兩個陣列作更改成動態陣列的方式,先只事先宣告成長度為 n 的陣列。
若stack空間不足,在 realloc 堆疊空間。
改成使用動態陣列的方式,這些資料會存放在 heap 的區塊,而原先的陣列方式為區域變數,所以會存放在堆疊的區塊。
使用 valgrind 的 massif 來追蹤使用動態陣列的程式碼堆積的使用狀況。
而原先的程式碼
從上面兩圖可以發現若使用動態陣列,會使得 heap 的使用區塊增加。
從 stackoverflow 節錄以下對話。
所以我使用 stackusage 這個工具來觀察堆疊的區塊使用率。
觀察改善後面的程式
觀察原本的程式
可以發現若使用動態陣列的方式可以有效去改善佔用太多堆疊區塊空間的問題。
Tim Sort 結合合併排序和插入排序的特點,如今已廣泛用於生活的各個地方,而 TimSort 主要的想法是在現實世界中,許多資料往往是由一些已經部分排序的序列組合而成,所以首先的策略是識別出資料中已排序的子序列 (這些子序列稱為 run),然後透過不斷合併這些 run 來達成全資料範圍的排序。
首先在使用 Time Sort 時會先將原本的串列的最後一個節點解除環狀佇列,並將它指向 NULL
而以下程式碼操作的流程為每次呼叫 find_run
後會找到一個 `un , 接下來使用 result.head 和 result.next 記錄整個 run 的頭尾,並且會記錄該 run 的長度在 head->next->prev 當中。
下圖為 find_run
所回傳的 struct pair
的 result , result.head 為此回合找到的 run 。
而在 find_run
中以下程式碼將 size_t 型態的變數強制轉型成 struct list_head * 型態,並且讓 head->next->prev
指標存放這個值,這個寫法個人覺得非常的有趣,因為指標裡頭的內容是存放目標位置,該目標位址的型態為指標的型態,但這邊作法是指標存放 len 裡頭存放的值,透過這指標可能會存取到非法區塊,並且這區塊的型態不是指標所指向的型態。
所以因為以上緣故,才可以透過 run_size
來存取此 run 的長度。
經過幾次的 find_run
,串列會被分成若干個 run ,而 tp 會指向堆疊中最上面的 run ,也就是最新的 run 。
之後使用 merge_collapse
檢查堆疊中上面 3 段的 run 的長度是否滿足以下原則:
在以下情況進行合併
之後會透過 merge_force_collapse
將多個 run 進行合併,直到 run 總數 < 3 為止。
最後因為經過 merge_force_collapse
合併後,run 總數 < 3 ,所以可能的總數為 1 或 2 ,若為 1 則直接呼叫 build_prev_link
, 若為 2 則呼叫 merge_final
合併最後的 run ,如以下程式碼所示。
給定由二個由整數構成陣列的前序 (preorder) 和中序 (inorder) 形式,分別是對同一棵二元樹的前序及中序走訪結果,藉此重建此二元樹,但是是運用 Linux 核心的 hash table 實作 來完成實作。
而在 linux 核心中 hash table 視覺圖如下
Hash table 中的元素為 hlist_head
,而 Hash table 中各個 bucket 的節點為 hlist_node
使用前序和中序來完成 buildTree
,首先會透過中序建立 hash table ,而雜湊函數的作法是使用 Division method
的方式
之後使用遞迴的方式呼叫 dfs
來完成建樹的工作
而在 dfs 中 TreeNode tn 的值為前序 index 最低的點(即 pre_low) ,在藉由透過 find
去尋找此點在中序的 index ,再依序 dfs 遞迴左子樹及右子樹完成建樹,而在左子樹和右子樹的前序和中序的 low , high index 的設定如下
此題是使用雜湊表來模仿 cache 的實作,考慮以下結構
使用 lRUCacheCreate
創建 cache ,透過 hhead[ ] ,可將要存放的資料存入 cache
使用 lRUCachePut
來存放資料
首先會去對應的雜湊表檢查要新增的值是否存在,若存在此 key 則忽略存入此值,並去在 dhead 中更新 LRU 的順序。
若沒在 hash table 中沒找到對應的 key ,則需存入該值,則會有以下狀況 :
hlist_add_head
將此節點新增到對應的雜湊表。使用 lRUCacheGet
可以去尋找 cache 中是否有對應的值並取出,若無該 key 則回傳 -1 。
新增 multiplication hash function 並將原先的 hash function 額外包裝使程式碼更易維護
實作 test.py 比較原先測驗題目中的 division hash function 與我新實作的 multiplication hash function 兩者間的 Hash Collision 次數
從上圖中可以發現 multiplication 的方法碰撞次數相較 division 較為平均
並且計算兩者的平均和變異數,multiplication 的方法皆比較低
lru_cache
是一個輔助的框架,它可以用於追蹤 index 和 label 間的關聯,並更改 Activate Set
中的 Objects 。
This header file (and its .c file; kernel-doc of functions see there)
define a helper framework to easily keep track of index:label associations,
and changes to an "active set" of objects, as well as pending transactions,
to persistently record those changes.
而它主要目的是使用於本地和遠端磁碟複製的 IO 操作, 若在複製節點時發生崩潰,需要重新同步
所有曾經被寫入的IO操作的目標區域(使用中或熱區域),因為無法確定這些寫入是否已經存到遠端磁碟中。為了避免完全重新同步,需要持續追蹤這些區域,稱為 write intent log
在 lru_cache.h
中定義了兩個結構
refcnt
: 紀錄目前被使用的次數,若為 0 可能會被 lru_cache
移動到 lru
或 free
的串列
在 lru_cache.c
中
lc_put
實作中可以看到若 refcnt 為 0 ,會把此 lc_element 移動到 lc_cache->lru 中
lc_del
則會將 lru 的 object 移動到 free 的串列
再透過 lc_unused_element_available
檢查 lc_cache 中是否可用的區塊