劍指offer-Go版實現 第三章:高質量的程式碼

棋佈發表於2021-12-28

主要是圍繞高質量程式碼,完成性、規範性和魯棒性。
測試用例的編寫,通常情況,我們為了完成單元測試覆蓋率都是草草了事,學習這一章我們可以發現,我們之前的測試用例實用性很差,只有正常情況的用例,並且很多時候只有一個。之後我也跟著用例從三方面來執行:1、功能測試,2、負面測試,3、邊界測試。負面測試可以理解為錯誤測試,異常情況的測試。

面試題11:數值的整數次方

leetcode-cn.com/problems/shu-zhi-d...
按照題意,很容易寫出來基本的邏輯程式碼。

func myPow(x float64, n int) float64 {
   var result = 1.0

   if n < 0 {
       x = 1 / x
       n = -n
   }
   for i := 0; i < n; i++ {
       result *= x
   }
   return result
}

可惜,會提示超時,尷尬的很,於是乎看了一遍原文的邏輯,重新實現一遍,邏輯如下:
快速冪 演算法
演算法流程:
當 x = 0 時:直接返回 0 (避免後續 x = 1 / x操作報錯)。
初始化 res = 1
當 n < 0 時:把問題轉化至 n≥0 的範圍內,即執行 x = 1/x ,n = - n ;
迴圈計算:當 n = 0時跳出;
當 n & 1 == 1 時:將當前 x乘入 res (即 res *= x);
執行 x = x^2 (即 x *= x)
執行 n 右移一位(即 n >>= 1)。
返回 res

func myPow(x float64, n int) float64 {
    var result = 1.0

    if n < 0 {
        x = 1 / x
        n = -n
    }
    for ; n > 0; {
        if n&1 == 1 { //又學了一招位運算,等價於 n % 2 == 1
            result *= x
        }
        x *= x
        n >>= 1 // 指數減半
    }
    return result
}

面試題12: 列印1到最大的n位數

leetcode-cn.com/problems/da-yin-co...
雖然能透過leetcode,但是不符合書本上出題的目的,因為要考慮大數的情況下才行,書上給出的思路是轉換成字串來進行輸出

func printNumbers(n int) []int {
   res := make([]int,0)
   var count = 1
   for i := 1; i < n; i++ {
       count *= 10
   }

   for i := 0; i < count; i++ {
       res = append(res,i)
   }

   return res
}

這不包括大數,不是書本出題的本意,修改如下:
考察的是程式碼的完整性 返回就不能是[]int 而應該是[]string,看具體程式碼,沒辦法在leetcode驗證,只能自己列印日誌看了。

func printNumbers(n int) []string {
    if n <= 0 {
        return nil
    }
    res := make([]int, 0)
    result := make([]string, 0)
    number := make([]int, n)
    for !increment(number) {
        for i := 0; i < len(number); i++ {
            res = append(res, number[i])
        }
    }
    //陣列按n位切分成而為陣列
    temp := make([]int, 0)
    for i := 0; i < len(res); i++ {
        temp = append(temp, res[i])
        if (i+1)%n == 0 { // 3餘數是0 。說明是n的整數倍
            result = append(result, getString(temp))
            temp = make([]int, 0)
        }
    }
    return result
}

func getString(number []int) string {
    var res string
    isBegin := false
    for i := 0; i < len(number); i++ {
        if number[i] != 0 {
            isBegin = true
        }
        if isBegin {
            res += fmt.Sprintf("%d", number[i])
        }
    }
    return res
}

func increment(number []int) bool {
    var isOverFlow = false //是否溢位結束
    var nTakeOver = 0      // 進位
    var nLength = len(number)
    for i := nLength - 1; i >= 0; i-- {
        nSum := number[i] + nTakeOver
        if nLength-1 == i {
            nSum++
        }
        if nSum >= 10 {
            if i == 0 {
                isOverFlow = true
            } else {
                nSum -= 10
                nTakeOver = 1
                number[i] = nSum
            }
        } else {
            number[i] = nSum
            break
        }
    }
    return isOverFlow
}

面試題13: 在O(1)時間刪除連結串列節點

leetcode-cn.com/problems/shan-chu-...
leetcode要求比書本上的低,沒有時間複雜度的要求

// 先不考慮O(1),遍歷是最直接的
func deleteNode(head *ListNode, val int){
   if head == nil {
       return
   }
   for head.Next != nil {
       if head.Next.Val == val {
           head.Next = head.Next.Next
       }
       head = head.Next
   }
}

