數據結構與算法之散列(線性/平方/雙平方探測法)

散列的基礎知識以及分離鏈式法參考
上一篇 數據結構與算法之散列(分離鏈接法)<七>

線性探測法
若產生衝突則放入下一個空閒區域 但是當數據多 需要發費很多的時間尋找空單元 更糟糕的是,即使表比較空,佔據的單元會出現聚集現象,稱之爲一次聚集 爲了解決一次聚集現象 出現了平方探測法

平方探測法
當出現衝突時 尋找空閒區域的步長以平方長度來計算,i^2 就是 1 4 9 16 25 …..等長度去尋找下一個空白單元,這樣的話衝突之間的距離分佈相對均勻,解決了一次聚集的現象。但是平方探測法雖然解決了一次聚集的情況但是散列到同一位置的那些元素將探測相同的備選單元(就是衝突佔用的單元再發生衝突其尋找空白單元的位置都是一樣的,出現了循環) 這就叫做二次聚集

代碼實現


/*
 * 解決哈希衝突
 * 平方探測法
 */
public class QuadraticProbingHashTable<T> {
    private static final int DEFAULT_SIZE = 11;

    public QuadraticProbingHashTable() {
        this(DEFAULT_SIZE);
    }

    QuadraticProbingHashTable(int DEFAULT_SIZE) {
        allocateArray(DEFAULT_SIZE);
    }

    @SuppressWarnings("unchecked")
    private void allocateArray(int arraySize) {
        // 根據arraySize的數值獲取下一個素數
        array = new HashEntry[nextPrime(arraySize)];
    }

    public void remove(T element) {
        // findPos這個位置對象可能爲null也可能與element相同
        int findPos = findPos(element);
        // 找到了與element相同的對象
        if (isActive(findPos)) {
            // 惰性刪除
            array[findPos].isActive = false;
            // 當前插入數據大小減1
            currentSize--;
        }
    }

    // 哈希對象
    private static class HashEntry<T> {
        // 保存數據
        public T element;
        // 是否被刪除 (惰性刪除)
        public boolean isActive;

        public HashEntry(T element) {
            this(element, true);
        }

        public HashEntry(T element, boolean isActive) {
            this.element = element;
            this.isActive = isActive;
        }
    }

    // 哈希數組
    private HashEntry<T>[] array;
    // 數組當前大小
    private int currentSize;

    // 插入數據
    public void insert(T element) {
        int findPos = findPos(element);
        // 若element已經存在於該hash中返回空
        if (isActive(findPos)) {
            return;
        }
        // 若不存在則插入
        array[findPos] = new HashEntry<T>(element);
        // 插入成功後已插入數據大小加1
        currentSize++;
        // 如果當前的已插入的數據大小比數組的1/2還要大則要更新數組
        // 因爲平方探測法只能探索數組大小的1/2
        if (currentSize > array.length / 2) {
            rehash();
        }
    }

    // 將數組擴大至原來的兩倍
    private void rehash() {
        // 保存舊數據
        HashEntry<T>[] oldArray = array;
        // 將數組擴大兩倍
        allocateArray(oldArray.length * 2);
        // 將舊數據導入新數據
        for (HashEntry<T> hashEntry : oldArray) {
            // 由於舊數據中有一半未插入數據所以這裏要篩選一下
            if (hashEntry != null && hashEntry.isActive)
                insert(hashEntry.element);
        }
    }

    // element是否存在
    public boolean contain(T element) {
        // findPos這個位置對象可能爲null也可能與element相同
        int findPos = findPos(element);
        // 判斷findPos這個位置的對象是否與element相同
        return isActive(findPos);
    }

    // 判斷該位置對象是否活躍
    private boolean isActive(int currentPos) {
        // 若該位置對象不爲null且是活躍的 則爲活躍
        return array[currentPos] != null && array[currentPos].isActive;
    }

    /*
     * 利用平方探測法查找衝突後下一個存儲點
     */
    private int findPos(T element) {
        // 偏移量
        int offset = 1;
        // 利用哈希找到此對象的哈希位置
        int currentPos = myhash(element);
        // 循環查找直到找到一個爲null或者返回與element對象相同的位置
        while (array[currentPos] != null && !array[currentPos].element.equals(element)) {
            // 移動到下一個探測點
            currentPos += offset;
            // 偏移量更新
            offset += 2;
            // 超出數組範圍
            if (currentPos >= array.length) {
                currentPos -= array.length;
            }
        }
        return currentPos;
    }

    private int myhash(T t) {
        // 獲取哈希值
        int hashval = t.hashCode();
        // 限定範圍
        hashval %= array.length;
        if (hashval < 0)
            hashval += array.length;
        return hashval;
    }

    private static int nextPrime(int n) {
        // 如果是偶數則加1變奇數(所有的偶數除2以外都不是素數)
        if (n % 2 == 0)
            n++;
        // 如果不是素數則加2
        for (; !isPrime(n); n += 2)
            ;
        return n;
    }

    public static boolean isPrime(int n) {
        // 2,3是素數
        if (n == 2 || n == 3)
            return true;
        // 1是特例不是素數
        // 若被偶數整除不是素數
        if (n == 1 || n % 2 == 0)
            return false;
        // 若被奇數整除則不是素數 (3,5,7,9,11,13,15,17.......)
        // 若i*i>=n 此時i已經是遍歷n的所有可能公因數
        for (int i = 3; i * i <= n; i += 2)
            if (n % i == 0)
                return false;
        // 若通過上述的判斷則爲素數。
        return true;
    }

雙平方探測法
爲了解決二次聚集現象發明了雙平方探測法 當衝突產生時 向該衝突點的雙向以步長i^2(1 4 9 16 25…) 探測 若保證散列表的長度是素數且滿足4K+3則可以遍歷整個散列表從而不存在二次聚集現象

代碼實現


/*
 * 解決哈希衝突
 * 雙平方探測法
 */
public class DoubleQuadraticProbingHashTable<T> {
    private static final int DEFAULT_SIZE = 11;

