数据结构:哈希表


项目地址:https://gitee.com/caochenlei/data-structures

第一章 哈希表介绍

设计一个写字楼通讯录,存放该写字楼所有公司的通讯信息,座机号码作为 key(假设座机号码最长是 8 位),公司详情(名称、地址等)作为 value,要求添加、删除、搜索的时间复杂度要求是 O(1),实现代码如下:

我们发现上述代码可以实现要求,但是空间复杂度极大,空间利用率极低,非常浪费内存空间,其实数组 companies 就是一个哈希表。

哈希表(Hash Table,也叫散列表),是根据关键码值(Key Value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做 哈希函数(散列函数) ,存放记录的数组叫做 哈希表(散列表)

第二章 哈希冲突

哈希冲突(Hash Collision,也叫哈希碰撞),简单的来说就是:2 个不同的 key,经过哈希函数计算出相同的结果。

解决哈希冲突有很多种方法,我们这里重点介绍链地址法(Separate Chaining),简单的来说就是:通过链表将同一index元素串起来。

第三章 哈希函数

哈希函数指将哈希表中元素的关键键值映射为元素存储位置的函数。

哈希表中哈希函数的实现步骤,大概如下:

  • 先生成 key 的哈希值(必须是整数)。
  • 再让 key 的哈希值跟数组的大小进行相关运算,生成一个索引值。

为了提高效率,可以使用 & 位运算取代 % 运算,但是这是有前提的,那就是必须将数组的长度设计为 2 的幂(2n)。

良好的哈希函数,可以让哈希值更加均匀分布,减少哈希冲突次数,提升哈希表的性能。

那么问题又来了,对于 key 来说,可能是不同的数据类型,那如何计算他的哈希值呢?

key 的常见种类可能有:整数、浮点数、字符串、自定义对象。不同种类的 key,哈希值的生成方式不一样,但目标是一致的,尽量让每个 key 的哈希值是唯一的,尽量让 key 的所有信息参与运算,并且最终生成的哈希值必须是一个整数。

Integer整数: 整数值当做哈希值,比如 10 的哈希值就是 10

Float浮点数: 将存储的二进制格式转为整数值

Long整数、Double浮点数:

这个 >>> 和 ^ 的作用是什么?

由于Long和Double的底层二进制位数远远超过了Integer的32位,为了满足尽量让 key 的所有信息参与运算,所以通过 >>> 右移,让当前值的高32位和低32位进行运算,混合计算出32位的整数哈希值,最终计算出来的数值高32无意义,舍去即可。这里给出一个案例:

字符串:

我们首先来思考一个问题,整数 5489 是如何计算出来的?

5489 = 5 ∗ 103 + 4 ∗ 102 + 8 ∗ 101 + 9 ∗ 100

而我们的字符串是由若干个字符组成的,比如字符串 jack,由 j、a、c、k 四个字符组成,字符的本质就是一个整数。

因此,jack 的哈希值可以表示为 j ∗ n3 + a ∗ n2 + c ∗ n1 + k ∗ n0,等价于 [ ( j ∗ n + a ) ∗ n + c ] ∗ n + k

在JDK中,乘数 n 为 31,为什么用 31?

由于 31 不仅仅是符合2n – 1,它还是个奇素数(既是奇数,又是素数,也就是质数),素数和其他数相乘的结果比其他方式更容易产成唯一性,减少哈希冲突,有那么多素数,那为啥是31呢,是经过观测分布结果后的选择,并且JVM会将 31 * i 优化成 (i << 5) – i

自定义对象:

自定义对象需要实现 equals 和 hashCode 方法,以下示例代码的 equals 和 hashCode 方法由 idea 自动生成。

注意:这里实现 equals 方法和生成 hashCode 并没有直接联系,仅仅是为了后边写代码方便,也就一块生成了。

这里给出 Objects.hash(...) 的底层实现:

第四章 哈希表实现

【实现代码】

public class HashTable<Key, Value> {
    private int M;                              //哈希表的大小
    private int N;                              //键值对的对数
    private SymbolTable<Key, Value>[] table;    //存放链表对象的数组,这里采用无序符号表,也可以用红黑树

    public HashTable(int M) {
        this.M = M;
        table = new SymbolTable[M];
        for (int i = 0; i < M; i++) {
            table[i] = new SymbolTable<>();
        }
    }

    //获取哈希表键值个数
    public int size() {
        return N;
    }

    //判断哈希表是否为空
    public boolean isEmpty() {
        return N == 0;
    }

    //获取指定key哈希值
    private int hash(Key key) {
        return key.hashCode() & (table.length - 1);
    }

    //向哈希表中添加键值
    public void put(Key key, Value value) {
        table[hash(key)].put(key, value);
        N++;
    }

    //删除哈希表中的key
    public void delete(Key key) {
        table[hash(key)].delete(key);
        N--;
    }

    //获取key对应的value
    public Value get(Key key) {
        return table[hash(key)].get(key);
    }

    //获取哈希表中所有key的集合
    public Set<Key> keySet() {
        Set<Key> keySet = new HashSet<>();
        for (SymbolTable<Key, Value> keyValueSymbolTable : table) {
            keySet.addAll(keyValueSymbolTable.keySet());
        }
        return keySet;
    }

    //获取哈希表中所有value的集合
    public Set<Value> valueSet() {
        Set<Value> valueSet = new HashSet<>();
        for (SymbolTable<Key, Value> keyValueSymbolTable : table) {
            valueSet.addAll(keyValueSymbolTable.valueSet());
        }
        return valueSet;
    }
}

【测试代码】

public class HashTableTest {
    public static void main(String[] args) {
        HashTable<Integer, String> hashTable = new HashTable<>(16);

        hashTable.put(1, "刘备");
        hashTable.put(2, "张飞");
        hashTable.put(3, "关羽");
        hashTable.put(4, "赵云");
        hashTable.put(5, "曹操");
        hashTable.put(6, "吕布");

        System.out.println(hashTable.size());
        System.out.println(hashTable.isEmpty());
        System.out.println(hashTable.keySet());
        System.out.println(hashTable.valueSet());
        for (Integer key : hashTable.keySet()) {
            System.out.println(key + ":" + hashTable.get(key));
        }
        for (Integer key : hashTable.keySet()) {
            hashTable.delete(key);
        }
        System.out.println(hashTable.size());
        System.out.println(hashTable.isEmpty());
        System.out.println(hashTable.keySet());
        System.out.println(hashTable.valueSet());
    }
}

【运行效果】

6
false
[1, 2, 3, 4, 5, 6]
[关羽, 张飞, 吕布, 刘备, 曹操, 赵云]
1:刘备
2:张飞
3:关羽
4:赵云
5:曹操
6:吕布
0
true
[]
[]
  • 69
    点赞
  • 196
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 51
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 51
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

轻松的小希

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值