日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

hashmap是散列表吗_一篇文章教你读懂哈希表-HashMap

發布時間:2025/3/20 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 hashmap是散列表吗_一篇文章教你读懂哈希表-HashMap 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
題圖Pid=68670770

在最近的學習過程中,發現身邊很多朋友對哈希表的原理和應用場景不甚了解,處于會用但不知道什么時候該用的狀態,所以我找出了剛學習Java時寫的HashMap實現,并以此為基礎拓展關于哈希表的實現原理。

什么是哈希表?

散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
給定表M,存在函數f(key),對任意給定的關鍵字值key,代入函數后若能得到包含該關鍵字的記錄在表中的地址,則稱表M為哈希(Hash)表,函數f(key)為哈希(Hash) 函數。

以上正式的解釋摘自百度百科哈希表頁面。

從這段解釋中,我們理應知道的:

  • 哈希表是一種數據結構
  • 哈希表表示了關鍵碼值和記錄的映射關系
  • 哈希表可以加快查找速度
  • 任意哈希表,都滿足有哈希函數f(key),代入任意key值都可以獲取包含該key值的記錄在表中的地址

官方解釋聽過了,那么如何用大白話來解釋呢?

簡單的來說,哈希表是一種表結構,我們可以直接根據給定的key值計算出目標位置。在工程中這一表結構實現通常采用數組。

與普通的列表不同的地方在于,普通列表僅能通過下標來獲取目標位置的值,而哈希表可以根據給定的key計算得到目標位置的值。

在列表查找中,使用最廣泛的二分查找算法,復雜度為O(log2n),但其始終只能用于有序列表。普通無序列表只能采用遍歷查找,復雜度為O(n)。

而擁有較為理想的哈希函數實現的哈希表,對其任意元素的查找速度始終為常數級,即O(1)。


圖解:

在一個典型的哈希表實現中,我們將數組總長度設為模數,將key值直接對其取模,所得的值為數組下標。

如圖所示的三組數據,分別被映射到下標為0和7的位置中,顯而易見的,第1組數據和第3組數據發生了哈希碰撞。


如何解決哈希碰撞?

常用的解決方案有散列法和拉鏈法。散列法又分為開放尋址法和再散列法等,此處不做展開。java中使用的實現為拉鏈法,即:在每個沖突處構建鏈表,將所有沖突值鏈入鏈表,如同拉鏈一般一個元素扣一個元素,故名拉鏈法。

需要注意的是,如果遭到惡意哈希碰撞攻擊,拉鏈法會導致哈希表退化為鏈表,即所有元素都被存儲在同一個節點的鏈表中,此時哈希表的查找速度=鏈表遍歷查找速度=O(n)。

哈希表有什么優勢?

通過前面的概念了解,哈希表的優點呼之欲出:通過關鍵值計算直接獲取目標位置,對于海量數據中的精確查找有非常驚人的速度提升,理論上即使有無限的數據量,一個實現良好的哈希表依舊可以保持O(1)的查找速度,而O(n)的普通列表此時已經無法正常執行查找操作(實際上不可能,受到JVM可用內存限制,機器內存限制等)。

哈希表的主要應用場景

在工程上,經常用于通過名稱指定配置信息、通過關鍵字傳遞參數、建立對象與對象的映射關系等。目前最流行的NoSql數據庫之一Redis,整體的使用了哈希表思想。

一言以蔽之,所有使用了鍵值對的地方,都運用到了哈希表思想。

Java中的哈希表實現-HashMap

在正式開始對HashMap的介紹和實現之前,你應當知道以下這些知識:

任意數對2的N次方取模時,等同于其和2的N次方-1作位于運算。

公式表述為:

k % 2^n = k & (2^n - 1)

而位于運算相比于取模運算速度大幅度提升(按照Bruce Eckel給出的數據,大約可以提升5~8倍)。

負載因子

負載因子是哈希表的重要參數,其定義為:哈希表中已存有的元素與哈希表長度的比值。

它是一個浮點數,表示哈希表目前的裝滿程度。由于表長是定值,而表中元素的個數越大,表中空余位置就會更少,發生碰撞的可能性也會進一步增大。

哈希表的擴容策略依賴于負載因子閾值。基于性能與空間的選擇,JDK標準庫將HashMap的負載因子閾值定為0.75。


HashMap繼承體系

首先來看HashMap的繼承體系:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>public abstract class AbstractMap<K,V> implements Map<K,V>public interface Map<K, V>

可以看到,抽象類AbstractMap就是對Map接口的抽象實現,HashMap通過繼承AbstractMap間接實現了Map接口,同時自身直接聲明了對Map接口的實現,即HashMap就是Map接口的直接實現。