    public DoubleQuadraticProbingHashTable() {
        this(DEFAULT_SIZE);
    }

    DoubleQuadraticProbingHashTable(int DEFAULT_SIZE) {
        allocateArray(DEFAULT_SIZE);
    }

    @SuppressWarnings("unchecked")
    private void allocateArray(int arraySize) {
        // 根據arraySize的數值獲取下一個素數
        array = new HashEntry[next4K_3Prime(arraySize)];
    }

    public void remove(T element) {
        // findPos這個位置對象可能爲null也可能與element相同
        int findPos = findPos(element);
        // 找到了與element相同的對象
        if (isActive(findPos)) {
            // 惰性刪除
            array[findPos].isActive = false;
            // 當前插入數據大小減1
            currentSize--;
        }
    }

    // 哈希對象
    private static class HashEntry<T> {
        // 保存數據
        public T element;
        // 是否被刪除 (惰性刪除)
        public boolean isActive;

        public HashEntry(T element) {
            this(element, true);
        }

        public HashEntry(T element, boolean isActive) {
            this.element = element;
            this.isActive = isActive;
        }
    }

    // 哈希數組
    private HashEntry<T>[] array;
    // 數組當前大小
    private int currentSize;

    // 插入數據
    public void insert(T element) {
        int findPos = findPos(element);
        // 如果滿了
        if (findPos == -1) {
            rehash();
            findPos = findPos(element);
        }
        // 若element已經存在於該hash中返回空
        if (isActive(findPos)) {
            return;
        }
        // 若不存在則插入
        array[findPos] = new HashEntry<T>(element);
        // 插入成功後已插入數據大小加1
        currentSize++;
    }

    // 將數組擴大至原來的兩倍
    private void rehash() {
        // 保存舊數據
        HashEntry<T>[] oldArray = array;
        // 將數組擴大兩倍
        allocateArray(oldArray.length * 2);
        // 將舊數據導入新數據
        for (HashEntry<T> hashEntry : oldArray) {
            // 由於舊數據中有一半未插入數據所以這裏要篩選一下
            if (hashEntry != null && hashEntry.isActive)
                insert(hashEntry.element);
        }
    }

    // element是否存在
    public boolean contain(T element) {
        // findPos這個位置對象可能爲null也可能與element相同
        int findPos = findPos(element);
        // 判斷findPos這個位置的對象是否與element相同
        return isActive(findPos);
    }

    // 判斷該位置對象是否活躍
    private boolean isActive(int currentPos) {
        // 若該位置對象不爲null且是活躍的 則爲活躍
        return currentPos >= 0 && array[currentPos] != null && array[currentPos].isActive;
    }

    /*
     * 利用雙平方探測法查找衝突後下一個存儲點
     */
    private int findPos(T element) {
        // 偏移量
        int offset = 1;
        // 雙平方探測所以有兩個探測點
        int currentPos1 = myhash(element);
        int currentPos2 = myhash(element);
        // 雙平方探測可以將數組全部探測當偏移量等於數組長度時數組已經全部探測
        while (offset < array.length) {
            // 移動到下一個探測點
            currentPos1 += offset;
            // 修正探測點
            if (currentPos1 >= array.length) {
                currentPos1 -= array.length;
            }
            // 找到一個位置對象爲null或者位置對象與element對象相同或者位置對象不活躍的位置
            if (array[currentPos1] == null || array[currentPos1].element.equals(element)
                    || !array[currentPos1].isActive)
                return currentPos1;

            // 移動到下一個探測點
            currentPos2 -= offset;
            // 修正探測點
            if (currentPos2 < 0) {
                currentPos2 += array.length;
            }
            // 找到一個位置對象爲null或者位置對象與element對象相同或者位置對象不活躍的位置
            if (array[currentPos2] == null || array[currentPos2].element.equals(element)
                    || !array[currentPos2].isActive)
                return currentPos2;
            // 偏移量更新
            offset += 2;
        }
        // 若全部探測後依然沒有找到位置則證明這個數組滿了
        return -1;
    }

    private int myhash(T t) {
        // 獲取哈希值
        int hashval = t.hashCode();
        // 限定範圍
        hashval %= array.length;
        if (hashval < 0)
            hashval += array.length;
        return hashval;
    }

    // 是否爲被4整除餘3的素數
    public static int next4K_3Prime(int n) {
        int nextPrime = nextPrime(n);
        while ((nextPrime % 4) != 3) {
            nextPrime += 2;
            nextPrime = nextPrime(nextPrime);
        }
        return nextPrime;
    }

    private static int nextPrime(int n) {
        // 如果是偶數則加1變奇數(所有的偶數除2以外都不是素數)
        if (n % 2 == 0)
            n++;
        // 如果不是素數則加2
        for (; !isPrime(n); n += 2)
            ;
        return n;
    }

    public static boolean isPrime(int n) {
        // 2,3是素數
        if (n == 2 || n == 3)
            return true;
        // 1是特例不是素數
        // 若被偶數整除不是素數
        if (n == 1 || n % 2 == 0)
            return false;
        // 若被奇數整除則不是素數 (3,5,7,9,11,13,15,17.......)
        // 若i*i>=n 此時i已經是遍歷n的所有可能公因數
        for (int i = 3; i * i <= n; i += 2)
            if (n % i == 0)
                return false;
        // 若通過上述的判斷則爲素數。
        return true;
    }

到此散列的基礎介紹結束。

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