LeetCode--哈希表

LeetCode--哈希表

基础知识

哈希表(Hash table)
哈希表是根据关键码的值而直接进行访问的数据结构。一般哈希表都是用来快速判断一个元素是否出现集合里。枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
哈希函数
哈希函数通过hashCode把某种数据转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值。再次对数值做一个取模的操作,保证了数据一定可以映射到哈希表上。可能会存在几个数据映射到同一个索引,出现哈希碰撞
哈希碰撞
可能将多个数据映射到哈希表中同一位置叫做哈希碰撞。由两种解决方法,拉链法和线性探测法。
拉链法
将发生冲突的元素都被存储在链表中。 这样就可以通过索引这些发生冲突的元素。拉链法要选择适当的哈希表的大小,这样既不会因为哈希表空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
在这里插入图片描述

线性探测法
要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。在冲突的位置向下再找一个空位放置冲突的元素。
在这里插入图片描述
常见的三种哈希表结构

数组
set(集合)
map(映射)

技巧

1.数组就是简单的哈希表,但是数组的大小是受限的。题目没有限制数值的大小,就无法使用数组来做哈希表了。数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
2.map是一种<key, value>的结构,本题可以用key保存数值,用value在保存数值所在的下标或其他。使用map的空间消耗要比数组大一些,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。所以数组更加简单直接有效。map其实很万能。
3.set是一个集合,里面放的元素只能是一个key。可以用来判断一个数是否重复出现过。

题目

1.有效的字母异位词
简单
在这里插入图片描述
思路:
数组其实就是一个简单哈希表,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数,大小为26 就可以了,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值。遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。
时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。
注意
charCodeAt() 方法可返回指定位置的字符的 Unicode 编码。这个返回值是 0 - 65535 之间的整数。
stringObject.charCodeAt(index) index表示字符串中某个位置的数字,即字符在字符串中的下标。

/**
 * @param {string} s
 * @param {string} t
 * @return {boolean}
 */
var isAnagram = function(s, t) {
    if(s.length!=t.length)return false;
    const count= new Array(26).fill(0);
    const base = 'a'.charCodeAt();
    for(let i = 0; i< s.length;i++){
        count[s[i].charCodeAt()-base]++;
    }
    for(let j = 0;j< t.length;j++){
        count[t[j].charCodeAt()-base]--;
    }
    for(let k = 0 ;k<26;k++){
        if(count[k]!=0)return false;
    }
    return true;
}

2.两个数组的交集
简单
在这里插入图片描述
思路:
使用数组来做哈希的题目,是因为题目都限制了数值的大小。如果题目没有限制数值的大小,就无法使用数组来做哈希表了。而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。本体限制了大小,返回结果要求非重复,也可以用集合做。
注意:
使用 new 关键字和 Set 构造函数可以创建一个空集合。如果想在创建的同时初始化实例,则可以给 Set 构造函数传入一个可迭代对象,其中需要包含插入到新集合实例中的元素(Set 可以包含任何 JavaScript 数据类型作为值)。Set结构不会添加重复的值,经常用Set解决数组去重问题。
let a = Array.from(s) Set实例转数组,add() 添加元素,has():查询Set实例是否存在某元素(返回布尔值)。

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function(nums1, nums2) {
    let set1 = new Set(nums1);
    let set2 = new Set();
    for(let i = 0;i<nums2.length;i++){
        if(set1.has(nums2[i])){
            set2.add(nums2[i]);
        }
    }
    let ret = Array.from(set2);
    return ret;
};

3.快乐数
简单
在这里插入图片描述
思路:
题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现。当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
求各位平方的和可以这样算:
在这里插入图片描述

/**
 * @param {number} n
 * @return {boolean}
 */
var isHappy = function(n) {
    let sums = new Set();
    let sum = 0;
    let arr = String(n);
    while(true){
        for(let i = 0;i<arr.length;i++){
            sum += arr[i]*arr[i];
        }
        if(sum==1)break;
        if(sums.has(sum))return false;
        else{
            sums.add(sum);
            arr = String(sum);
            sum = 0
        }
    }
    return true;
};

4.两数之和
简单
在这里插入图片描述
思路:
不仅要知道元素有没有遍历过,还有知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。再来看一下使用数组和set来做哈希法的局限:
数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value在保存数值所在的下标。

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function(nums, target) {
    let map = {};
    for(let i =0;i<nums.length;i++){
        if(map[target-nums[i]]!=undefined){
            return[i,map[target-nums[i]]];
        }
        map[nums[i]] = i;
    }
    return null;
};

5.四数相加||
中等
在这里插入图片描述
思路:
定义 一个map,key放a和b两数之和,value 放a和b两数之和出现的次数。遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。最后返回统计值 count 就可以了

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @param {number[]} nums3
 * @param {number[]} nums4
 * @return {number}
 */
var fourSumCount = function(nums1, nums2, nums3, nums4) {
    let twoNumsSum = new Map();
    let count = 0;
    for(let i = 0;i<nums1.length;i++){
        for(let j = 0;j<nums2.length;j++){
            sum = nums1[i]+nums2[j];
            twoNumsSum.set(sum,(twoNumsSum.get(sum)||0)+1);
        }
    }
    for(let i = 0;i<nums3.length;i++){
        for(let j = 0;j<nums4.length;j++){
            sum = nums3[i]+nums4[j];
            count += twoNumsSum.get(0-sum)||0;
        }
    }
    return count;
};

6.赎金信
简单
在这里插入图片描述
思路:
因为题目所只有小写字母,那可以采用空间换取时间的哈希策略, 用一个长度为26的数组还记录magazine里字母出现的次数。然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。

/**
 * @param {string} ransomNote
 * @param {string} magazine
 * @return {boolean}
 */
var canConstruct = function(ransomNote, magazine) {
    let magStr = new Array(26).fill(0);
    let base = 'a'.charCodeAt();
    for(let i =0;i<magazine.length;i++){
        magStr[magazine[i].charCodeAt()-base]++;
    }
    for(let j =0;j<ransomNote.length;j++){
        index = ransomNote[j].charCodeAt()-base;
        if(magStr[index]==0)return false;
        magStr[index]--;
    }
    return true
};

7.三数之和
中等
在这里插入图片描述
思路:
双指针法。首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
说道去重,其实主要考虑三个数的去重。 a, b ,c, 对应的就是 nums[i],nums[left],nums[right]。a 如果重复了怎么办,a是nums里遍历的元素,那么应该直接跳过去。但这里有一个问题,是判断 nums[i] 与 nums[i + 1]是否相同,还是判断 nums[i] 与 nums[i-1] 是否相同。不能有重复的三元组,但三元组内的元素是可以重复的。与后一位比,那么就忽略了三元组内元素可以重复的情况。所以应该跟前一位比。

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function(nums) {
    // 将数组升序排序
    nums.sort((a,b)=>a-b);
    let res = [];
    for(let i = 0;i<nums.length;i++){
        // 数组排过序,如果第一个数大于0直接返回res
        if(nums[i]>0)return res;
        let left = i+1, right = nums.length-1;
        //去重
        if(i>0&&nums[i]==nums[i-1]) continue;
        while(left<right){
            let sum = nums[i]+nums[left]+nums[right];
            if(sum<0)left++;
            else if (sum>0)right--;
            else{
                res.push([nums[i],nums[left],nums[right]]);
                //去重
                while(left<right&&nums[left]==nums[left+1]){
                    left++;
                }
                while(left<right&&nums[right]==nums[right-1]){
                    right--;
                }
                left++;
                right--;
            }
        }
    }
    return res;
};

8.四数之和
中等
在这里插入图片描述
思路:
四数之和,和三数之和 (opens new window)是一个思路,都是使用双指针法, 基本解法就是在三数之和 (opens new window)的基础上再套一层for循环。 不要判断nums[k] > target 就返回了,三数之和 可以通过 nums[i] > 0 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是[-4, -3, -2, -1],target是-10,不能因为-4 > -10而跳过。但是我们依旧可以去做剪枝,逻辑变成nums[i] > target && (nums[i] >=0 || target >= 0)就可以了。两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n3),四数之和的时间复杂度是O(n3) 。五数之和、六数之和等等都采用这种解法。对于三数之和 (opens new window)双指针法就是将原本暴力O(n3)的解法,降为O(n2)的解法,四数之和的双指针解法就是将原本暴力O(n4)的解法,降为O(n3)的解法。

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[][]}
 */
var fourSum = function(nums, target) {
    let ret = [];
    if(nums.length<4)return ret;
    nums.sort((a,b)=>a-b);
    for(let i = 0;i<nums.length-3;i++){
        if(i>0&&nums[i]==nums[i-1])continue;
        for(let j = i+1;j<nums.length-2;j++){
            if(j>i+1&&nums[j]==nums[j-1])continue;
            let l = j+1, r = nums.length-1;
            while(l<r){
                let sum = nums[i]+nums[j]+nums[l]+nums[r];
                if(sum<target)l++;
                else if(sum>target)r--;
                else{
                    ret.push([nums[i],nums[j],nums[l],nums[r]]);
                    while(l<r&&nums[l]==nums[l+1])l++;
                    while(l<r&&nums[r]==nums[r-1])r--;
                    l++;
                    r--;
                }
            }
        }
    }
    return ret;
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值