Java数据结构与算法分析(十一)散列表(哈希表)

简介: 散列表(Hash Table)也叫哈希表,是根据给定关键字(Key)来计算出该关键字在表中存储地址的数据结构。也就是说,散列表建立了关键字与存储地址之间的一种直接映射关系,将关键字映射到表中记录的地址,这加快了查找速度。

GitHub源码分享

主页地址:https://gozhuyinglong.github.io
源码分享:https://github.com/gozhuyinglong/blog-demos

1. 什么是散列表

散列表(Hash Table)也叫哈希表,是根据给定关键字(Key)来计算出该关键字在表中存储地址的数据结构。也就是说,散列表建立了关键字与存储地址之间的一种直接映射关系,将关键字映射到表中记录的地址,这加快了查找速度。

使用函数表达式来表示,应为:hash(key)=v,其中key为关键字,hash()为散列函数,v为散列地址。

1.1 相关术语

  • 关键字(Key):关键字是散列表的查找对象,也是散列函数的参数。其可以是记录本身,也可以是记录的唯一索引,这取决于散列表的设计。
  • 散列函数(Hash Function):将关键字映射为散列表中适当存储地址的函数称为散列函数,也叫做哈希函数。
  • 散列地址(Hash Address):将关键字通过散列函数计算出的地址称为散列地址,也叫做散列值、哈希值。
  • 散列表(Hash Table):这种使用散列技术来存储数据记录的表称为散列表。
  • 散列冲突(Hash Collision):当不同的关键字具有相同的散列地址,这种现象称为冲突,也叫做哈希冲突。而这些关键字对该散列函数来说是同义词。
  • 表长:散列表的大小,即能够存储散列地址的数量。用m表示。

1.2 举个例子

一个比较通俗的例子,就是我们手机中的通讯录。通讯录用于存储联系人的姓名及电话号码信息,它是一个按照姓名首字母进行顺序排列的表。比如我们找“嬴政”就可以通过字母“Y”进行快速定位,并找到“嬴政”。

散列表:通讯录

  • 在该例子中,姓名“嬴政”便是关键字
  • 取姓名的首字母的算法便是散列函数。即:hash(嬴政) = Y。
  • 通过
  • 当我们定位到“Y”地址后,不仅有“赢政”,还有“赢异人”,也就是说,通过关键字“赢异人”也能找到“Y”的地址,这种现象便是冲突。即:hash(嬴政) = hash(嬴异人) = Y。
  • 而“赢政”与“赢异人”对该散列函数来说是同义词
  • 这个用于存储联系人的通讯录便是散列表

1.3 小结

本章节介绍了散列表的概念及相关术语,并以一个通讯录的例子来加深对散列表的了解。但还有一些问题等待我们解决,那就是“散列冲突”。我们一方面可以通过精心设计散列函数来尽量减少冲突的次数,另一方面仍需要提供解决冲突的方法。后面章节会详细介绍散列函数的设计和解决冲突的方法。

2. 散列函数的设计

散列函数的设计原则应该遵循计算简单、散列地址分布均匀。下面我们介绍几种常用的散列函数的构造方法。

2.1 直接定址法

取关键字或关键字的某个线性函数值为散列地址。即hash(key) = key 或 hash(key) = a * key + b,其中a、b 为常数。这种散列函数也叫做自身函数。

如下图中,我们要获取某个岗位级别的信息,就可以使用级别减去1来作为地址,即:hash(key) = key - 1

直接定址法

直接定址法的优点是简单、均匀也不会产生冲突。但由于这是一种拿空间换时间的方式,所以适合查找表较小且连续的情况。比如年龄、岗位级别等。

2.2 数字分析法

通过分析一组数据,这些数据中可能出现的关键字都是事先知道的,则可以取关键字的若干数位组成散列地址。取出的这部分数据要在其数位上出现的频率小,这样出现冲突的几率就会很小。

比如手机号码,其前3位为网络识别号,中间4位为地区编码,而后4位才是真正的用户号码。那么在某个公司中,其员工的手机号码前7位出现冲突的概率会很大,而后4位出现冲突的概率会很小,则可以取后四位做为散列地址。

