本篇文章仅记录在平时刷题过程当中,让人眼前一亮的处理思路,因此本篇文章适合算法爱好者阅读及参考,没有算法功底的程序猿们,建议不用花费太多的时间在本篇文章node
1,题目描述:给定一个字符串数组,请根据“相同字符集”进行分组(摘自 LeetCode 49)面试
例 :Input: ["eat", "tea", "tan", "ate", "nat", "bat"],算法
Output:[数组
["ate","eat","tea"],
["nat","tan"],
["bat"]
]app
基础分析:这类问题的常见处理并不难,只须要将每一个字符记录对应值,内部循环比较,外部循环子串数组便可,时间复杂度 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-一、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 }
用心写代码,Refuse copy on coding,Refuse coding by butt.