連結串列少不了假頭、新連結串列、雙指標等輔助,目前就是使用假頭的最佳例項,和書上的不一樣哈,課本上的刪除就完了,不用返回,力扣是要刪除後返回頭節點的

func deleteNode(head *ListNode, val int) *ListNode {
   //for head.Next != nil {
   //    if val == head.Next.Val {
   //        head = head.Next.Next//刪除節點 課本上這就結束了。
   //    }
   //}
   dummy := &ListNode{}// 生成一個新連結串列
   tail := dummy
   for head != nil {
       if head.Val != val {
           //新增到新的連結串列中
           tail.Next = head
           tail = head
       }
       head = head.Next
   }
   tail.Next = nil //設定尾部為空
   return dummy.Next
}

上面的例子中,空間複雜度是O(n),想要是1的話,只能按照課本上的引數傳遞才行,否者不可實現

func deleteNode(head *ListNode, deleteNode *ListNode) {
   if head == nil || deleteNode == nil {
       return
   }
   if deleteNode.Next != nil {//不是尾節點
       //刪除的節點下一個值,替換需要刪除的節點,然後刪除下一個節點,等同於刪除當前節點
       deleteNode.Val = deleteNode.Next.Val
       deleteNode.Next = deleteNode.Next.Next
   }else if head == deleteNode {//刪除的是頭節點,只有一個節點
       head = nil
   }else{ // 多個節點,刪除的是尾節點 第一個if條件難道不包括嗎?
       for head.Next != deleteNode {
           head = head.Next
       }
       head.Next = nil
   }
}

面試題14: 調整陣列順序使奇數位於偶數前面

leetcode-cn.com/problems/diao-zhen...
思路:兩個指標,一個指向頭,一個指向尾部,兩邊同時向中間移動,如果頭指標的值偶數,尾指標是奇數就交換
通用性:判斷條件改為函式來判斷,分離出來nums[left]&1 == 1和nums[right]&1 == 0換成函式就可以了,很簡單,自行新增一下。

func exchange(nums []int) []int {
    if len(nums) == 0 {
        return nil
    }
    var left = 0
    var right = len(nums) - 1

    for left < right {
        for left < right && nums[left]&1 == 1 { //奇數,右移left
            left++
        }
        for left < right && nums[right]&1 == 0 { //偶數,左移right
            right--
        }
        if left < right { //交換
            nums[left], nums[right] = nums[right], nums[left]
        }
    }
    return nums
}

面試題15: 連結串列中倒數第k個節點

leetcode-cn.com/problems/lian-biao...
思路:兩個指標,一個指向頭,一個先走k步,簡稱:快慢指標

func getKthFromEnd(head *ListNode, k int) *ListNode {
   if head != nil {
       return nil
   }
   var first = head
   var second = head
   for i := 0; i < k; i++ {
       second = second.Next
   }
   for second != nil {
       first = first.Next
       second = second.Next
   }
   return first
}

面試題16: 反轉連結串列

leetcode-cn.com/problems/UHnkqh/
這題,如果沒有限制返回頭節點,僅僅是輸出的話,實現方式是N多種,陣列也是可以的。但是這裡的要求是返回反轉後的頭節點,所以,需要輔助手段。

//需要把節點儲存起來,斷開之後防止找不到
func reverseList(head *ListNode) *ListNode {
   var prevHead *ListNode //儲存遍歷的前一個節點
   var currentNode = head //儲存遍歷的當前節點
   for currentNode != nil {
       nextNode := currentNode.Next
       currentNode.Next = prevHead
       prevHead = currentNode
       currentNode = nextNode
   }
   return prevHead
}

按照課本上的做法,我臨摹過來的程式碼,但是,這個程式碼我感覺有點小問題,卻又不知道問題在哪,各位朋友知道的還望指出來,並改正一下,不勝感激。

func reverseList(head *ListNode) *ListNode {
    var reversedHead *ListNode //反轉後的頭節點
    var prev *ListNode
    var node = head
    for node != nil {
        next := node.Next
        if next == nil {
            reversedHead = node
        }
        node.Next = prev
        prev = node
        node = next
    }
    return reversedHead
}

面試題17: 合併兩個排序的連結串列

leetcode-cn.com/problems/he-bing-l...
同樣利用了連結串列的假頭和新連結串列作為輔助

