近期刷題有一個新的體會,那就是不要想著 pass 就完事了,得想辦法將自己的執行時間提速
以 leetcode 565 為例,這題大意如下:一個長度為 n 的整形陣列 A ,每個數的範圍都在 0 到 n-1 以內,假設從陣列的某個索引 i 開始,依次執行 A[i] 求值操作,將得到的數加入到集合 S 中,直到集合 S 出現重複元素為止,即中止運算。例如陣列 [5,4,0,3,1,6,2] ,我們從 0 開始,依次執行求值操作,有:
A[0]=5 -> A[5]=6 ->
A[6]=2 -> A[2]=0
-x-> A[0]=5
複製程式碼
在上個例子中我們的 S 集合為 {5,6,2,0}。現在給定一個陣列,我們要求出這個集合的最長長度。
剛開始看這題我感覺很容易啊,直接模擬不就完事了嗎,遍歷陣列的每個值,將索引 i 作為起點並依次執行 A[i] 操作(計數當前的操作次數),當某次求值操作與起點 i 相同時則終止,最後取最大值即可,相應程式碼如下
func arrayNesting(nums []int) int {
n := len(nums)
if n <= 1 {
return 1
}
result := 0
for i := 0; i < n; i++ {
s, current, tmpl := i, nums[i], 1
for current != s {
current = nums[current]
tmpl++
}
result = maxValue(result, tmpl)
}
return result
}
func maxValue(a, b int) int {
if a > b {
return a
}
return b
}
複製程式碼
這麼寫程式碼的話雖然也通過了,但是看了下耗時還是很不友好,居然是千毫秒級別
後面仔細想想,還是拿最初的陣列 [5,4,0,3,1,6,2] 做例子,從索引 0 出發得到的集合為 [5,6,2,0] ,然而本質上如果從索引值為 2 5 或 6 出發的話得到的也是這個集合,因為它們始終湊成一個環。既然如此,那麼我們可以**設定一個布林陣列,判斷當前值如果之前已經出現在集合 S 內就不需要再重複計算,具體程式碼如下
func arrayNesting(nums []int) int {
n := len(nums)
if n <= 1 {
return 1
}
visited := []bool{}
for i := 0; i < n; i++ {
visited = append(visited, false)
}
result := 1
for i := 0; i < n; i++ {
s, current, tmpl := i, nums[i], 1
visited[s] = true
if visited[current] {
continue
}
for current != s {
current = nums[current]
visited[current] = true
tmpl++
}
result = maxValue(result, tmpl)
}
return result
}
func maxValue(a, b int) int {
if a > b {
return a
}
return b
}
複製程式碼
結果這個執行時間就比之前好多了,直接 20 毫秒
類似的操作還有leetcode 240: 二維有序陣列排序,題目大意是給定一個 m*n 的二維陣列,從左到右以及從上到下都是有序的,如下面所示:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30],
]
複製程式碼
現在就讓你在這個矩陣內快速查詢某個值,找到則返回 true ,找不到則返回 false,上述例子中搜尋 5 則為 true,搜尋 20 則為 false
首先最直觀的思維就是遍歷矩陣的每一行,判斷當前要查詢的值是否在當前行第 0 個元素和最後一個元素之間,如果是則對當前行的陣列做二叉搜尋
func searchMatrix(matrix [][]int, target int) bool {
m := len(matrix)
if m == 0 {
return false
}
n := len(matrix[0])
if n == 0 {
return false
}
result := false
for _, row := range matrix {
if target >= row[0] && target <= row[n-1] {
result = binarySearch(row, n, target)
}
if result == true {
return true
}
}
return false
}
func binarySearch(row []int, n, target int) bool {
left, right := 0, n-1
for left <= right {
m := left + (right-left)/2
if row[m] == target {
return true
} else if target < row[m] {
right = m - 1
} else {
left = m + 1
}
}
return false
}
複製程式碼
這樣的效果雖然能通過,且時間也不算慢,但在此題執行時間的排名裡只超過了 31.25% 的 golang coder,那麼就說明了 O(m*log2(n)) 並不是這個演算法的最優時間複雜度。另外理論上 m 要小於 n 才能算是比較好的演算法,所以實際上我剛才的解法也忽視了分類討論的情況:當 m < n 時按行遍歷,當 m > n 時按列遍歷
像我上面最初的想法,本質上只利用了從左到右有序這個性質,並沒有充分利用從上到下有序這個性質。從這個思維點出發,怎麼樣才能讓這兩個性質都一起用起來,從而加快速度?
還是拿上面的陣列為例,比如我想搜尋 6 ,我可以從最右上角的數字 15 出發,因為 15 在最右上角,結合矩陣的性質, 15 左邊的元素都比 15 小(對應行), 15 下邊的元素都比 15 大(對應列)
[
[1, 4, 7, 11, 15]<-
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30],
]
複製程式碼
那麼 6 比 15 小,說明 6 不可能與 15 同列,於是我們陣列的範圍變為:
[
[1, 4, 7, 11]<-
[2, 5, 8, 12]
[3, 6, 9, 16]
[10, 13, 14, 17]
[18, 21, 23, 26]
]
複製程式碼
同理 6 比 11 和 7 小,所以陣列的搜尋範圍為:
[
[1, 4, 7]<-
[2, 5, 8]
[3, 6, 9]
[10, 13, 14]
[18, 21, 23]
]
[
[1, 4]<-
[2, 5]
[3, 6]
[10, 13]
[18, 21]
]
複製程式碼
繼續看最右上角的數,這次 6 比 4 和 5 都大,說明 6 不可能跟 4 或 5 同行,所以搜尋範圍又縮小為:
[
[2, 5]<-
[3, 6]
[10, 13]
[18, 21]
]
[
[3, 6]<-
[10, 13]
[18, 21]
]
複製程式碼
現在我們找到 6 了,可以看到這個演算法的時間複雜度最壞情況也是 O(m+n) ,比剛才有了一定的提升
func searchMatrix(matrix [][]int, target int) bool {
m := len(matrix)
if m == 0 {
return false
}
n := len(matrix[0])
if n == 0 {
return false
}
rightUp, currentRow, currentColumn := -1, 0, n-1
for currentRow >= 0 && currentRow < m
&& currentColumn >= 0 && currentColumn < n {
rightUp = matrix[currentRow][currentColumn]
if rightUp == target {
return true
} else if rightUp > target {
currentColumn--
} else {
currentRow++
}
}
return false
}
複製程式碼
提交上去後,執行時間很美滿