数据结构与算法之散列(线性/平方/双平方探测法)

散列的基础知识以及分离链式法参考
上一篇 数据结构与算法之散列(分离链接法)<七>

线性探测法
若产生冲突则放入下一个空闲区域 但是当数据多 需要发费很多的时间寻找空单元 更糟糕的是,即使表比较空,占据的单元会出现聚集现象,称之为一次聚集 为了解决一次聚集现象 出现了平方探测法

平方探测法
当出现冲突时 寻找空闲区域的步长以平方长度来计算,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;
    }

到此散列的基础介绍结束。

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