1. 哈希表
1.1 简介
哈希表又称为散列表,其通过简历键值对之间的映射,实现高效的元素查询,只需向哈希表输入一个键key,即可在O(1)时间内获取对应的value值(增删改查都是这个复杂度)。
参考:6.1 哈希表 - Hello 算法 与数组、链表对比如下:
- 添加元素:仅需将元素添加至数组(链表)的尾部即可,使用O(1)时间。
- 查询元素:由于数组(链表)是乱序的,因此需要遍历其中的所有元素,使用O(n)时间。
- 删除元素:需要先查询到元素,再从数组(链表)中删除,使用O(n)时间。
1.2 哈希表常用操作
哈希表的常见操作包括:初始化、查询操作、添加键值对和删除键值对等。
javascript
/* 初始化哈希表 */
const myMap = new Map()
/* 添加操作 */
myMap.set('hhh', 8897)
myMap.set('hh', 8896)
myMap.set('h', 8895)
/* 查询操作 */
myMap.get('hhh')
/* 删除操作 */
myMap.delete('h')
/* 遍历哈希表 */
for(let [key, value] of myMap.entries()){
// 遍历键值对
console.log(key, '->', value)
}
for (let key of myMap.keys()){
// 单独遍历键
console.log('键:', key)
}
for(let value of myMap.values()){
// 单独遍历值
console.log('值', value);
}
1.3 哈希表简单实现
采用最简单的数组实现,将数组中的每个空位称为「桶 bucket」,每个桶可存储一个键值对。因此,查询操作就是找到 key 对应的桶,并在桶中获取 value 。
输入一个 key ,哈希函数的计算过程分为以下两步:
- 通过某种哈希算法 hash() 计算得到哈希值。
- 将哈希值对桶数量(数组长度)capacity 取模,从而获取该 key 对应的数组索引 index 。
javascript
// 设数组长度 capacity = 100、哈希算法 hash(key) = key ,易得哈希函数为 key % 100
/* 哈希表的简单实现 */
// 键值对:Number -> String
class Pair {
constructor(key, value){
this.key = key
this.value = value
}
}
class ArrayHashMap {
#buckets;
constructor(){
// 初始化数组,包含100个桶
this.#buckets = new Array(100).fill(null)
}
// 哈希函数
#hashFunc(key){
return key % 100
}
// 查询操作
get(key){
let index = this.#hashFunc(key)
let pair = this.#buckets[index]
if(pair === null) return null
return pair.value
}
// 新增操作
set(key, value){
let index = this.#hashFunc(key)
this.#buckets[index] = new Pair(key, value)
}
// 删除操作
delete(key){
let index = this.#hashFunc(key)
this.#buckets[index] = null
}
// 获取所有键值对
entries(){
let arr = []
for(let i = 0; i < this.#buckets.length; i++){
if (this.#buckets[i]) {
arr.push(this.#buckets[i])
}
}
return arr
}
// 获取所有键
keys(){
let res = []
for(let i = 0; i < this.#buckets.length; i++){
if(this.#buckets[i]){
res.push(this.#buckets[i].key)
}
}
return res
}
// 获取所有的值
values(){
let res = []
for(let i = 0; i < this.#buckets.length; i++){
if(this.#buckets[i] && this.#buckets[i].value){
res.push(this.#buckets[i].value)
}
}
return res
}
// 打印哈希表
print(){
let pairs = this.entries()
for(const item of pairs){
console.log(`key:${item.key}->value:${item.value}`);
}
}
}
// 测试数据
const testMap = new ArrayHashMap()
testMap.set(55, 'value55')
testMap.set(56, 'value56')
testMap.set(57, 'value57')
testMap.set(58, 'value58')
testMap.print()
console.log('键:', testMap.keys());
console.log('值:', testMap.values());
testMap.delete(55)
console.log('删除后:', testMap.values());
测试结果如下:
1.4 哈希冲突与扩容
哈希冲突:我们把多个输入对应同一输出的情况成为哈希冲突。
本质上来看,哈希函数是将所有key构成的输入空间映射到数组所有索引构成的输出空间,而输入空间往往是大于输出空间的,所以,理论上一定存在多个输入对应相同输出的情况,如:
45611 % 100 = 11 33611 % 100 = 11
解决方式:哈希表容量越大,多个key被分配到同一个桶的概率就越低,故可以采用扩容的的方式来减少哈希冲突
2. 哈希冲突
待续
3. 题目
两数之和
思路 : 利用Map结构存储,依次遍历,如果当前值已存在,,则直接返回存储的index值和当前的index值,否则将当前值与target差值与当前值的index作为键值对存储在Map中。
时间复杂度:O(n) 空间复杂度:O(n)
javascript
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
const myMap = new Map()
const res = []
for(let i = 0; i < nums.length; i++){
if(!myMap.has(nums[i])){
myMap.set(target - nums[i], i)
}else{
return [myMap.get(nums[i]), i]
}
}
return res
};
字母异位词分组
思路 : 依旧是利用Map结构存储,值得注意的是题目中的字母异位词要求。我们可以换种思路,将每个字符串先转换为数组,再使用数组的排序方法,最后再重新转换为字符串即可。因为各个符合异位词条件的数组经过排序后相同的必定相同,故利用其作为key标识。用到的原生API包括split、sort、join等方法。
时间复杂度:O(nk*logk) 空间复杂度:O(n + k)
javascript
/**
* @param {string[]} strs
* @return {string[][]}
*/
var groupAnagrams = function(strs) {
const myMap = new Map()
for(let str of strs){
const newStr = str.split('').sort().join('')
if(!myMap.has(newStr)){
myMap.set(newStr, [str])
}
else{
myMap.set(newStr, [...myMap.get(newStr), str])
}
}
return Array.from(myMap.values())
};
最长连续序列
思路: 咋一眼看过去可能会被这个O(n)的要求吓住,先说思路,待会再解释为什么符合。本次我们使用的是另一个基于哈希表实现的数据结构Set结构,它可以在O(n)的时间复杂度条件下实现对数组的去重;
遍历Set里的每个数据,如果Set中存在比当前大1的数,则不需要考虑以当前数这开始的情况(采用从最大数依次往前减的策略,如存在更大的数,则以当前数位起始的结果肯定要必那个大一的数的结果小);如果Set中不存在比当前数大1的情况,则开始一个while循环,判断Set中有几个比当前数小1的连续数;本次循环终止后再判断是否更新最大计数值。
时间复杂度:O(n)。解释:尽管有内部的 while 循环,但这个 while 循环的迭代次数是有限的 ,而不是完全取决于数组的长度 n。具体来说,while 循环的迭代次数最多是当前元素值 item 周围的连续序列的长度,而不是整个数组的长度。在最坏情况下,整个 Set 中的元素都被遍历一次。对于每个元素,while 循环的迭代次数是有限的。因此,总的时间复杂度是 O(n),其中 n 是 Set 中的元素数量。
空间复杂度:O(n)
javascript
/**
* @param {number[]} nums
* @return {number}
*/
var longestConsecutive = function(nums) {
// 先去重一面重复数据干扰后续计数
const mySet = new Set(nums)
let maxCount = 0
for(let item of mySet){
// 判断是否存在比当前大1的数
if(!mySet.has(item + 1)){
let current = item
let count = 1
while(mySet.has(current - 1)){
count++
current--
}
maxCount = Math.max(count, maxCount)
}
}
return maxCount
};
赎金信
思路: 解决方案非常直接,对两字符串分别遍历循环,判断Map结构中存储的键值对就行
时间复杂度:O(m + n) 空间复杂度:O(m)
javascript
/**
* @param {string} ransomNote
* @param {string} magazine
* @return {boolean}
*/
var canConstruct = function (ransomNote, magazine) {
const myMap = new Map()
for (let item of magazine) {
myMap.set(item, (myMap.get(item) || 0) + 1)
}
for (let item of ransomNote) {
if (!myMap.has(item) || myMap.get(item) === 0) return false
myMap.set(item, myMap.get(item) - 1)
}
return true
};
同构字符串
思路: 采用两个Map结构记录两字符串分别以自身当前值为key时的键值对,然后判断是否有冲突即可
时间复杂度:O(n) 空间复杂度:O(n)
javascript
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isIsomorphic = function (s, t) {
if (s.length !== t.length) return false
const myMap = new Map()
const myMap2 = new Map()
for (let i = 0; i < s.length; i++) {
if (!myMap.has(s[i])) {
myMap.set(s[i], t[i])
}
if (!myMap2.has(t[i])) {
myMap2.set(t[i], s[i])
}
if (myMap.get(s[i]) !== t[i] || myMap2.get(t[i]) !== s[i]) {
return false
}
}
return true
};
单词规律
思路、复杂度与上题一致
javascript
/**
* @param {string} pattern
* @param {string} s
* @return {boolean}
*/
var wordPattern = function (pattern, s) {
const myMap = new Map()
const myMap2 = new Map()
const arr = s.split(' ')
for (let i = 0; i < s.length; i++) {
if (!myMap.has(pattern[i])) {
myMap.set(pattern[i], arr[i])
}
if (!myMap2.has(arr[i])) {
myMap2.set(arr[i], pattern[i])
}
if (myMap.get(pattern[i]) !== arr[i] || myMap2.get(arr[i]) !== pattern[i]) return false
}
return true
};
有效的字母异位词
思路: 维护一个Map结构,一次遍历分别更新键值对信息
时间复杂度:O(n) 空间复杂度:O(n)
javascript
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isAnagram = function(s, t) {
if(s.length !== t.length) return false
const myMap = new Map()
for(let i = 0; i < s.length; i++){
myMap.set(s[i], (myMap.get(s[i]) || 0) + 1 )
myMap.set(t[i], (myMap.get(t[i]) || 0) - 1)
}
for(const item of myMap.values()){
if(item !== 0) return false
}
return true
};
快乐数
思路:
核心是判断是否有循环出现以跳出计算过程 具体步骤如下:
初始化哈希表: 创建一个空的 set 集合,用于存储每次计算的结果。
循环计算: 在无限循环中,进行以下步骤:
将当前数字转换为字符串,然后拆分为字符数组。
使用 reduce 方法计算每个数字的平方和。
如果计算结果为 1,说明是快乐数,返回 true。
如果哈希表中已经包含当前计算结果,说明存在循环,返回 false。
否则,将当前计算结果添加到哈希表中。
- 循环终止条件: 循环终止的条件有两种情况:
- 如果计算结果为 1,说明是快乐数,返回 true。
- 如果哈希表中已经包含当前计算结果,说明存在循环,返回 false。
时间复杂度:每次循环都会对当前数字的每一位进行平方和计算,这个过程的时间复杂度是 O(log(n)),其中 n 是输入数字的位数,因此,总体时间复杂度为 O(k * log(n))
空间复杂度:O(n)
javascript
/**
* @param {number} n
* @return {boolean}
*/
var isHappy = function(n) {
const mySet = new Set()
while(true){
let str = String(n)
n = str.split('').reduce((a, b) => a + Math.pow(Number(b), 2), 0)
if(n===1) return true
// 如果 set 中已经包含当前计算结果,说明存在循环,返回 false
if(mySet.has(n)) return false
mySet.add(n)
}
};
存在重复元素 II
思路:
- 这个题的难点主要在于想通示例二的处理情况,可能那个相同的值有多个,该如何判断它们当中是否有一对存在index值相差小于k的情况。
- 其实也很好理解,从前往后面遍历,因为判断条件是小于k,而满足这个条件的必定是那些相同值中相邻的一对,故只需判断后更新键值对,下一次再比较和最近那个相同值的inde值差就行了
时间复杂度:O(n) 空间复杂度:O(n)
javascript
/**
* @param {number[]} nums
* @param {number} k
* @return {boolean}
*/
var containsNearbyDuplicate = function(nums, k) {
const myMap = new Map()
for(let i = 0; i < nums.length; i++){
const num = nums[i]
if(myMap.has(num) && i - myMap.get(num) <= k){
return true
}
myMap.set(num, i)
}
return false
};