首發於微信公衆號《前端成長記》,寫於 2019.11.05javascript
本文記錄刷題過程當中的整個思考過程,以供參考。主要內容涵蓋:前端
題目地址java
給定一個只包括 '(',')','{','}','[',']'
的字符串,判斷字符串是否有效。git
有效字符串需知足:github
注意空字符串可被認爲是有效字符串。面試
示例:算法
輸入: "()" 輸出: true 輸入: "()[]{}" 輸出: true 輸入: "(]" 輸出: false 輸入: "([)]" 輸出: false 輸入: "{[]}" 輸出: true
這道題從題面來看,仍然須要對字符串作遍歷處理,找到相互匹配的括號,剔除後繼續作處理便可。因此這道題個人解題想法是:數組
有幾點須要注意下,能夠減小一些計算量:微信
Ⅰ.記錄棧函數
代碼:
/** * @param {string} s * @return {boolean} */ var isValid = function(s) { if (s === '') return true; if (s.length % 2) return false; // hash 表作好索引 const hash = { '(': ')', '[': ']', '{': '}' } let arr = [] for (let i = 0; i < s.length; i++) { if (!hash[s.charAt(i)]) { // 推入的是右括號 if (!arr.length || hash[arr[arr.length - 1]] !== s.charAt(i)) { return false } else { arr.pop() } } else { if (arr.length >= s / 2) { // 長度超過一半 return false } arr.push(s.charAt(i)) } } return !arr.length };
結果:
O(n)
發現一個很暴力的解法,雖然效率不高,可是思路清奇。咱們來看看實現:
Ⅰ.暴力正則
代碼:
/** * @param {string} s * @return {boolean} */ var isValid = function(s) { if (s === '') return true; if (s.length % 2) return false; while(s.length) { const s_ = s s = s.replace('()','').replace('[]','').replace('{}','') if (s === s_) return false; } return true; };
結果:
O(n)
就這題而言,我仍是更傾向於增長一個輔助棧來作記錄。由於一旦去掉只包含括號的限制,那麼正則將沒法解答。
將兩個有序鏈表合併爲一個新的有序鏈表並返回。新鏈表是經過拼接給定的兩個鏈表的全部節點組成的。
示例:
輸入:1->2->4, 1->3->4 輸出:1->1->2->3->4->4
這道題從題面上就說明了這是一道鏈表相關問題,要進行鏈表合併,無非是修改鏈表指針指向,或者是鏈表拼接。因此,這道題我有兩種思路的解法:
兩種方式的區別很明顯,修改指針的方式須要存儲和不斷修改指針指向,拼接的方式直接作鏈表拼接。
固然這裏也有一些特殊值須要考慮進來。
Ⅰ.修改指針
代碼:
/** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var mergeTwoLists = function(l1, l2) { if (l1 === null) return l2 if (l2 === null) return l1 // 結果鏈表 let l = new ListNode(0) // 不斷更新的當前結點指針,對象賦值爲傳址,因此下面改指針指向便可 let cursor = l // 會有一個先遍歷完,變成 null while(l1 !== null && l2 !== null) { if (l1.val <= l2.val) { // 哪一個小,指針就指向哪 cursor.next = l1 l1 = l1.next } else { cursor.next = l2 l2 = l2.next } // 能夠理解爲 l.next.next.next ... cursor = cursor.next } // 有一個爲空則能夠直接拼接 cursor.next = l1 === null ? l2 : l1 return l.next };
結果:
O(m + n)
,分別表明兩個鏈表長度Ⅱ.鏈表拼接
代碼:
/** * @param {ListNode} l1 * @param {ListNode} l2 * @return {ListNode} */ var mergeTwoLists = function(l1, l2) { if (l1 === null) return l2 if (l2 === null) return l1 if (l1.val < l2.val) { l1.next = mergeTwoLists(l1.next, l2) return l1 // 這個是合併後的了 } else { l2.next = mergeTwoLists(l1, l2.next) return l2 // 這個是合併後的了 } };
結果:
O(m + n)
,分別表明兩個鏈表長度思路基本上都是這兩種,未發現方向不一樣的解法。
無非是有些解法額外開闢了新的鏈表來記錄,或者一些細節上的差別。
這裏的鏈表拼接解法,有沒有發現跟 上一期 14題中的分治思路是同樣的?對,實際上這個也是分治思路的一個應用。
給定一個排序數組,你須要在原地刪除重複出現的元素,使得每一個元素只出現一次,返回移除後數組的新長度。
不要使用額外的數組空間,你必須在原地修改輸入數組並在使用 O(1) 額外空間的條件下完成。
示例:
給定數組 nums = [1,1,2], 函數應該返回新的長度 2, 而且原數組 nums 的前兩個元素被修改成 1, 2。 你不須要考慮數組中超出新長度後面的元素。 給定 nums = [0,0,1,1,1,2,2,3,3,4], 函數應該返回新的長度 5, 而且原數組 nums 的前五個元素被修改成 0, 1, 2, 3, 4。 你不須要考慮數組中超出新長度後面的元素。
說明:
爲何返回數值是整數,但輸出的答案是數組呢?
請注意,輸入數組是以「引用」方式傳遞的,這意味着在函數裏修改輸入數組對於調用者是可見的。
你能夠想象內部操做以下:
// nums 是以「引用」方式傳遞的。也就是說,不對實參作任何拷貝 int len = removeDuplicates(nums); // 在函數裏修改輸入數組對於調用者是可見的。 // 根據你的函數返回的長度, 它會打印出數組中該長度範圍內的全部元素。 for (int i = 0; i < len; i++) { print(nums[i]); }
若是是單純的數組去重,那有不少種方法能夠作。因此題目也加了限制條件,總結一下比較重要的幾點:
O(1)
這意味着不容許使用新的數組來解題,也就是對原數組進行操做。最後一點注意點能夠看出,數組項的拷貝複製是一個方向,第二點能夠看出數組刪除是一個方向。刪除元素的話就不會超過,因此不須要考慮二者結合。因此這題我分兩個方向來解:
Ⅰ.拷貝數組元素
代碼:
/** * @param {number[]} nums * @return {number} */ var removeDuplicates = function(nums) { if (nums.length === 0) return 0; var len = 1 for(let i = 1; i < nums.length; i++) { if(nums[i] !== nums[i - 1]) { // 後一項不等於前一項 nums[len++] = nums[i] // 拷貝數組元素 } } return len };
結果:
O(n)
Ⅱ.刪除數組元素
代碼:
/** * @param {number[]} nums * @return {number} */ var removeDuplicates = function(nums) { if (nums.length === 0) return 0; for(let i = 1; i < nums.length;) { if(nums[i] === nums[i - 1]) { // 後一項等於前一項 nums.splice(i, 1) } else { i++ } } return nums.length };
結果:
O(n)
這裏看見一種很巧妙的解法,雙指針法。至關於一個用於計數,一個用於掃描。
Ⅰ.雙指針法
代碼:
/** * @param {number[]} nums * @return {number} */ var removeDuplicates = function(nums) { if (nums.length === 0) return 0; let i = 0; for(let j = 1; j < nums.length; j++) { if (nums[j] !== nums[i]) { nums[++i] = nums[j] } } return i + 1 // 下標 +1 爲數組長度 };
結果:
O(n)
就三種解法而言,刪除數組元素會頻繁修改數組,不建議使用。雙指針法和拷貝數組元素代碼邏輯類似,可是思路上是大相徑庭的。
給定一個數組 nums
和一個值 val
,你須要原地移除全部數值等於 val
的元素,返回移除後數組的新長度。
不要使用額外的數組空間,你必須在原地修改輸入數組並在使用 O(1)
額外空間的條件下完成。
元素的順序能夠改變。你不須要考慮數組中超出新長度後面的元素。
示例:
給定 nums = [3,2,2,3], val = 3, 函數應該返回新的長度 2, 而且 nums 中的前兩個元素均爲 2。 你不須要考慮數組中超出新長度後面的元素。 給定 nums = [0,1,2,2,3,0,4,2], val = 2, 函數應該返回新的長度 5, 而且 nums 中的前五個元素爲 0, 1, 3, 0, 4。 注意這五個元素可爲任意順序。 你不須要考慮數組中超出新長度後面的元素。
說明:
爲何返回數值是整數,但輸出的答案是數組呢?
請注意,輸入數組是以「引用」方式傳遞的,這意味着在函數裏修改輸入數組對於調用者是可見的。
你能夠想象內部操做以下:
// nums 是以「引用」方式傳遞的。也就是說,不對實參做任何拷貝 int len = removeElement(nums, val); // 在函數裏修改輸入數組對於調用者是可見的。 // 根據你的函數返回的長度, 它會打印出數組中該長度範圍內的全部元素。 for (int i = 0; i < len; i++) { print(nums[i]); }
這題跟上一題很是類似,因此咱們能夠沿用上題的方向來解這道題:
Ⅰ.刪除數組元素
代碼:
/** * @param {number[]} nums * @param {number} val * @return {number} */ var removeElement = function(nums, val) { if (nums.length === 0) return 0; for(let i = 0; i < nums.length;) { if (nums[i] === val) { nums.splice(i, 1) } else { i++ } } };
結果:
O(n)
Ⅱ.雙指針法
代碼:
/** * @param {number[]} nums * @param {number} val * @return {number} */ var removeElement = function(nums, val) { if (nums.length === 0) return 0; let i = 0 for(let j = 0; j < nums.length; j++) { if (nums[j] !== val) { nums[i++] = nums[j] } } return i };
結果:
O(n)
看到兩個略有差別的方法:
const of
替換一次遍歷,只是寫法區別,沒有本質提高Ⅰ.單指針法
代碼:
/** * @param {number[]} nums * @param {number} val * @return {number} */ var removeElement = function(nums, val) { if (nums.length === 0) return 0; let i = 0; for(const num of nums) { if(num !== val) { nums[i++] = num; } } return i; };
結果:
O(n)
Ⅱ.交換移除
代碼:
/** * @param {number[]} nums * @param {number} val * @return {number} */ var removeElement = function(nums, val) { if (nums.length === 0) return 0; let i = nums.length; for(let j = 0; j < i;) { if (nums[j] === val) { nums[j] = nums[--i] } else { j++ } } return i; };
結果:
O(n)
這裏開拓下思路:若是要移除的是多項,那麼仍是使用指針法作處理合適;若是是移除單項,那麼使用交互移除法其實遍歷次數最少。
實現 strStr()
函數。
給定一個 haystack
字符串和一個 needle
字符串,在 haystack
字符串中找出 needle
字符串出現的第一個位置 (從0開始)。若是不存在,則返回 -1
。
示例:
輸入: haystack = "hello", needle = "ll" 輸出: 2 輸入: haystack = "aaaaa", needle = "bba" 輸出: -1
說明:
當 needle
是空字符串時,咱們應當返回什麼值呢?這是一個在面試中很好的問題。
對於本題而言,當 needle
是空字符串時咱們應當返回 0
。這與 C
語言的 strstr()
以及 Java
的 indexOf()
定義相符。
這道題很明顯是一道字符串搜索的題目,估計是在考察算法,可是受限知識面,因此我就先以現有方式實現做答,再來學習算法了。
IndexOf
這個是原生方法,考察這個就沒有意義了,因此不作詳細論述IndexOf
Ⅰ.遍歷匹配
代碼:
/** * @param {string} haystack * @param {string} needle * @return {number} */ var strStr = function(haystack, needle) { if (needle === '') return 0 if (needle.length > haystack.length) return -1 if (needle.length === haystack.length && needle !== haystack) return -1 for(let i = 0; i < haystack.length; i++) { if (i + needle.length > haystack.length) { return -1 } else { const str = haystack.substr(i, needle.length) if (str === needle) { return i } } } return -1 };
結果:
O(n)
首先查閱《算法導論》,看到字符串匹配有如下四種:
而後再看題解,大概還找到如下三種算法:
Ⅰ.樸素字符串匹配算法
算法說明:
經過一個循環找到全部有效便宜,該循環對 n-m+1
個可能的 s
值進行檢測,看可否知足條件 P[1..m] = T[s+1...s+m]
。其中 n
是字符串長度, 'm' 是匹配字符串長度。
代碼:
/** * @param {string} haystack * @param {string} needle * @return {number} */ var strStr = function(haystack, needle) { if (needle === '') return 0 if (needle.length > haystack.length) return -1 if (needle.length === haystack.length && needle !== haystack) return -1 let i = 0; let j = 0; while(j < needle.length && i < haystack.length) { if(haystack[i] === needle[j]) { // 同位相等,繼續判斷下一位 i++; j++; } else { i = i - j + 1; // i 偏移 j = 0; // j 重置 if (i + needle.length > haystack.length) { // 我增長的優化點,減小一些運算 return -1 } } } if (j >= needle.length) { // 子串比完了,此時 j 應該等於 needle.length return i - needle.length; } else { return -1 } };
結果:
O(m*n)
Ⅱ.Rabin-Karp 算法
算法說明:
進行哈希運算,將字符串轉成對應的哈希值進行比對,相似16進制。這裏題目是字符串,我就用 ASCII
值來表示每一個字符的哈希值,那麼就能夠計算出模式串的哈希值,再進行滾動比較。
每次滾動只須要作固定的 -*+
三個操做,便可得出滾動串的哈希值了。
好比計算 bbc
,哈希值爲 hash = (b.charCodeAt() * 128 ^ 2 + b.charCodeAt() * 128 + c.charCodeAt())
,若是要計算後新值 bca
則爲 (hash - b.charCodeAt() * 128 ^ 2) * 128 + c.charCodeAt()
。
代碼:
/** * @param {string} haystack * @param {string} needle * @return {number} */ var strStr = function(haystack, needle) { if (needle === '') return 0 if (needle.length > haystack.length) return -1 if (needle.length === haystack.length && needle !== haystack) return -1 let searchHash = 0 // 搜索字符串的hash值 let startHash = 0 // 字符串起始的hash值 for(let i = 0; i < needle.length; i++) { searchHash += needle.charCodeAt(i) * Math.pow(2, needle.length - i - 1) startHash += haystack.charCodeAt(i) * Math.pow(2, needle.length - i - 1) } if (startHash === searchHash) return 0 for(let j = 1; j < haystack.length - needle.length + 1; j++) { startHash = (startHash - haystack.charCodeAt(j - 1) * Math.pow(2, needle.length - 1)) * 2 + haystack.charCodeAt(j + needle.length - 1) if (startHash === searchHash) { return j } } return -1 };
結果:
O(m*n)
注意:這裏可能會存在溢出的狀況,因此不是全部狀況都適用。
Ⅲ.利用有限自動機進行字符串匹配
算法說明:
經過對文本字符串 T
進行掃描,找出模式 P
的全部出現位置。它們只對每一個文本字符檢查一次,而且檢查每一個文本字符時所用的時間爲常數。一句話歸納:字符輸入引發狀態機狀態變動,經過狀態轉換圖獲得預期結果。
這裏主要的核心點是判斷每次輸入,找到最長的後綴匹配,若是最長時的長度等於查找字符串長度,那就必定包含該查找字符串。
代碼:
/** * @param {string} haystack * @param {string} needle * @return {number} */ var strStr = function(haystack, needle) { if (needle === '') return 0 if (needle.length > haystack.length) return -1 if (needle.length === haystack.length && needle !== haystack) return -1 // 查找最大匹配後綴長度 function findSuffix (Pq) { let suffixLen = 0 let k = 0 while(k < Pq.length && k < needle.length) { let i = 0; for(i = 0; i <= k; i++) { // 找needle中的多少項爲當前狀態對應字符串的匹配項 if (Pq.charAt(Pq.length - 1 - k + i) !== needle.charAt(i)) { break; } } // 全部項都匹配,即找到了後綴 if (i - 1 == k) { suffixLen = k+1; } k++ } return suffixLen } // 獲取全部輸入的字符集,好比 'abbc' 和 'cd' 合集爲 ['a','b','c','d'] const setArr = Array.from(new Set(haystack + needle)) // 用戶輸入的可選項 // 創建狀態機 const hash = {} for(let q = 0; q < haystack.length; q++) { for(let k = 0; k < setArr.length; k++) { const char = haystack.substring(0, q) + setArr[k] // 下個狀態的字符 const nextState = findSuffix(char) // 求例如 0.a 0.b 0.c 的值 if (!hash[q]) { hash[q] = {} } hash[q][char] = nextState } } // 根據狀態機求解 let matchStr = '' for(let n = 0; n < haystack.length; n++) { const map = hash[n] matchStr += haystack[n] const nextState = map[matchStr] if (nextState === needle.length) { return n - nextState + 1 } } return -1 };
結果:
O(n)
Ⅳ.KMP 算法
算法說明:
能夠理解爲在狀態機的基礎上,使用了一個前綴函數來進行狀態判斷。本質上也是前綴後綴的思想。
代碼:
// @lc code=start /** * @param {string} haystack * @param {string} needle * @return {number} */ var strStr = function(haystack, needle) { if (needle === '') return 0 if (needle.length > haystack.length) return -1 if (needle.length === haystack.length && needle !== haystack) return -1 // 生成匹配串各個位置下下的最長公共先後綴長度哈希表 function getHash () { let i = 0 // arr[i] 表示 i 前面的字符串的最長公共先後綴長度 let j = 1 let hash = { 0: 0 } while (j < needle.length) { if (needle.charAt(i) === needle.charAt(j)) { // 相等直接 i j 都後移 hash[j++] = ++i } else if (i === 0) { // i 爲起點且二者不相等,那麼必定爲0 hash[j] = 0 j++ } else { // 這裏解釋一下: 由於i前面的字符串與j前面的字符串擁有相同的最長公共先後綴,也就是說i前面字符串的最長公共後綴與j前面字符串的最長公共前綴相同,因此i只需回到i前面字符串最長公共前綴的後一位開始比較 i = hash[i - 1] } } return hash } const hash = getHash() let i = 0 // 母串中的位置 let j = 0 // 子串中的位置 while(i < haystack.length && j < needle.length) { if (haystack.charAt(i) === needle.charAt(j)) { // 兩個匹配,同時後移 i++ j++ } else if (j === 0) { // 兩個不匹配,而且j在起點,則母串後移 i++ } else { j = hash[j - 1] } } if (j === needle.length) { // 循環完了,說明匹配到了 return i - j } else { return -1 } };
結果:
O(n)
Ⅴ.BM 算法
算法說明:
基於後綴匹配,匹配從後開始,但移動仍是從前開始,只是定義了兩個規則:壞字符規則和好後綴規則。
通俗來說就是先驗證是否爲壞字符,而後判斷是否在搜索詞中進行對應的偏移進行下一步驗證。若是匹配的話就從後往前校驗,若是仍然匹配,就爲好後綴。核心思想是每次位移都在壞字符和好後綴規則中取較大值,因爲兩個規則都只與匹配項相關,因此能夠提早生成規則表。
代碼:
/** * @param {string} haystack * @param {string} needle * @return {number} */ var strStr = function(haystack, needle) { if (needle === '') return 0 if (needle.length > haystack.length) return -1 if (needle.length === haystack.length && needle !== haystack) return -1 function makeBadChar (needle) { let hash = {} for(let i = 0; i < 256; i++) { // ascii 字符長度 hash[String.fromCharCode(i)] = -1 // 初始化爲-1 } for(let i = 0; i < needle.length; i++) { hash[needle.charAt(i)] = i // 最後出現該字符的位置 } return hash } function makeGoodSuffix (needle) { let hashSuffix = {} let hashPrefix = {} for(let i = 0; i < needle.length; i++) { hashSuffix[i] = -1 hashPrefix[i] = false } for(let i = 0; i < needle.length - 1; i++) { // needle[0, i] let j = i k = 0 // 公共後綴子串長度,尾部取k個出來進行比較 while(j >= 0 && needle.charAt(j) === needle.charAt(needle.length - 1 - k)) { // needle[0,needle.length - 1] --j ++k hashSuffix[k] = j + 1 // 起始下標 } if (j === -1) { // 說明所有匹配,意味着此時公共後綴子串也是模式的前綴子串 hashPrefix[k] = true } } return { hashSuffix, hashPrefix } } function moveGoodSuffix (j, needle) { let k = needle.length - 1 - j let suffixes = makeGoodSuffix(needle).hashSuffix let prefixes = makeGoodSuffix(needle).hashPrefix if (suffixes[k] !== -1) { // 找到了跟好後綴同樣的子串,獲取下標 return j - suffixes[k] + 1 } for(let r = j + 2; r < needle.length; ++r) { if (prefixes[needle.length - r]) { // needle.length 是好後綴子串長度 return r // 對齊前綴到好後綴 } } return needle.length // 所有匹配,直接移動字符串長度 } let badchar = makeBadChar(needle) let i = 0; while(i < haystack.length - needle.length + 1) { let j for(j = needle.length - 1; j >= 0; --j) { if (haystack.charAt(i + j) != needle[j]) { break; // 壞字符,下標爲j } } if (j < 0) { // 匹配成功 return i // 第一個匹配字符的位置 } let moveLen1 = j - badchar[haystack.charAt(i + j)] let moveLen2 = 0 if (j < needle.length -1) { // 若是有好後綴 moveLen2 = moveGoodSuffix(j, needle) } i = i + Math.max(moveLen1, moveLen2) } return -1 };
結果:
O(n)
Ⅵ.Horspool 算法
算法說明:
將主串中匹配窗口的最後一個字符跟模式串中的最後一個字符比較。若是相等,繼續從後向前對主串和模式串進行比較,直到徹底相等或者在某個字符處不匹配爲止。若是不匹配,則根據主串匹配窗口中的最後一個字符在模式串中的下一個出現位置將窗口向右移動。
代碼:
/** * @param {string} haystack * @param {string} needle * @return {number} */ var strStr = function(haystack, needle) { if (needle === '') return 0 if (needle.length > haystack.length) return -1 if (needle.length === haystack.length && needle !== haystack) return -1 let hash = {} for(let i = 0; i < 256; i++) { hash[i] = needle.length // 默認初始化爲最大偏移量,也就是匹配串長度 } for(let i = 0; i < needle.length - 1; i++) { hash[needle.charCodeAt(i)] = needle.length - 1 - i // 每一個字符距離右側的距離 } let pos = 0 while(pos < (haystack.length - needle.length + 1)) { let j = needle.length - 1 // 從右往左 while(j >= 0 && haystack.charAt(pos + j) === needle.charAt(j)) { j-- } if (j < 0) { // 所有匹配 return pos } else { // 不匹配 pos += hash[haystack.charCodeAt(pos + needle.length - 1)] } } return -1 };
結果:
O(n)
Ⅶ.Sunday 算法
算法說明:
它的思想跟 BM 算法
類似,可是它是從前日後匹配,匹配失敗時關注主串內參與匹配的後一位字符。若是該字符不存在匹配字符中,則多偏移一位;若是存在,則偏移匹配串長度減該字符最右出現的位置。
代碼:
/** * @param {string} haystack * @param {string} needle * @return {number} */ var strStr = function(haystack, needle) { if (needle === '') return 0 if (needle.length > haystack.length) return -1 if (needle.length === haystack.length && needle !== haystack) return -1 let hash = {} for(let i = 0; i < needle.length; i++) { hash[needle.charAt(i)] = needle.length - i // 偏移表 } for(let i = 0; i < haystack.length;) { let j for(j = 0; j < needle.length; j++) { if (haystack.charAt(i + j) !== needle.charAt(j)) { break } } if(j === needle.length) { // 徹底匹配 return i } if (i + needle.length >= haystack.length) { // 未找到 return -1 } else { i += hash[haystack.charAt(i + needle.length)] || needle.length + 1 } } return -1 };
結果:
O(n)
就理解的難易度來說,我建議先看 Sunday 算法
和 Horspool 算法
,不過 RMP 算法
的匹配思路打開了眼界,利用後綴前綴來處理問題。這裏把常見的字符串算法都作了一次嘗試,總體下來收穫頗豐。
(完)
本文爲原創文章,可能會更新知識點及修正錯誤,所以轉載請保留原出處,方便溯源,避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗
若是能給您帶去些許幫助,歡迎 ⭐️star 或 ✏️ fork (轉載請註明出處:https://chenjiahao.xyz)