Day6-哈希表,leetcode242,349,202,1
哈希表
理论
-
哈希表,(散列表,hash table),根据关键码的值直接进行访问的数据结构。比如数组也是一张哈希表,关键码是数组的索引下标,然后通过下标直接访问数组中的元素。
-
哈希表,用来判断一个元素是否出现在集合里。
-
哈希函数,如通过hashCode把名字转化为数值,一般hashCode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就可以把名字映射为哈希表上的索引数字了。
-
哈希碰撞,映射到了同一个位置。哈希碰撞的解决方法:拉链法和线性探测法。
- 拉链法,选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
-
哈希结构分类:数组、Set(集合)、map(映射)
题目
- 有效的字母异位词
-
给定两个字符串 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
- 先判断再减一,可以直接发现“多余字符”
- 如果 map.get(item) 为 0,说明 t 中这个字符出现次数比 s 多,直接返回 false,不用再减。
- 如果先减一再判断,遇到 map.get(item) 为 1 的情况,减完变成 0,你就无法区分“刚好用完”还是“多出来了”。
- 这样写可以保证每次遇到多余字符都能及时返回
- 比如 s = "ab", t = "aab",遍历到第二个 a 时,map.get('a') 已经是 0,此时直接返回 false,不会出现负数。
- 逻辑更清晰
-
先判断有没有剩余,再消耗掉一次计数,符合“用一个扣一个”的直观思路。
-
总结:
-
先判断再减一,是为了保证只要 t 中某字符比 s 多,立刻返回 false,不会漏判。
-
如果先减一再判断,可能会把“刚好用完”和“多出来”混淆,导致判断不准确。
- 两个数组的交集
-
给定两个数组 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
}
- 快乐数
-
编写一个算法来判断一个数 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;
};
- 两数之和
-
给定一个整数数组 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 []
};