数据结构与算法——哈希表

24 阅读11分钟

📒 哈希表理论基础

1、概念

哈希表是根据关键码的值而直接进行访问的数据结构,哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,其实直白来讲其实数组就是一张哈希表。

2、作用

一般哈希表都是用来快速判断一个元素是否出现在集合里。 例如查询一个名字是否在这个学校里,只需要初始化的时候把这所学校的所有学生的名字通过哈希函数映射一张哈希表里,在查询的时候通过索引直接就可以知道这个同学是否在这所学校里,

3、哈希函数

哈希函数,通过hashCode把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。

如果hashCode得到的数值大于哈希表的tableSize,为了保证映射出来的值都落在哈希表上,我们会在再次对数值做一个取模的操作,这样我们就保证了学生姓名一定可以映射到哈希表上了。

如果学生的数量大于哈希表的大小此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表同一个索引下标的位置。也就是鸽巢原理

4、鸽巢原理

这个原理本身很简单,它是说,如果有 10 个鸽巢,有 11 只鸽子,那肯定有 1 个鸽巢中的鸽子数量多于 1 个,换句话说就是,肯定有 2 只鸽子在 1 个鸽巢内。

5、什么时候使用哈希表

当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候。

📒 相关题目和题解

242、有效字母词异位

image.png 以下为js代码

/**
 * @param {string} s
 * @param {string} t
 * @return {boolean}
 */
var isAnagram = function(s, t) {
    // 如果长度不相等,则直接返回false
    if(s.length !== t.length) return false;
    // 创造一个长度为26的数组,并且值都为0,因为字符a到字符z的ASCII也是26个连续的数值。
    let charArr = new Array(26).fill(0)
    // 因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。
    let baseChart = "a".charCodeAt()
    for(let i of s){
        // 字符串s中字符出现的次数
        let index = i.charCodeAt() - baseChart
        charArr[index]++
    }
    for(let i of t){
        let index = i.charCodeAt() - baseChart
        if(!charArr[index]) return false
        charArr[index]--
    }
    return true
};
349、两个数组的交集

image.png

以下为js代码

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function(nums1, nums2) {
    // 因为提示中说明数组成都<=1000,可以用数组的解法
    const numsArr = new Array(1005).fill(0)
    // 存放结果的哈希表,new Set()在add的时候可以自动去重
    const resultSet = new Set()
    for(i=0;i<nums1.length;i++){
        // 因为值唯一,所以只需要赋值为1当作特殊标识就可以
        const index = nums1[i]
        numsArr[index] = 1
    }
    for(i=0;i<nums2.length;i++){
        const index = nums2[i]
        // 如果numsArr中存在nums2的值,则自动添加到存放结果的哈希表中
        if(numsArr[index]===1){
            resultSet.add(nums2[i])
        }
    }
    return Array.from(resultSet)
};

也可以用map的方法来解答

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function(nums1, nums2) {
    // 对大的数组进行去重,对小的数组进行遍历
    if(nums1.length<nums2.length){
        let temp = nums1;
        nums1=nums2;
        nums2=temp;
    }
    // nums1转换成哈希表,后面可以用has方法查看nums2的值是否在nums1里面
    let nums1Set = new Set(nums1)
    // 创建一个resSet哈希表,遍历nums2添加时可以自动去重
    const resSet = new Set();
    
    for(let i = 0; i<nums2.length;i++){
        if(nums1Set.has(nums2[i])){
            resSet.add(nums2[i])
        } 

    }
    return Array.from(resSet)
};
202、快乐数

image.png 以下为js代码

/**
 * @param {number} n
 * @return {boolean}
 */
var isHappy = function(n) {
    let hashArr = new Set();
    const getsum =(n) => {
        let sum = 0;
        const arr = n.toString().split('');
        arr.map((item)=>{
            sum+=item*item
        })
        return sum;
    }
    while(true){
        // 如果hashArr出现过sum,则直接返回false
        if(hashArr.has(n)){
            return false
        }
        // 如果sum为1,证明是快乐数,返回true,退出循环
        if(n===1) {
            return true
        }
        hashArr.add(n)
        n = getsum(n)
    }
};
1、两数之合

image.png 解题思路:本题,我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适

再来看一下使用数组和set来做哈希法的局限。

  • 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
  • set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。

此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value再保存数值所在的下标。

