1

一、引言

哈希表是一种组合的数据结构,它通常的实现方式是数组加链表,或者数组加红黑树。哈希表是一种牺牲空间去换取时间的数据结构,需要在空间与时间上有取舍,哈希表是时间和空间之间的平衡。哈希表的核心是哈希函数,哈希表最关键的问题哈希冲突也是取决于哈希函数的设计。

二、哈希函数

1、什么是哈希函数

哈希函数是一种将“”转换为“索引”的逻辑规则。它的设计好坏对哈希表的性能影响巨大。优良的哈希函数能够最大程度的减少哈希冲突,使得哈希表中的元素分布得尽可能的均匀,离散程度更大,这样哈希表就会性能优越;较差的哈希函数设计,带来的可能是一场灾难,哈希冲突严重,空间利用率低,时间复杂度呈线性恶化,造成频繁的扩容操作。

2、哈希函数的设计

对于哈希函数的设计,以下举了一些简单的例子,都基本类型转换成整型处理,并不是唯一的方法,仅供参考。以下简单列了几条设计原则。

  • 一致性:如果a == b,则hash(a) == hash(b)
  • 高效性:哈希函数计算高效简便
  • 均匀行:哈希值均匀分布

1、整型

  • 小范围正整数可直接使用
  • 小范围负整数进行偏移

小范围负整数进行偏移.png

  • 大整数可以与一个合适的素数取模

大整数可以与一个合适的素数取模.png

说明:上图(左)与一个不合适的合数取模,获得的索引冲突严重,不可取;而上图(右)与一个质数取模,明显获得的索引分布更均匀,离散程度更好。点击获取合适的哈希表素数

2、浮点型

  • 浮点型转换成整型处理

浮点型转换成整型.png

3、字符串型

  • 字符串型转换成整型处理

字符串型转换成整型处理.png

说明:其中B为一个常数,M为一个合适的质数。上图将一个字符串型加入一些规则映射成了一个整型。

4、日期类型

  • 日期类型转换成整型处理

日期类型转换成整型处理.png

三、哈希冲突的处理

1、链地址法(Separate Chaining)

链地址法.png

说明:哈希冲突的元素可以用链表这种线性的数据结构保存,当然也可以用平衡树结构保存,Java8中的哈希表实现,当哈希冲突达到一定的程度,会将链表替换成红黑树,前提是哈希表中原本的元素具备可比较性。

2、开放地址法

1、线性探测

开放地址法.png

说明:当遭遇哈希冲突时,会按照规则顺延往下找空挡位置插入元素,这种方式如果哈希冲突比较严重,会造成寻找空挡位置效率变低。哈希表性能变差。这种方式就是要设置合适的哈希表容量。

2、平方探测

平方探测.png

说明:这种方式相对于线性探测,加大了寻找空挡位置的步长。

3、二次哈希

二次哈希.png

说明:当遭遇哈希冲突后,再运行一个哈希函数,对上一次哈希运算结果再计算一次结果,二次运算后得到的索引位置,是一个空挡位置的几率增大。

四、哈希表动态空间处理

哈希表动态空间处理.png

说明:当哈希冲突达到一个所能容忍的上界位置时(upperTol),对哈希表进行扩容操作,以减少哈希冲突;当哈希冲突降低到一个下界位置(lowerTol)时,对哈希表进行缩容操作,以节约空间。

五、哈希表实现

1、基于数组和TreeMap实现

import java.util.TreeMap;

/**
 * 哈希表
 * @param <K>
 * @param <V>
 */
public class HashTable<K extends Comparable<K>, V> {

    /**
     * 容量素数表,用于确定扩容、缩容的容量值
     */
    private final int[] capacity
            = {53, 97, 193, 389, 769, 1543, 3079, 6151, 12289, 24593,
            49157, 98317, 196613, 393241, 786433, 1572869, 3145739, 6291469,
            12582917, 25165843, 50331653, 100663319, 201326611, 402653189, 805306457, 1610612741};

    /**
     * 容忍度上界(扩容临界位)
     */
    private static final int upperTol = 10;

    /**
     * 容忍度下界(缩容临界位)
     */
    private static final int lowerTol = 2;

    /**
     * 初始化容量
     */
    private int capacityIndex = 0;

    /**
     * TreeMap数组容器
     */
    private TreeMap<K,V>[] hashTable;

    /**
     * 取一个合适的素数,次数决定数组的长度
     */
    private int m;

    /**
     * 哈希表中元素个数
     */
    private int size;

    public HashTable(int m) {
        this.m = capacity[capacityIndex];
        this.size = 0;
        this.hashTable = new TreeMap[m];
        for (int i = 0; i < m; i++) {
            hashTable[i] = new TreeMap<>();
        }
    }

    /**
     * 根据key获取哈希码,非负数
     * @param key
     * @return
     */
    private int hash(K key) {
        return (key.hashCode() & 0x7fffffff) % m;
    }

    /**
     * 获取哈希表中元素个数
     * @return
     */
    public int getSize() {
        return size;
    }

    /**
     * 向哈希表中添加元素
     * @param key
     * @param value
     */
    public void add(K key, V value) {
        TreeMap<K, V> map = hashTable[hash(key)];
        if (map.containsKey(key)) {
            map.put(key, value);
        } else {
            map.put(key, value);
            size ++;

            //扩容操作
            if (size >= upperTol * m && capacityIndex + 1 < capacity.length) {
                capacityIndex++;
                resize(capacity[capacityIndex]);
            }
        }
    }

    /**
     * 删除哈希表中指定的元素
     * @param key
     * @return
     */
    public V remove(K key) {
        TreeMap<K,V> map = hashTable[hash(key)];
        V ret = null;
        if (map.containsKey(key)) {
            ret = map.remove(key);
            size--;

            //缩容操作
            if (size < lowerTol * m && capacityIndex - 1 >= 0) {
                capacityIndex--;
                resize(capacity[capacityIndex]);
            }
        }
        return ret;
    }

    /**
     * 修改哈希表中指定的元素
     * @param key
     * @param value
     */
    public void set(K key, V value) {
        TreeMap<K, V> map = hashTable[hash(key)];
        if (!map.containsKey(key)) {
            throw new IllegalArgumentException(key + " doesn't exist");
        }
        map.put(key, value);
    }

    /**
     * 判断哈希表中是否包含元素
     * @param key
     * @return
     */
    public boolean contains(K key) {
        return hashTable[hash(key)].containsKey(key);
    }

    /**
     * 从哈希表中查询指定元素
     * @param key
     * @return
     */
    public V get(K key) {
        return hashTable[hash(key)].get(key);
    }

    /**
     * 扩容、缩容操作
     * @param newM 新的容量
     */
    private void resize(int newM) {
        TreeMap<K, V>[] newHashTable = new TreeMap[newM];
        for (int i = 0; i < newM; i++) {
            newHashTable[i] = new TreeMap<>();
        }

        int oldM = m;
        this.m = newM;
        for (int i = 0; i < oldM; i++) {
            TreeMap<K,V> map = hashTable[i];
            for (K key : map.keySet()) {
                newHashTable[hash(key)].put(key, map.get(key));
            }
        }

        this.hashTable = newHashTable;
    }

}

六、时间复杂度分析

哈希表的平均时间复杂度是O(1)级别的,这是哈希表用空间换来的性能,这是哈希表最大的优势。哈希表扩缩容时这种费时操作也是不常发生,平摊下去依然趋近于O(1)级别。

七、其它数据结构


neojayway
52 声望10 粉丝

学无止境,每天进步一点点