前提知識
寫在前面,為什麼num&(length - 1) 在length是2的n次冪的時候等價於num%length
n - 1意味著比n最高位小的位都為1,而高的位都為0,因此通過與可以剔除位數比n最高位更高的部分,只保留比n最高位小的部分,也就是取餘了。
而且用位運算取代%,效率會比較高。
基於以上幾點,我們再看看hashmap中如何計算hash值得
這裡吧key的hashcode取出來,然後把它右移16位,然後取異或
這裡從我Google得到的資訊是,int是4個位元組,也就是32位,我們右移16位也即是把高位的資料右移到低位的16位,然後做異或,那就是把高位和低位的資料進行重合
同時保留了低位和高位的資訊
但是為什麼是右移16位,這邊保留疑問,我要是右移8位,4位,2位呢???
不做右移肯定不是,不做右移直接異或,那不就是0麼
我們直接做個測試
public static int hash(Object key) { int h; //也就將key的hashCode無符號右移16位然後與hashCode異或從而得到hash值在putVal方法中(n - 1)& hash計算得到桶的索引位置 //注意,這裡h是int值,也就是32位,然後無符號又移16位,那麼就是折半,折半之後和原來的資料做異或操作,正好整合了高位和低位的資料 //混合原始雜湊碼的高位和低位,以此來加大低位的隨機性,而且混合後的低位摻雜了高位的部分特徵,這樣高位的資訊也被變相保留下來。 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } //測試,如果我們不做高位低位的操作看看hash衝突是大還是小 public static int hash2(Object key) { return (int) key; } public static int hash3(Object key) { int h = key.hashCode(); //我們不做右移試試,那就自己跟自己異或。。。沒意義,只能是0了 return (key == null) ? 0 : h ^ h; } public static int hash4(Object key) { int h; //我們不做右移試試,或者右移8位試試 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 8); } public static int hash5(Object key) { int h; //我們不做右移試試,或者右移8位試試 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 4); } public static int hash6(Object key) { int h; //我們不做右移試試,或者右移8位試試 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 2); }
hash3的測試可以去除不用考慮了
public void testHash() { //產生隨機數 int init = 64 * 64; int size = 64 * 64; for(int j = 0; j < 10; ++j) { // int size = init * (j+1); size *= 2; int hash1[] = new int[size]; int hash2[] = new int[size]; int hash3[] = new int[size]; int hash4[] = new int[size]; int hash5[] = new int[size]; int hash6[] = new int[size]; int testCount = size; int exist1 = 0; int exist2 = 0; int exist3 = 0; int exist4 = 0; int exist5 = 0; int exist6 = 0; for(int i = 0; i < testCount; ++i) { int key = (int) ((Math.random() * (size - 1)) + 1); if(hash1[MyHashMap.hash(key)&(size - 1)] != 0) { exist1++; } else { hash1[MyHashMap.hash(key)&(size - 1)] = 1; } } for(int i = 0; i < testCount; ++i) { int key = (int) ((Math.random() * (size - 1)) + 1); if(hash2[MyHashMap.hash2(key)&(size - 1)] != 0) { exist2++; } else { hash2[MyHashMap.hash2(key)&(size - 1)] = 1; } } // for(int i = 0; i < testCount; ++i) { // // int key = (int) ((Math.random() * (size - 1)) + 1); // // if(hash3[MyHashMap.hash3(key)&(size - 1)] != 0) { // exist3++; // } else { // hash3[MyHashMap.hash3(key)&(size - 1)] = 1; // } // } for(int i = 0; i < testCount; ++i) { int key = (int) ((Math.random() * (size - 1)) + 1); if(hash4[MyHashMap.hash4(key)&(size - 1)] != 0) { exist4++; } else { hash4[MyHashMap.hash4(key)&(size - 1)] = 1; } } for(int i = 0; i < testCount; ++i) { int key = (int) ((Math.random() * (size - 1)) + 1); if(hash5[MyHashMap.hash5(key)&(size - 1)] != 0) { exist5++; } else { hash5[MyHashMap.hash5(key)&(size - 1)] = 1; } } for(int i = 0; i < testCount; ++i) { int key = (int) ((Math.random() * (size - 1)) + 1); if(hash6[MyHashMap.hash6(key)&(size - 1)] != 0) { exist6++; } else { hash6[MyHashMap.hash6(key)&(size - 1)] = 1; } } System.out.println("衝突比較:\t1:" + exist1 + "\t2:" + exist2 + "\t4:" + exist4 + "\t5:" + exist5 + "\t6:" + exist6); } }
開始測試:
上面是size會遞增的,現在我們先測size不變的情況看看效果
size=64 * 64
從結果上看明顯是右移8位衝突比較少!!!
我們把size擴大一倍
再擴大一倍
這次還比較平價
有人會說這是因為每次隨機的數不一樣的,每次都是產生新的隨機數,沒有可比性
那麼我們每次用一個固定的數去進行hash碰撞
還是64*64開始,依次乘以2,4
@org.junit.jupiter.api.Test public void testHash2() throws InterruptedException { //產生隨機數 int init = 64 * 64; int size = 8; System.out.println("衝突比較:\t1:jdk1.8\t2:沒有操作\t\t4:右移8位\t\t5:右移4位\t\t6:右移2位"); for(int j = 0; j < 10; ++j) { // int size = init * (j+1); size = 8 * j + size; int hash1[] = new int[size]; int hash2[] = new int[size]; int hash4[] = new int[size]; int hash5[] = new int[size]; int hash6[] = new int[size]; int testCount = size / 3; int exist1 = 0; int exist2 = 0; int exist4 = 0; int exist5 = 0; int exist6 = 0; for(int i = 0; i < testCount; ++i) { Thread.sleep(i + 1); int key = (int) ((Math.random() * (size - 1)) + 1) * j * (i + 1); if(hash1[MyHashMap.hash(key)&(size - 1)] != 0) { exist1++; } else { hash1[MyHashMap.hash(key)&(size - 1)] = 1; } if(hash2[MyHashMap.hash2(key)&(size - 1)] != 0) { exist2++; } else { hash2[MyHashMap.hash2(key)&(size - 1)] = 1; } if(hash4[MyHashMap.hash4(key)&(size - 1)] != 0) { exist4++; } else { hash4[MyHashMap.hash4(key)&(size - 1)] = 1; } if(hash5[MyHashMap.hash5(key)&(size - 1)] != 0) { exist5++; } else { hash5[MyHashMap.hash5(key)&(size - 1)] = 1; } if(hash6[MyHashMap.hash6(key)&(size - 1)] != 0) { exist6++; } else { hash6[MyHashMap.hash6(key)&(size - 1)] = 1; } } System.out.println("衝突比較:\t1:" + exist1 + "\t\t2:" + exist2 + "\t\t4:" + exist4 + "\t\t\t5:" + exist5 + "\t\t6:" + exist6); } }
我們還是執行三次比較:
從第一次看結果好像jdk自帶的方式衝突還比較多。。。
我最後再來一次,怎麼感覺越來越不對勁。。。
總結:我懵逼了啊!!!為什麼啊,我測試出來感覺jdk自帶的右移16位的方式,並不能有效減少衝突,反而右移4或者8位測試效果比較好!!!
求大神解答!!!