哈希碰撞攻防戰——深入淺出Map/Set的底層實現

各位看官早安午安晚安呀

如果您覺得這篇文章對您有幫助的話

歡迎您一鍵三連,小編盡全力做到更好
歡迎您分享給更多人哦

今天我們來學習Map/Set的底層實現

目錄

問題一:hash會出現負數?數組越界

一:什么是二叉搜索樹?

1.1:創建一個二叉搜索樹

1.2.移除一個節點:

1.3.二叉搜索樹的缺陷:

1.4.引出map和set以及和紅黑樹之間的關系

二:關于搜索我們一般用到的方式

2.1.靜態查找(就只進行查找不進行其他的操作)

2.2.但是現實中的查找大多數都是動態查找:

2.3?模型

三:Map

3.1.關于Map.Entry (很重要)

3.2.Map常用方法說明

3.3.關于Map方法的注意事項

四:Set(去重很好)

4.2.Set注意事項

五:我們日常的比較

六:哈希表

6.1:哈希沖突

6.2:哈希沖突不可避免

6.2.1:沖突-避免-哈希函數設計

6.2.2.?沖突-避免-負載因子調節(重點掌握)

6.3:哈希沖突解決:

6.3.1.閉散列

6.3.2:開散列/哈希桶(特別重要,很好用)

6.4:泛型寫法

一定要重寫equals方法!!!

6.5:總結:性能分析

7. OJ練習

7.1:只出現一次的數字

7.2:隨機鏈表的復制

7.3:寶石與石頭

7.4:壞鍵盤打字

7.5:輸出前K個高頻單詞


問題一:hash會出現負數?數組越界

問題二:為什么會死循環?

問題三:第一個練習:萬一 一個數字出現了三次呢?

一:什么是二叉搜索樹?

二叉搜索樹又稱二叉排序樹,它或者是一棵空樹,或者是具有以下性質的二叉樹 :
  1. 若它的左子樹不為空,則左子樹上所有節點的值都小于根節點的值
  2. 若它的右子樹不為空,則右子樹上所有節點的值都大于根節點的值
  3. 它的左右子樹也分別為二叉搜索樹

1.1:創建一個二叉搜索樹

public class BinarySearchTree {static class TreeNode {public int val;public TreeNode left;public TreeNode right;public TreeNode(int val) {this.val = val;}}public TreeNode root;public boolean search(int key){TreeNode cur = root;if(root == null){return false;}while(cur != null){if(cur.val > key){cur = cur.left;} else if (cur.val <key) {cur = cur.right;}else{return true;}}return false;}public boolean insert(int key){TreeNode node = new TreeNode(key);if(root == null){//  如果根就是的話,直接添加了,然后return true;root = node;return true;}TreeNode cur = root;TreeNode parent = cur;//賦值while(cur != null){if(cur.val < key){parent = cur;  //這一步一定要提前cur = cur.right;} else if (cur.val >key) {parent = cur;cur = cur.left;}else{return false;}}if(parent.val < key){   //你就說為什么cur == null了把它小于的話(cur在該添加的位置上面等于了null)parent.right = node;}else{parent.left = node;}return true;}

1.2.移除一個節點:

要思考的問題:

代碼實現:

