算法很美 筆記 10.哈希表、哈希映射


10.哈希表、哈希映射

1.HashMap,HashSet

基本概念

  • 若關鍵字為k ,則其值存放在f(k)的存儲位置上。由此,不需比較便可直接取得所查記錄。稱這個對應關系f為散列函數,按這個思想建立的表為散列表

  • 對不同的關鍵字可能得到同一散列地址,即k1≠k2 ,而f(k1)=f(k2) ,這種現象稱為沖突(英語: Collision)。具有相同函數值的關鍵字對該散列函數來說稱做同義詞。綜上所述,根據散列函數f(k)和處理沖突的方法將一組關鍵字映射到一個有限的連續的地址集(區間).上,並以關鍵字在地址集中的“像”作為記錄在表中的存儲位置,這種表便稱為散列表,這一映射過程稱為散列造表或散列,所得的存儲位置稱散列地址。

壓縮映射

兩個關鍵

  • 散列函數

    • 直接定址法

    • 數字分析法

    • 平方取中法

    • 折疊法

    • 隨機數法

    • 除留余數法

  • 沖突解決

    • 開放定址法

    • 拉鏈法

    • 雙散列

    • 再散列

Java的HashMap

public class HashMapTest {
    public static void main(String[] args) {
        testHashMapAPIs();
    }
    private static void testHashMapAPIs() {
        // 初始化隨機種子
        Random r = new Random();
        // 新建HashMap
        HashMap map = new HashMap();
        // 添加操作
        map.put("one", r.nextInt(10));
        map.put("one", r.nextInt(10));
        map.put("two", r.nextInt(10));
        map.put("three", r.nextInt(10));

        // 打印出map
        System.out.println("map:" + map);

        // 通過Iterator遍歷key-value
        Iterator iter = map.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();
            System.out.println("next : " + entry.getKey() + " - " + entry.getValue());
        }

        // HashMap的鍵值對個數
        System.out.println("size:" + map.size());

        // containsKey(Object key) :是否包含鍵key
        System.out.println("contains key two : " + map.containsKey("two"));
        System.out.println("contains key five : " + map.containsKey("five"));

        // containsValue(Object value) :是否包含值value
        System.out.println("contains value 0 : " + map.containsValue(new Integer(0)));

        // remove(Object key) : 刪除鍵key對應的鍵值對
        map.remove("three");

        System.out.println("map:" + map);

        // clear() : 清空HashMap
        map.clear();

        // isEmpty() : HashMap是否為空
        System.out.println((map.isEmpty() ? "map is empty" : "map is not empty"));
    }
}
map:{one=9, two=0, three=9}
next : one - 9
next : two - 0
next : three - 9
size:3
contains key two : true
contains key five : false
contains value 0 : true
map:{one=9, two=0}
map is empty
  1. 擴容負載因子
  2. 拉鏈邊長,轉為紅黑樹
  3. 優化hash函數

2.布隆過濾器

