代码随想录算法训练营第六天|哈希表part01

87 阅读5分钟

哈希表理论基础

什么是哈希表

哈希表是根据关键码的值而直接进行访问的数据结构

常见的哈希结构有 数组,map,set

哈希表能解决什么问题

一般哈希表都是用来快速判断一个元素是否出现集合里

什么时候用哈希表

当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法

242.有效的字母异位词

题目链接:242. 有效的字母异位词 - 力扣(LeetCode)

第一想法

先把字符串转换为数组

对第一个数组从0开始查找元素

在第二个数组中使用一个循环,如果找到了,则把这个元素移动到第一个数组中元素的位置

第一个数组选择下一个元素

思路

用数组创建一个哈希表,表中0-26放置每一个字母出现的次数

对于第一个数组,统计其中字母出现次数

然后遍历第二个数组

出现一个字母就在表中对应位置减一

遍历完毕后,如果哈希表不是全0,则说明两个字符串中包含的字母不一样

这里有几个点:

  • JS中,字符串可以直接遍历
  • JS中,字母相减需要先用.charCodeAt()获取字母的Unicode编码

JS代码如下:

var isAnagram = function(s, t) {
    if(s.length != t.length){
        return false;
    }
    let arr = new Array(26).fill(0);
    // 求字母的编码
    let base = 'a'.charCodeAt();
    for(let i of s){
        let n = i.charCodeAt() - base;
        arr[n]++;
    }
    for(let i of t){
        let n = i.charCodeAt() - base;
        arr[n]--;
    }
    // 判断是不是数组中所有数字都是0
    for(let i = 0; i < 26; i++){
        if(arr[i] != 0){
            return false;
        }
    }
    return true;
​
};

看了一下答案代码,发现更加简洁:

var isAnagram = function(s, t) {
    if(s.length !== t.length) return false;
    const resSet = new Array(26).fill(0);
    const base = "a".charCodeAt();
    for(const i of s) {
        resSet[i.charCodeAt() - base]++;
    }
    for(const i of t) {
        if(!resSet[i.charCodeAt() - base]) return false;
        resSet[i.charCodeAt() - base]--;
    }
    return true;
};

这里用到了一个没考虑到的角度,也就是先保证两个字符串长度相同

这样的话,在t字符串中,每一个字母在数组上的映射必须不为0

如果是0的话,直接return false

这样就不用遍历数组检查是否全部为0了

总结

哈希表这个因为不大了解JS中怎么使用

所以基本都得看答案写了

就当是学习一遍了

这一题属于典型的:

当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法!

1002. 查找常用字符

题目链接:1002. 查找共用字符 - 力扣(LeetCode)

第一想法

建立三个哈希表,再进行比对

如果三个表中对应的元素都不为0,则找到共有元素

三个表中的最小值就是元素出现的次数

思路

大体思路和第一想法是一样的

不过只需要用两个哈希表就行

哈希表1记录第一个单词的情况

哈希表2记录其他全部单词的情况

精妙之处在于:

在其他单词的记录中,每循环一个单词,哈希表1就取哈希表1和哈希表2的最小值:

如果有一个单词该字母,则该位置的值就是0

如果每个单词都有该字母,因为已经取了最小值

所以最终改值为1

image-20230320142218542

最后输出哈希表1,就是重复情况

JS代码如下:

var commonChars = function(words) {
    let res = [];
    let hash = new Array(26).fill(0);
    let oth_hash = new Array(26).fill(0);
    
    let base = 'a'.charCodeAt();
    // 算出第一个单词的哈希表
    let first = words[0];
    for(let i of first){
        let n = i.charCodeAt() - base;
        hash[n]++;
    }
    // 算其他单词哈希表并且更新
    for(let i = 1; i < words.length; i++){
        for(let j of words[i]){
            let n = j.charCodeAt() - base;
            oth_hash[n]++;
        }
        for(let n = 0; n < 26; n++){
            hash[n] = oth_hash[n] < hash[n]?oth_hash[n]:hash[n];
        }
        oth_hash.fill(0);
        
    }
    // 根据哈希表来输出字母
    for(i = 0; i<26; i++){
        while(hash[i] > 0){
            res.push(String.fromCharCode(i + base));
            hash[i]--;
        }
    }
    return res;
​
};

总结

