在 JavaScript 刷题中,字典(Dictionary)和哈希表(Hash Table)通常用来存储键值对,提供快速的查找、插入和删除操作。它们在很多算法题目中都有广泛的应用,特别是在需要快速查找元素或统计元素出现次数的情况下。
字典和哈希表出现的场景
以下是一些常见的场景,可以使用字典和哈希表来解决:
- 两数之和(Two Sum):给定一个数组和一个目标值,在数组中找到两个数使它们的和等于目标值。
- 无重复字符的最长子串(Longest Substring Without Repeating Characters):找到一个字符串中最长的子串,其中没有重复的字符。
- 字母异位词分组(Group Anagrams):将给定的字符串数组按照字母异位词分组。
- 单词规律(Word Pattern):判断给定的模式字符串是否与给定的单词字符串匹配。
对于新手刷题,以下是一些建议:
- 熟悉常见的数据结构和算法:掌握常见的数据结构(如数组、链表、栈、队列、树、图等)和算法(如排序、搜索、动态规划等)是刷题的基础。
- 多练习:刷题是一个持续练习的过程,通过不断练习可以提高解题能力和编程技巧。
- 理解题目要求:在解题之前,仔细阅读题目要求,确保理解清楚题目的意思和要求。
- 注重细节:在编写代码时,注意边界条件、特殊情况和错误处理,确保代码的正确性和健壮性。
- 学会利用工具:熟练使用调试工具、在线编程平台和相关资源,可以帮助更快地解决问题和提高效率。
在js中如何创建哈希表或字典数据结构
在 JavaScript 中,可以使用对象(Object)或 Map 类型来创建哈希表或字典数据结构。下面分别介绍如何使用对象和 Map 类型来实现哈希表或字典:
使用对象(Object)
在 JavaScript 中,对象可以被用作哈希表或字典数据结构,其中对象的属性(key)可以是字符串或符号,值可以是任意类型。可以通过对象字面量或构造函数来创建对象,然后通过设置属性来存储键值对。
以下是一个简单示例:
// 创建一个空对象作为哈希表
let hashTable = {};
// 存储键值对
hashTable['key1'] = 'value1';
hashTable['key2'] = 'value2';
// 访问值
console.log(hashTable['key1']); // 输出 'value1'
使用 Map 类型
ES6 引入了 Map 类型,它提供了一种更灵活和强大的方式来创建哈希表或字典数据结构。
Map 类型可以存储任意类型的键和值,而对象的键只能是字符串或符号。
可以使用
new Map()
来创建一个空的 Map 对象,然后使用set()
方法来存储键值对。
以下是一个简单示例:
// 创建一个空的 Map 对象
let hashMap = new Map();
// 存储键值对
hashMap.set('key1', 'value1');
hashMap.set('key2', 'value2');
// 访问值
console.log(hashMap.get('key1')); // 输出 'value1'
使用对象还是Map?
使用对象或 Map 类型来创建哈希表或字典取决于具体的需求和场景。一般来说,如果需要存储简单的键值对且键是字符串类型,可以使用对象;如果需要更灵活的键类型或需要保持键值对的插入顺序,可以使用 Map 类型
Map对象的常用方法
关于字典或哈希表专项练习中,你必须要掌握以下对Map对象的操作
-
set(key, value):向 Map 对象中添加一个键值对,如果键已经存在,则更新其对应的值。
-
get(key):获取指定键对应的值,如果键不存在,则返回 undefined。
-
has(key):判断 Map 对象中是否包含指定的键,返回一个布尔值。
-
delete(key):删除 Map 对象中指定键的键值对,返回一个布尔值表示是否删除成功。
-
clear():清空 Map 对象,删除所有的键值对。
-
size:获取 Map 对象中键值对的数量。
-
keys():返回一个包含 Map 对象中所有键的迭代器。
-
values():返回一个包含 Map 对象中所有值的迭代器。
-
entries():返回一个包含 Map 对象中所有键值对的迭代器。
1. 两数之和
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
思路:这道题,作为leetcode的第一题,已经不是一个简单题了,劝退了很多刷算法的,其实就想做数学题一样,了解解题思想,便可知道怎么下手了。我们看到这道题就想用两个for循环去遍历,直到找到两数和等于target。这种方法时间复杂度高,但是没有空间复杂度。
如何一次遍历就能找到两数之和呢,可以考虑空间换时间。
将前面已经访问的数字和对应的下标存起来,判断后面的元素时,去已经存储的数据中去匹配即可。按照这种思想,看下题解怎么做的吧
/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function (nums, target) {
let map = new Map();
for (let i = 0; i < nums.length; i++) {
let num = target - nums[i];
if (map.has(num)) {
return [map.get(num), i];
} else {
map.set(nums[i], i);
}
}
};
通常使用Map的以下几个方法:
set:向哈希表或字典加元素,接收两个参数key:value
get:从哈希表中获取指定key的value值,接收key值,返回value值
has:判断哈希表中是否有key值,接收key值,返回true或false
217. 存在重复元素
给你一个整数数组 nums
。如果任一值在数组中出现 至少两次 ,返回 true
;如果数组中每个元素互不相同,返回 false
。
思路:有了上面题的引入,这道题就很简单了。还是将数组存储在字典中,如果比较的元素已经存在字典里,则return true。否则将当前元素放入字典中,这里的set的第二个参数value是没有用的,放什么都行。
/**
* @param {number[]} nums
* @return {boolean}
*/
var containsDuplicate = function (nums) {
let map = new Map();
for (let i = 0; i < nums.length; i++) {
if (map.has(nums[i])) {
return true;
} else {
map.set(nums[i], i);
}
}
return false;
};
349. 两个数组的交集
给定两个数组 nums1
和 nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
思路:如果不使用ES6的set去重的话。
可以将两个数组中其中一个比如nums1做字典,遍历nums2,返回一个重复的数组。注意nums2也有可能有数据重复
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersection = function (nums1, nums2) {
let map = new Map();
let arr = [];
let first = true;
nums1.forEach(item => {
map.set(item, first);
})
nums2.forEach(item => {
if (map.has(item) && map.get(item) === first) {
arr.push(item);
map.set(item, !first)
}
})
return arr;
};
设置用first表示当前元素是首次添加还是非首次添加的
进一步可以用set结合数组的filter过滤方法实现
优化后的代码
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number[]}
*/
var intersection = function (nums1, nums2) {
let arr1 = new Set(nums1);
let arr2 = new Set(nums2);
return Array.from(arr2).filter(item => arr1.has(item))
};
如果对数组的方法和ES的新提供的对象熟悉的话就很简单
通过Set方法将nums1和nums2去重。使用Array.from方法将set对象转换为数组对象,这样就可以使用数组自带的过滤方法。是不是很神奇!
1207. 独一无二的出现次数
给你一个整数数组 arr
,请你帮忙统计数组中每个数的出现次数。
如果每个数的出现次数都是独一无二的,就返回 true
;否则返回 false
。
思路:这道题也不算难,还是用到字典将数字和它出现的次数做存储。关键是如何对这个map字典判重处理。这里给出的代码充分利用了Set对象和Map对象,Set存储的键值具有唯一性可以去重。同时Map和Set对象都提供了size方法判断元素个数。
/**
* @param {number[]} arr
* @return {boolean}
*/
var uniqueOccurrences = function (arr) {
let map = new Map();
//获取元素出现的次数
arr.forEach(item => {
if (map.has(item)) {
map.set(item, map.get(item) + 1);
} else {
map.set(item, 1);
}
})
return map.size === new Set(map.values()).size;
};
进一步优化,简化判断
/**
* @param {number[]} arr
* @return {boolean}
*/
var uniqueOccurrences = function (arr) {
let map = new Map();
//获取元素出现的次数
arr.forEach(item => {
map.set(item, (map.get(item) || 0) + 1)
})
return map.size === new Set(map.values()).size;
只要将字典map和对字典values去重后的Set对象比较长度即可,如果有重复的Set的size会小与map.size。这道题很精彩
当然可以用Object存储字典
var uniqueOccurrences = function(arr) {
let count = {};
for (let item of arr) {
count[item] = (count[item] || 0) + 1;
}
return Object.keys(count).length === new Set(Object.values(count)).size;
};
3. 无重复字符的最长子串
给定一个字符串 s
,请你找出其中不含有重复字符的 最长子串的长度。
思路:这个题目也是比较经典的笔试题,如何获取无重复的子串呢,涉及到两个指针移动,这里左指针的移动就比较有意思了。首先左指针会移动说明中间出现了重复的,在左指针移动前要将当前的左右指针夹住的长度记一下。
那有重复的左指针怎么移动呢,比如示例1,当右指针到第二个a,左指针指向a的下标是0,左指针往后移动一个。从b开始然后右指针可以继续移动了。所以左指针移动是在左右指针闭合区间内,移动到重复元素的后一个位置上。
那右指针呢,其实不需要单独设置右指针,因为当前循环的index就是右指针啊。这样,一次遍历就能计算所有不重复的子串了。
而重复元素的判断就可以借用字典的思想了。
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function (s) {
let arr = s.split('');
let start = 0;
let map = new Map();
let maxLength = 0;
for (let i = 0; i < arr.length; i++) {
if (map.has(arr[i]) && map.get(arr[i]) >= start) {
let currentLength = i - start;
maxLength = Math.max(maxLength, currentLength);
start = map.get(arr[i]) + 1;
}
map.set(arr[i], i);
}
// 计算最后一次比较没有重复时的长度
let currentLength = arr.length - start;
return Math.max(currentLength, maxLength);
};
这里每次右指针碰到重复值则计算子串长度,但如果最后一次左指针改变后遍历时没有遇到重复的,最后一段的子串的长度就要单独统计。
13. 罗马数字转整数
罗马数字包含以下七种字符: I
, V
, X
, L
,C
,D
和 M
。
字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000
例如, 罗马数字 2
写做 II
,即为两个并列的 1 。12
写做 XII
,即为 X
+ II
。 27
写做 XXVII
, 即为 XX
+ V
+ II
。
通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII
,而是 IV
。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX
。这个特殊的规则只适用于以下六种情况:
I
可以放在V
(5) 和X
(10) 的左边,来表示 4 和 9。X
可以放在L
(50) 和C
(100) 的左边,来表示 40 和 90。C
可以放在D
(500) 和M
(1000) 的左边,来表示 400 和 900。
给定一个罗马数字,将其转换成整数。
思路:将所有需要翻译的字符存放在字典中,用object存储,key都是字符。除提到的特殊位置外,将当前值与上一步结果做累加。对于特殊值,主要比较上一个值和当前值关系,是不是符合特殊处理的类型,如果是,累加值要减去上一个值的2倍。思考一下为什么是2倍
因为重复计算了一次,还要参与剪掉当前值
/**
* @param {string} s
* @return {number}
*/
var romanToInt = function (s) {
let dictionary = {
'I': 1,
'V': 5,
'X': 10,
'L': 50,
'C': 100,
'D': 500,
'M': 1000,
}
let result = 0;
let lastStr = "";
for (let str of s) {
result += dictionary[str];
if ((lastStr === 'I' && str === 'V') || (lastStr === 'I' && str === 'X')) {
result += - 2;
}
if ((lastStr === 'X' && str === 'L') || (lastStr === 'X' && str === 'C')) {
result += - 20;
}
if ((lastStr === 'C' && str === 'D') || (lastStr === 'C' && str === 'M')) {
result += -200;
}
lastStr = str;
}
return result;
};
这里比较的是key值,当上一步的key和当前key匹配的时候处理。
更进一步,可以考虑value值的匹配,当上一步的value<当前的value。及小的值出现在了左边,需要对结果进行处理
/**
* @param {string} s
* @return {number}
*/
var romanToInt = function (s) {
let dictionary = {
'I': 1,
'V': 5,
'X': 10,
'L': 50,
'C': 100,
'D': 500,
'M': 1000,
}
let result = 0;
let lastStr = "";
for (let str of s) {
result += dictionary[str];
if (dictionary[lastStr] < dictionary[str]) {
result -= dictionary[lastStr] * 2;
}
lastStr = str;
}
return result;
};
169. 多数元素
给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
思路:这道题有很多种解法,比较容易理解的是字典、哈希表这种方式。将不重复的数字做key,出现次数统计为value,最后在输出value中比n/2大的元素。
/**
* @param {number[]} nums
* @return {number}
*/
var majorityElement = function (nums) {
let arr = [];
let elementMap = new Map();
for (let num of nums) {
elementMap.set(num, elementMap.get(num) + 1 || 1);
}
elementMap.forEach((value, key) => {
if (value > nums.length / 2) {
arr.push(key);
}
})
return arr;
};
这里不确定题目的意思是有多个大于n/2还是只要一个,就用数组了,但是看了题解,好像只有一个
202. 快乐数
编写一个算法来判断一个数 n
是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n
是 快乐数 就返回 true
;不是,则返回 false
。
思路:这题要抓题目关键字,“无限循环”,如果程序不知道什么时候跳出来,比如使用递归方式去求解,就会陷入死循环,超出最大调用栈限制。而如果我们知道了,不是快乐数的一定会进入循环,从这个点考虑
进入循环?就是已经出现的数字可以又出现了,看下官方题解里给的示例:
如果这个示例放在题目里就好解决了。我们用字典将过程中计算的结果存起来不就好了吗,如果出现了循环,说明和字典重复了,这个时候就结束循环了,返回false。当然如果过程中出现了1则循环也结束,返回true
/**
* @param {number} n
* @return {boolean}
*/
var isHappy = function (n) {
let map = new Map();
let result = n;
while (result) {
if (getNum(result) === 1) {
return true;
} else {
if (map.has(result)) {
return false;
} else {
map.set(result, 1);
result = getNum(result);
}
}
}
};
function getNum(num) {
let result = 0;
while (num) {
let n = num % 10;
result += n * n;
num = Math.floor(num / 10);
}
return result;
}
这里循环的条件可以用while(result),或者while(true)。只要把结束循环的条件写好就好啦。
另外养成习惯,能抽出来的方法单独写个方法。让主流程简单一些。
205. 同构字符串
给定两个字符串 s
和 t
,判断它们是否是同构的。
如果 s
中的字符可以按某种映射关系替换得到 t
,那么这两个字符串是同构的。
每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。
思路:这道题要读懂题目,否则容易漏掉判断条件。
总结有两点:
- s中作为key的元素,二次出现,t中对应位置的value要与之前的相等
- t中做为value的元素,二次出现,其s对应的key也要与之前的相等。
比如下面的示例,当t中的b二次出现时,key是d与之前的b不一样,就要返回false
/**
* @param {string} s
* @param {string} t
* @return {boolean}
*/
var isIsomorphic = function (s, t) {
if (s.length != t.length) return false;
let map = new Map();
let i = s.length;
while (i--) {
//如果s中key存在 且当前的value与map存的不一样返回false
if (map.has(s[i])) {
if (map.get(s[i]) != t[i]) {
return false;
}
} else if ([...map.values()].includes(t[i])) {
//如果key不存在,但是当前value已经在map中出现过了也返回false
return false;
}
map.set(s[i], t[i]);
}
return true;
};
通过map的get方法找到value很简单。如何通过value找get呢,这里没有按照这个思想,因为map首先已经存放的是唯一的value,如果value已经存在了则返回false就好了。注意这里的map.values()方法返回的是迭代对象,需要转换为数组,因为数组提供了更多的方法比如includes判断某个元素是否在数组中。
可以用[...迭代对象]也可以用Array.from(对象)来转换
有上面的题目可以看出,要想熟练掌握字典或哈希表数据结构,需要对ES6的Map对象基本操作铭记与心。
219. 存在重复元素 II
给你一个整数数组 nums
和一个整数 k
,判断数组中是否存在两个 不同的索引 i
和 j
,满足 nums[i] == nums[j]
且 abs(i - j) <= k
。如果存在,返回 true
;否则,返回 false
。
思路:跟之前的题比这题应该算简单了。题目主要意思是找最近两个相等元素,看两者的步长是否小与给定值。如果有字典存储key值的话,每次都要更新或新增key为nums[i]的value值。保证重复的value也是最近一次保存的index信息
/**
* @param {number[]} nums
* @param {number} k
* @return {boolean}
*/
var containsNearbyDuplicate = function (nums, k) {
let map = new Map();
for (let i = 0; i < nums.length; i++) {
if (map.has(nums[i]) && (i - map.get(nums[i])) <= k) {
return true;
}
map.set(nums[i], i);
}
return false;
};
用i-map.get(nums[i])判断相邻重复元素的差距