數據結構思維(十一)哈希

在本章中,我定義了一個比MyLinearMap更好的Map接口實現,MyBetterMap,並引入哈希,這使得MyBetterMap效率更高。


1.哈希

爲了提高MyLinearMap的性能,我們將編寫一個新的類,它被稱爲MyBetterMap,它包含MyLinearMap對象的集合。它在內嵌的映射之間劃分鍵,因此每個映射中的條目數量更小,這加快了findEntry,以及依賴於它的方法的速度。

類定義的開始:

public class MyBetterMap<K, V> implements Map<K, V> {

    protected List<MyLinearMap<K, V>> maps;

    public MyBetterMap(int k) {
        makeMaps(k);
    }

    // k值關係到hashCode的分配
    protected void makeMaps(int k) {
        maps = new ArrayList<MyLinearMap<K, V>>(k);
        for (int i=0; i<k; i++) {
            maps.add(new MyLinearMap<K, V>());
        }
    }
}

實例變量maps是一組MyLinearMap對象。構造函數接受一個參數k決定至少最開始,要使用多少個映射。然後makeMaps創建內嵌的映射並將其存儲在一個ArrayList中。

現在,完成這項工作的關鍵是,我們需要一些方法來查看一個鍵,並決定應該進入哪個映射(首先要放進去,放到哪裏用HashCode算法決定)。當我們put一個新的鍵時,我們選擇一個映射;當我們get同樣的鍵時,我們必須記住我們把它放在哪裏(重點是要確定如何跟蹤)。

一種可能性是隨機選擇一個子映射,並跟蹤我們把每個鍵放在哪裏(無法跟蹤)。

一個更好的方法是使用一個哈希函數,它接受一個Object,一個任意的Object,並返回一個稱爲哈希碼的整數。重要的是,如果它不止一次看到相同的Object,它總是返回相同的哈希碼。這樣,如果我們使用哈希碼來存儲鍵,當我們查找時,我們將得到相同的哈希碼。

在Java中,每個Object都提供了hashCode,一種計算哈希函數的方法。這種方法的實現對於不同的對象是不同的;我們會很快看到一個例子。

選擇到正確的子映射:

/**
* @Author Ragty
* @Description 爲一個給定的鍵選擇正確的子映射(同makeMaps的初始化數量有關)
* @Date 10:53 2019/4/23
**/
protected MyLinearMap<K, V> chooseMap(Object key) {
    int index = 0;
    if (key != null) { 
        index = Math.abs(key.hashCode()) % maps.size();
    }
    return maps.get(index);
}

如果keynull,我們選擇索引爲0的子映射。否則,我們使用hashCode獲取一個整數,調用Math.abs來確保它是非負數,然後使用餘數運算符%這保證結果在0maps.size()-1之間所以index總是一個有效的maps索引。然後chooseMap返回爲其所選的映射的引用。

我們使用chooseMapputget,所以當我們查詢鍵的時候,我們得到添加時所選的相同映射,我們選擇了相同的映射。至少應該是 - 稍後我會解釋爲什麼這可能不起作用

putget實現:

public V put(K key, V value) {
  MyLinearMap<K, V> map = chooseMap(key);
    return map.put(key, value);
}

public V get(Object key) {
    MyLinearMap<K, V> map = chooseMap(key);
    return map.get(key);
}

我們使用chooseMap來找到正確的子映射,然後在子映射上調用一個方法,我們考慮一下性能.

如果在k個子映射中分配了n個條目,則平均每個映射將有n/k個條目。當我們查找一個鍵時,我們必須計算其哈希碼,這需要一些時間,然後我們搜索相應的子映射。

因爲MyBetterMap中的條目列表,比MyLinearMap中的短k倍,我們的預期是ķ倍的搜索速度。但運行時間仍然與n成正比(n/k),所以MyBetterMap仍然是線性的。在下一個練習中,你將看到如何解決這個問題。