数字分析法

2.3 平方取中法

取关键字平方后的中间几位作为散列地址。该方法是一个产生伪随机数的方法,由冯·诺伊曼在1946年提出。

平方取中法

2.4 折叠法

将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为散列地址。这种方法比较适用于关键字位数很多的情况。

比如关键字为 1234567890,散列表的表长为3位:
(1)将关键字分为4组: 123|456|789|0
(2)将它们叠加求和:123 + 456 + 789 + 0 = 1368
(3)舍去进位,得:368 ,即为最终散列地址

2.5 除留余数法

假定散列表的表长为m,取一个不大于m但最接近或等于m的质数p,通过关键字对p取模运算来转换为散列地址。散列函数为:hash(key) = key % p,其中p $\leq$ m。

不仅可以对关键字直接取模,也可以在折叠法、平方取中法等运算后取模。对p的选择很重要,若选择不好很容易产生冲突。

除留余数法

3. 散列冲突的处理方法

解决冲突的方法有几种,这里我们将讨论其中最简单的两种:开放定址法和链地址法(拉链法)。

3.1 开放定址法

将产生冲突的散列地址做为自变量,通过某种冲突解决函数得到一个新的空闲散列地址。即当产生冲突后,寻找下一个空闲的散列地址。根据寻找的方式不同又分为几种方法,下面来介绍。

3.1.1 线性探测法

当产生冲突时, 顺序探测表中下一地址,直探测到一个空闲(自由)的地址,将记录插入。若一直探测到表尾地址m-1,则下一探测地址为表首地址0。当表满的时候,则探测失败。

比如下图散列表的散列函数使用除留余数法(m=10, p=10)。那么我们顺序插入12、13、24时顺利插入到对应地址中,再插入34、44时会产生冲突,使用线性探测法,继续探测下一地址,将插入到空闲地址上。

线性探测法

线性探测法会造成大量元素在相邻的散列地址上“聚集”起来,使得我们要不断处理冲突,这大大降低查找和存入效率。

3.1.2 平方探测法

平方探测法是消除线性探测法中的聚集问题的一种冲突解决方法。设发生冲突的地址为d,那么使用平方探测法得到的新的地址序列为d $\pm$ 1^2、d $\pm$ $2^2$、d $\pm$ $3^2$...d $\pm$ $key^2$(key $\leq$ m/2)。

还是刚才那个例子,当插入34时,在下标为4的位置产生冲突,我们先计算一下在该位置探测的序列: 4 + $1^2$ = 5、4 - $1^2$ = 3、4 + $2^2$ = 8、4 - $2^2$ = 0,即:5、2、8、0,所以34应该被放到5的位置。而44应该放到8的位置。

平方探测法

平方探测法的缺点是不能探测到散列表上所有的地址,但至少能探测到一半地址。

3.1.3 伪随机探测法

伪随机探测法是当发生地址冲突时,地址增量为伪随机数序列。

伪随机数是说,如果我们设置随机种子相同,则不断调用随机函数可以生成不会重复的数列,我们在查找时,用同样的随机种子,它每次得到的数列是相同的,相同的地址当然可以得到相同的散列地址。

3.1.4 双散列

双散列顾名思义是使用了两个散列函数,当执行第一个散列函数得到的地址发生冲突时,则执行第二个散列函数来计算该关键字的地址增量

一种常见的算法是:(hash1(key) + i * hash2(key)) mod m,其中i为冲突次数,hash1()为第一个散列函数,hash2()为第二个散列函数,m为散列表大小。当发生冲突后,我们通过重复增加步长i来寻址。

还是以上面的例子为例。第一个散列函数为hash1(key) = key mod 10,第二个散列函数设为hash2(key) = key mod 3。

通过上面公式,可以计算出关键字34的散列地址: (34 mod 10 + 1 (34 mod 3)) mod 10 = (4 + 1 1) mod 10 = 5。而关键字44的散列地址为:(44 mod 10 + 2 (44 mod 3)) mod 10 = (4 + 2 2) mod 10 = 8

双散列

3.2 链地址法