以下为用map解决的js代码

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    // 用map结构的哈希表存放已经访问过的数据,key为元素,value为index
    let hashHelp = new Map();
    // 定义一个数组存放结果
    let resArr  = [];
    // 遍历数组
    for(let i=0; i<nums.length;i++){
        //  get(key) : 获取指定键对应的值
        let index = hashHelp.get(target-nums[i])
        if(index!==undefined){
            // 在哈希中找到对应的值,存入结果数组
            resArr.push(i, index)
        }else {
            // 在哈希中找不到对应的值,存入访问过的哈希表中
            hashHelp.set(nums[i], i)
        }
    }
    return resArr
};

以下为用暴力遍历解决的js代码

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    // 从第一个数据开始遍历
    for(let i=0;i<nums.length;i++){
        // 从当前遍历的数据的后一个开始遍历
        for(let j=i+1; j<nums.length;j++){
            // 如果存在两数相加等一target,直接返回两数下标
            if(nums[i]+nums[j]===target){
                return [i, j]
            }
        }
    }
};
383、赎金信

用map的js代码

/**
 * @param {string} ransomNote
 * @param {string} magazine
 * @return {boolean}
 */
var canConstruct = function(ransomNote, magazine) {
    // 存放magazine中每个字符出现的次数,key为字符,value为次数
    let hashMap = new Map()
    for(let i = 0; i<magazine.length;i++){
        // 把magazine每个字符对应的次数通过set方法存入哈希表
        let count = hashMap.get(magazine[i]) ? hashMap.get(magazine[i]) : 0;
        hashMap.set(magazine[i], count+1);
    }
    for (let j=0; j<ransomNote.length; j++){
        // 通过get方法拿到key对应的次数,如果没有或者次数为0,直接返回false,退出循环
        let count = hashMap.get(ransomNote[j])
        if(count===0||count===undefined) {
            return false
        }
        // 如果能拿到对应的key并且值大于0,count减去1个
        hashMap.set(ransomNote[j], count-1);
    }
    return true
};

用数组的js代码

/**
 * @param {string} ransomNote
 * @param {string} magazine
 * @return {boolean}
 */
var canConstruct = function(ransomNote, magazine) {
    // 创建一个长度为26的数组用来存储magazine中字母出现的次数,元素为index,次数为值
    const hashArr = new Array(26).fill(0);
    const base = 'a'.charCodeAt();
    for(const i of magazine){
        const index = i.charCodeAt() - base;
        hashArr[index]++;
    }
    for (const j of ransomNote){
        const index = j.charCodeAt() - base;
        // 遍历ransomNote的元素在hashArr中的值为0的时候,说明没有可以用的值了,直接返回false
        if(hashArr[index]===0) return false;
        // 如果hashArr中该元素数量大于0,则抵扣掉一个,做减1操作
        hashArr[index]--;
    }
    return true
};

实际上在本题中,题目说只有小写字母,用一个长度为26的数组来记录magazine里字母出现的次数就可以,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!

15、三数之和

image.png 题目分析:

  1. 首先对数组进行排序,排序后固定一个数 nums[i],再使用左右指针指向 nums[i]后面的两端,数字分别为 nums[leftIndex] 和 nums[rightIndex],计算三个数的和 sum判断是否满足为 0,满足则添加进结果集
  2. 如果 nums[i]大于 0,则三数之和必然无法等于 0,break结束循环。
  3. 如果 nums[i]== nums[i−1],则说明该数字重复,会导致结果重复,所以应该跳过continue进入下次循环。
  4. 当 sum == 0 时,nums[leftIndex] == nums[leftIndex+1] 则会导致结果重复,应该跳过,L++。
  5. 当 sum == 0 时,nums[rightIndex] == nums[rightIndex−1] 则会导致结果重复,应该跳过,R−−。
  6. 时间复杂度:O(n^2)

js代码如下

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function(nums) {
    // 双指针法思路:遍历nums,
    // i表示当前位置,leftIndex=i+1,rightIndex=nums.length-1
    let resArr = [];
    if(nums == null || nums.length < 3) return resArr;
    nums.sort((a,b)=>a-b);
    for(let i=0; i<nums.length; i++){
        // 如果当前数字大于0,则三数之和一定大于0,所以结束循环
        if(nums[i]>0) break;
        // 对a去重,如果前面已经对这个数进行了查找,那么continue进入下一次循环
        if(i>0&& nums[i]===nums[i-1]){
            continue;
        }
        
        let leftIndex = i+1;
        let rightIndex = nums.length-1;
        while (rightIndex > leftIndex){
            const sum = nums[i]+nums[leftIndex]+nums[rightIndex]
            // 因为数组是排序之后的,如果三个数相加大于0,则右边的指针向左移动
            if(sum>0){
                rightIndex--;
            } else if(sum<0){
                // 因为数组是排序之后的,如果三个数相加大于0,则左边的指针向右移动
                leftIndex++;
            } else {
                // nums[i]+nums[leftIndex]+nums[rightIndex]===0
                resArr.push([nums[i], nums[leftIndex], nums[rightIndex]]);
                // 对c去重
                while(rightIndex>leftIndex&&nums[rightIndex]===nums[rightIndex-1]){
                    rightIndex--;
                }
                // 对b去重
                while(rightIndex>leftIndex&&nums[leftIndex]===nums[leftIndex+1]){
                    leftIndex++;
                }
                // 往后面继续寻找
                rightIndex--;
                leftIndex++;
            }
        }
    }
    return resArr;
};
18、四数之和

