算法初級面試題05——哈希函數/表、生成多個哈希函數、哈希擴容、利用哈希分流找出大文件的重複內容、設計RandomPool結構、布隆過濾器、一致性哈希、並查集、島問題

今天主要討論:哈希函數、哈希表、布隆過濾器、一致性哈希、並查集的介紹和應用。前端

 

題目一

認識哈希函數和哈希表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)

 

相關文章
相關標籤/搜索