劍指offer-Go版實現 第五章:優化時間和空間效率

棋佈發表於2022-01-09

雖說這章節主要的目的是優化時間和空間,但是我們都知道,這才是我們最頭痛的優化,因為各種原理和實現方式,是很難讓人理解的,也很難做到創新,即使是前人想出來的方案,我們依舊很難用程式碼實現,甚至理解都很難。上一片文章就說,這一章節是非常困難的,實際證明確實如此,這一章,是異常的艱辛。主要體會時間和空間的優化方案,大多數題都是可以通過二次迴圈做出來的,但是leetcode會提示超時,這也是一個考察的重點,防止使用雙迴圈,空間方面反倒是限制不大,只要你想得到,基本都是空間換時間的方式。

面試題29:陣列中出現次數超過一半的數字

leetcode-cn.com/problems/shu-zu-zh...
陣列中有一個數字出現的次數超過陣列長度的一半,請找出這個數字。
你可以假設陣列是非空的,並且給定的陣列總是存在多數元素。
很消耗記憶體 最好加上驗證不存在的情況,因為leetcode已經假設全部存在了,但是課本上的是沒有假設的

func majorityElement(nums []int) int {
    cacheTimes := make(map[int]int)
    for i := 0; i < len(nums); i++ {
        times := cacheTimes[nums[i]]
        times++
        cacheTimes[nums[i]] = times
        fmt.Println(times)
        if times > len(nums)/2 {
            return nums[i]
        }
    }
    return 0
}

這是一道找信心的題目。

面試題30:最小的k個數

leetcode-cn.com/problems/zui-xiao-...
輸入整數陣列 arr ,找出其中最小的 k 個數。例如,輸入4、5、1、6、2、7、3、8這8個數字,則最小的4個數字是1、2、3、4。
一個容器儲存這k個數

func getLeastNumbers(arr []int, k int) []int {
    //快排
    //sort.Ints(arr)
    //return arr[:k]
    //陣列容器
    cacheSlice := make([]int, 0) //排好序的容器
    for i := 0; i < len(arr); i++ {
        if len(cacheSlice) >= k {
            //isChange := false
            for j := k-1; j >0; j-- {
                fmt.Println("cacheSlice[j]",cacheSlice[j],arr[i])
                if cacheSlice[j] > arr[i] {
                    //isChange = true
                    cacheSlice[j] = arr[i]
                    sort.Ints(cacheSlice)//需要重新排序一次,必定超時
                    break
                }
            }
        } else {
            //fmt.Println(arr[i])
            cacheSlice = append(cacheSlice,arr[i])
            sort.Ints(cacheSlice)
        }
    }
    return cacheSlice
}

堆實現的方式

