散列(二)

上一章 散列(一) 主要介紹了散列的基本概念以及沖突解決方法--分離鏈表法。這一章主要介紹解決沖突的另一種方法---開放定址法。

開放定址法:嘗試另外一些單元,直到找出空的單元為止。
  • 線性探測法:當產生沖突時,它將尋找下一個空閑地址放入。
  • 平方探測法:使用 f(i) = i 2 的方法來解決沖突,并且保證如果表有一半為空,并且表的大小為素數,那么我們保證總能夠插入一個新的元素。
  • 雙散列:使用如下探測方法:


    double_hashing.png
線性探測法:

在線性探測法中,函數f是i的線性函數,典型的情形為f(i) = i 。 這相當于探測逐個單元(必要時可以回繞)以查找出一個空單元。

線性探測.png

如上圖,我們逐個插入關鍵字{89,18,49,58,69}。第一個沖突發生在插入49關鍵字,它和89產生了沖突(因為49%10=9且89%10=9),因此,49被推入下一個空閑位置,即位置0 (注意這里是可以回繞的) ,緊接著插入58,58和18沖突了,則找下一個空閑位置,找到位置1.對于69的沖突也是一樣的。

我們發現即使表相對較空,還是會發生一些占據的單元集中在一些塊區,這種現象我們成為一次聚集
也就是說,散列在區塊中的任何關鍵字都需要多次試選單元才能解決沖突,然后將關鍵字添加進去。

實驗證明,當裝填因子(散列表中元素個數與該表大小的比)在0 ~ 0.5之間所需探測的次數時較小的,考慮到探測次數和rehash的消耗,我們一般采用0.5作為裝填因子會達到比較好的效果。

線性探測.png
平方探測法
  • 平方探測法是消除線性探測中一次聚集問題的解決沖突的方法。平方探測就是沖突函數為二次的探測方法。
  • 對于線性探測,讓散列表中填滿元素并不是一個好主意,因為此時表的性能在下降。而對于平方探測方法情況甚至更糟:一旦表被填充了一半,當表的大小不是素數時甚至在表被填充一半之前,就不能保證一次找到空的單元了。這是因為最多有表的一半作為解決沖突的備選位置
  • 定理:** 如果使用平方探測,且表的大小是素數,那么當表至少有一半是空的時候,總能夠插入一個新的元素**。
  • 在探測散列表中的刪除操作,我們不能直接執行,因為相應的單元可能已經引起過沖突,被轉移到其他地方了。

a. 定義一個類用來標記每個位置的值以及其是否處于活動狀態(即是否存在值)

    /**
     * 定義一個類用來標記每個位置的情況
     * @param <AnyType>
     */
    private static class HashEntry<AnyType>{
        //當前位置的元素值
        public AnyType element;
        //當前位置是否為活動狀態,默認為活動狀態,但若刪除后,會設置其為非活動狀態
        public boolean isActive;

        public HashEntry(AnyType e){
            this(e, true);
        }

        public HashEntry(AnyType e, boolean b){
            element = e;
            isActive = b;
        }
    }

b. 定義所需變量:

    //默認表的大小
    private static final int DEFAULT_TABLE_SIZE = 11;
    //存儲表
    private HashEntry<AnyType> [] array;
    //當前表的大小
    private int currentSize;

c. 進行初始化操作:

    //無參數構造函數
    public QuadraticProbingHashTable(){
        this(DEFAULT_TABLE_SIZE);
    }
    //有參數構造函數
    public QuadraticProbingHashTable(int size){
        allocateArray(size);
        makeEmpty();
    }
    //清空表
    public void makeEmpty(){
        currentSize = 0;
        for (int i = 0; i < array.length; i ++){
            array[i] = null;
        }
    }
    //初始化表
    private void allocateArray(int size){
        array = new HashEntry[nextPrime(size)];
    }

c. 解決沖突位置:

    /**
     * 尋找空閑位置,以解決沖突
     * @param x
     * @return
     */
    private int findPos(AnyType x){
        //定義偏移量
        int offset = 1;
        //獲取到hash位置
        int currentPos = myHash(x);
        //若hash位置中存在元素,并且當前元素不等于傳入的元素
        while (array[currentPos] != null && !array[currentPos].element.equals(x)){
            //進行偏移
            currentPos += offset;
            //改變偏移量
            offset += 2;
            //考慮到溢出情況
            if (currentPos >= array.length){
                currentPos -= array.length;
            }
        }
        return currentPos;
    }

d. 插入操作:

    //插入元素
    public void insert(AnyType x){
        //獲取到空閑位置
        int currentPos = findPos(x);
        //若該位置為活動狀態,則返回,表示該位置已經存在元素
        //這種情況,實際上表示該位置上已經存在了該元素,那么不必重復插入
        if (isActive(currentPos)){
            return;
        }
        //否則,插入該元素
        array[currentPos] = new HashEntry<AnyType>(x);
        //判斷表的大小,超過一半,則進行rehash
        if (++ currentSize > array.length / 2){
            rehash();
        }
    }
    //判斷當前位置是否為活動狀態
    private boolean isActive(int currentPos){
        return array[currentPos] != null && array[currentPos].isActive;
    }

e. 刪除操作:

public void remove(AnyType x){
        //找到位置
        int currentPos = findPos(x);
        //若該位置為活動狀態,則進行刪除操作
        if (isActive(currentPos)){
            //令該位置為非活動狀態即可
            array[currentPos].isActive = false;
            currentSize --;
        }
    }

f. 查詢操作:

public boolean contains(AnyType x){
        int currentPos = findPos(x);
        //返回該位置是否為活動狀態
        return isActive(currentPos);
 }