Map接口中定義了一個Map實現類必須要實現的方法。所有Map實現類都應當實現這些方法。

Map接口定義的需要實現的方法:

在本篇文章剩余的篇幅中,將會基于Map接口實現一個我們自己的HashMap。

MyHashMap實現:

在動手之前,先分析清楚Map接口提供的方法,實現了哪些功能。其中關鍵的方法提取出來,結果為:

//實現查找功能。 //containsKey基于此方法實現。 V get(Object key); //實現新增功能。 //由于哈希表同key覆蓋特性,此方法同時實現了更新操作。 V put(K key, V value); //實現刪除功能。 V remove(Object key); //實現對Map的遍歷功能。 Set<Map.Entry<K, V>> entrySet(); Collection<V> values(); Set<K> keySet();

我們的HashMap采用泛型數組作為存儲數據的結構。此時應用到兩個類Node和Entry。Node類用作拉鏈法鏈表節點,其中每個Node存儲了一個Entry類,Entry中包含了Key和Value,是真正存儲數據的類型。

前文所述的與模運算等價的位與運算,當且僅當模數為2的N次冪時才會生效。所以我們的HashMap初始的數組長度將會定為16,擴容策略為每次擴容為上一次長度的2倍,負載因子0.75(這也是JDK標準庫所采用的配置)。

