算法笔记
本文我将分享一些常见算法题的模板,包括二分的几种不同区间写法、滑动窗口的模板、dfs bfs模板、优先队列、完全背包 01背包 多重背包模板等。 以及一些常见基础算法实现的代码手撕,如快排及其变种、归并排序,堆排序等。还总结了算法题中一些常见的函数,如字符串函数、sort包的函数和接口等。仅供学习与参考。
常见题模板 一、二分查找
二分查找是非常经典的减少查找复杂度的算法,其核心思想是“丢弃”,即在数据有序的前提下,快速舍弃掉不符合的区间,并对余下区间进行判断,一般复杂度为O(logn)。实现二分查找一般有三种写法:闭区间、开区间、半开半闭。不同点往往在于左右端点何时退出循环,返回的值是与哪个端点有关还是与mid有关。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 func binarySearch (nums []int ,target int ) int { ans := -1 l,r := 0 ,len (nums)-1 for l<=r{ mid := l + (r-l)/2 if nums[mid] == target{ ans = mid break } if nums[mid] < target{ l = mid + 1 }else { r = mid - 1 } } return ans } func searchInsert (nums []int , target int ) int { l,r := 0 ,len (nums)-1 for l<=r{ mid := l + (r-l)/2 if nums[mid] < target{ l = mid + 1 }else { r = mid - 1 } } return left }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 func binarySearch (nums []int ,target int ) int { ans := -1 l,r := -1 ,len (nums) for l + 1 < r{ mid := l + (r-l)/2 if nums[mid] == target{ ans = mid break } if nums[mid] < target{ l = mid }else { r = mid } } return ans } func searchInsert (nums []int , target int ) int { l,r := -1 ,len (nums) for l + 1 < r{ mid := l + (r-l)/2 if nums[mid] < target{ l = mid }else { r = mid } } return right }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 func binarySearch (nums []int ,target int ) int { ans := -1 l,r := -1 ,len (nums)-1 for l < r{ mid := l + (r-l)/2 if nums[mid] == target{ ans = mid break } if nums[mid] < target{ l = mid }else { r = mid - 1 } } return ans } func binarySearch (nums []int ,target int ) int { ans := -1 l,r := 0 ,len (nums) for l < r{ mid := l + (r-l)/2 if nums[mid] == target{ ans = mid break } if nums[mid] < target{ l = mid + 1 }else { r = mid } } return ans } func searchInsert (nums []int , target int ) int { l,r := 0 ,len (nums) for l < r{ mid := l + (r-l)/2 if nums[mid] < target{ l = mid + 1 }else { r = mid } } return left }
二、滑动窗口模板
滑动窗口其实也有几种区间写法,但无非就是什么时候读入第一个元素的问题,这里以力扣76. 最小覆盖子串 为例给出一个模板式的写法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 func minWindow (s string , t string ) string { m := make (map [rune ]int , 0 ) for _, v := range t { m[v]++ } window := make (map [rune ]int , 0 ) checkTimes := func () bool { for i, v := range m { if window[i] < v { return false } } return true } minLength := math.MaxInt ans := [2 ]int {} l, r := 0 , 0 for r < len (s) { window[rune (s[r])]++ r++ for l < r && window[rune (s[l])] > m[rune (s[l])] { window[rune (s[l])]-- l++ } if checkTimes() && r-l < minLength { ans = [2 ]int {l, r} minLength = r - l } } return s[ans[0 ]:ans[1 ]] }
三、两种搜索dfs与bfs
dfs和bfs其实能够解决非常多的问题,其变式也不少,这里只分享几种简单的写法,后续可能会结合例题再总结一些变式写法。总的来说,dfs一般适用于暴力搜索、记录层级状态并回溯、模拟路径遍历、求最大深度、自顶向下的dp等问题,而bfs则适用于图的连通性、最短路径问题、层次遍历、避免dfs栈溢出而使用bfs等场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func subsets (nums []int ) ans [][]int { ans := make ([][]int ,0 ) n:=len (nums) var dfs func ([]int ,int ) dfs = func (out []int ,index int ) { if index==n{ temp := make ([]int ,len (out)) copy (temp,out) ans = append (ans,temp) return } dfs(out,index+1 ) out = append (out,nums[index]) dfs(out,index+1 ) } dfs([]int {},0 ) return ans }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 func levelOrder (root *TreeNode) [][]int { if root == nil { return nil } result := [][]int {} q := make ([]*TreeNode,0 ) q = append (q,root) for Len(q) > 0 { sz := Len(q) currentLevel := []int {} for i := 0 ; i < sz; i++ { node := q[i] currentLevel = append (currentLevel, node.Val) if node.Left != nil { q = append (q,node.Left) } if node.Right != nil { q = append (q,node.Right) } } q = q[sz:] result = append (result, currentLevel) } return result }
四、优先队列
go中优先队列一般借助sort包中提供的接口来实现,其底层就是堆排序,可以自定义优先级。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 type People struct { Name string Age int } type PeopleQueue []Peoplefunc (p PeopleQueue) Len() int { return len (p) } func (p PeopleQueue) Less(i, j int ) bool { return p[i].Age < p[j].Age } func (p PeopleQueue) Swap(i, j int ) { p[i], p[j] = p[j], p[i] } func (p *PeopleQueue) Push(x interface {}) { *p = append (*p, x.(People)) } func (p *PeopleQueue) Pop() interface {} { n := len (*p) old := *p x := old[n-1 ] *p = old[0 : n-1 ] return x } func TestPriorityQueue (t *testing.T) { queue := PeopleQueue{ {"Alice" , 25 }, {"Bob" , 30 }, {"Charlie" , 20 }, {"David" , 35 }, {"Eve" , 28 }, } heap.Init(&queue) for queue.Len() > 0 { fmt.Println(heap.Pop(&queue)) } }
五、动态规划(背包问题)
动态规划作为变式极多,上限也极高的算法,这里无法作过多解析,只给出背包问题中典型的三个变式,从最开始的完全背包,到01背包,再到多重背包及其二进制优化。暂时并不涉及多重背包的单调队列优化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func completeKnapsack (V int ,values []int ,weights []int ) int { dp := make ([]int ,V+1 ) for i:=1 ;i<=V;i++{ for j,w := range weights{ if i-w>=0 { dp[i] = max(dp[i],dp[i-w]+values[j]) } } } return dp[V] } func completeKnapsack (V int ,values []int ,weights []int ) int { dp := make ([]int ,V+1 ) for i:=0 ;i<n;i++{ for j:= weight[i];j<=V;j++{ dp[j] = max(dp[j],dp[j-weight[i]]+value[i]) } } return dp[V] }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 func zeroOneKnapsack (weights []int , values []int , V int ) int { n:= len (weight) dp := make ([][]int ,n+1 ) for i,_:=range dp{ dp[i] = make ([]int ,V+1 ) } for i:=1 ;i<=n;i++{ for j:=1 ;j<=V;j++{ if j < weight[i]{ dp[i][j] = dp[i-1 ][j] }else { dp[i][j] = max(dp[i-1 ][j],dp[i-1 ][j-weights[i-1 ]]+values[i-1 ]) } } } return dp[n][V] } func zeroOneKnapsack (weights []int , values []int , V int ) int { n := len (weights) dp := make ([]int , V+1 ) for i := 0 ; i < n; i++ { for j := V; j >= weights[i]; j-- { dp[j] = max(dp[j], dp[j-weights[i]]+values[i]) } } return dp[V] }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 func multipleKnapsack (weight []int , value []int , count []int , capacity int ) int { n := len (weight) dp := make ([]int , capacity+1 ) for i := 0 ; i < n; i++ { for j := capacity; j >= weight[i]; j-- { for k := 1 ; k <= count[i] && k*weight[i] <= j; k++ { dp[j] = max(dp[j], dp[j-k*weight[i]]+k*value[i]) } } } return dp[capacity] } func multipleKnapsackOptimized (weight []int , value []int , count []int , capacity int ) int { var newWeights, newValues []int for i := 0 ; i < len (weight); i++ { c := count[i] k := 1 for c > k { newWeights = append (newWeights, weight[i]*k) newValues = append (newValues, value[i]*k) c -= k k *= 2 } newWeights = append (newWeights, weight[i]*c) newValues = append (newValues, value[i]*c) } return zeroOneKnapsack(newWeights, newValues, capacity) }
由于篇幅原因,常见排序的手撕和常用函数将放在下一篇文章中。