g. rehash操作:

private void rehash(){
        HashEntry<AnyType> [] oldArray = array;
        //擴充表的大小
        allocateArray(nextPrime(2 * oldArray.length));
        currentSize = 0;
        //將舊表的數據添加到新表中
        for (int i = 0; i < oldArray.length; i ++){
            if (oldArray[i] != null && oldArray[i].isActive){
                insert(oldArray[i].element);
            }
        }
    }
完整代碼:
public class QuadraticProbingHashTable<AnyType> {
    //無參數構造函數
    public QuadraticProbingHashTable(){
        this(DEFAULT_TABLE_SIZE);
    }
    //有參數構造函數
    public QuadraticProbingHashTable(int size){
        allocateArray(size);
        makeEmpty();
    }
    //清空表
    public void makeEmpty(){
        currentSize = 0;
        for (int i = 0; i < array.length; i ++){
            array[i] = null;
        }
    }

    public boolean contains(AnyType x){
        int currentPos = findPos(x);
        //返回該位置是否為活動狀態
        return isActive(currentPos);
    }

    //插入元素
    public void insert(AnyType x){
        //獲取到空閑位置
        int currentPos = findPos(x);
        //若該位置為活動狀態,則返回,表示該位置已經存在元素
        //這種情況,實際上表示該位置上已經存在了該元素,那么不必重復插入
        if (isActive(currentPos)){
            return;
        }
        //否則,插入該元素
        array[currentPos] = new HashEntry<AnyType>(x);
        //判斷表的大小,超過一半,則進行rehash
        if (++ currentSize > array.length / 2){
            rehash();
        }
    }

    public void remove(AnyType x){
        //找到位置
        int currentPos = findPos(x);
        //若該位置為活動狀態,則進行刪除操作
        if (isActive(currentPos)){
            //令該位置為非活動狀態即可
            array[currentPos].isActive = false;
            currentSize --;
        }
    }

    /**
     * 定義一個類用來標記每個位置的情況
     * @param <AnyType>
     */
    private static class HashEntry<AnyType>{
        //當前位置的元素值
        public AnyType element;
        //當前位置是否為活動狀態,默認為活動狀態,但若刪除后,會設置其為非活動狀態
        public boolean isActive;

        public HashEntry(AnyType e){
            this(e, true);
        }

        public HashEntry(AnyType e, boolean b){
            element = e;
            isActive = b;
        }
    }

    //默認表的大小
    private static final int DEFAULT_TABLE_SIZE = 11;
    //存儲表
    private HashEntry<AnyType> [] array;
    //當前表的大小
    private int currentSize;

    //初始化表
    private void allocateArray(int size){
        array = new HashEntry[nextPrime(size)];
    }

    //判斷當前位置是否為活動狀態
    private boolean isActive(int currentPos){
        return array[currentPos] != null && array[currentPos].isActive;
    }

    /**
     * 尋找空閑位置,以解決沖突
     * @param x
     * @return
     */
    private int findPos(AnyType x){
        //定義偏移量
        int offset = 1;
        //獲取到hash位置
        int currentPos = myHash(x);
        //若hash位置中存在元素,并且當前元素不等于傳入的元素
        while (array[currentPos] != null && !array[currentPos].element.equals(x)){
            //進行偏移
            currentPos += offset;
            //改變偏移量
            offset += 2;
            //考慮到溢出情況
            if (currentPos >= array.length){
                currentPos -= array.length;
            }
        }
        return currentPos;
    }


    private void rehash(){
        HashEntry<AnyType> [] oldArray = array;
        //擴充表的大小
        allocateArray(nextPrime(2 * oldArray.length));
        currentSize = 0;
        //將舊表的數據添加到新表中
        for (int i = 0; i < oldArray.length; i ++){
            if (oldArray[i] != null && oldArray[i].isActive){
                insert(oldArray[i].element);
            }
        }
    }


    //根據值獲取到其對應的hash位置
    private int myHash(AnyType x){
        int hashVal = x.hashCode();
        hashVal %= array.length;
        if (hashVal < 0){
            hashVal += array.length;
        }
        return hashVal;
    }

    //返回下一個素數
    private static int nextPrime(int n){
        while (!isPrime(n)){
            n ++;
        }
        return n;
    }
    //判斷是否為素數
    private static boolean isPrime(int n){
        for (int i = 2; i <= Math.sqrt(n); i ++){
            if (n % i == 0 && n != 2){
                return false;
            }
        }
        return true;
    }

}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Map 是一種很常見的數據結構,用于存儲一些無序的鍵值對。在主流的編程語言中,默認就自帶它的實現。C、C++ 中的...
    一縷殤流化隱半邊冰霜閱讀 9,317評論 23 67
  • 9.3.3 快速排序 ??快速排序將原數組劃分為兩個子數組,第一個子數組中元素小于等于某個邊界值,第二個子數組中的...
    RichardJieChen閱讀 1,869評論 0 3
  • 概念 散列表的實現常常叫做散列(hashing)。散列是一種用于以常數平均時間執行插入、刪除和查找的技術。散列函數...
    NoFacePeace閱讀 339評論 0 0
  • 沖突的普遍性 ? ? 生日悖論 我們可以考慮這樣一個實際問題:某課堂上的所有學生中,是否由某兩位在同一天過生日(稱...
    峰峰小閱讀 884評論 0 1
  • 冷靜漂泊 微光還在頻閃 我為你創造了所有燈紅和酒綠 步步為營助長那些理想的花兒 千山越過邊境 雨中 白玫瑰綴滿了我...
    瑾闊徐行閱讀 121評論 0 0