国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發(fā)設計 > 正文

輕松上手哈希表(HashMap)

2019-11-06 06:32:52
字體:
來源:轉載
供稿:網友

0.前言談到數(shù)據(jù)結構,用的最多的,也是最基本的兩種,就是數(shù)組和鏈表。為什么我們講HashMap之前,要先講一下數(shù)組和鏈表呢?這要從他們兩個的特點說起。首先簡單地說一下數(shù)組,它的特點有很多:只能用來存儲同一類型的東西;長度是固定的,一旦聲明了長度屬性,就不能修改(數(shù)組的長度不等于數(shù)組內存儲的元素的個數(shù));數(shù)組在內存中的存儲是連續(xù)的;數(shù)組內的元素通過下標(索引)來引用。所以,我們可以得到這樣的結論:數(shù)組的查找和修改速度很快,因為可以直接通過下標來引用,但是增加和刪除上不是很方便,因為數(shù)組的長度不能修改。很多時候,當要存儲的元素超出數(shù)組的長度時,只能通過新建一個更長的新數(shù)組來代替原來的數(shù)組的方法。而鏈表作為和數(shù)組齊名的兩種最基本的數(shù)據(jù)結構之一,它在很多方面與數(shù)組類似,主要的區(qū)別是,鏈表是鏈式結構,元素在鏈表中的存儲是不連續(xù)的,它們之間通過指針依次連接。所以,鏈表的查找和刪除很慢,要從根節(jié)點開始依次查找,但是增加和刪除很方便,只需要改變指針指向的元素就可以了。基于它們兩個各有優(yōu)缺,我們的主角終于可以出場了,兼具數(shù)組與鏈表的優(yōu)點又巧妙地避開缺點的HashMap。1.正文在應用層面,HashMap就具有很大的優(yōu)點。它不像數(shù)組那樣只能用連續(xù)的整形數(shù)字作為索引,而是可以使用任意類型的對象。這些用作索引的對象稱為“鍵”(Key),每個“鍵”在表中對應一個“值”(Value),組成一個“鍵值對”(”Associates the specified value with the specified keyin this map.” ---源碼中的描述),可以更直觀的儲存所有類型的元素。在存儲時,HashMap里面的元素不是連續(xù)的,元素順序也和存放順序無關。而是根據(jù)要存儲的Key,先進行一步hash計算(” Computeskey.hashCode() and sPReads (XORs) higher bits of hashto lower. ”---源碼中描述的具體的hash計算過程),得到他在HashMap中的索引(散列)位置,然后將Key和Value的值存儲在該位置。簡單來說,HashMap是通過鍵的映射找到表中的某一個位置進行存儲的。因此,根據(jù)這個關系,在進行查找操作時,可以不用遍歷的方法直接通過計算得到索引位置(“Returnsthe value to which the specified key is mapped”---源碼中對get()方法的描述),彌補了數(shù)組和鏈表在時間上的缺陷。附:hash計算源碼:

