算法导论之眼前一亮(持续更新)

本篇文章仅记录在平时刷题过程中,让人眼前一亮的处理思路,所以本篇文章适合算法爱好者阅读及参考,没有算法功底的程序猿们,建议不用花费太多的时间在本篇文章

 

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.

posted @ 2019-10-25 12:09  不想写代码的DBA  阅读(344)  评论(0编辑  收藏  举报