contributed by < kevinshieh0225
>
部分參考 freshLiver
的筆記作法進行改寫。
Two Sum 是在 leetcode 上的經典首選,讓大家見識到 hash table 演算法的妙用。本測驗希望我們進入到 hash table 內部實作中。
hash table (HT)的性質如下:輸入特定鍵(key),HT 將回傳其對應的值(value)。基於一對一關係,理想上可使索取的時間複雜度為 O(1)。
在 HT 的實作如下:由於建立一個能含括所有 key 的 HT 是不實際的,故我們先為 key 做編碼,對應到我們建立的有限目錄中,隨後以 linked list (ll)的結構將鍵值串接起來(如同我們先將資料分門別類,隨後從分類中尋找我們要的是否存在)。
(取自 jserv)
回到程式碼:map_t 建立 map ,其下會有 an array of hlist_head 。我們前面提到 key 會先編碼後置入目錄對應的 LL ,這裡即可看到我們會建立 個目錄與 LL 。
hash_key 與 hlist_node 即為 key/value 節點。在透過編碼後 hlist_node 被安插在對應目錄中,在尋鍵時透過尋訪 hlist_node 並以 container_of 取 hash_key 的值。
我們可以看到 map_t 初始化的過程,可以看到我們同時建立了一個 map_t 與(1 << bits == )個 hlist_head,若建立中有失敗則清除內容並返回 NULL。
執行 hash 的演算法。最後會生成並回傳一個 範圍內的無號整數。
find_key function:在 HT 中尋找是否有對應 key 值,若有則回傳對應的 hasy_key * ,若無則回傳 NULL。
這裡首先我們先確認 key 被放在哪一個編碼的 bucket 中,透過 hash(key, map->bits) 這個函式取得編碼,並在 array 中取得對應的 hlist_head。接著就是逐一尋訪該 LL 去確認是否有 key 於當中。
map_get function:從 HT 中確認是否有對應鍵值,若有則回傳鍵值對應的 value,若無則返還 NULL。
這裡特別的是此 function 的回傳值是 void pointer。這是一種很有彈性的作法,我們可以用 void pointer 去指不同的變數,再把它透過轉型(casting)還原回來。由於我們在這個實作中不確定是否能有對應的回傳鍵值,所以使用這個的方式,若回傳 NULL 即可讓 if-else 更直覺的判斷與執行對應操作。
map_add function:確認是否有在 HT 中有對應鍵值,若有則不執行,無則新增鍵值在 HT 中。
在 AAA 到 BBB 的 程式碼意思是要將新的 hlist_node *n 插入在 hlisthead *h 的 first 位置,其中如果 h->first 早就已經有 hlist_node 時,我就插入兩者之間。
AAA 應為 (c) n->next = first
BBB 應為 (a) n->pprev = &h->first
本題透過遞迴的方式實現刪除重複節點的函式。其中:
COND1 = head.next != null && head.val == head.next.val
COND2 = head.next != null && head.val == head.next.val
為何需要用 if 來包裝和裡面 while 相同的條件式呢?看似重複操作,但其實是因為條件內外執行操作是不同的行為,而必須在新增一個 if(COND1) 來隔離條件。
在 if(COND1) 內的操作是我們已經確定要移除當下重複的節點了,我們用 while 不斷將 head 重新指向其後,並在最後 return 遞迴中 head->next 的 ListNode 在這一番的操作下便可以把所有重複節點在這操作下完全移除。
相比於此,我們看 if(COND1) 外的操作:我們先對 head->next 後的節點進行 deleteDuplicates 的遞迴整理,最後回傳的還是 head node ,光是操作和回傳的形式都不同,而使他必須用 if(COND1) 來做內外切分。
在 Cache replacement policies 中,Least Recently Used 是一個策略之一。當 Cache 要更新時,將裡頭最後被使用的資料去除並更新。在 LRU 演算法我們面臨的問題是:如何在常數時間內達到兩件目的:如何確認 Cache hit、如何更新使用時間的紀錄。
透過 Hashmap 可在常數時間裡「確認是否 Cache hit」;透過 DoubleLinkedList 讓我們在常數時間裡「更新 Cache 紀錄」。
這時可能會有一個問題是:如果要做 DoubleLinkedList 的尋訪,那應該也需要線性時間吧?然而我們如果搭配著 list.h 與 container_of 的技巧:在 Hashmap 與 DoubleLinkedList 上使用共用的 list_head 節點,那麼當我要維護 DoubleLinkedList 時,只要從 Hashmap 取得該節點更新;或是當我要刪除最近未使用的節點時,從 DoubleLinkedList 找到最末的節點。
LRUNode 是紀錄在 LRUCache 中的 DoubleLinkedList(DLL) 節點。
我們觀察 LRUCache,dhead 與 hheads[] 分別就是 DLL 的首節點,與第一題類似形式的 Hashmap 實作。
capacity 代表 Cache 的大小, count 代表目前總共有多少筆資料存放其中。
在 lRUCacheCreate 中我們初始化紀錄使用時間 DLL 的 dhead,和紀錄是否存在其中的 Hashmap hhead[]。
在 lRUCacheFree 中我們也針對這些建立的動態記憶體位置依序釋放。
MM1 應填入 list_for_each_entry_safe ,在 DLL 中依序釋放節點,並不斷確認下一個節點是否為安全節點。
輸入整數 key,如果其值有在 hashmap 中找到,更新 &obj->dhead 的 DLL結構,把該節點更新到開頭,代表最近使用,並回傳該 lru->value;若未找到則回傳 -1。
這裡可以看到這裡的編碼方式是 key 值除 capacity 得到 的 hash ,相比前面的編碼來看是比較簡易的實作。
MMM2 應該是 list_for_each_entry。用該函數去尋訪 &obj->hheads[hash] 中的每個節點來確認是否該資料已存在於 hashmap 中。
MMM3 應填入 list_for_each_entry。
MMM4 應填寫 list_last_entry 取得 DLL 中最後一個節點,意即是最近未使用的節點。
答案卻是 list_for_each_entry?有點怪。
LLL = --left
RRR = ++right