一.哈希表
哈希表(Hash table,也叫散列表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
當使用哈希表進行查詢的時候,就是再次使用哈希函數將key轉換為對應的數組下標,並定位到該空間獲取value,如此一來,就可以充分利用到數組的定位性能進行數據定位。
數組的特點是:尋址容易,插入和刪除困難
而鏈表的特點是:尋址困難,插入和刪除容易
哈希表有多種不同的實現方法,我接下來解釋的是最常用的一種方法——拉鏈法,我們可以理解為“鏈表的數組”
左邊很明顯是個數組,數組的每個成員包括一個指針,指向一個鏈表的頭,當然這個鏈表可能為空,也可能元素很多。我們根據元素的一些特征把元素分配到不同的鏈表中去,也是根據這些特征,找到正確的鏈表,再從鏈表中找出這個元素。
舉一個例子,假如我的數組A中,第i個元素里面裝的key就是i,那么數字3肯定是在第3個位置,數字10肯定是在第10個位置。哈希表就是利用利用這種基本的思想,建立一個從key到位置的函數,然后進行直接計算查找。
好的散列函數=計算簡單+分布均勻(計算得到的散列地址分布均勻)
二.linux內核里面哈希表的實現
1.哈希鏈表表頭
2.哈希鏈表節點
因為哈希鏈表並不需要雙向循環的技能,它一般適用於單向散列的場景。所以,為了減少開銷,並沒有用struct hlist_node{}來代表哈希表頭,而是重新設計struct hlist_head{}這個數據結構。此時,一個哈希表頭就只需要4Byte了,相比於struct hlist_node{}來說,存儲空間已經減少了一半。這樣一來,在需要大量用到哈希鏈表的場景,其存儲空間的節約是非常明顯的,特別是在嵌入式設備領域。
3.兩級指針pprev的目的及意義
a.如果沒有pprev
顯然鏈表結構會變成單鏈表的結構,對於節點插入還好,但是對於節點刪除便無法直接刪除,需要遍歷鏈表才能找到刪除節點的前一個節點,造成效率降低
b.如果pprev為一級指針
則結構變為
在往哈希鏈myhlist里插入node1時
在插入node2~node4以及后續其他節點時
顯然在表頭的第一個位置上插入元素,和插入在哈希鏈表的其他位置上的代碼處理邏輯是不一樣的,因為哈希表頭是list_head類型,而其他節點都是list_node類型,這是linux內核設計者不能容忍的
c.采用二級指針pprev的方案后
還是以刪除節點為例,如果要刪除首節點,因為node1->pprev里保存的是myhlist的地址,而myhlist.first永遠都指向哈希鏈表的第一個節點,我們要間接改變表頭里的hlist_node類型的first指針的值,能想到的最直接的辦法當然是二級指針,這是兩級指針的宿命所決定的,為了間接改變一級指針所指的內存地址的場景。這樣一來,node節點里的pprev其實指向的是其前一個節點里的第一個指針元素的地址。對於hlist_head來說,它里面只有一個指針元素,就是first指針;而對於hlist_node來說,第一個指針元素就是next。具體如下所示:
當我們在代碼中看到類似與*(hlist_node->pprev)這樣的代碼時,此時正在哈希表里操作當前節點前一個節點里的第一個指針元素所指向的內存地址,只是以間接的方式實現罷了
刪除首節點時:
刪除非首節點的情況也一樣:
下面為linux內核中刪除節點的代碼
在頭指針后添加節點
連續添加節點效果圖如下
以上參考:
http://blog.chinaunix.net/uid-23069658-id-4975027.html
https://zhuanlan.zhihu.com/p/45430524