JavaScript 中常见的四种哈希结构
在解决哈希相关问题时,JavaScript 提供了四种常见的数据结构:
- 数组(Array)
- 对象(Object)
- 集合(Set)
- 映射(Map)
我们分别来看它们在哈希法中的应用与特点。
数组 Array
数组是 JavaScript 中最基础的数据结构,可以使用下标快速访问元素,性能高,代码简洁。适用于下标直接映射的场景,比如小范围的整数索引。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const arr = [];
arr[1] = 'apple'; arr[3] = 'banana';
console.log(arr[1]);
console.log(arr[1] !== undefined); console.log(arr[2] !== undefined);
delete arr[1]; console.log(arr[1]);
console.log(arr.length);
|
然而,数组并不适合用于存储非整数类型的键值对。因此在哈希场景中,数组只适用于索引为整数的映射问题。
对象 Object
对象是 JavaScript 中最传统的键值对结构,底层实现通常基于哈希表。对象的键只能是字符串或 Symbol,数字键会被自动转换为字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| const obj = {};
obj.name = 'Alice'; obj['age'] = 25; obj[123] = 'number key';
console.log(obj.name); console.log(obj['age']); console.log(obj[123]);
console.log('name' in obj); console.log(obj.hasOwnProperty('name')); console.log(obj.height !== undefined);
delete obj.age; console.log(obj.age);
console.log(Object.keys(obj));
console.log(Object.values(obj));
|
对象适用于需要快速查找的键值对场景,但键的类型有限制,且原型链可能带来一些意外的属性。
集合 Set
集合(Set
)是 ES6 引入的一种结构,用于存储不重复的值。底层实现是哈希表,支持高效的查找、插入和删除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const set = new Set();
set.add('apple'); set.add('banana'); set.add('apple');
console.log(set.has('apple')); console.log(set.has('orange'));
set.delete('banana'); console.log(set.has('banana'));
console.log(set.size);
set.clear(); console.log(set.size);
const fruits = new Set(['apple', 'banana', 'orange']); console.log(fruits.size);
fruits.forEach(fruit => console.log(fruit));
|
如果你需要一个高效、不重复的集合结构来快速查找或去重,Set
是最佳选择。
映射 Map
Map
是键值对(key-value)结构,允许使用任意类型的键(不仅限于字符串)。同样是基于哈希表实现,具有 O(1) 的查找和修改性能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| const map = new Map();
map.set('name', 'Alice'); map.set('age', 25); map.set(123, 'number key'); map.set(true, 'boolean key');
console.log(map.get('name')); console.log(map.get(123)); console.log(map.get(false));
console.log(map.has('name')); console.log(map.has('height'));
map.delete('age'); console.log(map.has('age'));
console.log(map.size);
map.clear(); console.log(map.size);
const userMap = new Map([ ['name', 'Bob'], ['age', 30], ['city', 'New York'] ]);
userMap.forEach((value, key) => { console.log(`${key}: ${value}`); });
console.log([...userMap.keys()]);
console.log([...userMap.values()]);
|
相比对象({}
),Map
支持任意类型的键,并且迭代更方便,是现代 JavaScript 中处理哈希映射的首选。
性能对比与选择建议
数据结构 |
键类型限制 |
时间复杂度 |
适用场景 |
Array |
整数索引 |
O(1) |
小范围整数索引映射 |
Object |
字符串/Symbol |
O(1) |
传统键值对,兼容性好 |
Set |
任意类型 |
O(1) |
需要去重的集合 |
Map |
任意类型 |
O(1) |
现代键值对映射 |
总结与建议
场景 |
建议数据结构 |
索引小范围整数 |
Array |
字符串键的简单映射 |
Object |
需要去重的列表 |
Set |
任意类型 key 的键值对映射 |
Map |
兼容老代码,简单映射需求 |
Object |
JavaScript 的 Set
和 Map
是哈希法在 JS 中的现代实现,配合 Array
和 Object
可以应对大多数需要高效查找、插入、删除的场景。在选择时,优先考虑 Map
和 Set
,除非有特殊的兼容性需求或性能要求。
哈希碰撞
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
拉链法发生冲突的元素都被存储在链表中,要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
给定两个字符串 s
和 t
,编写一个函数来判断 t
是否是 s
的 字母异位词。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| var isAnagram = function(s, t) { if(s.length !== t.length) return false; let char_count = new Map(); for(let item of s) { char_count.set(item, (char_count.get(item) || 0) + 1) ; } for(let item of t) { if(!char_count.get(item)) return false; char_count.set(item, char_count.get(item) - 1); } return true; };
|
给定两个数组 nums1
和 nums2
,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
var intersection = function (nums1, nums2) { const set1 = new Set(nums1), res = new Set(); for (const num of nums2) { set1.has(num) && res.add(num); } return Array.from(res); };
|
JavaScript 的迭代器(如for...of
、forEach
)是基于迭代协议(Symbol.iterator
)实现的,每次迭代都会执行以下操作:
- 创建迭代器对象
- 调用
next()
方法
- 检查
done
状态
- 处理闭包和上下文信息
这些操作会产生额外的性能开销,尤其是在处理大量数据时。相比之下,传统for
循环直接通过索引访问数组元素,没有中间层的抽象,因此更高效。