關於此問題的思考,現在考慮一種比較極端的情況,如果恰好所有數據都集中在一個子映射中,那麼效率將跟線性方法一樣.


2.哈希如何工作

哈希函數的基本要求是,每次相同的對象應該產生相同的哈希碼。對於不變的對象,這是比較容易的。對於具有可變狀態的對象,我們必須花費更多精力。

作爲一個不可變對象的例子,我將定義一個SillyString類,它包含一個String

public class SillyString {
    private final String innerString;

    public SillyString(String innerString) {
        this.innerString = innerString;
    }

    public String toString() {
        return innerString;
    }

使用它來展示,一個類如何定義它自己的哈希函數:

    @Override
    public boolean equals(Object other) {
        return this.toString().equals(other.toString());
    }

    @Override
    public int hashCode() {
        int total = 0;
        for (int i=0; i<innerString.length(); i++) {
            total += innerString.charAt(i);
        }
        return total;
    }

注意SillyString重寫了equalshashCode。這個很重要。爲了正常工作,equals必須和hashCode一致,這意味着如果兩個對象被認爲是相等的 - 也就是說,equals返回true - 它們應該有相同的哈希碼。但這個要求只是單向的;如果兩個對象具有相同的哈希碼,則它們不一定必須相等。(不可逆)

hashCode的原理是,迭代String中的字符並將它們相加。當你向int添加一個字符時,Java 將使用其 Unicode 代碼點,將字符轉換爲整數。

該哈希函數滿足要求:如果兩個SillyString對象包含相等的內嵌字符串,則它們將獲得相同的哈希碼。

這可以正常工作,但它可能不會產生良好的性能,因爲它爲許多不同的字符串返回相同的哈希碼。如果兩個字符串以任何順序包含相同的字母,它們將具有相同的哈希碼。即使它們不包含相同的字母,它們可能會產生相同的總量,例如"ac""bb"。或者abccab的哈希碼也是一致的

如果許多對象具有相同的哈希碼,它們將在同一個子映射中。如果一些子映射比其他映射有更多的條目,那麼當我們有k個映射時,加速比可能遠遠小於k所以哈希函數的目的之一是統一;也就是說,以相等的可能性,在這個範圍內產生任何值。(等概率的在子映射中分配條目,這正是之前我們所擔心的)


3.哈希和可變性

String是不可變的,SillyString也是不可變的,因爲innerString定義爲final。一旦你創建了一個SillyString,你不能使innerString引用不同的String,你不能修改所指向的String。因此,它將始終具有相同的哈希碼。

我們現在用一個Array,和以上的一樣,改變的是之前是String,現在是數組(可變)

public class SillyArray {
    private final char[] array;

    public SillyArray(char[] array) {
        this.array = array;
    }

    public String toString() {
        return Arrays.toString(array);
    }

    @Override
    public boolean equals(Object other) {
        return this.toString().equals(other.toString());
    }

    @Override
    public int hashCode() {
        int total = 0;
        for (int i=0; i<array.length; i++) {
            total += array[i];
        }
        System.out.println(total);
        return total;
    }

SillyArray也提供setChar,它能夠修改修改數組內的字符。

public void setChar(int i, char c) {
    this.array[i] = c;
}

現在假設我們創建了一個SillyArray,並將其添加到map

SillyArray array1 = new SillyArray("Word1".toCharArray());
map.put(array1, 1);

這個數組的哈希碼是461。現在如果我們修改了數組內容,之後嘗試查詢它,像這樣:

array1.setChar(0, 'C');
Integer value = map.get(array1);

修改之後的哈希碼是441使用不同的哈希碼,我們就很可能進入了錯誤的子映射。這就很糟糕了。

一般來說,使用可變對象作爲散列數據結構中的鍵是很危險的,這包括MyBetterMapHashMap。如果你可以保證映射中的鍵不被修改,或者任何更改都不會影響哈希碼,那麼這可能是正確的。但是避免這樣做可能是一個好主意。(個人認爲還是直接避免這種不安全的操作比較好)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章