static final int hash(Object key) {        int h;        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}附:存儲元素的put方法源碼:
/**     * Associates the specified value with the specified key in this map.     * If the map previously contained a mapping for the key, the old     * value is replaced.     *     *...     */    public V put(K key, V value) {        return putVal(hash(key), key, value, false, true);//putVal()是具體進行存儲的方法,這里只是理解它首先進行了hash計算,具體的源碼在下面會講到。 }

附:取出元素的get方法源碼:

public Vget(Object key) {	Node<K,V> e;	return (e = getNode(hash(key), key)) == null ? null :e.value;// 取出元素時同樣先進行了hash計算}再深入一點,元素在HashMap中到底是如何存儲的?繼續(xù)參考源碼,我們找到上面出現(xiàn)過的putVal()方法。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                   boolean evict) {        Node<K,V>[] tab; Node<K,V> p; int n, i;        if ((tab = table) == null || (n = tab.length) == 0)            n = (tab = resize()).length;// 數(shù)組為空或者長度為零,增加數(shù)組的長度          if ((p = tab[i = (n - 1) & hash]) == null)            tab[i] = newNode(hash, key, value, null);//索引位置為空時,將新節(jié)點存到數(shù)組的該位置          else {            Node<K,V> e; K k;            if (p.hash == hash &&                ((k = p.key) == key || (key != null && key.equals(k))))                e = p;            else if (p instanceof TreeNode)                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);            else {                for (int binCount = 0; ; ++binCount) {                    if ((e = p.next) == null) {                        p.next = newNode(hash, key, value, null);                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                            treeifyBin(tab, hash);                        break;                    }                    if (e.hash == hash &&                        ((k = e.key) == key || (key != null && key.equals(k))))                        break;                    p = e;                }            }            if (e != null) { // existing mapping for key                V oldValue = e.value;                if (!onlyIfAbsent || oldValue == null)                    e.value = value;                afterNodeaccess(e);                return oldValue;            }        }        ++modCount;        if (++size > threshold)            resize();//節(jié)點超出限度,調用resize()方法增加數(shù)組長度          afterNodeInsertion(evict);        return null; }這段代碼比較重要,所以我完整的拷貝了上來。我們可以發(fā)現(xiàn),所有元素都會以節(jié)點形式進行存儲,其中包含了Key-Value等相關信息。當我們要存儲一個新的節(jié)點時,系統(tǒng)首先會檢查一個叫做table的對象,然后我們看到table的聲明和部分描述是這樣的:
 /**     * The table, initialized on first use, and resized as     * necessary. When allocated, length is always a power of two.     * (We also tolerate length zero in some Operations to allow     * bootstrapping mechanics that are currently not needed.)     */    transient Node<K,V>[] table;很明顯,這是一個用來存儲節(jié)點類型元素的數(shù)組。也就是說,在我們的HashMap里面,也用到了數(shù)組,而且會首先考慮將數(shù)據(jù)存儲在數(shù)組里面。我們經過hash計算得到了一個索引位置后,會檢查數(shù)組中該位置是否為空。如果該位置為空,會直接將新節(jié)點存儲在該位置;如果不為空,首先判斷索引位置處的舊節(jié)點與新節(jié)點的Key是否相同。如果相同,替換舊節(jié)點;如果不同,在該索引位置的基礎上,建立一個鏈表,將新節(jié)點存儲為這個鏈表的最后一個節(jié)點,在這之前也要保證原來鏈表中不存在相同Key的節(jié)點。可以參照下面的圖片進行理解。

(圖片來自·百度百科)由此可見,HashMap原則上是可以不斷存儲新的節(jié)點,但是隨著節(jié)點的增加,同一索引位置的節(jié)點太多,就不能發(fā)揮HashMap的優(yōu)勢。因此,HashMap必須解決的一個問題就是“哈希沖突”:在不斷的存儲過程中,對于兩個不同的Key,進過計算,對應同一個索引(散列)位置的現(xiàn)象,我們就叫做“哈希沖突(碰撞)”。這個問題在系統(tǒng)的HashMap中已經有了一些特定的解決方法,比如事先定義一個限度(threshold),每次添加新的節(jié)點之后,計算一下已存儲的節(jié)點數(shù),如果超出這個限度,就調用一次resize()方法,增加數(shù)組的長度,并將原來的節(jié)點進行一次rehash(),重新進行存儲,來減小哈希沖突的影響。2.實現(xiàn)

最好的學習方法就是動手!為了加深理解,建議自己手寫一個簡單實現(xiàn)的HashMap,最好再與系統(tǒng)的比較一下,對比源碼進行修改。下面是我簡單實現(xiàn)的一個HashMap代碼,包括了存放元素、獲取元素、獲取元素個數(shù)、刪除元素最基本的幾項功能。 

public class myHashMap<K, V> {	int num = 0;// 存儲的元素的個數(shù)	Object[] data;// 初始化一個長度為6的數(shù)組	float rate = 0.75f;// 因子	public myHashMap() {		data = new Object[6];	}	public myHashMap(int size) {		Object[] data = new Object[size];	}	/**	 * 存放元素	 */	public void put(K key, V value) {		// 計算Hash值(索引)		int index = Hash(key);		// 創(chuàng)建節(jié)點		Node<K, V> node = new Node<K, V>(key, value);		// 判斷索引位置是否為空		if (data[index] == null) {			data[index] = node;			num++;		} else {			node.next = (Node<K, V>) data[index];			data[index] = node;			// 判斷是否為重復的鍵			// 若不重復 元素個數(shù)+1 ;			// 若重復 元素個數(shù)不變 (重復的沒有被覆蓋,只是排在了后面,不會被get到)			if (!node.key.equals(node.next.key))				num++;		}	}	/**	 * 哈希計算	 */	public int Hash(K key) {		int index = key.hashCode() % data.length;		// System.out.println(index);		return index;	}	/**	 * 獲取元素	 */	public V get(K key) {		// 計算Hash值(索引)		int index = Hash(key);		// 得到索引位置的節(jié)點		Node<K, V> node;		if (data[index] != null) {// 判斷索引位置是否存在節(jié)點			node = (Node<K, V>) data[index];// 若存在 找到key相等的節(jié)點并返回			while (!key.equals(node.key)) {				if (node.next != null)					node = node.next;				else{					return null;				} 			}		} else {// 若不存在 輸出一條語句 返回一個null			return null;		}		return node.value;	}	/**	 * 獲取元素個數(shù)	 */	public int size() {		return num;	}	/**	 * 刪除元素	 */	public void remove(K key) {		// 計算Hash值(索引)		int index = Hash(key);		// 得到索引位置的節(jié)點		Node<K, V> node = (Node<K, V>) data[index];		// 判斷索引位置是否存在節(jié)點		if (node != null) {// 若存在 判斷是不是第一個節(jié)點			if (key.equals(node.key)) {// 若是第一個節(jié)點 判斷是否只有根節(jié)點				if (node.next == null) {// 如果只有根節(jié)點 直接將根節(jié)點設為空					data[index] = null;					num--;				} else { // 如果有其他節(jié)點 直接將數(shù)組的索引位置指向第二個節(jié)點					data[index] = node.next;					num--;				}			} else {// 若不是第一個節(jié)點 判斷索引位置是否只有根節(jié)點				if (node.next == null)// 若索引位置只有根節(jié)點 且根節(jié)點的key不是要刪除的key 輸出一條提示語句					System.out.println("key不存在");				else {// 若索引位置有其他節(jié)點 則需要找到對應key的節(jié)點的上一個結點					while (node.next != null && !key.equals(node.next.key)) {						node = node.next;					}// 然后判斷對應key的節(jié)點是不是最后一個節(jié)點					if (node.next.next == null) {// 若是最后一個結點,將它的前一個結點的下一個節(jié)點指向null						node.next = null;						num--;					} else {// 若不是 將他的前一個節(jié)點指向他的后一個節(jié)點						node.next = node.next.next;						num--;					}				}			}		} else {// 若不存在 輸出一條提示語句			System.out.println("key不存在");		}	}} public class Node<K,V> {	K key;	V value;	Node<K,V> next;		public Node(K key, V value) {		super();		this.key = key;		this.value = value;	}} 3.總結這篇文章從初學者角度,簡單地介紹了一下HashMap的應用場景,存儲原理,以及簡單的源碼分析和實現(xiàn)的示范。在此基礎上,我們還可以進一步地分析系統(tǒng)的HashMap及與之相似的 HashTable, HashSet源代碼,了解一致性哈希、布隆過濾,尋找更有效的散列算法、rehash算法......通過拓展不斷加深理解。 


發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 叙永县| 修水县| 贵南县| 醴陵市| 来凤县| 太谷县| 芒康县| 土默特左旗| 永德县| 吴忠市| 乌拉特后旗| 庆元县| 团风县| 温宿县| 贵州省| 丹棱县| 临沧市| 哈尔滨市| 和顺县| 海口市| 漳州市| 涞水县| 玉林市| 辽源市| 屏山县| 尼木县| 翁牛特旗| 临湘市| 巧家县| 阿坝县| 诸城市| 西平县| 璧山县| 江都市| 宣汉县| 洞头县| 乐昌市| 南开区| 临颍县| 崇礼县| 明水县|