今天主要討論:哈希函數、哈希表、布隆過濾器、一致性哈希、並查集的介紹和應用。前端
認識哈希函數和哈希表node
一、輸入無限大 面試
二、輸出有限的S集合 算法
三、輸入什麼就輸出什麼 數組
四、會發生哈希碰撞 服務器
五、會均勻分佈,哈希函數的離散性,打亂輸入規律負載均衡
public class Code_01_HashMap { public static void main(String[] args) { HashMap<String, String> map = new HashMap<>(); map.put("zuo", "31"); System.out.println(map.containsKey("zuo")); System.out.println(map.containsKey("chengyun")); System.out.println("========================="); System.out.println(map.get("zuo")); System.out.println(map.get("chengyun")); System.out.println("========================="); System.out.println(map.isEmpty()); System.out.println(map.size()); System.out.println("========================="); System.out.println(map.remove("zuo")); System.out.println(map.containsKey("zuo")); System.out.println(map.get("zuo")); System.out.println(map.isEmpty()); System.out.println(map.size()); System.out.println("========================="); map.put("zuo", "31"); System.out.println(map.get("zuo")); map.put("zuo", "32"); System.out.println(map.get("zuo")); System.out.println("========================="); map.put("zuo", "31"); map.put("cheng", "32"); map.put("yun", "33"); for (String key : map.keySet()) { System.out.println(key); } System.out.println("========================="); for (String values : map.values()) { System.out.println(values); } System.out.println("========================="); map.clear(); map.put("A", "1"); map.put("B", "2"); map.put("C", "3"); map.put("D", "1"); map.put("E", "2"); map.put("F", "3"); map.put("G", "1"); map.put("H", "2"); map.put("I", "3"); for (Entry<String, String> entry : map.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + "," + value); } System.out.println("========================="); // you can not remove item in map when you use the iterator of map // for(Entry<String,String> entry : map.entrySet()){ // if(!entry.getValue().equals("1")){ // map.remove(entry.getKey()); // } // } // if you want to remove items, collect them first, then remove them by // this way. List<String> removeKeys = new ArrayList<String>(); for (Entry<String, String> entry : map.entrySet()) { if (!entry.getValue().equals("1")) { removeKeys.add(entry.getKey()); } } for (String removeKey : removeKeys) { map.remove(removeKey); } for (Entry<String, String> entry : map.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); System.out.println(key + "," + value); } System.out.println("========================="); } }
推論:若是結果都%一個M,那麼0~m-1這個區域也是均勻分佈的。dom
怎麼擁有1000個相對獨立的哈希函數。函數
把h計算出的16位數,分紅高8位h1和低8位h2,而後h1 + 1*h2 =h3優化
生成新的哈希函數。(每一個位置都是獨立的,都是經過hash函數不斷異或處理計算出來的)
哈希表經典結構:
哈希擴容:
擴容要把之前的元素拿出來,從新計算而後放入新的空間,爲了避免影響效率也可使用離線時間進行擴容(push就同時兩個都push,get的話先從原來的地方拿)。
增刪改查,全爲o(1)
JVM裏面的實現:
利用了平衡搜索二叉樹
數組+紅黑色的哈希表,使用了TreeMap結構
哈希表多有用?引入一道題目:
有一個大文件(100T),每行是一個字符串,想把大文件裏面重複的內容打印出來
問面試官:你給我多少臺機器?1000臺機器
給機器編號0~999
而後從100T裏面開始讀文本,而後把文本按照hash函數算出hashcode再%上1000,若是是0就扔到0機器...,這樣就把大文件分到1000臺機器上。
根據hash的性質,相同的文本會來到同一臺機器上。而後再單臺機器上統計哪些重複的。
若是還太大的話,能夠再機器裏面再分文件。(hash函數作分流)
設計RandomPool結構
【題目】 設計一種結構,在該結構中有以下三個功能:insert(key):將某個key加入到該結構,作到不重複加入。delete(key):將本來在結構中的某個key移除。 getRandom():等機率隨機返回結構中的任何一個key。
【要求】 Insert、delete和getRandom方法的時間複雜度都是 O(1)
作法:準備兩張hash表和整形變量size,每加入一個數就分別存在兩個hash表中,利用math.random隨機從第二個hash表中返回一個數。
怎麼解決刪的問題?
拿最後一個值去填這個洞,而後刪了最後一個。size再減一。
public class Code_02_RandomPool { public static class Pool<K> { private HashMap<K, Integer> keyIndexMap; private HashMap<Integer, K> indexKeyMap; private int size; public Pool() { this.keyIndexMap = new HashMap<K, Integer>(); this.indexKeyMap = new HashMap<Integer, K>(); this.size = 0; } public void insert(K key) { if (!this.keyIndexMap.containsKey(key)) { this.keyIndexMap.put(key, this.size); this.indexKeyMap.put(this.size++, key); } } public void delete(K key) { if (this.keyIndexMap.containsKey(key)) { int deleteIndex = this.keyIndexMap.get(key); int lastIndex = --this.size; K lastKey = this.indexKeyMap.get(lastIndex); this.keyIndexMap.put(lastKey, deleteIndex); this.indexKeyMap.put(deleteIndex, lastKey); this.keyIndexMap.remove(key); this.indexKeyMap.remove(lastIndex); } } public K getRandom() { if (this.size == 0) { return null; } int randomIndex = (int) (Math.random() * this.size); // 0 ~ size -1 return this.indexKeyMap.get(randomIndex); } } public static void main(String[] args) { Pool<String> pool = new Pool<String>(); pool.insert("zuo"); pool.insert("cheng"); pool.insert("yun"); System.out.println(pool.getRandom()); System.out.println(pool.getRandom()); System.out.println(pool.getRandom()); System.out.println(pool.getRandom()); System.out.println(pool.getRandom()); System.out.println(pool.getRandom()); } }
認識布隆過濾器(面試搜索相關的公司幾乎都會問到)
就是一個某種類型的集合,不過會有失誤率。
實現0~m-1比特的數組(處理黑名單問題)
本來的數 | 1 << 16 就能夠把32字節裏面的第16位改成1
public class c05_03BloemFilter { //實現0~m-1比特的數組 public static void main(String[] args) { //int 4個字節 32個比特 int[] arr = new int[1000];//4*8*1000 = 32000; //數量不夠可使用二維數組實現 long[][] map = new long[1000][1000]; int index = 30000;//想把第30000位置描黑 int intIndex = index / 4 / 8;//查看這個bit來自哪一個整數位置 int bitIndex = index % 32;//在定位來自這個整數的哪一個bit位 arr[intIndex] = arr[intIndex] | (1 << bitIndex); } }
一個URL通過K個hash函數,計算出K個位置都描黑。(這個URL就進入到布隆過濾器當中了)
接下來每一個URL都這樣計算加入到bit類型的數組裏面。(數組要夠大)
怎麼查?
這個URL通過K個hash函數,算出來K個位置,若是K個位置都是黑的就說這個URL在黑名單中,若是有一個不是黑的就不在黑名單裏
數組空間越大,失誤率會下降,空間多大和樣本量、預計失誤率有關係
數組的大小M(bit)有一個公式計算。22.3G
肯定hash函數的個數K,最後P會在肯定了M和K後計算出來
若是面試官感受經典結構太費,就問面試官允不容許有失誤率,失誤率是多少,容許就講布隆過濾器的原理,URL通過K個hash而後描黑數組,檢查URL的時候經過K個hash來檢查。都黑就在,不然就不在。
數組開多大,由樣本量、失誤率,計算出bit後還要除以8纔是字節數。
若是計算出16G,面試官給出20G空間就適當調整大到18G
接着就計算hash的個數K,向上取整。
最後再計算下失誤率。
認識一致性哈希(服務器設計)
服務器經典結構怎麼作到負載均衡,前端經過同一份hash函數,計算出hashcode再%3,獲得0/1/2而後存在不一樣的服務器中。因爲hash函數的性質,這個服務器巨均衡。
當想加減機器的時候,這個結構就幹了。和hash表擴容同樣。全部的數據歸屬全變了。(代價很大)
引入一致性哈希結構。
把hash函數的返回值想象成一個環。再把機器M1/M2/M3的IP通過hash計算放在環裏面,接着要進入一個數據」zuo」就入環,順時針找到最近的機器存進去。
怎麼實現?
把機器的hash值排序後作成數組,存在每一個前端服務器中。
在數據訪問的時候,經過計算hash值,二分的方式查詢機器數組,查詢出最近的大於等於機器。
前端服務器二分的查找服務器過程,就是一個順時針找最近服務器的過程。
新增一個機器的狀況:
M4經過IP計算出位置,數據遷移只須要一小部分。新增和刪除都只須要一小部分數據。
在機器數量小的時候,不能確保機器均勻分佈。
什麼技術能夠解決這個問題?
虛擬節點技術。
給M1/M2/M3,1000個虛擬節點。
準備一張路由表,虛擬節點能夠找到本身對應的節點。
把3000個節點。存入環中,那麼機器們負責的數據就差很少同樣了
新增了M4以後,也加入1000個節點,把相應的數據進行調整。
幾乎全部須要集羣化都進行了一致性哈希的改造。
島問題
一個矩陣中只有0和1兩種值,每一個位置均可以和本身的上、下、左、右四個位置相連,若是有一片1連在一塊兒,這個部分叫作一個島,求一個矩陣中有多少個島?
舉例:
0 0 1 0 1 0
1 1 1 0 1 0
1 0 0 1 0 0
0 0 0 0 0 0
這個矩陣中有三個島。
若是矩陣巨大無比,可是有幾個CPU,設計一個多任務並行的算法。
經典解法:
遍歷矩陣,碰到1就啓動感染函數(遞歸改變數值的函數),把1周圍的變爲2,島嶼+1,直到遍歷結束。
public class Code_03_Islands { public static int countIslands(int[][] m) { if (m == null || m[0] == null) { return 0; } int N = m.length; int M = m[0].length; int res = 0; for (int i = 0; i < N; i++) { for (int j = 0; j < M; j++) { if (m[i][j] == 1) { res++; infect(m, i, j, N, M); } } } return res; } public static void infect(int[][] m, int i, int j, int N, int M) { if (i < 0 || i >= N || j < 0 || j >= M || m[i][j] != 1) { return; } m[i][j] = 2; infect(m, i + 1, j, N, M); infect(m, i - 1, j, N, M); infect(m, i, j + 1, N, M); infect(m, i, j - 1, N, M); } public static void main(String[] args) { int[][] m1 = { { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 1, 1, 0, 1, 1, 1, 0 }, { 0, 1, 1, 1, 0, 0, 0, 1, 0 }, { 0, 1, 1, 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 1, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, }; System.out.println(countIslands(m1)); int[][] m2 = { { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 1, 1, 1, 1, 1, 1, 0 }, { 0, 1, 1, 1, 0, 0, 0, 1, 0 }, { 0, 1, 1, 0, 0, 0, 1, 1, 0 }, { 0, 0, 0, 0, 0, 1, 1, 0, 0 }, { 0, 0, 0, 0, 1, 1, 1, 0, 0 }, { 0, 0, 0, 0, 0, 0, 0, 0, 0 }, }; System.out.println(countIslands(m2)); } }
多任務解題思路:
要解決合併島的問題
把島的數量和邊界信息存儲起來。
邊界信息要如何合併:
標記感染中心。(並查集應用)
看邊界A和邊界C是否合併過,沒有就合併(指向同一個標記),島數量減一。
如何一路下去會碰到B和C,再次檢查,合併,島減一。
連成一片的這個概念,用並查集這個結構能很是好作,在結構上,怎麼避免已經合完的部分,不重複減島這個問題,用並查集來解決。
多邊界的話就是收集的信息多一點而已,合併思路是同樣的。
能夠把邊界信息都扔在一個並查集裏面合併。(和麪試官吹水的部分)
能夠分紅多個部分給多個CPU操做,獲得結果後再合併最後的結果。(看具體狀況,可以使用二分法)
認識並查集結構(用以前給全部的數據)
一、很是快的檢查兩個元素是否在同一個集合。isSameSet
二、兩個元素各類所在的集合,合併在一塊兒。Union(元素,元素)
使用list的話合併快,查詢是否在同一個集合慢。
使用set的話查詢快,合併慢。
本身指向本身的就是表明節點。
A/B向上找表明節點,相同就是在同一個集合。
怎麼合併
少元素的掛在多元素的底下。
優化:(路徑壓縮)
在一次查詢後,把路徑上的節點統一打平。
public class Code_04_UnionFind { public static class Node { // whatever you like } public static class UnionFindSet { public HashMap<Node, Node> fatherMap; public HashMap<Node, Integer> sizeMap; //建立的時候就要一次性導入全部的節點 public UnionFindSet(List<Node> nodes) { fatherMap = new HashMap<Node, Node>(); sizeMap = new HashMap<Node, Integer>(); makeSets(nodes); } private void makeSets(List<Node> nodes) { fatherMap.clear(); sizeMap.clear(); for (Node node : nodes) { fatherMap.put(node, node);//一開始本身是本身的父親 sizeMap.put(node, 1);//大小爲1 } } private Node findHead(Node node) { //非遞歸版本 Stack<Node> Nodes = new Stack<>(); Node cur = node; Node parent = fatherMap.get(cur); while(cur != parent){ Nodes.push(cur); cur = parent; parent = fatherMap.get(cur); } while(!Nodes.isEmpty()){ fatherMap.put(Nodes.pop(),parent); } return parent; //遞歸版本 /* //得到節點的父節點 Node father = fatherMap.get(node); if (father != node) {//這樣找是由於頭節點是本身指向本身的 //一路向上找父節點 father = findHead(father); } fatherMap.put(node, father);//路徑壓縮 return father;*/ } public boolean isSameSet(Node a, Node b) { return findHead(a) == findHead(b); } public void union(Node a, Node b) { if (a == null || b == null) { return; } Node aHead = findHead(a); Node bHead = findHead(b); if (aHead != bHead) { int aSetSize= sizeMap.get(aHead); int bSetSize = sizeMap.get(bHead); if (aSetSize <= bSetSize) {//a小於b fatherMap.put(aHead, bHead); sizeMap.put(bHead, aSetSize + bSetSize); } else {//a大於b fatherMap.put(bHead, aHead); sizeMap.put(aHead, aSetSize + bSetSize); } } } } public static void main(String[] args) { } }
並查集是1964年別人腦補的一個算法,到證實結束是1989年,這個證實也是夠漫長的。
並查集的效率很是高,當有N個數據的時候,假設查詢次數到了N以後,其時間複雜度僅爲o(1)!!
查詢次數+合併次數逼近o(n)以上,平均時間複雜度o(1)