力扣热题——哈希

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 ,哈希函数的计算过程分为以下两步:

  1. 通过某种哈希算法 hash() 计算得到哈希值。
  2. 将哈希值对桶数量(数组长度)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
};

快乐数

思路:

核心是判断是否有循环出现以跳出计算过程 具体步骤如下:

  1. 初始化哈希表: 创建一个空的 set 集合,用于存储每次计算的结果。

  2. 循环计算: 在无限循环中,进行以下步骤:

  • 将当前数字转换为字符串,然后拆分为字符数组。

  • 使用 reduce 方法计算每个数字的平方和。

  • 如果计算结果为 1,说明是快乐数,返回 true

  • 如果哈希表中已经包含当前计算结果,说明存在循环,返回 false

  • 否则,将当前计算结果添加到哈希表中。

  1. 循环终止条件: 循环终止的条件有两种情况:
  • 如果计算结果为 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
};
相关推荐
我爱学习_zwj20 分钟前
深入浅出Node.js-1(node.js入门)
前端·webpack·node.js
IT 前端 张1 小时前
2025 最新前端高频率面试题--Vue篇
前端·javascript·vue.js
喵喵酱仔__1 小时前
vue3探索——使用ref与$parent实现父子组件间通信
前端·javascript·vue.js
_NIXIAKF1 小时前
vue中 输入框输入回车后触发搜索(搜索按钮触发页面刷新问题)
前端·javascript·vue.js
InnovatorX1 小时前
Vue 3 详解
前端·javascript·vue.js
布兰妮甜1 小时前
html + css 顶部滚动通知栏示例
前端·css·html
种麦南山下1 小时前
vue el table 不出滚动条样式显示 is_scrolling-none,如何修改?
前端·javascript·vue.js
杨荧2 小时前
【开源免费】基于Vue和SpringBoot的贸易行业crm系统(附论文)
前端·javascript·jvm·vue.js·spring boot·spring cloud·开源
狄加山6753 小时前
数据结构(查找算法)
数据结构·数据库·算法
陌然。。3 小时前
【701. 二叉搜索树中的插入操作 中等】
数据结构·c++·算法·leetcode·深度优先