这一题对于其他单词的哈希表的统计的思路十分的精妙,可以记一下

根据哈希表来输出字母一开始写的时候不知道怎么用JS输出

看了一下,用string.fromCharCode()

最后一部分的hash[i]--

一开始忘了加,导致显示内存溢出,排除了好一会才找到原因

以后写代码要仔细啊!!

349. 两个数组的交集

题目链接:349. 两个数组的交集 - 力扣(LeetCode)

第一想法

直接暴力解法,两个遍历解决

思路

如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费!

可以将数组转化为set,再进行比较

在JS中,Set 是一个特殊的类型集合 —— “值的集合”(没有键),它的每一个值只能出现一次。

用来做这一题很合适

JS代码如下:

var intersection = function(nums1, nums2) {
    let set1 = new Set(nums1);
    let set2 = new Set(nums2);
    let arr = [];
    for(let i of set1){
        if(set2.has(i)){
            arr.push(i);
        }
    }
    return arr;
};

这里直接操作set,但是运行速度比较慢,看了一下,主要有这两点:

  • 直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的
  • 循环比迭代器快,for(倒序循环)是最快的

答案代码如下:

var intersection = function(nums1, nums2) {
    // 根据数组大小交换操作的数组
    if(nums1.length < nums2.length) {
        const _ = nums1;
        nums1 = nums2;
        nums2 = _;
    }
    const nums1Set = new Set(nums1);
    const resSet = new Set();
    // for(const n of nums2) {
    //     nums1Set.has(n) && resSet.add(n);
    // }
    // 循环 比 迭代器快
    for(let i = nums2.length - 1; i >= 0; i--) {
        nums1Set.has(nums2[i]) && resSet.add(nums2[i]);
    }
    return Array.from(resSet);
};

这里先找到数量更小的数组,确定了循环次数

然后做一个检查:

如果set中有该元素,则把元素添加到set中去

最后把set转换成数组

总结

set中没有重复的元素,没有键,只有value

是一种用起来很方便的结构,但是

直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。

不要小瞧 这个耗时,在数据量大的情况,差距是很明显的

set的方法和数组有点类似,需要好好掌握一下

202. 快乐数

题目链接:202. 快乐数 - 力扣(LeetCode)

第一想法

利用一个数组来存储该正整数每个位置上的数字:

num[i++] = n%10

n = n/10

循环的条件是while(n/10 != 0)

遍历数组将数字平方再相加

如果不是0,则进行循环

(没想明白和set有什么关系)

思路

题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!

所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止

JS 代码如下:

var getSum = function (n) {
    let sum = 0;
    while (n) {
        sum += (n % 10) ** 2;
        n =  Math.floor(n/10);
    }
    return sum;
}
var isHappy = function(n) {
    let set = new Set();   // Set() 里的数是惟一的
    // 如果在循环中某个值重复出现,说明此时陷入死循环,也就说明这个值不是快乐数
    while (n !== 1 && !set.has(n)) {
        set.add(n);
        n = getSum(n);
    }
    return n === 1;
};

总结

把题目转换为sum是否会重复出现这一步非常重要!!

当遇到需要判断个元素是否出现在集合中时,就需要用到哈希法!

求和这一步也可以用reduce来做:

while(totalCount !== 1) {
        let arr = (''+(totalCount || n)).split('');
        totalCount = arr.reduce((total, num) => {
            return total + num * num
        }, 0)

1. 两数之和

题目链接:1. 两数之和 - 力扣(LeetCode)

第一想法

暴力解法,遍历

思路

本题可以转换一下

从第一个元素开始,遍历数组中的元素

并且使用一个集合存储遍历过的元素

当前指向x时,判断集合中是否有target-x

如果有,则成功找到

如果遍历结束,都没有符合的

则说明数组中不存在这两个元素

因为需要下标,所以需要key和value,因此这里使用map

JS代码如下:

var twoSum = function(nums, target) {
    let map = new Map();
    for(let i = 0; i < nums.length; i++){
        if(map.has(target - nums[i])){
            let n = map.get(target - nums[i])
            return [i, n];
        }
        map.set(nums[i],i);
    }
};

总结

这里的思路转换很重要

即把找两个元素变成寻找之前遍历过的元素中是否有符合条件的元素这一步

在这里用map非常的方便

JS中map的方法需要好好看看