image.png 题目解析: 都是使用双指针法, 基本解法就是在三数之和的基础上再套一层for循环。

js代码如下

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[][]}
 */
var fourSum = function(nums, target) {
    const len = nums.length;
    if(len < 4) return [];
    nums.sort((a, b) => a - b);
    const res = [];
    for(let i = 0; i < len - 3; i++) {
        // 去重i
        if(i > 0 && nums[i] === nums[i - 1]) continue;
        for(let j = i + 1; j < len - 2; j++) {
            // 去重j
            if(j > i + 1 && nums[j] === nums[j - 1]) continue;
            let l = j + 1, r = len - 1;
            while(l < r) {
                const sum = nums[i] + nums[j] + nums[l] + nums[r];
                if(sum < target) { l++; continue}
                if(sum > target) { r--; continue}
                res.push([nums[i], nums[j], nums[l], nums[r]]);
		
		// 对nums[left]和nums[right]去重
                while(l < r && nums[l] === nums[++l]);
                while(l < r && nums[r] === nums[--r]);
            }
        } 
    }
    return res;
};
454、四数相加2

image.png 题目解析:

js代码如下:

var fourSumCount = function(nums1, nums2, nums3, nums4) {
    const twoSumMap = new Map();
    let count = 0;
    // 统计nums1和nums2数组元素之和,和出现的次数,放到map中
    for(const n1 of nums1) {
        for(const n2 of nums2) {
            const sum = n1 + n2;
            twoSumMap.set(sum, (twoSumMap.get(sum) || 0) + 1)
        }
    }
    // 找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来
    for(const n3 of nums3) {
        for(const n4 of nums4) {
            const sum = n3 + n4;
            count += (twoSumMap.get(0 - sum) || 0)
        }
    }

    return count;
};

📒 题外话:JavaScript中的new map()和new set()相关知识总结

1、简介:
  • new Map(): 在JavaScript中,new Map()用于创建一个新的 Map 对象。Map 对象是一种键值对的集合,其中的键是唯一的,值可以重复。
  • new Set(): 在JavaScript中, new Set() 是用来创建一个新的 Set 对象的语法。Set 对象是一种集合,其中的值是唯一的,没有重复的值。 new Set() 可以用来创建一个空的 Set 对象,在创建时传入一个数组或类数组对象,Set 会自动去除重复的值。
2、new Map()的基本特性和相关方法
  1. new Map() 是用来创建一个新的 Map 对象的构造函数。Map 对象保存键值对,并记住键的原始插入顺序。这意味着你可以迭代 Map 对象,按键的插入顺序获取键值对。
  2. Map 对象与普通的对象(使用字符串作为键)不同,因为 Map 可以使用任何类型作为键(包括函数、对象或任何原始值),而不仅仅是字符串或符号。
  3. 相关方法
  • set(key, value) : 向 Map 对象中添加一个键值对。
  • get(key) : 获取指定键对应的值。
  • has(key) : 判断 Map 对象中是否存在指定的键。
  • delete(key) : 删除指定键及其对应的值。
  • size : 返回 Map 对象中键值对的数量。
  • clear() : 清空 Map 对象中的所有键值对。
  • keys() : 返回一个包含 Map 对象中所有键的迭代器。
  • values() : 返回一个包含 Map 对象中所有值的迭代器。
  • entries() : 返回一个包含 Map 对象中所有键值对的迭代器。
3、new Set()的基本特性和相关方法
  1. 唯一性:在 Set 中,每个值只出现一次,可以实现简单的数组去重,即使是两个完全相同的对象,它们在 Set 中也只会被存储一次。
  2. 无序性:Set 中的元素没有特定的顺序。
  3. 相关方法
  • add(value): 向 Set 对象中添加一个值。如果值已存在,则不会进行任何操作。
  • delete(value): 从 Set 对象中删除一个值。如果值存在,则删除并返回 true;否则,返回 false
  • has(value): 返回一个布尔值,表示 Set 对象中是否包含指定的值。
  • clear(): 清空 Set 对象,移除所有元素。