算法随笔-数据结构(哈希表)
本文主要介绍数据结构中哈希表在生活中的应用、主要特点、ES6如何实现HashTable类及一些leetcode真题解析。供自己以后查漏补缺,也欢迎同道朋友交流学习。
引言
哈希表(Hash Table),也称为散列表,是一种通过哈希函数将键(Key)映射到表中一个位置以便快速访问记录的数据结构。
哈希表听起来我们可能稍微陌生一点,但在生活中超市收银扫的商品条形码、图书馆的索引、手机通讯录都是哈希表的应用之一,它的本质就是在某个存储空间中存储数据,通过键值对来定位数据。
对于前端来说,简单的对象或者ES6的Map、Set等数据结构都可以认为是哈希表的一种应用,其实就是key、value的键值对。
所以,上一章介绍的字典就是哈希表的一种应用。
可以详看我写的算法随笔-数据结构(字典)
主要特点
- 键值对存储:存储数据的方式是键值对(
Key-Value),其中键是唯一的,用于通过哈希函数计算存储位置。 - 快速访问:哈希表通过键(
Key)映射到表中的一个位置,访问速度非常快,平均时间复杂度为O(1)。 - 解决冲突:由于哈希函数可能将
不同的键映射到同一个位置,哈希表需要有机制来解决这种冲突。常见的方法包括链地址法(Chaining)和开放寻址法(Open Addressing)。 - 动态调整大小:为了保持操作的效率,哈希表可以根据
负载因子(已存储元素数量与表大小的比率)动态地调整其大小。 - 非顺序存储:与数组或链表不同,哈希表不保持元素的任何特定顺序。这意味着它不适合需要按顺序处理元素的应用。
- 空间效率:哈希表通常需要
额外的空间来存储哈希桶(Buckets)或链表,这可能会增加存储成本,但为了快速访问数据,这种成本通常是值得的。
ES6实现HashTable类
借助 ES6 的 Class 实现一个更接近传统哈希表的自定义HashTable类:
class HashTable {
constructor() {
this.table = [];
}
hashCode ( key ) {
let hash = 0;
for (let i = 0; i < key.length; i++) {
hash = (hash + key.charCodeAt(i) * i) % 37;
// key.charCodeAt(i): 这部分获取键字符串中第i个字符的Unicode编码
// * i:将字符的Unicode编码乘以它在字符串中的位置i。
// 这样做可以增加不同字符位置的权重,使得相同字符在不同位置对最终哈希值的贡献不同。
// hash + ...:将当前字符的贡献累加到哈希值上。初始时,hash可能被设置为0。
// % 37:取模运算确保哈希值在0到36之间,这是因为哈希表的大小被设定为37(这是一个素数,通常选择素数作为哈希表的大小,因为它们在哈希分布上通常表现得更好)。
// 取模运算的结果将哈希值限制在哈希表的索引范围内。
}
return hash;
}
set( key, val ) {
let hashKey = this.hashCode(key);
this.table[hashKey] = val;
}
get(key) {
let hashKey = this.hashCode(key);
return this.table[hashKey];
}
delete(key) {
let hashKey = this.hashCode(key);
if (this.table[hashKey]) {
delete this.table[hashKey];
return true;
}
return false;
}
}
let myHashTable = new HashTable();
myHashTable.set('key1', 'value1');
myHashTable.get('key1'); // "value1"
myHashTable.delete('key1'); // true
myHashTable.delete('key2'); // false
leetcode真题解析
1. 两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
题解1:
利用 JS 的对象存储下标:
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
// 利用obj存储值的index下标
const obj = {}
for (let i = 0; i < nums.length; i++) {
// 值
const number = nums[i];
// 差值
const curNumber = target - number;
// 判断在hashMap里有没有值
if (obj[curNumber] !== undefined) {
return [ obj[curNumber], i];
} else {
// 没有就存obj
obj[number] = i
}
}
};
题解2:
利用 ES6 的 Map 存储下标:
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
// 利用hashMap存储值的index下标
const hashMap = new Map()
for (let i = 0; i < nums.length; i++) {
// 值
const number = nums[i];
// 差值
const curNumber = target - number;
// 判断在hashMap里有没有值
if (hashMap.has(curNumber)) {
return [ hashMap.get(curNumber), i];
} else {
// 没有就存hashMap
hashMap.set(number, i)
}
}
};
217. 存在重复元素
给你一个整数数组 nums 。如果任一值在数组中出现至少两次 ,返回true;如果数组中每个元素互不相同,返回false。
示例 1:
输入:nums = [1,2,3,1]
输出:true
解释:元素 1 在下标 0 和 3 出现。
示例 2:
输入:nums = [1,2,3,4]
输出:false
解释:所有元素都不同。
示例 3:
输入:nums = [1,1,1,3,3,4,3,2,4,2]
输出:true
题解:
/**
* @param {number[]} nums
* @return {boolean}
*/
var containsDuplicate = function(nums) {
let hashMap = new Map();
for (let item of nums) {
if (hashMap.has(item)) {
return true;
}
hashMap.set(item)
}
return false
};
3. 无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。
示例 1:
输入:s = "abcabcbb"
输出:3
解释:因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入:s = "bbbbb"
输出:1
解释:因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入:s = "pwwkew"
输出:3
解释:因为无重复字符的最长子串是 "wke",所以其长度为 3。
题解:
思路:利用滑动窗口去找最长子串,用一个滑动窗口来维护一个子串,窗口的左边界和右边界。
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
let start = 0; // 窗口的左边界
let maxLen = 0; // 最长子串的长度
let charMap = {}; // 存储字符及其索引的映射
for (let i = 0; i < s.length; i++) {
const char = s[i];
// 如果字符已经在映射中,并且它的索引在当前窗口内(即比开始索引大),则移动开始索引
if (charMap[char] !== undefined && charMap[char] >= start) {
start = charMap[char] + 1;
}
// 更新字符的索引
charMap[char] = i;
console.log('@@@@ charMap', charMap)
// 更新最长子串的长度
maxLen = Math.max(maxLen, i - start + 1);
}
return maxLen;
};