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
- 擴容負載因子
- 拉鏈邊長,轉為紅黑樹
- 優化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)