Day6-哈希表,leetcode242,349,202,1

哈希表

理论

  • 哈希表,(散列表,hash table),根据关键码的值直接进行访问的数据结构。比如数组也是一张哈希表,关键码是数组的索引下标,然后通过下标直接访问数组中的元素。

  • 哈希表,用来判断一个元素是否出现在集合里。

  • 哈希函数,如通过hashCode把名字转化为数值,一般hashCode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就可以把名字映射为哈希表上的索引数字了。

  • 哈希碰撞,映射到了同一个位置。哈希碰撞的解决方法:拉链法和线性探测法。

    • 拉链法,选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
  • 哈希结构分类:数组、Set(集合)、map(映射)



题目

  1. 有效的字母异位词
  • 给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的 字母异位词。

  • 思路:

  • 定义:有效字母异位词,字符串中的字母都相同,但位置不同。

  • 方法1:用数组哈希表,用数组做哈希映射,可以存放26个字母,hash[26],借助小写字母a-z,对应的ascii码为97~122,把字符映射到数组即哈希表的索引下标上,统计A字符串中的每个字母出现的频率,再遍历第二个字符串的时候,第二个字符串每个字母出现的频率,在hash数组里再做对应的减法,如果最后哈希数组里所有元素都是0的话,说明这两个字符串就是有效字母异位词

  • 方法2:用set哈希表,先遍历s字符串,key-value统计,key为对应字符串中的某个字母,value为出现的次数,默认为0,遇到一次+1,再遍历t字符串,如果遇到t中字符串不在set哈希表的情况,直接返回,判断为非字母异位词,如果在哈希表中,则将对应哈希表中的统计次数-1,最后遍历哈希表判断哈希数组里所有元素都是0的话,说明这两个字符串就是有效字母异位词

// 数组hash
// charCodeAt 是字符串的方法,用于获取某个字符的 Unicode 编码(ASCII 码)。
var isAnagram = function (s, t) {
    if(s.length !== t.length) return false
    const arr = new Array(26).fill(0)
    const base = 'a'.charCodeAt()
    // 因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。97~122号为26个小写英文字母
    for(var i = 0; i<s.length; i++ ) {
        arr[s[i].charCodeAt() - base]++
    }
    for (i = 0; i < t.length; i++) {
        if (!arr[t[i].charCodeAt() - base]) return false
        arr[t[i].charCodeAt() - base]--
    }
    return true
};

// map hash
var isAnagram = function(s, t) {
    if (s.length !== t.length) return false
    let map = new Map()
    for (let item of s) {
        map.set(item, (map.get(item) || 0) + 1)
    }
    for (let item of t) {
        if (!map.get(item)) return false
        map.set(item, map.get(item)-1)
    }
    return true
}

  • 关于最后再判断哈希表里的值是不是全 0,上面代码里却没有判断的原因如下:

    • 在遍历 t 时,每遇到一个字符,都会让 map 里对应的计数减 1。
    • 如果某个字符在 t 中出现次数比 s 多,map.get(item) 会变成 0 或负数,这时 if (!map.get(item)) return false 就会提前返回 false。
    • 如果所有字符都能正常减到 0,说明两个字符串的字符和出现次数完全一致。
  • 前提是:

    • 两个字符串长度相等(开头已经判断)。
    • 每次遇到 map.get(item) 为 0 时就直接返回 false,保证不会出现多余字符。
  • 举例说明:

    • s = "aab", t = "aba",遍历完后 map 里所有值都为 0,返回 true。
    • s = "aab", t = "abb",遍历到第二个 b 时,map.get('b') 变成 0,直接返回 false。
  • 所以,这种写法已经保证了 map 里不会有非 0 的情况,不需要最后再遍历判断。


  • 这里怎么不是先-1,再判断是否为0
  1. 先判断再减一,可以直接发现“多余字符”
  • 如果 map.get(item) 为 0,说明 t 中这个字符出现次数比 s 多,直接返回 false,不用再减。
  • 如果先减一再判断,遇到 map.get(item) 为 1 的情况,减完变成 0,你就无法区分“刚好用完”还是“多出来了”。
  1. 这样写可以保证每次遇到多余字符都能及时返回
  • 比如 s = "ab", t = "aab",遍历到第二个 a 时,map.get('a') 已经是 0,此时直接返回 false,不会出现负数。
  1. 逻辑更清晰
  • 先判断有没有剩余,再消耗掉一次计数,符合“用一个扣一个”的直观思路。

  • 总结:

  • 先判断再减一,是为了保证只要 t 中某字符比 s 多,立刻返回 false,不会漏判。

  • 如果先减一再判断,可能会把“刚好用完”和“多出来”混淆,导致判断不准确。



  1. 两个数组的交集
  • 给定两个数组 nums1 和 nums2 ,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。

  • 思路:

  • 方法1: Set哈希,用set做哈希映射,数组nums1,nums2,将nums1放到hash表set中,然后遍历nums2,判断数据是否出现在hash表中,如果是的话,把该数据放到result结果中,最后再对result结果进行去重

  • 方法2: 数组哈希,直接用下标做哈希映射,数据量<1000,可以用数组hash,先定义哈希表,然后遍历nums1放到hash表,设置值为1,遍历nums2,判断每个值是否在哈希表中

