18_哈希表_深入链地址法_哔哩哔哩_bilibili
哈希表(Hash Table),又称为散列表,是一种通过哈希函数组织数据以实现快速访问的数据结构。下面将从其概述、底层实现和前端应用场景等方面进行详细阐述。
概述
哈希表的基本思路是,设要存储的元素个数为n,设置一个长度为m(m≥n)的连续内存单元,以每个元素的关键字k为自变量,通过哈希函数把k映射为内存单元的地址(下标),并把该元素存储在这个内存单元中,如此构造的存储结构称为哈希表。简单来说,哈希表就像是一个巨大的书架,每个书架格都有一个编号(哈希值),而每个编号下都可以存放一本书(键值对)。当你想要找一本书时,只需知道它的编号,就能迅速定位到它所在的位置。
哈希函数是哈希表的核心,它接收一个输入(键),并返回一个输出(哈希值),这个哈希值通常是一个整数,用于确定键在表中的位置。理想情况下,哈希函数应该尽可能减少冲突(不同键产生相同哈希值的情况)。当两个键产生相同的哈希值时,就需要冲突解决机制。常见的冲突解决方法有开放寻址法和链地址法。
相关概念
一、哈希化
哈希化,又称散列,是通过关于键值(key)的函数,将数据映射到内存存储中的一个位置来访问的过程。这个过程称为哈希,而这个映射函数则称为散列函数或哈希函数。哈希化通过计算缩小范围,先分类再查找,从而加快查找速度。
二、哈希函数
哈希函数是指将哈希表中元素的关键键值映射为元素存储位置的函数。表示为:Addr=H(key),其中Addr表示存储地址,H表示哈希函数,key表示关键字。
常见的哈希函数构造方法有:
- 直接定址法:取Key或者Key的某个线性函数值为散列地址。Hash(k)=k,或者Hash(k)=a×k+b,(a、b均为常数)。
- 数字分析法:需要知道Key的集合,并且Key的位数比地址位数多,选择Key数字分布均匀的位作为散列地址。
- 平方取中法:取Key平方值的中间几位作为Hash地址。因为在设置散列函数时不一定知道所有关键字,选取哪几位不确定。一个数的平方的中间几位和数本身的每一位都有关,这样可以使随机分布的Key得到的散列地址也是随机分布的。
- 折叠法:将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址。当Key的位数较多且数字分布均匀时,适合采用这种方案。具体的叠加方法有两种:移位法和折叠法。
- 除留余数法:取关键字被某个除数p求余,得到的余数作为散列地址。即H(Key)=Key%p。这是最常用的构造哈希函数的方法。
- 随机数法:选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key)=random(key),其中random为随机函数。通常用于关键字长度不等时采用此法。
哈希函数的选择应尽量减少产生冲突,即不同的Key值对应同一个Hash地址的情况。但不管选用何种散列函数,都不可避免地会产生冲突。
三、哈希表
哈希表(Hash Table)又称散列表,是一种数据结构,用于实现关联数组(Associative Array),即可以通过键(Key)来查找对应的值(Value)。哈希表使用哈希函数将键转换为数组下标,从而实现快速查找、插入和删除操作。
哈希表的主要优点是查询速度非常快,时间复杂度接近O(1)。但是,哈希表也有缺点,即在数据量较大时,可能会出现哈希冲突。解决哈希冲突的方法有多种,如开放寻址法(Open Addressing)、链地址法(Chaining,又称拉链法)等。
- 开放寻址法:如果h(k)已经被占用,则按一定的增量序列探查新的位置,直到找到一个空闲的位置为止。根据探查函数p(i)的不同,开放定址法又分为线性探查法、二次探查法、随机探查法和双散列函数法等。
不推荐:开放寻址,探测长度随着填充因子(数据项/哈希表长)增大而程指数趋势增长
- 链地址法:将具有同一散列地址的记录存储在一条线性链表中。当发生冲突时,只需将新元素插入到对应链表的末尾即可。这种方法虽然解决了冲突问题,但增加了编程复杂度,并且可能导致链表过长,影响查找效率。
常见的哈希表实现有Java中的HashMap、Python中的字典(dict)等。
底层实现
-
哈希函数:哈希函数的设计至关重要,它决定了哈希表的性能和效率。一个好的哈希函数应该具有快速计算、均匀分布等特点。在JavaScript中,Object类型的哈希表实际上是通过链地址法来解决冲突的,即每个数组索引指向一个链表,链表中存储所有哈希值相同的键值对。
-
冲突解决:
- 链地址法:每个哈希表的槽位存储一个链表,所有哈希值相同的键值对都存储在这个链表中。这种方法在处理冲突时比较灵活,但可能会增加额外的空间开销。
- 开放寻址法:当发生冲突时,通过一定的探测策略(如线性探测、二次探测、再哈希等)找到一个空闲的槽位来存储键值对。这种方法不需要额外的存储空间,但可能会增加查找时间。
-
动态扩容:当哈希表中的数据量增加到一定程度时,会进行动态扩容,以避免哈希冲突和保持查找效率。扩容通常涉及重新计算所有键值对的哈希值,并将它们重新插入到新的哈希表中。
前端应用场景
- 缓存系统:在Web开发中,经常需要将频繁访问的数据存储在缓存中以提高访问速度。哈希表以其快速查找的特性,成为了缓存系统的理想选择。
- 去重与计数:在处理需要快速去重或计数的场景时,哈希表表现出色。例如,在统计一个字符串中每个字符出现的次数时,可以使用哈希表来记录每个字符及其对应的计数。
- 路由表:在Web服务器中,路由表负责将URL映射到相应的处理函数。哈希表能够快速根据URL定位到处理函数,提高路由效率。
- 用户输入处理:在处理用户输入时,可以使用哈希表来快速检查输入数据是否已存在或是否满足某些条件。
总之,哈希表作为数据结构中的瑰宝,在JavaScript开发中发挥着不可替代的作用。通过深入理解哈希表的原理和实现方式,可以更好地利用这一强大工具来优化代码性能和提高开发效率。
手动实现哈希表
实现哈希表(Hash Table)通常涉及两个主要部分:哈希函数(hash function)和存储桶(buckets)。在 JavaScript 中,我们可以使用数组和链表来实现一个简单的哈希表。数组将作为存储桶的集合,而链表将用于处理哈希冲突(即多个键映射到同一个索引的情况)。
以下是一个简单的实现:
- 定义链表节点:
链表节点将存储键值对以及指向下一个节点的指针。
class ListNode {
constructor(key, value) {
this.key = key;
this.value = value;
this.next = null;
}
}
- 定义哈希表:
哈希表将包含一个数组(存储桶)和一个哈希函数。
class HashTable {
constructor(size = 10) {
this.size = size;
this.buckets = new Array(size).fill(null);
}
// 简单的哈希函数
hashFunction(key) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = (hash * 31 + key.charCodeAt(i)) % this.size;
}
return hash;
}
// 插入键值对
set(key, value) {
const index = this.hashFunction(key);
const bucket = this.buckets[index];
if (bucket === null) {
// 如果桶为空,创建一个新的链表节点
this.buckets[index] = new ListNode(key, value);
} else {
// 如果桶不为空,遍历链表查找键或插入新节点
let currentNode = bucket;
while (currentNode !== null) {
if (currentNode.key === key) {
// 如果键已存在,更新值
currentNode.value = value;
return;
}
if (currentNode.next === null) {
// 链表末尾,插入新节点
currentNode.next = new ListNode(key, value);
return;
}
currentNode = currentNode.next;
}
}
}
// 获取值
get(key) {
const index = this.hashFunction(key);
const bucket = this.buckets[index];
if (bucket === null) {
// 如果桶为空,返回 undefined
return undefined;
} else {
// 遍历链表查找键
let currentNode = bucket;
while (currentNode !== null) {
if (currentNode.key === key) {
return currentNode.value;
}
currentNode = currentNode.next;
}
}
// 如果键不存在,返回 undefined
return undefined;
}
// 删除键值对
remove(key) {
const index = this.hashFunction(key);
let bucket = this.buckets[index];
if (bucket === null) {
// 如果桶为空,不执行任何操作
return;
} else if (bucket.key === key) {
// 如果要删除的节点是桶的第一个节点
this.buckets[index] = bucket.next;
} else {
// 遍历链表查找并删除节点
let currentNode = bucket;
while (currentNode.next !== null) {
if (currentNode.next.key === key) {
currentNode.next = currentNode.next.next;
return;
}
currentNode = currentNode.next;
}
}
}
}
- 使用哈希表:
现在我们可以创建哈希表实例并插入、获取和删除键值对。
const hashTable = new HashTable();
hashTable.set('name', 'Alice');
hashTable.set('age', 30);
hashTable.set('city', 'New York');
console.log(hashTable.get('name')); // 输出: Alice
console.log(hashTable.get('age')); // 输出: 30
console.log(hashTable.get('city')); // 输出: New York
hashTable.remove('age');
console.log(hashTable.get('age')); // 输出: undefined
这个简单的哈希表实现展示了如何使用数组和链表来处理哈希冲突,并通过哈希函数将键映射到数组索引。注意,这只是一个基础实现,实际生产中的哈希表可能会使用更复杂的哈希函数、动态数组大小调整(如哈希表的扩展和收缩)以及其他优化技术。