哈希表
哈希表 用于存储键值对,并使我们能够快速的根据键获取值。哈希表的基础知识可以浏览这里-->哈希表
当我们需要快速的获取一个值是否存在时,哈希表是不二之选 ,此外哈希表的键是唯一的。
本节主要是一些使用到哈希表的算法题目。
LeetCode-242.有效的字母异位词
给定两个字符串 s
和 t
,编写一个函数来判断 t
是否是 s
的字母异位词。
注意: 若 s
和 t
中每个字符出现的次数都相同,则称 s
和 t
互为字母异位词。
示例:
text
输入: s = "anagram", t = "nagaram"
输出: true
分析:我们只需要知道字符串s
中每个字符出现的次数,然后遍历字符串t
中每个字符,在s
中出现就将出现次数减一。
看代码:
js
var isAnagram = function (s, t) {
const originMap = new Map();
if (s.length !== t.length) return false; //长度不同则不可能是字母异位词
for (let i = 0; i < s.length; i++) { //将s中每个字符及其出现次数存入map
originMap.set(s[i], (originMap.get(s[i]) || 0) + 1);
}
for (let i = 0; i < s.length; i++) {
//如果t中出现了所需字符,则将对应的map减一,表示抵消
originMap.set(t[i], originMap.get(t[i]) - 1);
if (originMap.get(t[i]) === 0) { //如果已经抵消完了就删除掉
originMap.delete(t[i]);
}
}
//此时,只要map为空,说明所有字符都被抵消,两个字符串拥有相同的字符
if (originMap.size === 0) return true;
return false;
};
这样做实际就是两个集合A,B(允许重复),判断A和B是否相等。
此外还有一个更简单的方法,就是将两个字符串变得有序,如果他们是异位词,那么他们将相等。
js
var isAnagram = function (s, t) {
return s.split('').sort().join('') === t.split('').sort().join('')
};
LeetCode-383.赎金信
给你两个字符串:ransomNote
和 magazine
,判断 ransomNote
能不能由 magazine
里面的字符构成。
如果可以,返回 true
;否则返回 false
。
magazine
中的每个字符只能在 ransomNote
中使用一次。
- 这道题相较于上一题,你会发现,两个字符串的关系发生了变化。
- 本题实际上是两个集合A,B(允许重复),判断A是否是B的子集
那于上一题的代码很像,只需要记录目标集合(ransomNote)中字符出现次数,只要在magazine集合中拥有这些字符并次数大于等于即可。
js
var canConstruct = function (ransomNote, magazine) {
let targetMap = new Map();
for (let i = 0; i < ransomNote.length; i++) { //ransomNote 中字符出现次数
targetMap.set(ransomNote[i], ((targetMap.get(ransomNote[i]) || 0) + 1));
}
for (let i = 0; i < magazine.length; i++) {
if (targetMap.has(magazine[i])) { //magazine中是否有我们需要的,有就抵消。
targetMap.set(magazine[i], targetMap.get(magazine[i]) - 1);
if (targetMap.get(magazine[i]) === 0) {
targetMap.delete(magazine[i]);
}
}
}
if (targetMap.size === 0) return true;
return false;
};
LeetCode-49.字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
示例:
text
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
本题即是找到相同的字母异位词,我们可以利用两层循环判断出哪些字符串相互为异位词,这样的当然是可行的,不过你需要考虑去重,因为针对第一项的可能在遍历第二项时又出现,简化去重的方法就是用set/map存储。
这里我们利用前面提到的排序方法,将排序后的结果作为键,值则存储异位词数组。
js
var groupAnagrams = function (strs) {
const map = new Map();
for (let i = 0; i < strs.length; i++) {
let sorted = strs[i].split('').sort().join(); //排序
if (map.has(sorted)) {
map.set(sorted, [...map.get(sorted), strs[i]]);
} else { //没有对应的分组,创建分组
map.set(sorted, [strs[i]]);
}
}
return Array.from(map.values());
};
LeetCode-438.找到字符串中所有字母异位词
给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
示例 :
text
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
你能否看出这是一个滑动窗口题 呢? 滑动窗口题目见双指针②--滑动窗口
对于滑动窗口,我们需要关心的是:
- 何时扩大?扩大时需要做什么?
- 何时缩小?缩小时需要做什么?
- 何时扩大?每次循环都需要扩大,直到遍历到尾部。扩大时我们需要知道当前进来的字符是不是需要的,如果是需要的我们记录一下。
- 何时缩小?当我们当前窗口中的长度大于等于p时,开始缩小。缩小时我们应该判断当前是否满足异位词并记录答案,然后清除扩大时带来的副作用。
引出了一个问题,如何判断当前是否满足异位词?
- 如果与之前一样,每次去抵消map判断是否为异位词的话,那么窗口在移动过程中会由于map在判断一次后不能重新初始化--------为此我们需要重新维护一个map,存储当前窗口中的值。
- 如果在滑动窗口while循环中再去判断是否是异位词,那么无疑增加了时间复杂度--------我们引入一个变量来记录当前窗口内满足异位的字符种类,这样就可以在每次找到时更新,缩小窗口时判断是否是异位词了。
- 引入的这些新变量,都需要在对应扩大和缩小时进行更新。
可能还有点抽象,多读几遍,结合代码看看:
js
var findAnagrams = function (s, p) {
let neededMap = new Map();//所需的字符以及出现的次数
for (let i = 0; i < p.length; i++) {
neededMap.set(p[i], (neededMap.get(p[i]) || 0) + 1);
}
let neededCount = neededMap.size;//所需字符的种类;
let left = 0, right = 0;
const res = [];
let currentCount = 0; //窗口内满足条件的字符种类
let currentMap = new Map(); //窗口内满足条件的字符
while (right < s.length) {
if (neededMap.has(s[right])) {//当前窗口右端点是所需字符
currentMap.set(s[right], (currentMap.get(s[right]) || 0) + 1);
//如果出现次数相同,那么当前已有种类加一
if (currentMap.get(s[right]) === neededMap.get(s[right])) {
currentCount++;
}
}
//窗口大于等于p的长度时,判断是否是结果,然后逐步缩小
while (right - left + 1 >= p.length) {
if (currentCount === neededCount) {
res.push(left);//记录结果
}
//缩小
if (currentMap.has(s[left])) {
//缩小前判断当前出去的字符是否会影响到所需字符种类,
//窗口内出现的次数是需要的次数,那么缩小后当前的字符种类一定少1种,故减一
if (currentMap.get(s[left]) === neededMap.get(s[left])) {
currentCount--;
}
//更新窗口内的字符出现次数
currentMap.set(s[left], currentMap.get(s[left]) - 1);
if (currentMap.get(s[left]) === 0) {
currentMap.delete(s[left]);
}
}
left++;
}
right++;
}
return res;
};
代码中的while
缩小部分代码实际上可以改为if
,因为窗口一旦到达目标长度,就会一个个扩大缩小,属于定长滑动窗口。
LeetCode-349.两个数组的交集
给定两个数组 nums1
和 nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
求两个数组的交集,不需要关心每个数字出现的次数,很快就能想到使用Set
来存储了。 很简单,直接看代码:
js
//使用filter会比较慢
var intersection = function (nums1, nums2) {
const set = new Set(nums1);
return Array.from(new Set(nums2.filter(num => set.has(num))));
};
//使用循环
var intersection = function (nums1, nums2) {
const set = new Set(nums1);
const res = new Set();
for (let num of nums2) {
if (set.has(num)) res.add(num);
}
return Array.from(res);
};
LeetCode-350.两个数组的交集 II
给你两个整数数组 nums1
和 nums2
,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。
类似于LeetCode-242.有效的字母异位词。这里无非是求有哪些数字。
js
var intersect = function (nums1, nums2) {
const originMap = new Map();
const res = [];
for (let i = 0; i < nums1.length; i++) {
originMap.set(nums1[i], (originMap.get(nums1[i]) || 0) + 1);
}
for (let i = 0; i < nums2.length; i++) {
if (originMap.has(nums2[i])) {
res.push(nums2[i]);
originMap.set(nums2[i], originMap.get(nums2[i]) - 1);
if (originMap.get(nums2[i]) === 0) originMap.delete(nums2[i]);
}
}
return res;
};
此外还可以使用排序加双指针解决此问题:
js
var intersect = function (nums1, nums2) {
nums1 = nums1.sort((a, b) => a - b);
nums2 = nums2.sort((a, b) => a - b);
let i = 0, j = 0;
let res = [];
while (i < nums1.length && j < nums2.length) {
if (nums1[i] === nums2[j]) {
res.push(nums1[i]);
i++; j++;
} else if (nums1[i] < nums2[j]) {
i++;
} else {
j++;
}
}
return res;
};
LeetCode-202.快乐数
编写一个算法来判断一个数 n
是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
分析:我们每次对n进行分解并平方求和,如果为1就返回true。题目告诉我们他可能会一直循环,那么我们就需要知道是否存在循环,想要知道是否存在循环就需要存储之前出现过的结果,为了能够快速判断是否已经出现过,就能想到使用哈希表存储,这里使用Set进行存储。
js
function getSum(n) {
let sum = 0;
while (n) {
let num = n % 10;
n = Math.floor(n / 10);
sum += num * num;
}
return sum;
}
var isHappy = function (n) {
const computedSum = new Set();
while (n !== 1) {
if (computedSum.has(n)) return false;
computedSum.add(n);
n = getSum(n);
}
return true;
};
总结
- 使用哈希表进行数据存储,能够让我们快速的获取某个值是否存在。
- Set适合存储去重的数据
- Map适合存储键值对,其中键是去重唯一的。