// Set哈希表
var intersection = function(nums1, nums2) {
    const nums1Set = new Set(nums1)
    const resultSet = new Set()
    for (var i = 0; i< nums2.length; i++) {
        if (nums1Set.has(nums2[i])) {
           resultSet.add(nums2[i])
        }
    }
    return Array.from(resultSet)
};
console.log(intersection([1,2,3], [1,2]));

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(let i = nums2.length - 1; i >= 0; i--) {
        nums1Set.has(nums2[i]) && resSet.add(nums2[i]);
    }
    return Array.from(resSet);
};

// 数组哈希
var intersection = function(nums1, nums2) {
    // 找到最大值,确定哈希表长度
    const max = Math.max(...nums1, ...nums2)
    const hash = new Array(max + 1).fill(0)
    // 标记nums1中的元素
    for (let i = 0; i < nums1.length; i++) {
        hash[nums1[i]] = 1
    }
    const res = []
    for (let i = 0; i < nums2.length; i++) {
        if (hash[nums2[i]] === 1) {
            res.push(nums2[i])
            hash[nums2[i]] = 2 // 防止重复添加
        }
    }
    return res
}


  1. 快乐数
  • 编写一个算法来判断一个数 n 是不是快乐数。

  • 「快乐数」 定义为:

    • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
    • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
    • 如果这个过程 结果为 1,那么这个数就是快乐数。
  • 如果 n 是 快乐数 就返回 true ;不是,则返回 false 。

  • 思路:

// 快乐数:哈希表判环法
var isHappy = function(n) {
    const set = new Set();
    // 计算各位数字的平方和
    function getSum(num) {
        let sum = 0;
        while (num > 0) {
            let digit = num % 10;
            sum += digit * digit;
            num = Math.floor(num / 10);
        }
        return sum;
    }
    // 如果出现重复数字,说明进入循环,不是快乐数
    while (n !== 1 && !set.has(n)) {
        set.add(n);
        n = getSum(n);
    }
    return n === 1;
};
console.log(isHappy(19)); // true
console.log(isHappy(2));  // false


var isHappy = function (n) {
    let m = new Map()

    const getSum = (num) => {
        let sum = 0
        while (n) {
            sum += (n % 10) ** 2
            n = Math.floor(n / 10)
        }
        return sum
    }

    while (true) {
        // n出现过,证明已陷入无限循环
        if (m.has(n)) return false
        if (n === 1) return true
        m.set(n, 1)
        n = getSum(n)
    }
}

// 方法二:使用环形链表的思想 说明出现闭环 退出循环
var isHappy = function(n) {
    if (getN(n) == 1) return true;
    let a = getN(n), b = getN(getN(n));
    // 如果 a === b 
    while (b !== 1 && getN(b) !== 1 && a !== b) {
        a = getN(a);
        b = getN(getN(b));
    }
    return b === 1 || getN(b) === 1 ;
};

// 方法三:使用Set()更简洁
/**
 * @param {number} n
 * @return {boolean}
 */

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

// 方法四:使用Set(),求和用reduce
var isHappy = function(n) {
    let set = new Set();
    let totalCount;
    while(totalCount !== 1) {
        let arr = (''+(totalCount || n)).split('');
        totalCount = arr.reduce((total, num) => {
            return total + num * num
        }, 0)
        if (set.has(totalCount)) {
            return false;
        }
        set.add(totalCount);
    }
    return true;
};


  1. 两数之和
  • 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

  • 思路:

  • 为什么会想到用哈希表:每当遇到要判断这个元素是否出现过,或者要判断这个元素是否在这个集合里出现过的时候。(数组、set、map哪个哈希表结构合适,)

  • 哈希表为什么用map:不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。

  • 本题map是用来存什么的:map目的用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的(也就是相加等于target)。

  • map中的key和value用来存什么的:判断元素是否出现,这个元素就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标。所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。

var twoSum = function (nums,, target) {
    let hash = {}
    for(let i = 0; i< nums.length; i++) {
        // 遍历当前元素,并在map中寻找是否有匹配的key
        if (hash[target - nums[i]] !== undefined) {
            return [i, hash[target - nums[i]]]
        }
        // 如果没找到匹配对,就把访问过的元素和下标加入到map中
        hash[nums[i]] = i
    }
    return []
}

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



参考&感谢各路大神

posted @ 2025-06-02 16:54  安静的嘶吼  阅读(3)  评论(0)    收藏  举报