func (h IHeap) Len() int           { return len(h) }
func (h IHeap) Less(i, j int) bool { return h[i] > h[j] }
func (h IHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

func (h *IHeap) Push(x interface{}) {
    *h = append(*h, x.(int))
}

func (h *IHeap) Pop() interface{} {
    old := *h
    n := len(old)
    x := old[n-1]
    *h = old[0 : n-1]
    return x
}

// 時間和記憶體都消耗很大,重點是實現方式了,不要在意這些細節
func getLeastNumbers(arr []int, k int) []int {
    cacheHeap := &IHeap{}
    heap.Init(cacheHeap)
    for i := 0; i < len(arr); i++ {
        heap.Push(cacheHeap, arr[i])
        if cacheHeap.Len() > k {
            heap.Pop(cacheHeap)
        }
    }
    cacheSlice := make([]int, k, k)
    for i := 0; i < k; i++ {
        cacheSlice[i] = heap.Pop(cacheHeap).(int)
    }
    return cacheSlice
}

面試題31:連續子陣列的最大和

leetcode-cn.com/problems/lian-xu-z...
動態規劃

func maxSubArray(nums []int) int {
    length := len(nums)
    if length == 0 {
        return 0
    }
    dp := make([]int, length, length)
    dp[0] = nums[0]
    maxSum := dp[0]
    for i := 1; i < len(nums); i++ {
        if dp[i-1] < 0 {
            dp[i] = nums[i]
        } else {
            dp[i] = dp[i-1] + nums[i]
        }
        if maxSum < dp[i] {
            maxSum = dp[i]
        }
    }
    return maxSum
}

面試題32:1~n 整數中 1 出現的次數

leetcode-cn.com/problems/1nzheng-s...
輸入一個整數 n ,求1~n這n個整數的十進位制表示中1出現的次數。
困難級別的題

// 超時,起碼可以實現,用字串的方式
func countDigitOne(n int) int {
    count := 0
    for i := 0; i <= n; i++ {
        intStr := fmt.Sprintf("%s",i)
        for _,v := range intStr{
            if v == '1' {
                count++
            }
        }
    }
    return count
}

看官方的解答,我表示不懂,水平太差了,之所以分享出來是因為為了湊整?

func countDigitOne(n int) (ans int) {
    // mulk 表示 10^k
    // 在下面的程式碼中,可以發現 k 並沒有被直接使用到(都是使用 10^k)
    // 但為了讓程式碼看起來更加直觀,這裡保留了 k
    for k, mulk := 0, 1; n >= mulk; k++ {
        ans += (n/(mulk*10))*mulk + min(max(n%(mulk*10)-mulk+1, 0), mulk)
        mulk *= 10
    }
    return
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

面試題33:把陣列排成最小的數

leetcode-cn.com/problems/ba-shu-zu...
輸出結果可能非常大,所以你需要返回一個字串而不是整數
拼接起來的數字可能會有前導 0,最後結果不需要去掉前導 0
並非單純的比較兩個數的大小,而是兩個數拼接後的大小比較
思路:轉換成字串的陣列比較,按從小到大的順序直接拼接就是,至於為什麼是這樣,我也表示看了答案才知道這麼做的。

func minNumber(nums []int) string {
    sort.Slice(nums, func(i, j int) bool {
        x := fmt.Sprintf("%d%d", nums[i], nums[j])
        y := fmt.Sprintf("%d%d", nums[j], nums[i])
        return x < y
    })
    //fmt.Println(nums)
    res := ""
    for i := 0; i < len(nums); i++ {
        res += fmt.Sprintf("%d", nums[i])
        //fmt.Println(res)
    }
    return res
}

面試題34:醜數

leetcode-cn.com/problems/chou-shu-...
我們把只包含質因子 2、3 和 5 的數稱作醜數(Ugly Number)。求按從小到大的順序的第 n 個醜數。習慣上,我們把1當作第一個醜數
n 不超過1690

// 超時版本
func nthUglyNumber(n int) int {
    count := 0
    res := 1
    for i := 1; count < n; i++ {
        if isUglyNumber(i) {
            count++
            //fmt.Println("count-> ",count,"i->",i)
            res = i
        }
    }
    return res
}
func isUglyNumber(n int) bool {
    for n%2 == 0 {
        n /= 2
    }
    for n%3 == 0 {
        n /= 3
    }
    for n%5 == 0 {
        n /= 5
    }
    return n == 1
}

//動態規劃 空間換時間版本

func nthUglyNumber(n int) int {
    uglyNumbers := make([]int, n)
    uglyNumbers[0] = 1
    mult2, mult3, mult5 := 0,0,0
    for i := 1; i < n; i++ {
        temp2, temp3, temp5 := uglyNumbers[mult2]*2, uglyNumbers[mult3]*3, uglyNumbers[mult5]*5
        uglyNumbers[i] = min(min(temp2, temp3), temp5)
        if uglyNumbers[i] == temp2 {
            mult2++
        }
        if uglyNumbers[i] == temp3 {
            mult3++
        }
        if uglyNumbers[i] == temp5 {
            mult5++
        }
    }
    return uglyNumbers[n-1]
}
func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

面試題35:第一個只出現一次的字元

leetcode-cn.com/problems/di-yi-ge-...
在字串 s 中找出第一個只出現一次的字元。如果沒有,返回一個單空格。 s 只包含小寫字母。

func firstUniqChar(s string) byte {
    cacheByte := make(map[byte]int)
    for i := 0; i < len(s); i++ {
        cacheByte[s[i]] = cacheByte[s[i]] + 1
    }
    for i := 0; i < len(s); i++ {
        if cacheByte[s[i]] == 1 {
            return s[i]
        }
    }
    return ' '
}

面試題36:陣列中的逆序對

leetcode-cn.com/problems/shu-zu-zh...
在陣列中的兩個數字,如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。輸入一個陣列,求出這個陣列中的逆序對的總數。
毫無疑問,雙迴圈一定會超時,程式碼不貼了,沒意義

// 抄襲官網,歸併排序,我也不會,也要加強練習練習
func reversePairs(nums []int) int {
    return mergeSort(nums, 0, len(nums)-1)
}

func mergeSort(nums []int, start, end int) int {
    if start >= end {
        return 0
    }
    mid := start + (end-start)/2
    cnt := mergeSort(nums, start, mid) + mergeSort(nums, mid+1, end)
    tmp := []int{}
    i, j := start, mid+1
    for i <= mid && j <= end {
        if nums[i] <= nums[j] {
            tmp = append(tmp, nums[i])
            cnt += j - (mid + 1)
            i++
        } else {
            tmp = append(tmp, nums[j])
            j++
        }
    }
    for ; i <= mid; i++ {
        tmp = append(tmp, nums[i])
        cnt += end - (mid + 1) + 1
    }
    for ; j <= end; j++ {
        tmp = append(tmp, nums[j])
    }
    for i := start; i <= end; i++ {
        nums[i] = tmp[i-start]
    }
    return cnt
}

面試題37:兩個連結串列的第一個公共結點

leetcode-cn.com/problems/liang-ge-...
輸入兩個連結串列,找出它們的第一個公共節點。

//Definition for singly-linked list.
type ListNode struct {
    Val  int
    Next *ListNode
}

func getIntersectionNode(headA, headB *ListNode) *ListNode {
    lenA := getNodeLength(headA)
    lenB := getNodeLength(headB)
    longNode := headA
    shotNode := headB
    kipSteps := lenA - lenB
    if lenB > lenA {
        kipSteps = lenB - lenA
        longNode = headB
        shotNode = headA
    }
    //長連結串列先走kipSteps,之後和短的一起走N步共同達到相同節點
    for i := 0; i < kipSteps; i++ {
        longNode = longNode.Next
    }

    for longNode != nil {
        if longNode == shotNode {
            return longNode
        }
        longNode = longNode.Next
        shotNode = shotNode.Next
    }
    return nil
}
// 獲取連結串列長度
func getNodeLength(root *ListNode) (length int) {
    tail := root
    for tail != nil {
        length++
        tail = tail.Next
    }
    return length
}

這裡涉及到兩道困難級別,可想而知,我也有幾道題是看官方解答才知道的,象徵性的貼了下程式碼,我自己也要多練習了,有時間還要review一下這些題,都是經典且難的題,就算不為面試,實際業務上也有用得著的地方。演算法之所以難,是因為我們想不到實現方案,業務簡單是因為有人把業務的實現方式一步一步怎麼操作的詳細的告訴我們了。都是動腦吧,多練習,不會錯。後面的兩章是道題,我估計起碼要兩週,春節前能吃透已經很不錯了。好在年底了,工作不會太忙,空閒時間會比較多。有人說,看不下去,沒心情也沒動力。只能說,當作面試跳槽的八股文,那肯定沒心情也沒精力,當作興趣愛好吧,權當大學老師給你多佈置了題目,總之找個能堅持的理由吧,畢竟腦袋不用就會生鏽的。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
歡迎轉載分享提意見,一起code

相關文章