本篇文章僅記錄在平時刷題程序中,讓人眼前一亮的處理思路,所以本篇文章適合演算法愛好者閱讀及參考,沒有演算法功底的程式猿們,建議不用花費太多的時間在本篇文章
1,題目描述:給定一個字串陣列,請根據“相同字符集”進行分組(摘自 LeetCode 49)
例 :Input: ["eat", "tea", "tan", "ate", "nat", "bat"],
Output:[
["ate","eat","tea"],
["nat","tan"],
["bat"]
]
基礎分析:這類問題的常見處理并不難,只需要將每個字符記錄對應值,內部回圈比較,外部回圈子串陣列即可,時間復雜度 O(K2[子串平均長度] * N * Log(N)2)
晉級分析:我在基礎之上,將字串的和存了下來,將內部回圈比較的次數降低,時間復雜度可以達到 O(K2 * N * Log(N) * Min(N)[代表字串和相同的次數])
高級分析:首先引進一組概念:正整數唯一分解定理,每個大于1的自然數,要么本身為質數,要么可以由2個或以上的質數相乘,且組合唯一;上述定理結合問題來看,我們僅需要將字串中的每個字符與質數一一對應,并將字串所有字符對應的質數乘積保存下來,即可確保字串的 hash 唯一,時間復雜度 O(K * N * Log(N))
Coding :
1 func GroupAnagramsOpt(strs []string) [][]string {
2 var res [][]string
3 strMap := make(map[int][]string)
4 prime := []int{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103}
5
6 for _, str := range strs {
7 sum := 1
8 for _, char := range str {
9 sum = prime[char-'a'] * sum
10 }
11 if _, ok := strMap[sum]; !ok {
12 strMap[sum] = []string{str}
13 } else {
14 strMap[sum] = append(strMap[sum], str)
15 }
16 }
17
18 for _, v := range strMap {
19 res = append(res, v)
20 }
21
22 return res
23 }
2,題目描述:給定一個 n,代表 n x n 的棋盤,擺放 n 個皇后,使其相互之間無法攻擊(同一橫豎斜僅一個旗子,8個皇后問題),回傳所有的擺放情況(摘自 LeetCode 50)
例 :Input: 4
Output: [
[".Q..","...Q","Q...","..Q."],
["..Q.","Q...","...Q",".Q.."]
]
基礎分析:采用遞回法與回朔法,每次檢查當前位置可否落子,落子后將當前位置所在的橫豎斜 4 個方向全部置位不可落子,最終得出所有可能
晉級分析:對于 n x n 矩陣,不可落子不需要記錄到具體的點,僅需要記錄(行、列、左斜(i+j)、右斜(i-j+n))即可快速判斷當前行,列、位置可否落子,省去記錄和判斷的時間復雜度
高級分析:我自己做的時候,發現一直在糾結重復性問題,所以第一版是在將結果放入佇列時,進行重復排查,發現效率較低;再想通過遞回的回傳值,確定當前位置是否可以再次落子,以排除相同可能性,發現每次回溯后都必須處理標記資料;再后來發現每次遞回的行,無需從 0 開始(同行僅有1個旗子),回圈時不需要每次都從(0,0)點開始判斷,節省相同擺法的重復性時間消耗,
Coding :
1 // 51. N-Queens 2 func SolveNQueens(n int) [][]string { 3 stepMap := make([][]bool, 3) 4 qMap := make([][]bool, n) 5 for i, _ := range stepMap { 6 stepMap[i] = make([]bool, 2*n) 7 } 8 for i, _ := range qMap { 9 qMap[i] = make([]bool, n) 10 } 11 12 t := &struct{ a [][]string }{a: make([][]string, 0)} 13 SolveNQueensSub(stepMap, qMap, t, n, 0) 14 return t.a 15 } 16 17 func SolveNQueensSub(stepMap [][]bool, qMap [][]bool, res *struct{ a [][]string }, n, i int) { 18 // trans to res 19 if i == n { 20 var vs []string 21 for _, row := range qMap { 22 v := make([]byte, n) 23 for i, r := range row { 24 if r { 25 v[i] = 'Q' 26 } else { 27 v[i] = '.' 28 } 29 } 30 vs = append(vs, string(v)) 31 } 32 res.a = append(res.a, vs) 33 return 34 } 35 36 for j := 0; j < n; j++ { 37 // col + / + \ 38 if !stepMap[0][j] && !stepMap[1][i+j] && !stepMap[2][j-i+n] { 39 qMap[i][j] = true 40 stepMap[0][j] = true 41 stepMap[1][i+j] = true 42 stepMap[2][j-i+n] = true 43 SolveNQueensSub(stepMap, qMap, res, n, i+1) 44 qMap[i][j] = false 45 stepMap[0][j] = false 46 stepMap[1][i+j] = false 47 stepMap[2][j-i+n] = false 48 } 49 } 50 }
3,題目描述:給定兩個字串 A,B,我們可以對 A 字串進行 3 種操作,
“插入一個字符“
”替換一個字符“
”洗掉一個字符“
如何在最少的操作次數后,可以使 A 與 B 相等(摘自 LeetCode 72)
例 : input: “horse“,”ros“
output: 3(1,h -> r,"rorse";2,del 'r',"rose";3,del 'e',"ros")
基礎分析:暴力法進行遞回回圈,每次成功后,記錄次數回傳最小次數,理論上是 n^3(這種方法我沒有嘗試,一般 leetcode 上的題目超過 n^2 的解法,往往都會超時,這里只是提出一種解法)
晉級分析:一般看到“最少”/"最大"/“最小”... 這類字眼,我們首先腦子中要冒出 4 個字“動態規劃“,這是一個演算法工程師的基本素質,這道題,我們如果用動態規劃的思路去考慮,就會一目了然,"horse" 和 "ros" 的匹配,我們可以根據動態規劃的思路去進行降級,分別求 "horse" 和 "ro"、"hors" 和 “ros”、 “hors” 和 “ro” 這三種情況下最小次數,這里我們將這三種情況分別稱為 A、B、C 下的最小次數,則最終的最小次數與三種情況的結果息息相關,具體的關系靜下心來推導:
A 情況:當前最小次數 + 1
B 情況:當前最小次數 + 1
C 情況:如果最后一位 'e' 和 's' 相等(這里只是提出假設),則回傳當前最小次數,如果最后一位不相等,則回傳當前最小次數 + 1
而我們需要的最小次數便是上面 3 種情況最小值,這里就可以給出公式
if str1[n] == str2[m] -> map[n][m] = min(map[n-1][m] + 1,map[n][m-1] + 1, map[n][m])
else -> map[n][m] = min(map[n-1][m] + 1,map[n][m-1] + 1, map[n][m] + 1)
當時做題時,根據這樣的邏輯進行編碼后,發現在跑測驗用例時會計算少,為什么呢?其實能看到原因是第一列、第一行是有些特殊的,首先是沒有 m-1、n-1 去比較計算,是能根據前一位判斷當前的最小次數,這一行的邏輯是比較簡單的,簡單就是 "m" 和 "djfioncvohghmnhs" 的匹配,這里不詳細給出推導,有些演算法功底的同學應該是可以很快寫出代碼的,直接上代碼
1 func MinDistanceV2(word1 string, word2 string) int { 2 if len(word1) == 0 { 3 return len(word2) 4 } 5 if len(word2) == 0 { 6 return len(word1) 7 } 8 9 n := len(word1) 10 m := len(word2) 11 disMap := make([][]int, n) 12 for i := 0; i < n; i++ { 13 disMap[i] = make([]int, m) 14 } 15 16 // first column 17 isUse := false 18 for i := 0; i < n; i++ { 19 disMap[i][0] = i + 1 20 if word1[i] == word2[0] { 21 isUse = true 22 } 23 if isUse { 24 disMap[i][0]-- 25 } 26 } 27 28 // first row 29 isUse = false 30 for i := 0; i < m; i++ { 31 disMap[0][i] = i + 1 32 if word1[0] == word2[i] { 33 isUse = true 34 } 35 if isUse { 36 disMap[0][i]-- 37 } 38 } 39 40 for i := 1; i < n; i++ { 41 for j := 1; j < m; j++ { 42 dis := Common.MAXINTNUM 43 if word1[i] == word2[j] { 44 if dis > disMap[i-1][j-1] { 45 dis = disMap[i-1][j-1] 46 } 47 } else { 48 if dis > disMap[i-1][j-1]+1 { 49 dis = disMap[i-1][j-1] + 1 50 } 51 } 52 if dis > disMap[i-1][j]+1 { 53 dis = disMap[i-1][j] + 1 54 } 55 if dis > disMap[i][j-1]+1 { 56 dis = disMap[i][j-1] + 1 57 } 58 disMap[i][j] = dis 59 } 60 } 61 return disMap[n-1][m-1] 62 }
高級分析:有些人給出一些比較有趣的解法,相較于上述的解法并沒有太多的優化,但多了一份巧妙,可以看到下圖,既然第一行、第一列需要特殊處理,那可以在每個字串前面加一列不存在的字符,初始化是使用 for 回圈對第一行、第一列先進行簡單賦值后再進行公式計算,(這里就不給出代碼了,我個人使用的是第二種方法,之所以將這種方法列為高級分析,僅僅是解題思路需要適當的巧妙,可以讓代碼邏輯看起來簡單很多)

4,題目描述:給定一個排序陣列和一個目標值,找出陣列中是否含有當前目標(摘自 LeetCode 81)
例 : input: [1,3,6,7,9];3
output: true
基礎分析:根據原題,當時笨方法 for 回圈,另一種業務中常用的方法便是二分查找法
晉級分析:之前參加過一個國內知名公司的面試,該公司比較注重演算法,幾乎每場面試都有一個演算法題等著你,而我這次碰到的便是這道題的迭代,在原題的基礎上將陣列進行一次翻滾,將后面一部分(有可能是0-n)按順序挪到陣列前,在這種條件下,我也是沒有任何慫,萬物都有解決的辦法嘛,大不了就是笨方法,但面試官肯定不會對這種笨方法有任何欣賞的點,肯定是需要二分查找法,最終也是利用1-3分鐘將思路和代碼寫了出來,
很多人其實這里糾結的是每次選前半段還是后半段,其實我們進行拆分,就可以很明確的知道選擇哪邊;
1,判斷當前中心是在翻滾點的左邊還是右邊,其實就是判斷中間點的指是否大于最后一個值,大于則代表在翻滾點左側,小于則代表在翻滾點右測(這第一步往往很重要,但經常有人考慮不到,包括我自己第一次的思路,因為兩種不同的結果決定下面我們判斷的方式)
2,當中點在翻滾點左側時,我們只需要比較當前目標是否比首個數字大,如果大,則代表需要查前半段,否則就是后半段;相反,當中點在翻滾點右側時,我們只需要比較當前目標是否比最后一個數字小,如果小,則代表需要查后半段,否則就是前半段
按上述兩點進行回圈,即可以 O(logn) 的時間復雜度得到結果
高級分析:當我看似艱難的將上面的代碼寫好之后(其實想的腦闊疼,改了好幾版),還未囂張,面試官突然問如果陣列中有重復的數字時,是否需要做什么修改,我考慮了幾秒,覺得是沒問題,面試官一笑就過了,我以為我的聰明征服了面試官大大,面試結束后興起稍微寫了一下代碼在本地跑完之后,才發現有了重復測驗案例后,結果是錯誤的,冥思苦想覺得例外丟人,并且想了好久的解決方法,其實很簡單,在上面的解法之前做一次判斷
1,當前中點是否與首位陣列相等,相等則回圈拋棄首位陣列,start++;反之則是 end--;我們直接丟棄掉就可以啦,只是這種方案最壞時間復雜度就降到了 O(n),上最終版代碼
1 func Search(nums []int, target int) bool { 2 if len(nums) == 0 { 3 return false 4 } 5 6 start := 0 7 end := len(nums) - 1 8 for start <= end { 9 mid := (end-start)/2 + start 10 if nums[mid] == target { 11 return true 12 } 13 14 for start != mid && nums[mid] == nums[start] { 15 start++ 16 if start == mid { 17 start = mid + 1 18 goto OUT 19 } 20 } 21 for end != mid && nums[mid] == nums[end] { 22 end-- 23 if end == mid { 24 end = mid - 1 25 goto OUT 26 } 27 } 28 29 if nums[mid] < nums[end] { 30 if target > nums[mid] { 31 if target == nums[end] { 32 return true 33 } else if target < nums[end] { 34 start = mid + 1 35 end-- 36 } else { 37 end = mid - 1 38 } 39 } else { 40 end = mid - 1 41 } 42 43 } else { 44 if target < nums[mid] { 45 if target == nums[start] { 46 return true 47 } else if target > nums[start] { 48 start++ 49 end = mid - 1 50 } else { 51 start = mid + 1 52 } 53 } else { 54 start = mid + 1 55 } 56 } 57 OUT: 58 } 59 return false 60 }
5,題目描述:給定一個陣列,每個數字代表當前位置的柱子高度,請回傳柱子組成所能組成的最大高度(摘自 LeetCode 84)
例 : input: [3,1,5,4,1]
output: 選擇 5,4 -> 4 * 2 = 8
基礎分析:暴力美學,有用的就是好的方法,對任意兩個位置遍歷并每次計算兩者之間的最低柱子,進行面積計算得出最大的面積,時間復雜度O(n3),按 leetcode 的尿性,這種復雜度是別想跑過測驗用例的
晉級分析:上述的演算法中,其實我們可以使用 n 的空間,來記錄以當前為起點,后面柱子的最低高度,這樣我們每次就可以省去找出兩點之間高度的次數,時間復雜度O(n2),其實這種方法已經達到了一般演算法喜好者的水準,可作為一名刷題者,這種方法只能讓你通過面試,絕對達不到驚艷的地步,給出代碼:
1 func LargestRectangleArea(heights []int) int { 2 if len(heights) == 0 { 3 return 0 4 } 5 6 highs := make([]int, 0, len(heights)) 7 res := 0 8 for i := 0; i < len(heights); i++ { 9 for j := 0; j < len(highs); j++ { 10 if highs[j] > heights[i] { 11 highs[j] = heights[i] 12 } 13 curArx := highs[j] * (i - j + 1) 14 if curArx > res { 15 res = curArx 16 } 17 } 18 if heights[i] > res { 19 res = heights[i] 20 } 21 highs = append(highs, heights[i]) 22 } 23 return res 24 }
高級分析:其實可以看到,上述的難點在于我們無法動態的滑動前后兩端,保證每次滑動都為最優解,如果能夠解決這個問題,我們就可以在 O(n)的時間下完成演算法,其實換個角度想,向后滑動時,新的柱子高度如果大于等于上一個柱子,那盡管往上加,面積一定是大的;而如果下一個柱子比當前柱子小,則需要將前面所有的高柱子進行一次計算,得到這一部分的最大面積后,高的那些柱子已經失去意義了(可以將這一段比作一個區間,A-B 之間存在一些高柱子,A 比 B 小,那 A 前列的柱子和 B 后續的柱子無論如何都不可能再用到中間的高柱子,前列的直接按 A 的高度算,后續的直接按 B 的高度算)

由前往后,又要由后往前計算并排除,直接使用堆疊工具,可以給出步驟:
1,對陣列進行回圈處理,回圈 1,2,3 步,直到所有陣列處理完畢
1,當前位置高度大于等于堆疊頂的數值時,直接 Push 到堆疊里面
2,當前位置高度小于堆疊頂數值時,進行 3 步驟回圈,當堆疊為慷訓者堆疊頂數值小于當前位置高度,跳出回圈
3,取出堆疊頂的數值,進行面積計算,公式:當前高度(h) * 兩點距離
4,堆疊不為空時,說明還有需要處理的資料,這時候回圈 5 步,直到堆疊為空
5,取出堆疊頂的陣列,進行面積計算,公式:堆疊頂高度(h)* (陣列長度 - 堆疊頂下標)
備注:堆疊中存盤(下標,高度),防止最后一個資料未處理,可以提前插入一個 (-1,0) 的資料,當然也可以利用一些邏輯判斷特殊處理
給出代碼:
1 func LargestRectangleAreaOpt(heights []int) int { 2 if len(heights) == 0 { 3 return 0 4 } 5 stack := &Common.Stack{} 6 res := 0 7 8 type node struct { 9 index int 10 num int 11 } 12 stack.Push(&node{index: -1, num: 0}) 13 for i := 0; i < len(heights); i++ { 14 for stack.Size() > 1 { 15 top := stack.Top().(*node) 16 if heights[i] >= top.num { 17 break 18 } 19 20 stack.Pop() 21 nextTop := stack.Top().(*node) 22 area := top.num * (i - nextTop.index - 1) 23 if area > res { 24 res = area 25 } 26 } 27 stack.Push(&node{index: i, num: heights[i]}) 28 } 29 30 for stack.Size() > 1 { 31 top := stack.Pop().(*node) 32 nextTop := stack.Top().(*node) 33 area := top.num * (len(heights) - 1 - nextTop.index) 34 if area > res { 35 res = area 36 } 37 } 38 return res 39 }
6,題目描述:給定一個二維陣列,每個位置給定 0 或 1,回傳所能組成最大面積的矩陣(摘自 LeetCode 85)
例 : input: [1,0,1,1,0],[1,1,1,1,0]
output: 選擇 2 * 2 或 1 * 4 = 4
基礎分析:暴力解法,每個兩個點之間進行判斷,每次遍歷兩點所組成的矩形是否全部為 1,時間復雜度 O(n2m2)不用想了,除非不考慮性能才會這樣做
晉級分析:動態規劃,在對二維資料的回圈程序中,分別記錄其向上,向左的連續數量,判斷公式:
if data[i][j] == 1 {left[i] = left[i-1]+1; right[j] = right[j-1] + 1} else {left[i] = 0; right[i] = 0},
這里可以將每個點的向左向右連續統計出來,而對于當前點的最大面積只需要對一個方向進行遍歷求解即可,簡單給出一張圖

一個點從下往上回圈計算,便可以的到當前點的最大面積,最終時間復雜度 O(n2m)
高級分析:動態規劃,很厲害的一種演算法,完全是沒有想到的,這里給出別人的思路,每個點的面積(這里其實并不是最大面積),為當前點的最高高度,按最高高度擴展其寬度,算面積,如圖

可以看到,黃點的面積可以如圖所示計算,開始的時候我很糾結,這樣并不是黃點的最大面積,但我忽略一個重要的問題

按新的面積計算方案,黃點的左右兩側總能找到最大的面積點,所以根據這個思路走下去,利用動態規劃計算新的面積,每個點分別計算當前點的最高高度、高度對應的左側范圍,高度對應的右側范圍,公式分別為
if data[i][j] == 1 {height[i] = height[i]-1} else {height = 0}
if data[i][j] == 1 {left[i] = min(left[i-1], continue(左側連續為1的數量)} else {left[i] = 0}
if data[i][j] == 1 {right[i] = min(right[i-1], continue(右側連續為1的數量)} else {right[i] = 0}
給出代碼:
1 func MaximalRectangle(matrix [][]byte) int { 2 if len(matrix) == 0 || len(matrix[0]) == 0 { 3 return 0 4 } 5 6 type node struct { 7 height int 8 left int 9 right int 10 } 11 res := 0 12 n := len(matrix) 13 m := len(matrix[0]) 14 nodeMat := make([][]node, n) 15 for i := 0; i < n; i++ { 16 nodeMat[i] = make([]node, m) 17 } 18 19 continueNum := 0 20 // full height and left 21 for i := 0; i < n; i++ { 22 for j := 0; j < m; j++ { 23 if matrix[i][j] == '1' { 24 continueNum++ 25 if i == 0 { 26 nodeMat[i][j].height = 1 27 } else { 28 nodeMat[i][j].height = 1 + nodeMat[i-1][j].height 29 } 30 31 if j == 0 { 32 nodeMat[i][j].left = 1 33 } else { 34 nodeMat[i][j].left = continueNum 35 if i != 0 && nodeMat[i-1][j].left != 0 && (nodeMat[i-1][j].left < nodeMat[i][j].left) { 36 nodeMat[i][j].left = nodeMat[i-1][j].left 37 } 38 } 39 } else { 40 continueNum = 0 41 } 42 } 43 continueNum = 0 44 } 45 46 // full right 47 for i := 0; i < n; i++ { 48 for j := m - 1; j >= 0; j-- { 49 if matrix[i][j] == '1' { 50 continueNum++ 51 if j == m-1 { 52 nodeMat[i][j].right = 1 53 } else { 54 nodeMat[i][j].right = continueNum 55 if i != 0 && nodeMat[i-1][j].right != 0 && (nodeMat[i-1][j].right < nodeMat[i][j].right) { 56 nodeMat[i][j].right = nodeMat[i-1][j].right 57 } 58 } 59 curArx := nodeMat[i][j].height * (nodeMat[i][j].right + nodeMat[i][j].left - 1) 60 if curArx > res { 61 res = curArx 62 } 63 } else { 64 continueNum = 0 65 } 66 } 67 continueNum = 0 68 } 69 70 return res 71 }
7,題目描述:給定一個陣列,所有元素僅一個出現一次的值,其余均出現三次(摘自 LeetCode 137)
例 :input: 2,2,1,2
:out: 1
基礎分析:暴力解法,遍歷整個陣列元素,使用一個 hash-map 存盤所有的 key-num 值,當出現3次后移除指定的 key,最后僅留的一個 key 回傳即可,時間復雜度為 O(N * 1),這里既是理論值 1,實際應用中 hash 解沖突、hash 定位均需要花費 O(1) 以上的時間,
晉級分析:位運算,其實位運算除了用來做運算子之外,還有需要其他的用途,比如模擬乘法、判斷數值是否為2的冪次方、判斷一個數的二進制有多少位是1...諸如此類,有興趣的可以到 https://www.zhihu.com/question/38206659 了解一下,
其實說到位運算可能有些了解的同學已經有反應過來,那就是當其余元素均出現兩次的情況,我們就可以通過^(按位異或)的運算子,遍歷整個陣列,即可消除所有相同的元素,最后僅保留一個僅出現元素;那回來再看,我們如何利用運算子去抵消出現三次的元素呢;我們可以通過多個運算子,模擬出三進制的情形(實際計算機中既不存在三進制、更不存在三進制的位運算,我們只是想辦法去模擬 出現三次抵消掉 而已)
1,使用變數 a 記錄個位的數值(二進制的 1),使用變數 b 記錄復位的數值(二進制的 2)
2,當遍歷數值某一個位為 1 時,分別判斷 3,4
3,當變數 b 的當前位為 1,則說明已經當前位已經出現過 2 次,則本次直接抵消,即 b 和 a 的當前位均置 0
4,否則當變數 a 的當前位為 0,則改變變數 a 的當前位為 1,
5,否則即變數 a 的當前位為 1,則修改變數 a 的當前位為 0,修改變數 b 的當前位為 1
6,遍歷結束后,回傳 a 即可
上述邏輯看似復雜,但實際代碼中,無需對每位進行判斷,只需使用位運算代理上述邏輯,給出代碼:
1 func SingleNumberV2Opt(nums []int) int { 2 a := 0 3 b := 0 4 5 for _, num := range nums { 6 tmp := a & num // 得到需要進位的位數字 7 a = a ^ num // 計算當前 a 的最終值 8 b = b ^ tmp // 計算當前 b 的最終值 9 10 tmp = a & b // 得到 a 和 b 均為 1 的位 11 a = a - tmp // 消除出現三次的位 12 b = b - tmp // 消除出現三次的位 13 } 14 15 return a 16 }
用心寫代碼,Refuse copy on coding,Refuse coding by butt.
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/137725.html
標籤:其他
上一篇:獲取陣列中所有重復的元素
下一篇:【題解】游戲