    public void remove(int val){TreeNode cur = root;//要移除這個節點,首先我要先遍歷二叉搜索樹找到這個節點TreeNode parent = null;if(root == null){return;}while(cur != null)if(cur.val < val) { parent = cur;cur = cur.right;}else if(cur.val > val){parent = cur;cur = cur.left;}else{//要刪除的節點,找到了removeTreeNode(cur,parent);}}private void removeTreeNode(TreeNode cur,TreeNode parent) {if(cur.left == null){if(cur.val > parent.val){//這一步就純純的是判讀那cur走到了parent的左邊還是右邊,parent.right = cur.right;//你這一點都不知道,我滴媽你讓parent的左邊接力還是右邊?}else{parent.left = cur.right;}}else if(cur.right == null){       //cur的右邊空了if(cur.val > parent.val){//和上面一樣parent.right = cur.left;}else{parent.left = cur.left;}}else{    // cur 的兩邊都不為空TreeNode target = cur.right; //準備TreeNode parentTarget = null;while(target.left != null){parentTarget = target;target = target.left;}cur.val = target.val;if(parentTarget != null){parentTarget.left = target.right;}else{cur.right = target.right;}}}

1.3.二叉搜索樹的缺陷:

插入和刪除操作都必須先查找,查找效率代表了二叉搜索樹中各個操作的性能。

對有 n 個結點的二叉搜索樹,若每個元素查找的概率相等,則二叉搜索樹平均查找長度是結點在二叉搜索樹的深度的函數,即結點越深,則比較次數越多。
但對于同一個關鍵碼集合,如果各關鍵碼插入的次序不同,可能得到不同結構的二叉搜索樹:
  • 最優情況下,二叉搜索樹為完全二叉樹,其平均比較次數為:
  • 最差情況下,二叉搜索樹退化為單支樹,其平均比較次數為:N/2(這個時候效率已經沒了)


1.4.引出map和set以及和紅黑樹之間的關系

TreeMap TreeSet java 利用搜索樹實現的 Map Set ;實際上用的是紅黑樹,而紅黑樹是一棵近似平衡的二叉搜索樹,即在二叉搜索樹的基礎之上 + 顏色以及紅黑樹性質驗證。

二:關于搜索我們一般用到的方式

2.1.靜態查找(就只進行查找不進行其他的操作)

1. 直接遍歷,時間復雜度為 O(N) ,元素如果比較多效率會非常慢
2. 二分查找,時間復雜度為log(N), 但搜索前必須要求序列是有序的

2.2.但是現實中的查找大多數都是動態查找

1. 根據姓名查詢考試成績
2. 通訊錄,即根據姓名查詢聯系方式
3. 不重復集合,即需要先搜索關鍵字是否已經在集合中
可能在查找時進行一些插入和刪除的操作,即動態查找,下面我將進行講解Map和Set。 Map Set 是一種適合動態查找的集合容器(它們都是接口)。

2.3?模型

一般把搜索的數據稱為關鍵字( Key ),和關鍵字對應的稱為值( Value ),將其稱之為 Key-value 鍵值對(具有映射關系),所以模型會有兩種:
1. 純 key 模型(TreeSet,HashSet)
比如:
有一個英文詞典,快速查找一個單詞是否在詞典中,快速查找某個名字在不在通訊錄中
2. Key-Value 模型(TreeMap,HashMap)
比如:
統計文件中每個單詞出現的次數,統計結果是每個單詞都有與其對應的次數: < 單詞,單詞出現的次數 >
Map 中存儲的就是 key-value 的鍵值對, Set 中只存儲了 Key

三:Map

Map官方文檔

Map是一個接口類,該類沒有繼承自Collection,該類中存儲的是<K,V>結構的鍵值對(這個鍵值對往大了點說就是一個內部類)(譬如樹里面的節點里面存儲了左右孩子節點),并且K一定是唯一的,不能重復(那這個<K,V>)

3.1.關于Map.Entry<K, V>(很重要)

首先Entry<K,V>是Map接口里面的一個內部接口),自然,TreeMap里面的Entry<K,V>這個類也是實現了Entry這個內部接口

如圖:TreeMap里面的Entry<K,V>這個類也是實現了Entry這個內部接口

關于Map.Entry<K,V>(在Map里面的內部接口)這個類就相當于二叉樹里面的節點

總的來說Map.Entry<K, V> Map內部實現的用來存放<key, value>鍵值對映射關系的內部類,該內部類中主要提供了<key, value>的獲取,value的設置以及Key的比較方式

注意:Map.Entry<K,V>并沒有提供設置Key的方法

3.2.Map常用方法說明

解釋:在 Java 中,當你嘗試將一個對象賦值給一個基本數據類型時,實際上會發生自動拆箱(unboxing)操作,但這之前還有一個更關鍵的檢查點:空值檢查。

在這個例子中,map.get("lihua")?返回一個?Integer?對象(或者?null,如果鍵?"lihua"?不存在)。但是,當你嘗試將這個?Integer?對象賦值給一個?int?類型的變量?val?時,Java 編譯器會插入一個自動拆箱操作。

然而,在自動拆箱之前,Java 運行時環境會檢查?Integer?對象是否為?null。如果對象是?null,那么嘗試進行拆箱操作(即將?Integer?轉換為?int)是不合法的,因為基本數據類型?int?不能是?null。在這種情況下,Java 不會拋出類型轉換異常(ClassCastException),而是會拋出一個空指針異常(NullPointerException)。

類型轉換異常(ClassCastException)通常發生在嘗試將一個對象強制轉換為不兼容類型的對象時。但在這個例子中,問題不在于類型的不兼容,而在于嘗試將一個空引用(null)用作基本數據類型的值。

import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;public class Test {public static void main(String[] args) {Map<String,Integer> map = new TreeMap<>();
//1.put 和 get(Object key) 返回 key 對應的 valuemap.put("zhangsan",18);map.put("zhangsan",20);//會進行更新,key值不會重復map.put("lisi",18);map.put("wangWu",50);//int val = map.get("lihua");返回的是一個空指針異常而不是類型轉換異常,很有講究的//這個時候就可以用這個方法map.getOrDefault("lihua",20);// 沒有key的話就相當于一次put,(key是lihua)(val是20)
//3.(keyset):Set<K> keySet() 返回所有 key 的不重復集合Set<String> set = map.keySet();  //經過排序的從大到小for (String s: set) {System.out.print(s + " ");}//或者System.out.println(set);
//4:Set<K> keySet() 返回所有 key 的不重復集合Collection<Integer> collection = map.values();//也是有順序的,一般都是按照key的進行排序System.out.println(collection);System.out.println("==============");
//5:Set<Map.Entry<K, V>> entrySet() 返回所有的 key-value 映射關系Set<Map.Entry<String,Integer>> entries = map.entrySet();for (Map.Entry<String,Integer> entry:entries) {System.out.println("key:" + entry.getKey() + " val:" + entry.getValue());}//或者直接打印也行System.out.println(entries);System.out.println("===========");}

結果:

3.3.關于Map方法的注意事項

  • 1. Map是一個接口,不能直接實例化對象,如果要實例化對象只能實例化其實現類TreeMap或者HashMap。
  • 2. Map中存放鍵值對的Key是唯一的,value是可以重復的
  • 3. TreeMap中插入鍵值對時,key不能為空,否則就會拋NullPointerException異常(在我看來放入TreeMap的key需要進行比較利用compareTo方法,(這個時候null調用compareTo方法就會報空指針異常)所以不能插入key為空的情況)(TreeMap是基于紅黑樹實現的,它要求所有的key都必須實現Compare接口,或者通過構造函數傳入Comparator。value可以為空。但是HashMapkeyvalue都可以為空。
  • 4. Map中的Key可以全部分離出來,存儲到Set中(Set<K> keySet()來進行訪問(因為Key不能重復)
  • 5. Map中的value可以全部分離出來,存儲在Collection的任何一個子集合中(value可能有重復)
  • 6. Map中鍵值對的Key不能直接修改,value可以修改,如果要修改key,只能先將該key刪除掉,然后再來進行重新插入。

四:Set(去重很好)

Set的官方文檔

4.2.Set注意事項

注意:

  • 1. Set是繼承自Collection的一個接口類
  • 2. Set中只存儲了key,并且要求key一定要唯一
  • 3. TreeSet的底層是使用Map來實現的,其使用keyObject的一個默認對象作為鍵值對插入到Map中的
  • 4. Set最大的功能就是對集合中的元素進行去重
  • 5. 實現Set接口的常用類有TreeSetHashSet,還有一個LinkedHashSetLinkedHashSet是在HashSet的基礎
  • 上維護了一個雙向鏈表來記錄元素的插入次序。
  • 6. Set中的Key不能修改,如果要修改,先將原來的刪除掉,然后再重新插入
  • 7. TreeSet中不能插入nullkey(和TreeMap的解釋一樣,都是要進行比較的)HashSet可以。

五:我們日常的比較

順序結構以及平衡樹 中,元素關鍵碼與其存儲位置之間沒有對應的關系,因此在 查找一個元素時,必須要經過關鍵 碼的多次比較 順序查找時間復雜度為 O(N) ,平衡樹中為樹的高度,即 O(
) ,搜索的效率取決于搜索過程中元素的比較次數。
那有沒有一種理想的搜索方式使得我們直接就能找到呢?
函數:數學函數!!!給一個x就能得出一個y,時間復雜度就是O(1)

至此,我們成功引入哈希表!!!

六:哈希表

插入元素:我們通過對關鍵碼(Key)的運算,計算出該元素存儲的位置,然后直接放在該位置

搜索元素:對元素的Key進行同樣的計算,把求得的函數值當做元素的存儲位置,在結構中按此位置取元素比較,若關鍵碼相等,則搜索成功
該方式即為哈希 ( 散列 ) 方法, 哈希方法中使用的轉換函數稱為哈希 ( 散列 ) 函數,構造出來的結構稱為哈希表 (Hash Table)( 或者稱散列表 )
例如:數據集合 {1 7 6 4 5 9}
哈希函數設置為 hash(key) = key % capacity ; capacity 為存儲元素底層空間總的大小

但我再加一個為44的元素呢?結果就是完蛋,和4這個元素你倆撞上了

6.1:哈希沖突

上述的兩個元素撞上了,就是你倆通過一個哈希函數然后計算出的哈希值一模一樣,導致你倆放的位置一樣,這樣就是大名鼎鼎的——哈希沖突

具體解釋:

對于兩個數據元素的關鍵字 和 (i != ) ,有k i != kj? ,但有: Hash( ki) == Hash(kj) ,即: 不同關鍵字通過相同哈 希哈數計算出相同的哈希地址,該種現象稱為哈希沖突或哈希碰撞
把具有不同關鍵碼而具有相同哈希地址的數據元素稱為 同義詞

6.2:哈希沖突不可避免

首先,我們需要明確一點,由于我們哈希表底層數組的容量往往是小于實際要存儲的關鍵字的數量的,這就導致一個問題,沖突的發生是必然的 ,但我們能做的應該是盡量的 降低沖突
那么我們用該如何盡量避免沖突呢?
  1. 設計合理的哈希函數
  2. 擴容(和負載因子有關):底層數組盡量可以根據放入元素的多少進行擴容(底層容量改變了,沖突也會盡量少一點)

6.2.1:沖突-避免-哈希函數設計

哈希函數設計不夠合理 哈希函數設計原則

? ? ? ?1.? 哈希函數的定義域必須包括需要存儲的全部關鍵碼,而如果散列表允許有m個地址時,其值域必須在0到m-1之間(我是數組我要連續(*^▽^*))

? ? ? 2.? 哈希函數計算出來的地址能均勻分布在整個空間中

? ? ? 3.哈希函數應該比較簡單

常見的哈希函數(有的大家只需要了解就行了)

1. 直接定制法 --( 常用 )
取關鍵字的某個線性函數為散列地址: Hash Key = A*Key + B 優點:簡單、均勻 缺點:需要事先知道關 鍵字的分布情況 使用場景:適合查找比較小且連續的情況
面試題:只出現一次 的字符
2. 除留余數法 --( 常用 )
設散列表中允許的地址數為m ,取一個不大于 m ,但最接近或者等于 m 的質數 p 作為除數,按照哈希函數:
Hash(key) = key% p(p<=m), 將關鍵碼轉換成哈希地址
3. 平方取中法 --( 了解 )
假設關鍵字為 1234 ,對它平方就是 1522756 ,抽取中間的 3 227 作為哈希地址; 再比如關鍵字為 4321 ,對它平方就是18671041 ,抽取中間的 3 671( 710) 作為哈希地址 平方取中法比較適合:不知道關鍵字的分 布,而位數又不是很大的情況
4. 折疊法 --( 了解 )
折疊法是將關鍵字從左到右分割成位數相等的幾部分 ( 最后一部分位數可以短些 ) ,然后將這幾部分疊加求和,并按散列表表長,取后幾位作為散列地址。
折疊法適合事先不需要知道關鍵字的分布,適合關鍵字位數比較多的情況
5. 隨機數法 --( 了解 )
選擇一個隨機函數,取關鍵字的隨機函數值為它的哈希地址,即 H(key) = random(key), 其中 random 為隨機數函數。
通常應用于關鍵字長度不等時采用此法
6. 數學分析法 --( 了解 )
設有 n d 位數,每一位可能有 r 種不同的符號,這 r 種不同的符號在各位上出現的頻率不一定相同,可能在某些位上分布比較均勻,每種符號出現的機會均等,在某些位上分布不均勻只有某幾種符號經常出現。可根據散列表的大小,選擇其中各種符號分布均勻的若干位作為散列地址。例如
假設要存儲某家公司員工登記表,如果用手機號作為關鍵字,那么極有可能前 7 位都是 相同的,那么我們可以選擇后面的四位作為散列地址,如果這樣的抽取工作還容易出現 沖突,還可以對抽取出來的數字進行反轉( 如1234改成 4321) 、右環位移 ( 1234 改成 4123) 、左環移位、前兩數與后兩數疊加 ( 1234 改成 12+34=46) 等方法。
數字分析法通常適合處理關鍵字位數比較大的情況,如果事先知道關鍵字的分布且關鍵字的若干位分布較均 勻的情況
注意:哈希函數設計的越精妙,產生哈希沖突的可能性就越低,但是無法避免哈希沖突

6.2.2.?沖突-避免-負載因子調節(重點掌握)

小編用兩張圖來進行講解

6.3:哈希沖突解決:

一般是這兩種方法

  1. 二次探測和線性探測(閉散列 讓你們兩個沖突的元素,第一個放在計算出來的位置,然后再給另一個元素再設計一個函數進行放置(也稱之為二次探測),? 不進行二次探測的話,也可以把另外一個元素進行線性探測(既然這個位置已經有人了,那我就放在下一個空的位置(反正小編不想這么干))
  2. 數組 + 鏈表開散列/哈希桶(小編很喜歡這個):既然你倆計算出來相同的位置了,你倆都別委屈了,都來吧(

等等方法………………(接下倆小編為大家講解這幾個方法)

6.3.1.閉散列

我們來想想

閉散列:也叫開放定址法,當發生哈希沖突時,如果哈希表未被裝滿,說明在哈希表中必然還有空位置,那么可以 key 存放到沖突位置中的 下一個 空位置中去。 那如何尋找下一個空位置呢?
帶著這個問題我們迎來兩個解決辦法
解決一: 線性探測:從發生沖突的位置開始,依次向后探測,直到尋找到下一個空位置為止。

線性探測的缺點:?線性探測會導致產生沖突的數據堆積在一塊,這與其找下一個空位置有關系(一個一個挨著找,這種情況很正常)而二次探測可以避免這個問題

解決二:二次探測:

找下一個空位置的方法為:Hi?= (H0 + i^2) % m,或者? ?:? ?Hi?= (H0 + i^2) % m其中:i = 1,2,3… , H0是通過散列函數Hash(x) 對元素的關鍵碼 key 進行計算得到的位置,m是表的大小(數組的長度)。 如果要插入 44 ,和一開始的元素4產生沖突,使用解決后的情況為:

研究表明:當表的長度為質數且表裝載因子 a 不超過 0.5 時9(在這個情況下),新的元素一定能夠插入,而且任何一個位置都不會被探查兩次。因此只要表中有一半的空位置,就不會存在表滿的問題。在搜索時可以不考慮表裝滿的情況,但在插入時必須確保表的裝載因子a 不超過 0.5 ,如果超出必須考慮增容。
因此:比散列最大的缺陷就是空間利用率比較低,這也是哈希的缺陷。

6.3.2:開散列/哈希桶(特別重要,很好用)

開散列/哈希桶(數組 + 鏈表)

雖然這樣可行,但是萬一沖突的元素太多了怎么辦呢?(我還不是要遍歷鏈表?)

好好好,那我就把你鏈表變成二叉樹

這個思想就是不斷地把(大集合的搜索問題轉化為小集合的搜索問題

6.3.3:開散列/哈希桶的實現

// key-value 模型
public class HashBucket {public static class Node {private int val;private int key;public Node next;//這里一定要注意,你如果在這里上來就構造了一個根節點的public Node(int key, int val) {this.key = key;this.val = val;}}private static final double LOAD_FACTOR = 0.75;//負載因子//根本不需要根節點,我底層是數組進行實現的,要你沒有用//public Node root;public Node[] array = new Node[5];    //我這個就是HsahMap的底層實現,開散列:數組 + 鏈表private int usedSize;   //一般只要一個數據結構底層運用了數組,一般都會有usedSize去記錄有效元素public boolean search(int key) {   //這個要判斷val嗎?int index = key % array.length;Node cur = array[index];//   我直接計算出這個key應該在的位置,然后在數組的這個位置進行查找while (cur != null) {if (cur.key == key) {return true;}cur = cur.next;}return false;}public void put(int key, int val) {int index = key % array.length;Node cur = array[index];while (cur != null) {  // 先遍歷一遍這個鏈表,判讀這個key在不在里面,如果在的話,就更新val值if (cur.key == key) {cur.val = val;}cur = cur.next;}//走到這里說明不在這里,直接進行頭插Node node = new Node(key,val);node.next = array[index];array[index] = node;usedSize++;if(usedSize * 1.0/ array.length > LOAD_FACTOR){  //這里要乘  1.0revise(array);}}private void revise(Node[] array) {  //這里要是有參數的話,下面的array,一定要加this啊!!!!!!!!!!!//首先讓數字進行擴容//最后再返回數組Node[] tmpArray = Arrays.copyOf(array,2*array.length);//先遍歷一遍原來的數組,再在每個數組元素上遍歷鏈表for (int i = 0; i < array.length; i++) {Node cur = array[i];while(cur != null){//計算出新的下標,再進行頭插int newIndex = cur.key % tmpArray.length;//這個節點就是cur其實沒必要再多定義了/*Node node = new Node(cur.key,cur.val);node.next = tmpArray[newIndex];tmpArray[newIndex] = node;cur = cur.next;
*///你們是一樣的道理Node curNext = cur.next;cur.next = tmpArray[newIndex];tmpArray[newIndex] = cur;cur =curNext;}}array = tmpArray;//   這里一定要this啊!!!!!!!!!!!!!//  除非上面沒有參數}public int get(int key){int index = key % array.length;Node cur = array[index];//   我直接計算出這個key應該在的位置,然后在數組的這個位置進行查找while (cur != null) {if (cur.key == key) {return cur.val;}cur = cur.next;}return -1;}
}

Test(測試)

public class Test {public static void main(String[] args) {HashBucket hashBucket = new HashBucket();hashBucket.put(1,1);hashBucket.put(2,2);hashBucket.put(3,3);hashBucket.put(4,4);hashBucket.put(5,5);//就這里i = 1下標出問題了System.out.println("===============");System.out.println(hashBucket.get(1));System.out.println(hashBucket.get(2));System.out.println(hashBucket.get(5));System.out.println(hashBucket.get(9));}
}

我這里給大家提出一個問題,如果我上述有revise(array)并且后面array = tmpArray,(我也沒加this,為什么最后會死循環呢?)

大家可以去運行一下,為什么最后會死循環?

6.4:泛型寫法

class Person{String name;public Person(String name) {this.name = name;}//一定要重寫equals方法,因為我要看看你一開始在不在里面,相同名字的lihua@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return Objects.equals(name, person.name);}//這個也是@Overridepublic int hashCode() {return Objects.hash(name);}
}public class HashBucket1 <K,V>{public class Node<K,V>{  //為什么不能是靜態的?(靜態的不依賴于類,所以說我根本不知道K,V是誰?private K key;private V val;private Node<K,V> next;//這里一定要注意,你如果在這里上來就構造了一個根節點的public Node( K key, V val) {this.key = key;this.val = val;}}private Node[] array ;private static final double LOAD_FACTOR = 0.75;//負載因子public HashBucket1() {this.array = new Node[10];}private int usedSize;public void put(K key, V val){int hash = key.hashCode();//還能是負的?int index = Math.abs(hash % array.length);Node<K,V> cur = array[index];//先找一遍看有沒有,沒有直接頭插法while(cur != null){if(cur.key.equals(key)){   //所以說這個key一定要重寫equals方法cur.val = val;return;}cur = cur.next;}// 到這里就肯定沒有了,沒有直接頭插法Node<K,V> node = new Node<>(key,val);node.next = array[index];array[index] = node;usedSize++;// 判斷負載因子大不大if(usedSize*1.0 / array.length > LOAD_FACTOR){revise();}}private void revise() {Node<K,V> [] tmpArray = Arrays.copyOf(array,2*array.length);for (int i = 0; i < array.length; i++) {Node<K,V> cur = array[i];while(cur != null){Node<K,V> curNext = cur.next;//找到新下標直接進行插入int newIndex = Math.abs(cur.hashCode() % tmpArray.length);cur.next = tmpArray[newIndex];tmpArray[newIndex] = cur;cur = curNext;}}this.array = tmpArray;}public V get(K key){int index = key.hashCode() % array.length;Node<K,V> cur = array[index];while(cur != null){if(cur.key == key){return cur.val;}cur = cur.next;}return null;}public boolean search(K key){int index = key.hashCode() % array.length;Node<K,V> cur = array[index];while(cur != null){if(cur.key == key){return true;}cur = cur.next;}return false;}
}

Test測試

public class TestHashBucket1 {public static void main(String[] args) {Person person = new Person("zhangSan");Person person1 = new Person("zhangSan");Person person2 = new Person("zhangSan");HashBucket1<Person,Integer> hashBucket1= new HashBucket1();//這個key一定要重寫equals方法(不然俺咋判斷,我寫的哈希表里面有沒有這個元素hashBucket1.put(person,18);hashBucket1.put(person1,18);hashBucket1.put(person2,18);System.out.println("============");}
}

一定要重寫equals方法!!!

寫了:

沒寫:

6.5:總結:性能分析

雖然哈希表一直在和沖突做斗爭,但在實際使用過程中,我們認為哈希表的沖突率是不高的,沖突個數是可控的,也就是每個桶中的鏈表的長度是一個常數,所以,通常意義下,我們認為哈希表的插入 / 刪除 / 查找時間復雜度是 O(1)
5.12 java 類集的關系
  1. ?HashMap HashSet java 中利用哈希表實現的 Map Set
  2. ?java 中使用的是哈希桶方式解決沖突的
  3. ?java 會在沖突鏈表長度大于一定閾值后,將鏈表轉變為搜索樹(紅黑樹)
兩個方法:(hasCode和equals方法)
?java 中計算哈希值實際上是調用的類的 hashCode 方法,進行 key 的相等性比較是調用 key equals 方法。所以如果要用自定義類作為 HashMap key 或者 HashSet 的值, 必須重寫 ?hashCode equals 法。
而且要做到 equals 相等的對象, hashCode 一定是一致的。

7. OJ練習

7.1:只出現一次的數字

只出現一次的數字

方法一:我們沒學哈希表之前(直接全部異或就好了)

方法二:

class Solution {public int singleNumber(int[] nums) {Set<Integer> set = new HashSet<>();
//一邊進去,相同的出來for(int i = 0; i < nums.length; i++){if(set.contains(nums[i])){set.remove(nums[i]);}else{set.add(nums[i]);}}
//如果包含就是你了(只出現過一次)for(int i = 0; i < nums.length; i++){if(set.contains(nums[i])){return nums[i];}}return -1;}
}


7.2:隨機鏈表的復制

隨機鏈表的復制

import java.util.Map;
class Solution {public Node copyRandomList(Node head) {Node cur = head;Map<Node,Node> map = new HashMap<>();while(cur != null){Node node = new Node(cur.val);map.put(cur,node);   //幫下面的鏈接上,通過HashMapcur = cur.next;//node = node.next;       這樣寫肯定不對,因為我每次都要new一個Node對象,就鏈接不上了}cur = head;while(cur != null){map.get(cur).random = map.get(cur.random);map.get(cur).next = map.get(cur.next);cur = cur.next;}return map.get(head);}
}

7.3:寶石與石頭

寶石與石頭

class Solution {public int numJewelsInStones(String jewels, String stones) {Set<Character> set = new HashSet<>();for(int i = 0; i<jewels.length(); i++){set.add(jewels.charAt(i));   //    是add而不是put}int count = 0;for(int i = 0; i<stones.length(); i++){char ch = stones.charAt(i);if(set.contains(ch)){count++;}}return count;}
}

7.4:壞鍵盤打字

壞鍵盤打字

 public static void find(String s1,String act){Set<Character> set = new HashSet<>();for (char ch: act.toUpperCase().toCharArray()) {set.add(ch);}Set<Character> set2 = new HashSet<>();for (char ch: s1.toUpperCase().toCharArray()) {if(! set.contains(ch)){set2.add(ch);}}for (Object o:set2.toArray()) {System.out.println(o);}}public static void main(String[] args) {String s1 = "abc def 7 abc";String s2 = "abc";find(s1,s2);}

需要掌握,String的toUpperCase()(轉換大寫)toCharArray()(轉換成字符數組)

以及Set的toArray()將里面的元素轉換成數組

7.5:輸出前K個高頻單詞

輸出前K個高頻單詞

public  List<String> topKFrequent(String[] words, int k) {// 一個哈希表,統計所有單詞出現的個數Map<String,Integer> map = new HashMap<>();for(String word : words){if( !map.containsKey(word)){map.put(word,1);}else{int count =  map.get(word);map.put(word,++count);}}//建立小根堆,重寫比較方法//  這里的Entry<>記得前面加上Map,因為他是Map里面的內部類,所有的都要加PriorityQueue<Map.Entry<String,Integer>> minHeap = new PriorityQueue<>(new Comparator<Map.Entry<String, Integer>>() {@Overridepublic int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {if((o1.getValue() - o2.getValue()) == 0 ){return o2.getKey().compareTo(o1.getKey());}return o2.getValue() - o1.getValue();}});for (Map.Entry<String,Integer> entry : map.entrySet()) {Map.Entry<String,Integer> top =  minHeap.peek();if(minHeap.size() <= k){minHeap.offer(entry);}else{//   如果你倆的出現的頻率相同,讓字母的ASCII碼值進行比較大的進來if(top.getValue().equals(entry.getValue())){if(top.getKey().compareTo(entry.getKey()) > 0){minHeap.poll();minHeap.offer(entry);}}else{// 下一個元素的val大,讓下一個元素進來if(entry.getValue().compareTo(top.getValue()) > 0){minHeap.poll();minHeap.offer(entry);}}}}List<String> ret = new ArrayList<>();for (int i = 0; i < k; i++) {Map.Entry<String,Integer> top = minHeap.poll();ret.add(top.getKey());}Collections.reverse(ret);return ret;}

上述就是哈希碰撞攻防戰——深入淺出Map/Set的底層實現

的全部內容了,哈希碰撞還有很多我們的未解之謎,讓未來的我們一起去解決吧~~~

能看到這里相信您一定對小編的文章有了一定的認可。

有什么問題歡迎各位大佬指出
歡迎各位大佬評論區留言修正~~

您的支持就是我最大的動力???!!!!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/896713.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/896713.shtml
英文地址,請注明出處:http://en.pswp.cn/news/896713.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

win10使用haneWIN NFS Server掛載NFS v2服務,u-boot通過NFS下載zImage

1. haneWIN NFS Server掛載NFS v2服務 https://www.hanewin.net/nfs-e.htm netstat -ano | findstr ":2049"TCP 0.0.0.0:2049 0.0.0.0:0 LISTENING 3824UDP 0.0.0.0:2049 *:* 38…

Linux文件系統與目錄結構

Linux系統中一切皆文件 bin 是Binary 的縮寫, 這個目錄存放著最經常使用的命令 boot 這里存放的是啟動Linux時使用的一些核心文件&#xff0c;包括一些連接文件以及鏡像文件&#xff0c;自 己的安裝別放這里。 cdrom 這個目錄通常專門用來掛載光盤。當系統剛安裝時&#x…

一文詳解基于NarrotoAI的短劇短視頻自動解說、混剪AI平臺搭建

背景 前陣給孩子做電子相冊學了點剪輯技術&#xff0c;就想湊個熱鬧剪剪短劇玩玩&#xff0c;一是學以 致用&#xff0c;再者也好奇短劇創作為啥這么火&#xff0c;跟個風。 初步了解情況后&#xff0c;發現我的剪輯技術已經落后了&#xff0c;行家們玩的主要是解說 &#xf…

計算機畢業設計Hadoop+Spark+DeepSeek-R1大模型音樂推薦系統 音樂數據分析 音樂可視化 音樂爬蟲 知識圖譜 大數據畢業設計

溫馨提示&#xff1a;文末有 CSDN 平臺官方提供的學長聯系方式的名片&#xff01; 溫馨提示&#xff1a;文末有 CSDN 平臺官方提供的學長聯系方式的名片&#xff01; 溫馨提示&#xff1a;文末有 CSDN 平臺官方提供的學長聯系方式的名片&#xff01; 作者簡介&#xff1a;Java領…

《Canvas修仙傳·第三重天金丹境(下集)》 ——量子煙花與物理宇宙的混沌法則

各位道友久候&#xff01;上集我們煉就了《靈蛇奇譚》的元神&#xff0c;今日將開啟Canvas修仙路上最絢麗的篇章——掌控微觀粒子的創世之力&#xff01;(&#xff89;≧?≦)&#xff89; 章前黑話詞典 &#x1f50d; 量子境術語表&#xff1a; 對象池&#xff08;Object Po…

c++ namespace名字域空間

在C中&#xff0c;namespace 是一個非常重要的概念&#xff0c;用于組織代碼&#xff0c;避免名稱沖突。namespace&#xff08;命名空間&#xff09;是一個邏輯上的代碼組織單元&#xff0c;用于將代碼&#xff08;類、函數、變量等&#xff09;分組&#xff0c;從而避免命名沖…

獲取阿里云OSS預簽名URL下載(java)

1,引入依賴 <dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId> </dependency> <!--AliSms--> <dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-s…

中間件專欄之Redis篇——Redis的基本IO網絡模型

Redis主要采用的是單線程的事件驅動模型&#xff0c;通過I/O多路復用來實現高效的并發請求處理。 一、單線程模型 Redis 采用 單線程模型 來處理所有請求&#xff0c;包括網絡 I/O 和命令執行。雖然現代多核 CPU 能夠并行處理任務&#xff0c;但 Redis 的設計原則是盡量避免多…

Python 線程同步

Python 線程同步 Python 線程同步 Python 線程同步 線程同步是一種確保兩個或多個線程不同時執行同一塊共享代碼的機制。共享塊中的代碼通常是訪問共享數據或資源&#xff0c;這種共享塊被稱作臨界區。這個概念可以用下面的圖清晰地表示出來&#xff1a; #mermaid-svg-2TivIuc…

Linux操作系統5-進程信號3(信號的捕捉流程,信號集,sigaction)

上篇文章&#xff1a;Linux操作系統5-進程信號3&#xff08;信號的保存, 用戶態與內核態&#xff0c;內核空間&#xff09;-CSDN博客 本篇Gitee倉庫&#xff1a;???????myLerningCode/l26 橘子真甜/Linux操作系統與網絡編程學習 - 碼云 - 開源中國 (gitee.com) 本篇重點…

【機器學習chp10】降維——(核化)PCA + MDS + lsomap + 拉普拉斯特征映射 + t-NSE + UMAP

目錄 一、降維的意義與本質 1、意義 2、本質 3、常見降維方法 &#xff08;1&#xff09;線性降維 &#xff08;2&#xff09;非線性降維 二、基于重構的降維 1、PCA 2、核化PCA &#xff08;1&#xff09;實現過程 步驟一&#xff1a;數據映射與核函數定義 步驟二…

代碼的解讀——自用

代碼來自&#xff1a;https://github.com/ChuHan89/WSSS-Tissue?tabreadme-ov-file 借助了一些人工智能 run_pipeline.sh 功能總結 該腳本用于執行一個 弱監督語義分割&#xff08;WSSS&#xff09; 的完整流程&#xff0c;包含三個階段&#xff1a; Stage1&#xff1a;訓…

Powershell和BTEQ工具實現帶多組參數和標簽的Teradata數據庫批量數據導出程序

設計一個基于多個帶標簽SQL模板作為配置文件和多組參數的Powershell代碼程序和BTEQ工具&#xff0c;實現根據不同的輸入參數&#xff0c;自動批量地將Teradata數據庫的數據導出為CSV文件到指定目錄上&#xff0c;標簽和多個參數&#xff08;以“_”分割&#xff09;為組成導出數…

CF 118A.String Task(Java實現)

題目分析 輸入一個字符串&#xff0c;遍歷每一個字符&#xff0c;如果是元音字母就刪除&#xff0c;輔音字母就在其前面增加一個.&#xff0c;且所有字母輸出都是小寫。 思路分析 將輸入的字符串改為字符數組&#xff0c;考慮到任意位置插入的情況&#xff0c;所以主要選擇Lin…

LLM進階

prologue&#xff1a;最近大模型火出天際&#xff0c;I’m definitely aware I’m late to the party&#xff0c;2022年畢業之后就很少在系統的跟蹤一個domain了&#xff0c;所以這次下定決心要跟蹤一下大模型的技術細節和實現過程&#xff0c;不做AI丁真 本文三條主線&#…

Ubuntu 下查看進程 PID 和終止進程方法

查看進程 PID 使用 ps 命令: ps aux | grep <process_name>例如&#xff0c;查看名為 python 的進程&#xff1a; ps aux | grep python使用 pgrep 命令: pgrep <process_name>例如&#xff0c;查看名為 python 的進程&#xff1a; pgrep python使用 top 命令: top…

Java基礎語法練習34(抽象類-abstract)(抽象類最佳實踐-模版設計模式)

一抽象類-abstract、 父類方法不確定性的問題故將該方法設計為抽象類&#xff08;沒有實現的方法&#xff09;&#xff0c;但一般來說被子類繼承然后實現 細節&#xff1a; 1、抽象類不可以被實例化 2、抽象類可以不包含抽象方法而且可以有實現的其他非抽象方法 3、abstra…

Android SDK與NDK的區別

Android SDK&#xff08;Software Development Kit&#xff09;與NDK&#xff08;Native Development Kit&#xff09;在Android應用開發中各自扮演著重要角色&#xff0c;它們之間存在顯著的區別。以下是Android SDK與NDK的主要區別&#xff1a; 一、定義與用途 Android SDK…

DeepSeek在PiscTrace上完成個性化處理需求案例——光流法將煙霧動態可視化

引言&#xff1a;PiscTrace作為開放式的視圖分析平臺提供了固定格式的類型參數支持個性化定制處理需求&#xff0c;本文一步步的實現光流分析按照不同需求根據DeepSeek的代碼處理視頻生成數據。 光流法&#xff08;Optical Flow&#xff09;是一種基于圖像序列的計算機視覺技術…

Linux網絡 TCP全連接隊列與tcpdump抓包

TCP全連接隊列 在 Linux 網絡中&#xff0c;TCP 全連接隊列&#xff08;也稱為 Accept 隊列&#xff09;是一個重要的概念&#xff0c;用于管理已經完成三次握手&#xff0c;即已經處于 established 狀態但尚未被應用程序通過 accept( ) 函數處理的 TCP 連接&#xff0c;避免因…