在 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 类型可以存储任意类型的键和值,
map
对象会维护键值对的插入顺序。而对象的键只能是字符串或符号,且对象中的key不会根据写入顺序遍历。
可以使用
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;
};
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])判断相邻重复元素的差距
49. 字母异位词分组
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
思路:考虑用map存储按照sort排序后的key值,val值可以考虑存出现的字母异位数组。但是后面还多了一个遍历map拼接val的过程。这里有个技巧,就是哈希表+数组,在哈希表存储key值,真正的val存在数组中,而数组下标存在val里,这样数组就是最终返回的结果,而过程只是多了一个key到index的映射。
/**
* @param {string[]} strs
* @return {string[][]}
*/
var groupAnagrams = function (strs) {
let arr = [];//用arr存放最终结果
let map = new Map();//存放唯一str,val是在arr的index
for (let i = 0; i < strs.length; i++) {
let str = strs[i].split('').sort().join('');//根据unicode编码排序后返回str
if (map.has(str)) {
arr[map.get(str)].push(strs[i]);
} else {
arr.push([strs[i]])
map.set(str, arr.length - 1);
}
}
return arr;
};
128. 最长连续序列
给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)
的算法解决此问题。
思路:借助set实现。先将数组利用set去重。之后遍历数组,尝试找序列的起始值。怎么找起始值,如果当前值-1没有,说明以当前值作为序列的起始值没有计算过序列长度。以它为起始值,在set找后续的序列。否则,当前值-1存在,说明当前值参与过序列计算了。不用处理。
每次生成新的序列需要重新计算最大count值
/**
* @param {number[]} nums
* @return {number}
*/
var longestConsecutive = function (nums) {
let set = new Set(nums);//构建一个去重set
let count = 0;
let maxCount = 0;
for (let i = 0; i < nums.length; i++) {//遍历一次数组
//在set中找连续值
if (!set.has(nums[i] - 1)) {//当前不是某个序列的终点,作为新序列的起点,看是否向后找到更长的连续值
count = 1;
let find = nums[i] + 1;
while (set.has(find)) {//找当前值+1的连续序列
count++;
find++;
}
maxCount = Math.max(count, maxCount);//取过程中的较大值
}
}
return maxCount;
};
347. 前 K 个高频元素
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
思路:可以使用哈希表来统计每个元素出现的频率,并使用桶排序的思想来找出频率前 k 高的元素。首先利用哈希表统计每个元素的频率,然后根据频率构建桶,最后从高频率向低频率遍历桶,找出前 k 高的元素。这个算法的时间复杂度为 O(n)
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function (nums, k) {
let map = new Map();
let bucket = [];
//循环一遍统计出现的频率
for (let i = 0; i < nums.length; i++) {
map.set(nums[i], (map.get(nums[i]) || 0) + 1);
}
//循环map,初始化桶数据
for (let [key, value] of map) {
if (!bucket[value]) {//用出现的频率作为桶的索引地址,相同频率的key放一块
bucket[value] = [key];
} else {
bucket[value].push(key);//key肯定是唯一的
}
}
let result = [];
for (let i = bucket.length - 1; i > 0 && result.length < k; i--) {
if(bucket[i]){//从后往前遍历桶,索引会有空的
result.push(...bucket[i]);
}
}
return result;
};
字符串题型
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;
};
12. 整数转罗马数字
罗马数字包含以下七种字符: 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。
给你一个整数,将其转为罗马数字。
思路:用哈希表存储键值对+使用相减法获取每步加入的字符。这里使用map构建哈希表,因为map
对象会维护键值对的插入顺序 。为什么要考虑顺序呢?因为相减法会尝试用最大值做减法,用被减数减它,每次相减可获得减数对应的val值。比如3200这个数,3200-1000,得到一个1000,用M表示,剩余2200,是不是还可以用1000减,得到MM,还有1200,在用1000减,得到MMM,剩余200,在尝试从剩余的数值中找比200小的第一个,100。用100减得到MMMC,最后MMMCC
/**
* @param {number} num
* @return {string}
*/
var intToRoman = function (num) {
let map = new Map();
let carry = 1;
let str = '';
//map存储键值对,在遍历的时候顺序和插入的顺序保持一致,大的值放前面
map.set(1000, 'M');
map.set(900, 'CM');
map.set(500, 'D');
map.set(400, 'CD');
map.set(100, 'C');
map.set(90, 'XC');
map.set(50, 'L');
map.set(40, 'XL');
map.set(10, 'X');
map.set(9, 'IX');
map.set(5, 'V');
map.set(4, 'IV');
map.set(1, 'I');
//用相减比取余简单一点。一直尝试用map中较高的值做减数
map.forEach((val, key) => {
while (num >= key) {//对每个key值如果可以作为减数,一直减,直到不符合,在尝试下一个key
str += val;
num -= key;
}
})
return str;
};
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还是只要一个,就用数组了,但是看了题解,好像只有一个
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对象基本操作铭记与心。
451. 根据字符出现频率排序
给定一个字符串 s
,根据字符出现的 频率 对其进行 降序排序 。一个字符出现的 频率 是它出现在字符串中的次数。返回 已排序的字符串 。如果有多个答案,返回其中任何一个。
思路:按字符出现的频率对出现的字符进行排序。首先可能想到可以用字典统计字符出现的次数,然后按次数进行排序。但是如果key存的是字符,value存的是次数,在对次数排序的时候不知道key对应的是哪个了。如果将次数改成相同字符组成的子串呢?子串的长度不就是出现的频率了吗
/**
* @param {string} s
* @return {string}
*/
var frequencySort = function (s) {
let map = new Map();
//遍历字符串,字典key是字符,value是相同字符拼接的子串
for (let i = 0; i < s.length; i++) {
map.set(s[i], map.get(s[i]) ? map.get(s[i]) + s[i] : s[i]);
}
//拿到values组成的字符串数组
const values = [...map.values()];
//根据字符串长度按照从大到小的顺序排序
values.sort((a, b) => b.length - a.length);
//返回values的字符串形式
return values.join("");
};
数字题型
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)。只要把结束循环的条件写好就好啦。
另外养成习惯,能抽出来的方法单独写个方法。让主流程简单一些。
矩阵
73. 矩阵置零
给定一个 m x n
的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
示例 1:
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]] 输出:[[1,0,1],[0,0,0],[1,0,1]]
思路:使用两个set记录行和列等于0的行的下标和列的下标。遍历矩阵,将出现在set中行或列的元素置为0
/**
* @param {number[][]} matrix
* @return {void} Do not return anything, modify matrix in-place instead.
*/
var setZeroes = function (matrix) {
const rows = matrix.length;
const cols = matrix[0].length;
let rowSet = new Set();//记录原矩阵有0的行
let colSet = new Set();//记录原矩阵有0的列
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (matrix[i][j] == 0) {
rowSet.add(i);
colSet.add(j);
}
//判断当前i和j是否在rowSet和colSet里,如果是的话,置为0
}
}
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
if (rowSet.has(i) || colSet.has(j)) {
matrix[i][j] = 0;//判断当前i和j是否在rowSet和colSet里,如果是的话,置为0
}
}
}
};
设计类考察js的构造函数
208. 实现 Trie (前缀树)
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie()
初始化前缀树对象。void insert(String word)
向前缀树中插入字符串word
。boolean search(String word)
如果字符串word
在前缀树中,返回true
(即,在检索之前已经插入);否则,返回false
。boolean startsWith(String prefix)
如果之前已经插入的字符串word
的前缀之一为prefix
,返回true
;否则,返回false
。
思路:构造函数里通过this定义子类共同使用的属性。这里首先需要一个数组存储所有push的字符串。其次,可以通过Set对象存储已放入数组中的非重复元素。在查找时通过set更方便。
var Trie = function () {
this.trie = [];//前缀数组
this.set = new Set();//出现的不重复的字符串集合
};
/**
* @param {string} word
* @return {void}
*/
Trie.prototype.insert = function (word) {
this.trie.unshift(word);//push入数组
this.set.add(word);//更新或新增word
};
/**
* @param {string} word
* @return {boolean}
*/
Trie.prototype.search = function (word) {
if (this.set.has(word)) {//使用set查找
return true;
} else {
return false;
}
};
/**
* @param {string} prefix
* @return {boolean}
*/
Trie.prototype.startsWith = function (prefix) {
for(let key of this.set.keys()){//遍历set,通过字符串startsWith方法查找前缀
if(key.startsWith(prefix)){
return true;
}
}
return false;
};
/**
* Your Trie object will be instantiated and called as such:
* var obj = new Trie()
* obj.insert(word)
* var param_2 = obj.search(word)
* var param_3 = obj.startsWith(prefix)
*/
380. O(1) 时间插入、删除和获取随机元素
实现RandomizedSet
类:
RandomizedSet()
初始化RandomizedSet
对象bool insert(int val)
当元素val
不存在时,向集合中插入该项,并返回true
;否则,返回false
。bool remove(int val)
当元素val
存在时,从集合中移除该项,并返回true
;否则,返回false
。int getRandom()
随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。
你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1)
。
思路:看到插入和删除时间复杂度为 O(1) 时,立马联想到使用哈希表,但哈希表无法在随机获取值的时候达到O(1) ,需要使用数组配合。因此同时使用哈希表和数组结构
在插入时,哈希表记录对应值所在的下标,并将值从数组队尾插入;
在删除时,获取值对应的数组下标,并用队尾元素覆盖写入对应下标,且重新给哈希表对应的记录更新下标;
在查找时,只要随机一个下标值访问数组即可
var RandomizedSet = function () {
this.map = new Map();//存储val和index
this.arr = [];
};
/**
* @param {number} val
* @return {boolean}
*/
RandomizedSet.prototype.insert = function (val) {
if (this.map.has(val)) {
return false;
} else {
this.map.set(val, this.arr.length);
this.arr.push(val);
return true;
}
};
/**
* @param {number} val
* @return {boolean}
*/
RandomizedSet.prototype.remove = function (val) {
if (this.map.has(val)) {
const index = this.map.get(val);
this.arr[index] = this.arr[this.arr.length - 1];
this.map.set(this.arr[index], index);
this.arr.pop();
this.map.delete(val);
return true;
} else {
return false;
}
};
/**
* @return {number}
*/
RandomizedSet.prototype.getRandom = function () {
let randomIndex = Math.floor(Math.random() * this.arr.length);
return this.arr[randomIndex];
};
/**
* Your RandomizedSet object will be instantiated and called as such:
* var obj = new RandomizedSet()
* var param_1 = obj.insert(val)
* var param_2 = obj.remove(val)
* var param_3 = obj.getRandom()
*/