public class MyHashMap<K, V> implements Map<K, V> {private class Node {private MyEntry<K, V> entry = null;public Node next = null;}class MyEntry<K, V> implements Entry<K, V> {private int hash;private K key;private V value;}//常量區private static final double LOAD_FACTOR = 0.75; //負載因子閾值private static final int INITIAL_SIZE = 16; //數組初始大小//成員變量區private int element_count = 0; //當前元素計數private Node[] node_list = (Node[]) Array.newInstance(Node.class, INITIAL_SIZE); //存儲數組。//略去Map列表的實現方法 }

值得注意的是

private Node[] node_list = (Node[]) Array.newInstance(Node.class, INITIAL_SIZE)

Java中并不支持直接申請泛型類的數組。只能通過Array.newInstance靜態方法構造數組并強制轉換為泛型類的數組。

resize操作時同樣需要用到此方法。

Hash表的核心操作就是通過對key值的計算直接查找目標元素下標,因此我們首先參考標準庫編寫(fuzhi)出getIndex方法:

private int getIndex( int hash, int mod ){return (hash & 0x7fffffff) & (mod - 1); }

(hash & 0x7fffffff)是為了確保結果為正數。

為什么要對0x7fffffff做位于操作?

0x7fffffff是int可以表達的最大正整數,除了首位為0其他31位都為1。正數& 0x7fffffff結果為其本身,負數& 0x7fffffff結果為正數。

為什么不用Math.abs?

前面說過,位運算很快。而且由于Math.abs只是簡單的return -a,因此Math.abs(Integer.MIN_VALUE)時結果仍然為負數,如下圖所示:

hash & 0x7fffffff保證結果為正數。

(結果是不是負數的絕對值不重要,只要參數同樣時每次計算都可以得出同樣的結果,就可以作為哈希函數)

基于getIndex方法,我們可以寫出put和remove方法。

@Override public V put( K key, V value ){put(new MyEntry<>(key, value), node_list, true);return value; }private void put( MyEntry<K, V> entry, Node[] target, boolean check ){put(new Node(entry), target, check); }/*** 如果目標位置為空,則創建節點并保存目標位置* 否則在列表中查找并替換重復項。* 如果沒有重復項,則插入鏈表尾部。** @param node : 被加入數組的節點。* @param target : 目標數組。* @param check : 指示方法是否檢查數組的當前元素數量。*/ private void put( Node node, Node[] target, boolean check ){int index = getIndex(node.getEntry().getHash(), node_list.length);if (target[index] == null) {target[index] = new Node(null);}if (target[index].next == null) {target[index].next = node;if (check) {//檢查哈希表大小++element_count;checkLoadFactor();}return;}Node temp = target[index].next;while (temp != null) {if (temp.getEntry().getHash() == node.getEntry().getHash()) {temp.setEntry(node.getEntry());return;}if (temp.next == null) {temp.next = node;temp.next.next = null; //截斷節點,防止出現循環引用if (check) {//檢查哈希表大小++element_count;checkLoadFactor();}}temp = temp.next;} }

其中幾個值得注意的點:

check參數:指示方法是否檢查數組的當前元素數量。由于擴容時同樣會使用這個方法作數組元素的遷移行為,一個檢查的開關是必須的,否則會出現死循環。

temp.next.next = null :同樣,在數據遷移操作時,如果未截斷鏈表的每個節點,會導致新老數組中對應列表發生串聯,最終產生死循環。

最終MyHashMap中將集成經典的鏈表操作。

接著實現remove方法:

@Override public V remove( Object key ){if (key == null) {return null;}int index = getIndex(key.hashCode(), node_list.length);if (node_list[index] == null || node_list[index].next == null) {return null;}//在目標位置的鏈表中查找目標鍵值。Node last = node_list[index];Node current = node_list[index].next;while (current != null) {if (current.getEntry().getHash() == key.hashCode()) {last.next = current.next;--element_count; //減少數組元素計數return current.getEntry().getValue();}last = last.next;current = current.next;}return null; }

在remove方法中,將會計算得到目標節點下標,遍歷目標鏈表節點,當查找到目標元素時,斷開并重連鏈表將目標元素從鏈表中移除。

非常典型的鏈表操作。

接下來實現最重要的get操作。然而在HashMap的CRUD三個操作中,get操作最為簡單,因為其不需要移動鏈表節點或改變鏈表結構,僅需要遍歷鏈表即可。

/*** 從Map中查找目標Key。* @param key* @return*/ @Override public V get( Object key ){int index = getIndex(key.hashCode(), node_list.length);//目標位置為空則直接返回nullif (node_list[index] == null || node_list[index].next == null) {return null;}//目標位置不為空則遍歷鏈表,查找相同的keyNode temp = node_list[index].next;while (temp != null) {if (temp.getEntry().getHash() == key.hashCode()) {return temp.getEntry().getValue();}temp = temp.next;}return null; }

接下來是resize方法,它實現了數組元素的遷移操作。

但在resize方法之前,我們先來看一個有趣的方法,也是我的實現中不同于JDK標準庫的方法,它提供了對元素數組的遍歷操作,采用雙指針法實現。它接受一個Consumer接口作為參數,它會對當前數組中的所有Node調用Consumer.accept方法。

values方法,containsValue方法,keySet方法,entrySet方法都基于它來實現:

//遍歷list,并對其中的每一個元素執行指定的操作 private void traversing( Node[] nl, Consumer<Node> con ){int head = 0, foot = nl.length - 1;Node node;while (head <= foot) {if (nl[head] != null && nl[head].next != null) {node = nl[head];while ((node = node.next) != null) {con.accept(node);}}if (nl[foot] != null && nl[foot].next != null) {node = nl[foot];while ((node = node.next) != null) {con.accept(node);}}++head;--foot;} }

有了traversing方法,可以用輕松(甚至是偷懶)的方式寫出values,keySet,entrySet,containsValue:

@Override public Collection<V> values(){Collection<V> collection = new ArrayList<>();traversing(node_list, (node -> {collection.add(node.getEntry().getValue());}));return collection; }@Override public Set<K> keySet(){Set<K> set = new HashSet<>();traversing(node_list, (node -> {set.add(node.entry.getKey());}));return set; }@Override public Set<Entry<K, V>> entrySet(){Set<Entry<K, V>> set = new HashSet<>();traversing(node_list, ( node ) -> {set.add(node.getEntry());});return set; }//在最壞情況下,這種實現會將HashMap遍歷兩次。 //這樣寫僅僅是為了偷懶。 //如果你要寫一個用于生產環境的containsValue,不要這樣做。 @Override public boolean containsValue( Object value ){//遍歷哈希表查找值for (Entry<K, V> entry : entrySet()) {V temp_value = entry.getValue();if (temp_value != null && temp_value.equals(value)) {return true;}}return false; }

用于對HashMap進行擴容的resize方法如下,它的實現原理非常簡單易懂:創建一個新數組,隨后調用traversing和本類的put方法將原始數組中的所有元素插入到新數組中,最終使用新數組替換原始數組。

隨便一提,(hash & 0x7fffffff) & (mod - 1)可以保證將每個鏈表中的元素平均的放入新數組中的兩個對應位置。

/*** 列表擴容。*/ private void resize(){//創建新列表Node[] new_list = (Node[]) Array.newInstance(Node.class, node_list.length << 1);traversing(node_list, (node -> {put(node, new_list, false);}));//移動完成后替換當前列表。node_list = new_list; }

大功告成!Map接口中的所有核心方法都被實現了。


在OrsPced的Github可以找到本文中的完整實現。

如果有更好的想法,評論或建議,歡迎在評論區提出。

對閱讀至此的您表示誠摯的感謝。

總結

以上是生活随笔為你收集整理的hashmap是散列表吗_一篇文章教你读懂哈希表-HashMap的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。