Hi 你们好,我是张小猪。欢迎来到『宝宝也能看懂』系列特别篇 - 30-Day LeetCoding Challenge。git
这是一个 leetcode 官方的小活动。能够在官网看到,从 4 月 1 号开始,天天官方会选出一道题,在 24 小时内完成便可得到一点小奖励。虽然奖励彷佛也没什么用,不过做为一个官方的打卡活动,小猪仍是来打一下卡吧,正好做为天天下班回家后的娱乐。github
这里是 4 月 6 号的题,也是题目列表中的第 49 题 -- 『字母异位词分组』shell
给定一个字符串数组,将字母异位词组合在一块儿。字母异位词指字母相同,但排列不一样的字符串。segmentfault
示例:数组
输入: ["eat", "tea", "tan", "ate", "nat", "bat"], 输出: [ ["ate","eat","tea"], ["nat","tan"], ["bat"] ]
说明:性能
MEDIUM优化
因为题目不限制返回数据的顺序,因此咱们只须要作字符串内容的区分便可。而这个区分,笼统的来讲,就是找到一个格式化或者是序列化的方式,这个方式只关心不一样字母出现的次数,并不关心它们的位置。spa
基于上面的思路,咱们能够轻松的想到一个最基础的方式,即统计单词中字母出现的频次。这种方式实现起来很是轻松,可是性能实在是不敢恭维。code
在遍历每个单词的时候,咱们能够统计其中不一样字母出现的次数,并将统计结果作互相比较,从而作到将不一样类型的单词归类输出。具体代码以下:blog
const wordHash = word => { const map = new Map(); for (let i = 0; i < word.length; ++i) { const char = word[i]; map.set(char, map.has(char) ? map.get(char) + 1 : 1); } return map; }; const compareHash = (map1, map2) => { if (map1.size !== map2.size) return false; for (let [char, count] of map1.entries()) { if (!map2.has(char) || map2.get(char) !== count) return false; } return true; }; const groupAnagrams = strs => { const ret = []; const hashMap = new Map(); for (const word of strs) { const hash = wordHash(word); let exist = false; for (let [map, idx] of hashMap.entries()) { if (!compareHash(hash, map)) continue; exist = true; ret[idx].push(word); break; } !exist && hashMap.set(hash, ret.push([word]) - 1); } return ret; };
上面思路里,处理过程的逻辑咱们是比较难优化的,不过这个序列化的方式咱们可让它变得更快一些。
首先,因为只有小写字母,因此咱们能够基于字母顺序对单词内的字符进行排序,这样就能够完成单词的归类啦。具体代码以下:
const groupAnagrams = strs => { const hashMap = new Map(); for (const word of strs) { const serializeWord = word.split("").sort().join(""); hashMap.has(serializeWord) ? hashMap.get(serializeWord).push(word) : hashMap.set(serializeWord, [word]); } return [...hashMap.values()]; };
再看上面的过程,其中字符串的排序过程仍是比较耗时的。因此咱们能够尝试再换一下特征提取方式。
咱们能够申明一个长度为 26 的数组,基于字母 charCode 为下标来记录每一个字母出现的次数,而后把这个数组转换为一个字符串做为序列化的结果。这样便可完成单词的归类啦。具体代码以下:
const groupAnagrams = strs => { const BASE_CODE = 97; const hashMap = new Map(); for (const word of strs) { const countList = new Int8Array(26); for (let j = 0; j < word.length; ++j) { ++countList[word.charCodeAt(j) - BASE_CODE]; } const serializeWord = countList.join(""); hashMap.has(serializeWord) ? hashMap.get(serializeWord).push(word) : hashMap.set(serializeWord, [word]); } return [...hashMap.values()]; };
在上面的转化过程当中,咱们仍是每次都须要借助额外的数组,以及把数组转化为字符串。此次咱们尝试一个单纯的数字计算,从而再优化这部分的性能。
咱们能够利用质数的特殊性质来参与计算,从而实现只用一个数字就能完成特征标识的任务。不过这里有一个前提,即计算的结果不会超出数字的表示范围。具体代码以下:
const groupAnagrams = strs => { const BASE_CODE = 97; const CHAR_VAL_MAP = [2, 3, 5, 7, 11 ,13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101]; const hashMap = new Map(); for (const word of strs) { let val = 1; for (let j = 0; j < word.length; ++j) { val *= CHAR_VAL_MAP[word.charCodeAt(j) - BASE_CODE]; } hashMap.has(val) ? hashMap.get(val).push(word) : hashMap.set(val, [word]); } return [...hashMap.values()]; };
终于出现不是 EASY 难度的题了,可喜可贺!
这道题的核心逻辑并不复杂,很容易 AC,只是在具体的序列化部分咱们能够有一些优化空间,从而提高性能。小猪给出了我本身的优化思路,但愿能帮到有须要的小伙伴。
若是以为不错的话,记得『三连』哦。小猪爱大家哟~