func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode {
    dummy := &ListNode{}
    tail := dummy
    for l1 != nil && l2 != nil {
        if l1.Val < l2.Val {
            tail.Next = l1
            l1 = l1.Next
        } else {
            tail.Next = l2
            l2 = l2.Next
        }
        tail = tail.Next
    }
    if l1 != nil {// 如果l1還有,直接拼接到後面
        tail.Next = l1
        tail = tail.Next
        l1 = l1.Next
    }

    if l2 != nil {//如果l2還有,直接拼接後面
        tail.Next = l2
        tail = tail.Next
        l2 = l2.Next
    }
    return dummy.Next
}

面試題18: 樹的子結構

leetcode-cn.com/problems/shu-de-zi...
輸入兩棵二叉樹A和B,判斷B是不是A的子結構。(約定空樹不是任意一個樹的子結構)
主要和樹相關的,就沒有簡單級別的,當然,和遞迴相關的也都是沒有簡單的。不熟悉遞迴和樹的,這題需要點時間去理解。

func isSubStructure(A *TreeNode, B *TreeNode) bool {
    var result = false
    if A != nil && B != nil {
        //先比較根節點,再比較左右子樹節點
        if A.Val == B.Val {
            //根節點滿足,判斷不是子結構
            result = isStructure(A,B) //不用遞迴,也可寫成左右子樹的判斷
        }
        //比較左子樹為根節點,開始遍歷比較
        if !result {
            result = isSubStructure(A.Left,B)
        }
        //比較右子樹為根節點,開始遍歷比較
        if !result {
            result = isSubStructure(A.Right,B)
        }
    }
    return result
}

func isStructure(a *TreeNode, b *TreeNode) bool {
    if b == nil {//說明樹 B 已匹配完成(越過葉子節點),因此返回 true
        return true
    }
    if a == nil {//說明已經越過樹 A 葉子節點,即匹配失敗,返回 false
        return false
    }
    if a.Val != b.Val {//值不同,匹配失敗,返回 false
        return false
    }
    return isStructure(a.Left,b.Left) && isStructure(a.Right,b.Right)
}
``````go
func isSubStructure(A *TreeNode, B *TreeNode) bool {
    var result = false
    if A != nil && B != nil {
        //先比較根節點,再比較左右子樹節點
        if A.Val == B.Val {
            //根節點滿足,判斷不是子結構
            result = isStructure(A,B) //不用遞迴,也可寫成左右子樹的判斷
        }
        //比較左子樹為根節點,開始遍歷比較
        if !result {
            result = isSubStructure(A.Left,B)
        }
        //比較右子樹為根節點,開始遍歷比較
        if !result {
            result = isSubStructure(A.Right,B)
        }
    }
    return result
}

func isStructure(a *TreeNode, b *TreeNode) bool {
    if b == nil {//說明樹 B 已匹配完成(越過葉子節點),因此返回 true
        return true
    }
    if a == nil {//說明已經越過樹 A 葉子節點,即匹配失敗,返回 false
        return false
    }
    if a.Val != b.Val {//值不同,匹配失敗,返回 false
        return false
    }
    return isStructure(a.Left,b.Left) && isStructure(a.Right,b.Right)
}
``````go
func isSubStructure(A *TreeNode, B *TreeNode) bool {
    var result = false
    if A != nil && B != nil {
        //先比較根節點,再比較左右子樹節點
        if A.Val == B.Val {
            //根節點滿足,判斷不是子結構
            result = isStructure(A,B) //不用遞迴,也可寫成左右子樹的判斷
        }
        //比較左子樹為根節點,開始遍歷比較
        if !result {
            result = isSubStructure(A.Left,B)
        }
        //比較右子樹為根節點,開始遍歷比較
        if !result {
            result = isSubStructure(A.Right,B)
        }
    }
    return result
}

func isStructure(a *TreeNode, b *TreeNode) bool {
    if b == nil {//說明樹 B 已匹配完成(越過葉子節點),因此返回 true
        return true
    }
    if a == nil {//說明已經越過樹 A 葉子節點,即匹配失敗,返回 false
        return false
    }
    if a.Val != b.Val {//值不同,匹配失敗,返回 false
        return false
    }
    return isStructure(a.Left,b.Left) && isStructure(a.Right,b.Right)
}

接下來的演算法都是非常難的,我也不知道多久能研究透徹,可以確定的是更新時間肯定比第二第三章要晚。

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

相關文章