散列表2: 基于探测法的散列表

不用链表的散列表

分离链接法的缺点

使用了一些链表。给新单元分配地址需要时间,导致算法的速度会有些慢,同时算法还要求对第二种数据结构的实现。
而,不用链表解决冲突的方法是:尝试另外的单元,直到找到空的单元。

装填因子

散列表的装填因子(load factor) 为散列表中的元素个数对该表大小的比。
分离链接法的因子为1。
探测散列表:因子小于0.5

探测散列表

线性探测法

线性探测法会存在一次聚集的问题

平方探测法

平方探测法排除了一次聚集,但是仍会存在二次聚集,采用双散列的方法来解决。

平法探测法Java实现

package com.hash;

/**
 * 基于平方探测法的散列表
 */
public class QuadraticProbingHashTable {
    private static final int DEFAULT_TABLE_SIZE = 101; // 默认表大小

    private HashEntry[] array;// 散列表
    private int occupied; // 占据单元个数
    private int theSize; // 当前位置

    /*
     * 用来装数据,标记是否active(惰性删除)
     */
    private static class HashEntry {
        public Item element; // 元素
        public boolean isActive; // 如果被标记为删除,返回false

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

        public HashEntry(Item e, boolean i) {
            element = e;
            isActive = i;
        }
    }

    /*
     * 构造
     */
    public QuadraticProbingHashTable() {
        this(DEFAULT_TABLE_SIZE);
    }

    public QuadraticProbingHashTable(int size) {
        allocateArray(size);
        doClear();
    }

    /*
     * 分配数组
     */
    private void allocateArray(int arraySize) {
        array = new HashEntry[nextPrime(arraySize)];  // 大于size的素数作为表长度
    }

    /*
     * 插入
     */
    public boolean insert(Item x) {
        // Insert x as active
        int currentPos = findPos(x);
        if (isActive(currentPos))
            return false;

        if (array[currentPos] == null)
            ++occupied;
        array[currentPos] = new HashEntry<>(x, true);
        theSize++;

        // 再散列
        if (occupied > array.length / 2)
            rehash();

        return true;
    }

    /*
     * 扩展hash表
     */
    private void rehash() {
        HashEntry[] oldArray = array;

        // Create a new double-sized, empty table
        allocateArray(2 * oldArray.length);
        occupied = 0;
        theSize = 0;

        // Copy table over
        for (HashEntry entry : oldArray)
            if (entry != null && entry.isActive)
                insert(entry.element);
    }

    /*
     * 平方探测解决
     */
    private int findPos(Item x) {
        int offset = 1;
        int currentPos = myhash(x);

        while (array[currentPos] != null && !array[currentPos].element.equals(x)) {
            currentPos += offset; // Compute ith probe
            offset += 2;
            if (currentPos >= array.length)
                currentPos -= array.length;
        }

        return currentPos;
    }

    /*
     * 从表中删除
     */
    public boolean remove(Item x) {
        int currentPos = findPos(x);
        if (isActive(currentPos)) {
            array[currentPos].isActive = false;
            theSize--;
            return true;
        } else
            return false;
    }

    /*
     * 获取当前元素个数
     */
    public int size() {
        return theSize;
    }

    /*
     * 获取表长度
     */
    public int capacity() {
        return array.length;
    }

    /*
     * 表中是否包含某元素
     */
    public boolean contains(Item x) {
        int currentPos = findPos(x);
        return isActive(currentPos);
    }

    /*
     * 如果当前位置存在,并可用,返回true
     */
    private boolean isActive(int currentPos) {
        return array[currentPos] != null && array[currentPos].isActive;
    }

    /*
     * 使hash表为空
     */
    public void makeEmpty() {
        doClear();
    }

    private void doClear() {
        occupied = 0;
        for (int i = 0; i < array.length; i++)
            array[i] = null;
    }

    private int myhash(Item x) {
        int hashVal = x.hashCode();

        hashVal %= array.length;
        if (hashVal < 0)
            hashVal += array.length;

        return hashVal;
    }

    /*
     * 大于n的素数
     */
    private static int nextPrime(int n) {
        if (n % 2 == 0)
            n++;

        while (!isPrime(n))
            n += 2;

        return n;
    }

    /*
     * 是否素数
     */
    private static boolean isPrime(int n) {
        if (n == 2 || n == 3)
            return true;

        if (n == 1 || n % 2 == 0)
            return false;

        for (int i = 3; i * i <= n; i += 2)
            if (n % i == 0)
                return false;

        return true;
    }

    // 测试
    public static void main(String[] args) {
        QuadraticProbingHashTable H = new QuadraticProbingHashTable<>();

        long startTime = System.currentTimeMillis();

        final int NUMS = 2000000;
        final int GAP = 37;

        System.out.println("Checking... (no more output means success)");

        for (int i = GAP; i != 0; i = (i + GAP) % NUMS)
            H.insert("" + i);
        for (int i = GAP; i != 0; i = (i + GAP) % NUMS)
            if (H.insert("" + i))
                System.out.println("OOPS!!! " + i);
        for (int i = 1; i < NUMS; i += 2)
            H.remove("" + i);

        for (int i = 2; i < NUMS; i += 2)
            if (!H.contains("" + i))
                System.out.println("Find fails " + i);

        for (int i = 1; i < NUMS; i += 2) {
            if (H.contains("" + i))
                System.out.println("OOPS!!! " + i);
        }

        long endTime = System.currentTimeMillis();

        System.out.println("Elapsed time: " + (endTime - startTime));
    }
}

标准库中的散列表

标准库中HashSet类HashMap类是散列表的实现。两类通常使用分离链接散列实现。

小结

  1. 散列表可以以常数平均时间实现insert和查找操作。
  2. 当关键字不是短的串或整数时,需要自习选择散列函数
  3. 分离链接散列法,装填因子尽量接近于1;而探测散列算法,装填因子不应该超过0.5
  4. 使用散列表不可能找出最小元素。

参考资料

《数据结构与算法分析 Java语言描述》第三版
《算法第四版》

你可能感兴趣的:(数据结构与算法,数据结构与算法分析)