链地址法又称拉链法,是利用链表来解决冲突的一种方法,即把具有相同散列地址的关键字记录存入到同一个单链表中,该链表称为同义词链表。每一个散列地址都有一个对应的链表。

还是上面的例子,使用链地址法的存储模型如下图所示。

链地址法

4. 代码实现

我们使用链地址法来实现上例中散列表,其散列函数使用除留余数法

4.1 节点类

因为链表不是本篇重点,所以这里设计一个简单的节点类。关于链表的相关知识请参阅:《 链表(单链表、双链表、环形链表)

class Node {
   
   
    private final int key; // 关键字
    private Node next; // 下一节点

    public Node(int key) {
   
   
        this.key = key;
    }

    public int getKey() {
   
   
        return key;
    }

    public Node getNext() {
   
   
        return next;
    }

    public void setNext(Node next) {
   
   
        this.next = next;
    }
}

4.2 散列表类

散列表类使用数组存储表数据,其指向链表的头节点,其散列函数使用除留余数法。

本类主要实现了散列表的添加关键字、删除关键字和匹配关键字。

class HashTable {
   
   

    private final int size; // 散列表大小
    private final Node[] table; // 散列表

    private HashTable(int size) {
   
   
        this.size = size;
        this.table = new Node[size];
    }

    /**
         * 散列函数 - 除留余数法
         *
         * @param key
         * @return
         */
    private int hash(int key) {
   
   
        return key % size;
    }

    /**
         * 添加关键字
         *
         * @param key
         */
    public void add(int key) {
   
   
        int hashAddress = hash(key);
        if (table[hashAddress] == null) {
   
   
            table[hashAddress] = new Node(key);
            return;
        }
        add(table[hashAddress], key);
    }

    /**
         * 添加关键字 - 递归
         *
         * @param node
         * @param key
         */
    private void add(Node node, int key) {
   
   
        if (node.getNext() == null) {
   
   
            node.setNext(new Node(key));
            return;
        }
        add(node.getNext(), key);
    }

    /**
         * 匹配关键字
         *
         * @param key
         * @return
         */
    public boolean contains(int key) {
   
   
        int hashAddress = hash(key);
        Node headNode = table[hashAddress];
        if (headNode == null) {
   
   
            return false;
        }

        return contains(headNode, key);
    }

    /**
         * 匹配关键字 - 递归
         *
         * @param node
         * @param key
         * @return
         */
    private boolean contains(Node node, int key) {
   
   
        if (node == null) {
   
   
            return false;
        }
        if (node.getKey() == key) {
   
   
            return true;
        }
        return contains(node.getNext(), key);
    }

    /**
         * 移除关键字
         *
         * @param key
         */
    public void remove(int key) {
   
   
        int hashAddress = hash(key);
        Node headNode = table[hashAddress];
        if (headNode == null) {
   
   
            return;
        }
        if (headNode.getKey() == key) {
   
   
            table[hashAddress] = headNode.getNext();
            return;
        }
        remove(headNode, key);
    }

    /**
         * 移除关键字 - 递归
         *
         * @param node
         * @param key
         */
    private void remove(Node node, int key) {
   
   
        if (node.getNext() == null) {
   
   
            return;
        }
        if (node.getNext().getKey() == key) {
   
   
            node.setNext(node.getNext().getNext());
            return;
        }
        remove(node.getNext(), key);
    }

}

5. 完整代码

完整代码请访问我的Github,若对你有帮助,欢迎给个Star,谢谢!

https://github.com/gozhuyinglong/blog-demos/blob/main/java-data-structures/src/main/java/io/github/gozhuyinglong/datastructures/hashtable/HashTableDemo.java

推荐阅读

相关文章
|
4月前
|
负载均衡 算法 关系型数据库
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
本文聚焦 MySQL 集群架构中的负载均衡算法,阐述其重要性。详细介绍轮询、加权轮询、最少连接、加权最少连接、随机、源地址哈希等常用算法,分析各自优缺点及适用场景。并提供 Java 语言代码实现示例,助力直观理解。文章结构清晰,语言通俗易懂,对理解和应用负载均衡算法具有实用价值和参考价值。
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
|
4月前
|
存储 缓存 监控
上网行为监控系统剖析:基于 Java LinkedHashMap 算法的时间序列追踪机制探究
数字化办公蓬勃发展的背景下,上网行为监控系统已成为企业维护信息安全、提升工作效能的关键手段。该系统需实时记录并深入分析员工的网络访问行为,如何高效存储和管理这些处于动态变化中的数据,便成为亟待解决的核心问题。Java 语言中的LinkedHashMap数据结构,凭借其独有的有序性特征以及可灵活配置的淘汰策略,为上网行为监控系统提供了一种兼顾性能与功能需求的数据管理方案。本文将对LinkedHashMap在上网行为监控系统中的应用原理、实现路径及其应用价值展开深入探究。
101 3
|
4月前
|
人工智能 算法 NoSQL
LRU算法的Java实现
LRU(Least Recently Used)算法用于淘汰最近最少使用的数据,常应用于内存管理策略中。在Redis中,通过`maxmemory-policy`配置实现不同淘汰策略,如`allkeys-lru`和`volatile-lru`等,采用采样方式近似LRU以优化性能。Java中可通过`LinkedHashMap`轻松实现LRUCache,利用其`accessOrder`特性和`removeEldestEntry`方法完成缓存淘汰逻辑,代码简洁高效。
182 0
|
5月前
|
前端开发 Java
java实现队列数据结构代码详解
本文详细解析了Java中队列数据结构的实现,包括队列的基本概念、应用场景及代码实现。队列是一种遵循“先进先出”原则的线性结构,支持在队尾插入和队头删除操作。文章介绍了顺序队列与链式队列,并重点分析了循环队列的实现方式以解决溢出问题。通过具体代码示例(如`enqueue`入队和`dequeue`出队),展示了队列的操作逻辑,帮助读者深入理解其工作机制。
153 1
|
3月前
|
存储 算法 安全
Java中的对称加密算法的原理与实现
本文详细解析了Java中三种常用对称加密算法(AES、DES、3DES)的实现原理及应用。对称加密使用相同密钥进行加解密,适合数据安全传输与存储。AES作为现代标准,支持128/192/256位密钥,安全性高;DES采用56位密钥,现已不够安全;3DES通过三重加密增强安全性,但性能较低。文章提供了各算法的具体Java代码示例,便于快速上手实现加密解密操作,帮助用户根据需求选择合适的加密方案保护数据安全。
332 58
|
2月前
|
存储 负载均衡 算法
我们来说一说 Java 的一致性 Hash 算法
我是小假 期待与你的下一次相遇 ~
|
2月前
|
存储 监控 算法
企业上网监控场景下布隆过滤器的 Java 算法构建及其性能优化研究
布隆过滤器是一种高效的数据结构,广泛应用于企业上网监控系统中,用于快速判断员工访问的网址是否为违规站点。相比传统哈希表,它具有更低的内存占用和更快的查询速度,支持实时拦截、动态更新和资源压缩,有效提升系统性能并降低成本。
58 0
|
3月前
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
189 3
|
5月前
|
存储 机器学习/深度学习 监控
如何监控员工的电脑——基于滑动时间窗口的Java事件聚合算法实现探析​
在企业管理场景中,如何监控员工的电脑操作行为是一个涉及效率与合规性的重要课题。传统方法依赖日志采集或屏幕截图,但数据量庞大且实时性不足。本文提出一种基于滑动时间窗口的事件聚合算法,通过Java语言实现高效、低资源占用的监控逻辑,为如何监控员工的电脑提供一种轻量化解决方案。
124 3
|
5月前
|
存储 Java 编译器
Java 中 .length 的使用方法:深入理解 Java 数据结构中的长度获取机制
本文深入解析了 Java 中 `.length` 的使用方法及其在不同数据结构中的应用。对于数组,通过 `.length` 属性获取元素数量;字符串则使用 `.length()` 方法计算字符数;集合类如 `ArrayList` 采用 `.size()` 方法统计元素个数。此外,基本数据类型和包装类不支持长度属性。掌握这些区别,有助于开发者避免常见错误,提升代码质量。
434 1

热门文章

最新文章