基本概念

  • 布隆過濾器(英語:Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率和刪除困難。

  • 如果想判斷一個元素是不是在一個集合里,一般想到的是將集合中所有元素保存起來,然后通過比較確定。鏈表、樹、散列表(又叫哈希表,Hash table)等數據結構都是這種思路。但是隨着集合中元素的增加,我們需要的存儲空間越來越大。同時檢索速度也越來越慢,上述三種結構的檢索時間復雜度分別為O(n),O(logn),O(n/k)。布隆過濾器的原理是,當一個元素被加入集合時,通過K個散列函數將這個元素映射成一個位數組中的K個點,把它們置為1。檢索時,我們只要看看這些點是不是都是1就(大約)知道集合中有沒有它了:如果這些點有任何一個0,則被檢元素一定不在;如果都是1,則被檢元素很可能在。這就是布隆過濾器的基本思想。

  • 一個Bloom Filter是基於一個m位的位向量(1...bm) ,這些位向量的初始值為0。另外,還有一
    系列的hash函數(h...hk),這些hash函數的值域屬於1~m。下圖是一個bloom filter插入x,y,z並
    判斷基個值w是否在該數據集的示意圖:

不用保存原始的數據,只存儲位圖

簡化版實現

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

/**簡化版本的布隆過濾器的實現*/
public class BloomFilter {
    public static final int NUM_SLOTS = 1024 * 1024 * 8;//位圖的長度
    public static final int NUM_HASH = 8;//hash函數的個數,一個hash函數的結果用於標記一個位
    private BigInteger bitmap = new BigInteger("0");//位圖

    public static void main(String[] args) {
        //測試代碼
        BloomFilter bf = new BloomFilter();
        ArrayList<String> contents = new ArrayList<>();
        contents.add("sldkjelsjf");
        contents.add("ggl;ker;gekr");
        contents.add("wieoneomfwe");
        contents.add("sldkjelsvrnlkjf");
        contents.add("ksldkflefwefwefe");

        for (int i = 0; i < contents.size(); i++) {
            bf.addElement(contents.get(i));
        }
        System.out.println(bf.check("sldkjelsvrnlkjf"));
        System.out.println(bf.check("sldkjelnlkjf"));
        System.out.println(bf.check("ggl;ker;gekr"));
    }

    /**將message+n映射到0~NUM_SLOTS-1之間的一個值*/
    private int hash(String message, int n) {
        message = message + String.valueOf(n);
        try {
            MessageDigest md5 = MessageDigest.getInstance("md5");//將任意輸入映射成128位(16個字節)整數的hash函數
            byte[] bytes = message.getBytes();
            md5.update(bytes);
            byte[] digest = md5.digest();
            BigInteger bi = new BigInteger(digest);//至此,獲得message+n的md5結果(128位整數)

            return Math.abs(bi.intValue()) % NUM_SLOTS;
        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(BloomFilter.class.getName()).log(Level.SEVERE, null, ex);
        }
        return -1;
        // return (int)Math.abs(HashFunctions.bernstein(message,NUM_SLOTS));
    }

    /*處理原始數據
     * 1.hash1(msg)標注一個位……  hash的值域0~NUM_SLOTS-1
     * */
    public void addElement(String message) {
        for (int i = 0; i < NUM_HASH; i++) {
            int hashcode = hash(message, i);//代表了hash1,hash2……hash8
            //結果,用於標注位圖的該位為1
            if (!bitmap.testBit(hashcode)) {//如果還不為1
                //標注位圖的該位為1
                bitmap = bitmap.or(new BigInteger("1").shiftLeft(hashcode));
            }
        }

    }

    public boolean check(String message) {
        for (int i = 0; i < NUM_HASH; i++) {
            int hashcode = hash(message, i);
            //hashcode代表一個位置
            if (!this.bitmap.testBit(hashcode)) {
                //如果位圖的該位為0,那么message一定不存在
                return false;
            }
        }
        return true;//不精確,有可能誤判
    }
}

3.一致性hash

緩存集群/負載均衡

基本思路

先構造一個長度為232的整數環(這個環被稱為一致性Hash環),根據節點名稱的Hash值(其分布為[0, 232-1])將緩存服務器節點放置在這個Hash環上,然后根據需要緩存的數據的Key值計算得到其Hash值(其分布也為[0, 232-1]),然后在Hash環上順時針查找距離這個Key值的Hash值最近的服務器節點,完成Key到服務器的映射查找。

增加/刪除節點

  • 如果往集群中添加一個新的節點NODE4,通過對應的哈希算法得到KEY4,並映射到環中

  • 按順時針遷移的規則,那么被分割的對象被遷移到了NODE4中其它對象還保持這原有的存儲位置。

數據傾斜

  • 如果機器較少,很有可能造成機器在整個環上的分布不均勻,從而導致機器之間的負載不均衡

虛擬節點

代碼實現

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;

/*不考慮數據傾斜*/
public class ConsistentHashing1 {
    //hash算法,將關鍵字映射到2^32的環狀空間里面
    static long hash(String key) {
        ByteBuffer buf = ByteBuffer.wrap(key.getBytes());
        int seed = 0x1234ABCD;
        ByteOrder byteOrder = buf.order();
        buf.order(ByteOrder.LITTLE_ENDIAN);
        long m = 0xc6a4a7935bd1e995L;
        int r = 47;
        long h = seed ^ (buf.remaining() * m);
        long k;
        while (buf.remaining() >= 8) {
            k = buf.getLong();
            k *= m;
            k ^= k >>> r;
            k *= m;

            h ^= k;
            h *= m;
        }
        if (buf.remaining() > 0) {
            ByteBuffer finish = ByteBuffer.allocate(8).order(
                    ByteOrder.LITTLE_ENDIAN);
            // for big-endian version, do this first:
            // finish.position(8-buf.remaining());
            finish.put(buf).rewind();
            h ^= finish.getLong();
            h *= m;
        }
        h ^= h >>> r;
        h *= m;
        h ^= h >>> r;

        buf.order(byteOrder);
        return Math.abs(h);
    }

    //機器節點==網絡節點
    static class Node implements HashNode {
        String name;
        String ip;
        public Node(String name, String ip) {
            this.name = name;
            this.ip = ip;
        }
        @Override
        public String toString() {
            return this.name + "-" + this.ip;
        }
        @Override
        public String getName() {
            return name;
        }
    }

    interface HashNode {
        String getName();
    }

    //  節點列表
    List<Node> nodes;
    TreeMap<Long, Node> hashAndNode = new TreeMap<>();
    TreeMap<Long, Node> keyAndNode = new TreeMap<>();

    public ConsistentHashing1(List<Node> nodes) {
        this.nodes = nodes;
        init();
    }

    private void init() {
        for (int i = 0; i < nodes.size(); i++) {
            Node node = nodes.get(i);
            long hash = hash(node.ip);
            hashAndNode.put(hash, node);
        }
    }

    private void add(String key) {
        long hash = hash(key);
        SortedMap<Long, Node> subMap = hashAndNode.tailMap(hash);//找到map中key比fromKey大的所有的鍵值對,組成一個子Map
        if (subMap.size() == 0) {//hash值大於所有機器的hash歸屬於第一台機器
            keyAndNode.put(hash, hashAndNode.firstEntry().getValue());
        } else {//在大於hash中找到最小,
            Node node = subMap.get(subMap.firstKey());//第一個節點,key應該歸屬的節點
            keyAndNode.put(hash, node);
        }
    }

    /**
     * 增加一個新的機器節點
     * @param newNode
     */
    private void add(Node newNode) {
        long hash = hash(newNode.ip);
        hashAndNode.put(hash, newNode);
        //  數據遷移
        SortedMap<Long, Node> pre = hashAndNode.headMap(hash);//key小於hash的子map
        if (pre.size() == 0) {
            SortedMap<Long, Node> between = keyAndNode.subMap(0L, hash);
            for (Map.Entry<Long, Node> e : between.entrySet()) {
                e.setValue(newNode);
            }
            between = keyAndNode.tailMap(hashAndNode.lastKey());
            for (Map.Entry<Long, Node> e : between.entrySet()) {
                e.setValue(newNode);
            }
        } else {
            long from = pre.lastKey();
            long to = hash;
            SortedMap<Long, Node> between = keyAndNode.subMap(from, to);
            for (Map.Entry<Long, Node> e : between.entrySet()) {
                e.setValue(newNode);
            }
        }
    }

    public static void main(String[] args) {
        List<Node> nodes = new ArrayList<>();
        nodes.add(new Node("node1", "192.168.1.2"));
        nodes.add(new Node("node2", "192.168.1.3"));
        nodes.add(new Node("node3", "192.168.1.4"));
        nodes.add(new Node("node4", "192.168.1.5"));
        nodes.add(new Node("node5", "192.168.1.6"));
        ConsistentHashing1 obj = new ConsistentHashing1(nodes);
        for (Map.Entry<Long, Node> entry :
                obj.hashAndNode.entrySet()) {
            System.out.println(entry.getKey() + ":" + entry.getValue().getName());
        }
        obj.add("a");
        obj.add("b");
        obj.add("c");
        obj.add("e");
        obj.add("zhangsan");
        obj.add("lisi");
        obj.add("wangwu");
        obj.add("zhaoliu");
        obj.add("wangchao");
        obj.add("mahan");
        obj.add("zhanglong");
        obj.add("zhaohu");
        obj.add("baozheng");
        obj.add("gongsun");
        obj.add("zhanzhao");
        for (Map.Entry<Long, Node> entry :
                obj.keyAndNode.entrySet()) {
            System.out.println(entry.getKey() + " ,歸屬到:" + entry.getValue().getName());
        }
        System.out.println("===========");
        obj.add(new Node("node6", "192.168.1.77"));
        for (Map.Entry<Long, Node> entry :
                obj.keyAndNode.entrySet()) {
            System.out.println(entry.getKey() + " ,歸屬到:" + entry.getValue().getName());
        }
    }
}

4.題解

1、位(bit) 來自英文bit,音譯為“比特”,表示二進制位。

2、字節(byte) 字節來自英文Byte,音譯為“拜特”,習慣上用大寫的“B”表示

題1:出現次數最多的數

有一個包含20億個全是32位整數的大文件,在其中找到出現次數最多的數

通常的做法是使用hashmap

(4字節int型)key---具體的某一種數

(4字節int型)value---這種數出現的次數

那么一條key-value記錄占有8字節

當記錄數為2億時,大約占用1.6G內存

那么如果20億數據全部不相同,明顯內存會溢出

優化解決方法:

使用哈希函數進行分流成16個小文件,由於哈希函數的性質,同一種數不會被分流到不同文件,而且對於不同的數,因為哈希函數分流是比較均勻的分配的,所以一般不會出現一個文件含有2億個不同的整數情況,每個文件含有的種樹也幾乎一樣

然后分別計算出每個文件中出現次數的第一名。

然后對這些第一名全部拿出來進行排序即可

題2:所有沒出現過的數

32位無符號整數的范圍是0~4294967295,現在有一個正好包含40億個無符號整數的文件,所以在整個范圍中必然有沒出現過的數。可以使用最多1G的內存,怎么找到所有沒出現過的數。

申請一個bit數組,數組大小為4294967295,大概為40億bit,40億/8 = 5億字節,那么需要0.5G空間, bit數組的每個位置有兩種狀態0和1,那么怎么使用這個bit數組呢?呵呵,數組的長度剛好滿足我們整數的個數范圍,那么數組的每個下標值對應4294967295中的一個數,逐個遍歷40億個無符號數,例如,遇到100,則bitArray[100] = 1,遇到9999,則bitArray[9999] = 1,遍歷完所有的數,將數組相應位置變為1。

題3:重復的URL

找到100億個URL中重復的URL以及搜索詞匯的topK問題。使用哈希函數進行分流成n個機器,n個機器又分流成n個小文件。利用小根堆排序選出每個文件top100,然后再進行整理選出每台機器的top100,最終再次整理得到總的top100(利用堆排序處理topK 的問題比較方便,時間復雜度為nlogn)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM