哈希算法

67 阅读11分钟

哈希法

什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。比如需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。

有效的字母异位词

  • leetcode.cn/problems/va…

  • var isAnagram = function(s, t) {
        let map = new Map()
        for(let item of s){
            map.set(item, (map.get(item)||0) + 1)
        }
        let size = map.size
        for(let item of t){
            console.log(map.get(item))
            if(map.get(item)){
                map.set(item,map.get(item) - 1)
                if(map.get(item)==0) size--
                continue
            }
            return false
        }
        return size === 0? true : false
    };
    

    牛马算法

    image-20230306090050599

  • 修改:定义一个数组叫做record用来上记录字符串s里字符出现的次数。

    需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。

    再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。

    那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。

    那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。

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;
};

两个数组的交集

  • leetcode.cn/problems/in…

    不知道为啥 一遇到去重 我就想用set。。。但是直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。不要小瞧 这个耗时,在数据量大的情况,差距是很明显的。

  • var intersection = function(nums1, nums2) {
        let set1 = new Set(nums1)
        let set2 = new Set(nums2)
        let set3 = new Set()
        for(let item of set1){
            if(set2.has(item)){
                set3.add(item)
            }
        }
        return [...set3]
    };
    
  • 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);
    };
    
  • 但是要注意,使用数组来做哈希的题目,是因为题目都限制了数值的大小。

    而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。

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

快乐数

  • leetcode.cn/problems/ha…

  • var isHappy = function(n) {
        function getSum(num) {
            let sum = 0
            while(num){
                let temp = num % 10
                sum += temp*temp
                num = Math.floor(num/10)
            }
            return sum
        }
        let set = new Set()
        n = getSum(n)
        while(n!==1 && !set.has(n)){
            set.add(n)
            n = getSum(n)
        }
        return n===1? true: false
    };
    
  • 这道题。em 没什么好说的

两数之和!

  • 万物起源之第一题 两数之和!典中典

  • leetcode.cn/problems/tw…

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

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

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

    此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value在保存数值所在的下标。在遍历数组的时候,只需要向map去查询是否有和目前遍历元素比配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。

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

三数之和

  • leetcode.cn/problems/3s…

  • 这题双指针算法,之前刷过 印象比较深刻,记得判断去重复逻辑

    var threeSum = function(nums) {
        let res = []
        nums.sort((a,b) => a-b)
        if(nums.length < 3) return res
        for(let  i = 0 ; i < nums.length - 2 ; i++){
            if(nums[i] > 0) return res
            if(i>0&&nums[i] == nums[i-1]) continue
            let left = i + 1
            let right = nums.length - 1
            while(left < right) {
                if(left < right && nums[i] + nums[left] + nums[right] < 0){
                     left++
                     continue
                }
                if(left < right && nums[i] + nums[left] + nums[right] > 0){
                     right--
                     continue
                }
                res.push([nums[i],nums[left],nums[right]])
                left++
                right--
                while(left< right && nums[left] === nums[left-1]) left++;
                while(left < right && nums[right] === nums[right+1]) right--;
            }
        }
        return res
    };
    
  • 使用了一下两数之和的方法 超时

    var threeSum = function(nums) {
        let res = []
        nums.sort((a,b) => a-b)
        if(nums.length < 3) return res
        for(let  i = 0 ; i < nums.length - 2 ; i++){
            if(nums[i] > 0) return res
            if(i>0&&nums[i] == nums[i-1]) continue
            let target = -nums[i]
            let set = new Set()
            for(let j = i + 1 ; i < nums.length ; j ++){
                if(j>i+1 && nums[j]==nums[j-1]) continue
                if(set.has(target - nums[j])){
                    res.push([nums[i],num[j],target-num[j]])
                }
                set.add(nums[j])
            }
        }
        return res
    };
    
  • 随想录整理的递归法 看了一遍没动手写

    /**
     *  nsum通用解法,支持2sum,3sum,4sum...等等
     *  时间复杂度分析:
     *  1. n = 2时,时间复杂度O(NlogN),排序所消耗的时间。、
     *  2. n > 2时,时间复杂度为O(N^n-1),即N的n-1次方,至少是2次方,此时可省略排序所消耗的时间。举例:3sum为O(n^2),4sum为O(n^3)
     * @param {number[]} nums
     * @return {number[][]}
     */
    var threeSum = function (nums) {
        // nsum通用解法核心方法
        function nSumTarget(nums, n, start, target) {
            // 前提:nums要先排序好
            let res = [];
            if (n === 2) {
                res = towSumTarget(nums, start, target);
            } else {
                for (let i = start; i < nums.length; i++) {
                    // 递归求(n - 1)sum
                    let subRes = nSumTarget(
                        nums,
                        n - 1,
                        i + 1,
                        target - nums[i]
                    );
                    for (let j = 0; j < subRes.length; j++) {
                        res.push([nums[i], ...subRes[j]]);
                    }
                    // 跳过相同元素
                    while (nums[i] === nums[i + 1]) i++;
                }
            }
            return res;
        }
    
        function towSumTarget(nums, start, target) {
            // 前提:nums要先排序好
            let res = [];
            let len = nums.length;
            let left = start;
            let right = len - 1;
            while (left < right) {
                let sum = nums[left] + nums[right];
                if (sum < target) {
                    while (nums[left] === nums[left + 1]) left++;
                    left++;
                } else if (sum > target) {
                    while (nums[right] === nums[right - 1]) right--;
                    right--;
                } else {
                    // 相等
                    res.push([nums[left], nums[right]]);
                    // 跳过相同元素
                    while (nums[left] === nums[left + 1]) left++;
                    while (nums[right] === nums[right - 1]) right--;
                    left++;
                    right--;
                }
            }
            return res;
        }
        nums.sort((a, b) => a - b);
        // n = 3,此时求3sum之和
        return nSumTarget(nums, 3, 0, 0);
    };
    

四数之和

  • leetcode.cn/problems/4s…

  • 就是在三数之和基础上嵌套一层for循环而已

    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;
    };
    

四数相加||

  • leetcode.cn/problems/4s…

  • var fourSumCount = function(nums1, nums2, nums3, nums4) {
        let map = new Map()
        for(let i = 0 ; i < nums1.length ; i++){
            for(let j = 0 ; j < nums2.length ; i++){
                let sum = nums1[i] + nums2[j]
                map.set(sum , (map.get(sum)||0) + 1)
            }
        }
        let res = 0
        for(let i = 0 ; i < nums3.length ; i++){
            for(let j = 0 ; j < nums4.length ; j++){
                let sum = nums3[i] + nums4[j]
                res += (map.get(-sum)||0)
            }
        }
    
        return res
    };
    
  • 人家迷惑 这样居然是超时的。。。。

  • image-20230307185255026

  • 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;
    };
    
  • 然后卡哥的代码就不超时了 没看出什么区别。

赎金信

  • leetcode.cn/problems/ra…

  • 太简单了 跟上面题 一样 没什么好说的

  • var canConstruct = function(ransomNote, magazine) {
        let map = new Map()
        for(let i of magazine){
            map.set(i,(map.get(i)||0)+1)
        }
        for(let j of ransomNote){
            if(!map.has(j)||map.get(j)==0) return false
            map.set(j,map.get(j)-1)
        }
        return true
    };
    

    哈希法

什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。比如需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。

有效的字母异位词

  • leetcode.cn/problems/va…

  • var isAnagram = function(s, t) {
        let map = new Map()
        for(let item of s){
            map.set(item, (map.get(item)||0) + 1)
        }
        let size = map.size
        for(let item of t){
            console.log(map.get(item))
            if(map.get(item)){
                map.set(item,map.get(item) - 1)
                if(map.get(item)==0) size--
                continue
            }
            return false
        }
        return size === 0? true : false
    };
    

    牛马算法

    image-20230306090050599

  • 修改:定义一个数组叫做record用来上记录字符串s里字符出现的次数。

    需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。

    再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。

    那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。

    那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。

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;
};

两个数组的交集

  • leetcode.cn/problems/in…

    不知道为啥 一遇到去重 我就想用set。。。但是直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。不要小瞧 这个耗时,在数据量大的情况,差距是很明显的。

  • var intersection = function(nums1, nums2) {
        let set1 = new Set(nums1)
        let set2 = new Set(nums2)
        let set3 = new Set()
        for(let item of set1){
            if(set2.has(item)){
                set3.add(item)
            }
        }
        return [...set3]
    };
    
  • 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);
    };
    
  • 但是要注意,使用数组来做哈希的题目,是因为题目都限制了数值的大小。

    而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。

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

快乐数

  • leetcode.cn/problems/ha…

  • var isHappy = function(n) {
        function getSum(num) {
            let sum = 0
            while(num){
                let temp = num % 10
                sum += temp*temp
                num = Math.floor(num/10)
            }
            return sum
        }
        let set = new Set()
        n = getSum(n)
        while(n!==1 && !set.has(n)){
            set.add(n)
            n = getSum(n)
        }
        return n===1? true: false
    };
    
  • 这道题。em 没什么好说的

两数之和!

  • 万物起源之第一题 两数之和!典中典

  • leetcode.cn/problems/tw…

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

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

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

    此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value在保存数值所在的下标。在遍历数组的时候,只需要向map去查询是否有和目前遍历元素比配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。

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

三数之和

  • leetcode.cn/problems/3s…

  • 这题双指针算法,之前刷过 印象比较深刻,记得判断去重复逻辑

    var threeSum = function(nums) {
        let res = []
        nums.sort((a,b) => a-b)
        if(nums.length < 3) return res
        for(let  i = 0 ; i < nums.length - 2 ; i++){
            if(nums[i] > 0) return res
            if(i>0&&nums[i] == nums[i-1]) continue
            let left = i + 1
            let right = nums.length - 1
            while(left < right) {
                if(left < right && nums[i] + nums[left] + nums[right] < 0){
                     left++
                     continue
                }
                if(left < right && nums[i] + nums[left] + nums[right] > 0){
                     right--
                     continue
                }
                res.push([nums[i],nums[left],nums[right]])
                left++
                right--
                while(left< right && nums[left] === nums[left-1]) left++;
                while(left < right && nums[right] === nums[right+1]) right--;
            }
        }
        return res
    };
    
  • 使用了一下两数之和的方法 超时

    var threeSum = function(nums) {
        let res = []
        nums.sort((a,b) => a-b)
        if(nums.length < 3) return res
        for(let  i = 0 ; i < nums.length - 2 ; i++){
            if(nums[i] > 0) return res
            if(i>0&&nums[i] == nums[i-1]) continue
            let target = -nums[i]
            let set = new Set()
            for(let j = i + 1 ; i < nums.length ; j ++){
                if(j>i+1 && nums[j]==nums[j-1]) continue
                if(set.has(target - nums[j])){
                    res.push([nums[i],num[j],target-num[j]])
                }
                set.add(nums[j])
            }
        }
        return res
    };
    
  • 随想录整理的递归法 看了一遍没动手写

    /**
     *  nsum通用解法,支持2sum,3sum,4sum...等等
     *  时间复杂度分析:
     *  1. n = 2时,时间复杂度O(NlogN),排序所消耗的时间。、
     *  2. n > 2时,时间复杂度为O(N^n-1),即N的n-1次方,至少是2次方,此时可省略排序所消耗的时间。举例:3sum为O(n^2),4sum为O(n^3)
     * @param {number[]} nums
     * @return {number[][]}
     */
    var threeSum = function (nums) {
        // nsum通用解法核心方法
        function nSumTarget(nums, n, start, target) {
            // 前提:nums要先排序好
            let res = [];
            if (n === 2) {
                res = towSumTarget(nums, start, target);
            } else {
                for (let i = start; i < nums.length; i++) {
                    // 递归求(n - 1)sum
                    let subRes = nSumTarget(
                        nums,
                        n - 1,
                        i + 1,
                        target - nums[i]
                    );
                    for (let j = 0; j < subRes.length; j++) {
                        res.push([nums[i], ...subRes[j]]);
                    }
                    // 跳过相同元素
                    while (nums[i] === nums[i + 1]) i++;
                }
            }
            return res;
        }
    
        function towSumTarget(nums, start, target) {
            // 前提:nums要先排序好
            let res = [];
            let len = nums.length;
            let left = start;
            let right = len - 1;
            while (left < right) {
                let sum = nums[left] + nums[right];
                if (sum < target) {
                    while (nums[left] === nums[left + 1]) left++;
                    left++;
                } else if (sum > target) {
                    while (nums[right] === nums[right - 1]) right--;
                    right--;
                } else {
                    // 相等
                    res.push([nums[left], nums[right]]);
                    // 跳过相同元素
                    while (nums[left] === nums[left + 1]) left++;
                    while (nums[right] === nums[right - 1]) right--;
                    left++;
                    right--;
                }
            }
            return res;
        }
        nums.sort((a, b) => a - b);
        // n = 3,此时求3sum之和
        return nSumTarget(nums, 3, 0, 0);
    };
    

四数之和

  • leetcode.cn/problems/4s…

  • 就是在三数之和基础上嵌套一层for循环而已

    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;
    };