一、深入了解哈希表
1、理解哈希表的本质
大家都听说过哈希表,但到底是什么呢?
百度百科告诉我们:散列表(哈希表),是一种根据关键码值而直接进行访问的数据结构。也就是说,通过一个散列函数将关键码值映射到表中的位置,以加快查找的速度。简单来说,哈希表就像一个超级智能的数组,能够快速找到或存储数据。
维基百科也给了我们一个定义:哈希表是根据键(Key)直接访问存储器位置的数据结构。这就是说,通过计算一个键值的函数,将需要查询的数据映射到表中一个位置,使得访问更快速。这个计算函数就叫做散列函数,而存放记录的数组则被称为散列表。
我的理解 :哈希表是一种数据结构,其核心思想是通过一个哈希函数将关键字映射到数组的位置,实现快速的查找、插入和删除操作。比如,想象一下学生的座位号是通过学号减去100得到的,这就是一个简单的哈希函数的应用。
2、哈希冲突是什么?
理解了哈希表的概念后,有人可能会问,如果两个关键字通过散列函数得到相同的值怎么办?
这就是我们常说的哈希冲突。
1、哈希冲突的定义
哈希冲突发生在两个不同的关键字被映射到相同的存储位置的情况下。由于哈希函数将无限的输入映射到有限的输出空间,不同的关键字可能会映射到相同的位置,导致冲突。
2、如何解决哈希冲突?
a)链地址法
链地址法是一种简单而直观的解决哈希冲突的方法。它通过在哈希表的每个位置维护一个链表,将相同位置的元素都放入链表中。即使发生冲突,元素仍然可以被存储,查找时只需在链表中遍历。
简单来说,如果有两个以上的Key存储到相同下标的数组下的时候,直接给数组拉一个链表,以后但凡是有冲突的都继续往链表里边塞。
一个简单的java示例。
java
import java.util.LinkedList;
// 定义哈希表的每个节点
class HashNode {
int key;
int value;
public HashNode(int key, int value) {
this.key = key;
this.value = value;
}
}
// 定义哈希表
class MyHashTable {
private static final int TABLE_SIZE = 10;
private LinkedList<HashNode>[] table;
public MyHashTable() {
table = new LinkedList[TABLE_SIZE];
// 初始化每个位置的链表
for (int i = 0; i < TABLE_SIZE; i++) {
table[i] = new LinkedList<>();
}
}
// 哈希函数,获取键的哈希值
private int getHash(int key) {
return key % TABLE_SIZE;
}
// 向哈希表中插入键值对
public void insert(int key, int value) {
int hash = getHash(key);
LinkedList<HashNode> list = table[hash];
// 检查是否已经存在相同的键,如果是,则更新值
for (HashNode node : list) {
if (node.key == key) {
node.value = value;
return;
}
}
// 如果没有相同的键,则将新节点加入链表
list.add(new HashNode(key, value));
}
// 从哈希表中获取键对应的值
public int get(int key) {
int hash = getHash(key);
LinkedList<HashNode> list = table[hash];
// 在链表中查找相应的键值对
for (HashNode node : list) {
if (node.key == key) {
return node.value;
}
}
// 如果未找到,则返回一个默认值,或者抛出异常等处理方式
return -1;
}
}
public class Main {
public static void main(String[] args) {
// 创建哈希表实例
MyHashTable hashTable = new MyHashTable();
// 向哈希表中插入键值对
hashTable.insert(1, 10);
hashTable.insert(2, 20);
hashTable.insert(11, 30);
// 从哈希表中获取键对应的值
System.out.println("Value for key 1: " + hashTable.get(1)); // 输出:10
System.out.println("Value for key 2: " + hashTable.get(2)); // 输出:20
System.out.println("Value for key 11: " + hashTable.get(11)); // 输出:30
}
}
b)开放定址法
开放定址法通过线性探测、二次探测等方法,寻找下一个可用的位置存储冲突的元素。这避免了链表的额外存储开销,但需要保证找到的下一个位置不会发生新的冲突。
如果有两个以上的Key存储到相同下标的数组下的时候,让这个key去寻找一个不用的位置存着。
一个简单的java示例。
java
// 定义哈希表
class MyHashTable {
private static final int TABLE_SIZE = 10;
private int[] table;
public MyHashTable() {
table = new int[TABLE_SIZE];
}
// 哈希函数,获取键的哈希值
private int getHash(int key) {
return key % TABLE_SIZE;
}
// 开放定址法中的线性探测
private int linearProbe(int hash, int i) {
return (hash + i) % TABLE_SIZE;
}
// 向哈希表中插入键值对
public void insert(int key) {
int hash = getHash(key);
int index = hash;
// 线性探测,找到下一个可用的位置
int i = 1;
while (table[index] != 0) {
index = linearProbe(hash, i);
i++;
}
// 将键存储在找到的位置
table[index] = key;
}
// 从哈希表中获取键对应的值
public boolean contains(int key) {
int hash = getHash(key);
int index = hash;
// 线性探测,查找键的位置
int i = 1;
while (table[index] != 0) {
if (table[index] == key) {
return true; // 找到了键
}
index = linearProbe(hash, i);
i++;
}
return false; // 未找到键
}
}
public class Main {
public static void main(String[] args) {
// 创建哈希表实例
MyHashTable hashTable = new MyHashTable();
// 向哈希表中插入键值对
hashTable.insert(1);
hashTable.insert(2);
hashTable.insert(11);
// 检查键是否存在于哈希表中
System.out.println("Contains key 1: " + hashTable.contains(1)); // 输出:true
System.out.println("Contains key 3: " + hashTable.contains(3)); // 输出:false
}
}
c)双散列法
双散列法使用两个不同的哈希函数,如果发生冲突,就尝试第二个哈希函数。这样可以增加冲突解决的灵活性。
如果有两个以上的Key存储到相同下标的数组下的时候,通过另外一个散列函数将这个key存到这个数组的另一个位置去。
一个简单的java示例。
java
// 定义哈希表
class MyHashTable {
private static final int TABLE_SIZE = 10;
private int[] table;
public MyHashTable() {
table = new int[TABLE_SIZE];
}
// 第一个哈希函数
private int hashFunction1(int key) {
return key % TABLE_SIZE;
}
// 第二个哈希函数
private int hashFunction2(int key) {
// 选择一个不同于 TABLE_SIZE 的质数
return 7 - (key % 7);
}
// 双散列法解决冲突
private int doubleHash(int hash1, int i, int hash2) {
return (hash1 + i * hash2) % TABLE_SIZE;
}
// 向哈希表中插入键值对
public void insert(int key) {
int hash1 = hashFunction1(key);
int hash2 = hashFunction2(key);
int index = hash1;
// 双散列法,找到下一个可用的位置
int i = 1;
while (table[index] != 0) {
index = doubleHash(hash1, i, hash2);
i++;
}
// 将键存储在找到的位置
table[index] = key;
}
// 从哈希表中获取键对应的值
public boolean contains(int key) {
int hash1 = hashFunction1(key);
int hash2 = hashFunction2(key);
int index = hash1;
// 双散列法,查找键的位置
int i = 1;
while (table[index] != 0) {
if (table[index] == key) {
return true; // 找到了键
}
index = doubleHash(hash1, i, hash2);
i++;
}
return false; // 未找到键
}
}
public class Main {
public static void main(String[] args) {
// 创建哈希表实例
MyHashTable hashTable = new MyHashTable();
// 向哈希表中插入键值对
hashTable.insert(1);
hashTable.insert(2);
hashTable.insert(11);
// 检查键是否存在于哈希表中
System.out.println("Contains key 1: " + hashTable.contains(1)); // 输出:true
System.out.println("Contains key 3: " + hashTable.contains(3)); // 输出:false
}
}
二、哈希表的优点
1. 快速的查找和操作时间
哈希表通过散列函数将关键字映射到存储位置,使得查找、插入和删除等操作可以在平均情况下在常数时间内完成。这使得哈希表在大量数据的情况下能够快速响应各种操作。
2. 灵活的数据存储
哈希表的存储方式相对灵活,不像数组一样需要提前分配固定大小的空间。它可以根据需要自动调整大小,避免了数组大小的限制,使得在动态环境中能够更加灵活地存储数据。
3. 均匀的数据分布
通过合理设计散列函数,哈希表可以使关键字在存储位置上均匀分布,减少哈希冲突的可能性。这有助于提高哈希表的性能,确保在查找时不会出现大量的线性探测或链表冲突。
4. 适应不同数据类型
哈希表并不要求关键字必须是整数或特定类型,它适应于多种数据类型。这使得哈希表在不同场景和应用中都能够灵活地处理各种数据。
5. 高效的内存利用
哈希表在处理大量数据时能够高效利用内存。相比于一些静态数据结构,它可以根据实际需求进行动态调整,避免了不必要的内存浪费。
6. 易于实现
哈希表的实现相对简单,使用散列函数进行关键字映射后,插入和查找等操作可以通过直接计算位置完成。这使得哈希表易于实现和理解,适用于多种编程语言。
7. 广泛的应用场景
由于哈希表具有快速查找、动态调整大小等特点,它在计算机科学的众多领域得到了广泛的应用,包括数据库索引、缓存实现、编译器优化等方面。
三、设计优秀的散列函数
设计散列函数时需要考虑一些关键因素:
1. 均匀性
散列函数应确保关键字在哈希表中均匀分布,避免发生大量关键字映射到同一个位置的情况。均匀分布有助于减小哈希冲突的概率,提高哈希表的性能。
2. 简单性
好的散列函数应该简单而高效,能够在短时间内计算出哈希值。过于复杂的函数可能会增加计算成本,影响哈希表的性能。一般来说,散列函数的计算时间应该是常数级别的。
3. 无规律性
散列函数应该对输入关键字的微小变化非常敏感,以避免相似的关键字产生相似的哈希值,降低冲突的可能性。这有助于提高散列函数的随机性。
4. 避免特殊模式
散列函数应该能够防止特定模式的输入导致冲突。例如,简单地将关键字的每个字符相加可能会导致具有相同字符的关键字产生相似的哈希值,造成冲突。
5. 考虑哈希表大小
散列函数的设计还应考虑到哈希表的大小,以确保哈希值在表的范围内。通常,通过取余操作来确保哈希值在合适的范围内。
6. 随机性
在一些场景中,引入一些随机性可以增加散列函数的良好性。例如,通过使用随机种子或者与随机数相关的操作,使得散列函数对于输入的变化更为敏感。
7. 考虑业务特点
最终,设计散列函数还应考虑业务的特点。根据实际应用的数据分布和查询模式,定制化散列函数,以满足特定场景的需求。
四、哈希表的广泛应用
现在我们知道了哈希表的概念和优势,那么它具体在哪些地方应用广泛呢?
1. 数据库索引
在关系型数据库中,哈希表常被用作索引的数据结构。通过散列关键字,可以快速定位数据库中的记录,提高查询性能。
2. 缓存实现
哈希表常被用作缓存的底层数据结构。通过将缓存键映射到哈希表的位置,可以快速查找并获取缓存数据,减轻数据库或其他存储系统的负载。
3. 字典和关联数组
哈希表提供了一种有效的方式来实现字典和关联数组。在编程语言中,哈希表通常被用来存储键值对,提供快速的查找和插入操作。
4. 文件系统
在文件系统中,哈希表被广泛用于加速文件的查找。通过将文件名映射到哈希表的位置,可以在大型文件系统中迅速定位文件。
5. 路由表
在网络路由中,哈希表被用于加速路由表的查找。通过将目标 IP 地址映射到哈希表的位置,可以快速决定数据包的路由路径。
6. 哈希集合和哈希集合
在编程中,哈希表常被用来实现集合和集合数据结构。通过散列唯一标识符,可以实现高效的成员检查和插入操作。
7. 分布式系统
在分布式系统中,哈希表被用于确定数据的分片和分布。通过散列键来选择特定的分片,可以实现分布式存储和计算。
8. 加密和安全
哈希表在加密算法和安全领域中也有一些应用。例如,密码哈希函数被用于存储密码的安全散列,防止明文密码泄漏。
9. 编译器优化
在编译器优化中,哈希表被用于符号表和其他数据结构,以便更快地查找变量、函数等标识符。
10. 分布式缓存
在分布式系统中,哈希表被用于实现分布式缓存。通过散列键来决定缓存数据存储在哪个节点,提高缓存的效率。
总结
哈希表是一种强大的数据结构,能够在各种场景下发挥作用。理解了它的原理和应用,我们能更好地应对数据存储和检索的需求。在设计散列函数时,要考虑数据分布的均匀性和哈希表的大小,以及业务特点,才能设计出高效的哈希表。在实际应用中,哈希表的灵活性和性。
一定要多思考,如果人永远待在舒适圈的话,人永远不会成长。共勉。
觉得作者写的不错的,值得你们借鉴的话,就请点一个免费的赞吧!这个对我来说真的很重要。૮(˶ᵔ ᵕ ᵔ˶)ა