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,只是在具體的序列化部分咱們能夠有一些優化空間,從而提高性能。小豬給出了我本身的優化思路,但願能幫到有須要的小夥伴。
若是以爲不錯的話,記得『三連』哦。小豬愛大家喲~