劍指offer-Go版實現 第四章:解決面試題的思路

棋佈發表於2022-01-03
 看題目就應該知道是演算法題的思路,這裡不理解為完全為了面試,這樣會很累的,權當做技術的追求吧,我們除了增刪改查,複製貼上,寫服務,封裝程式碼,總要找點有其他動腦子的事情做吧,雖說全域性觀很重要,架構很重要,但是我們總要給自己找個學習的理由吧?
回到正題,我們寫需求之前都會寫個概要設計文件(當然現在很多人已經沒有這習慣,公司也沒有這要求了,但是不妨礙我們的習慣)。演算法也是一樣,知道遠離和實現方式,剩下寫程式碼似乎也並不是很輕鬆的一件事,因為要涵蓋很多種情況,有時候僅僅是一個條件判斷條件寫錯了,就會讓你找好久。但是一切的前提就是,你得知道怎麼實現,通用情況下怎麼實現的,然後才能完善其他的測試用例。
本章節主要介紹的是兩方面:1、抽象問題形象化 2、複雜問題簡單化。理論永遠是簡單的,實踐才是難題。這一張我看的本身就很累,前面已經說過了,時間會比較久。後面三章時間會更久,因為目前為止還沒有涉及到困難級別的題,默哀吧,騷年。

面試題19:二叉樹的映象

leetcode-cn.com/problems/er-cha-sh...
請完成一個函式,輸入一個二叉樹,該函式輸出它的映象。
顯然,優先使用前序遍歷,首先要學會前序遍歷一棵樹

func mirrorTree(root *TreeNode) *TreeNode {
    if root == nil {
        return nil
    }
    root.Right, root.Left = root.Left, root.Right
    if root.Left != nil {
        mirrorTree(root.Left)
    }
    if root.Right != nil {
        mirrorTree(root.Right)
    }
    return root
}

這題明顯就是讓我們找快感的題左右互換而已。

面試題20:順時針列印矩陣

leetcode-cn.com/problems/shun-shi-...

func spiralOrder(matrix [][]int) []int {
    result := make([]int, 0)
    if matrix == nil || len(matrix) == 0 {
        return nil
    }
    var row = len(matrix)
    var col = len(matrix[0])
    if row <= 0 || col <= 0 { //一行或者一列,直接返回遍歷之後的一維陣列
        for i := 0; i < row; i++ {
            for j := 0; j < col; j++ {
                result = append(result, matrix[i][j])
            }
        }
        return result
    }
    startIndex := 0 //起始新增的位置
    for col > startIndex*2 && row > startIndex*2 {
        endX := col - startIndex - 1
        endY := row - startIndex - 1

        //從左到右新增
        for i := startIndex; i <= endX; i++ {
            result = append(result, matrix[startIndex][i])
        }
        //從上到下新增
        if startIndex < endY {
            for i := startIndex + 1; i <= endY; i++ {
                result = append(result, matrix[i][endX])
            }
        }
        //從右到左新增
        if startIndex < endX && startIndex < endY {
            for i := endX - 1; i >= startIndex; i-- {
                result = append(result, matrix[endY][i])
            }
        }
        //從下到上新增
        if startIndex < endX && startIndex < endY-1 {
            for i := endY - 1; i >= startIndex+1; i-- {
                result = append(result, matrix[i][startIndex])
            }
        }
        startIndex++
    }

    return result
}

不要小瞧這道題,看原理我們都知道怎麼實現,但是寫程式碼的難度比理解遠離更困難,重點是,力扣認為這是一道簡單題。用網友的話說,這怕不是對簡單兩個字有什麼誤解。。。

面試題21:包含min函式的棧

leetcode-cn.com/problems/bao-han-m...
定義棧的資料結構,請在該型別中實現一個能夠得到棧的最小元素的 min 函式在該棧中,呼叫 min、push 及 pop 的時間複雜度都是 O(1)。
這是一道設計題,考察的完全是棧和輔助棧的用法,我覺得沒必要去看,本著面試寫程式碼的原則,寫了本地也沒法測試,只能交給leetcode。

面試題22:棧的壓入、彈出序列

leetcode-cn.com/problems/zhan-de-y...
輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否為該棧的彈出順序。
假設壓入棧的所有數字均不相等。例如,序列 {1,2,3,4,5} 是某棧的壓棧序列,序列 {4,5,3,2,1} 是該壓棧序列對應的一個彈出序列,但 {4,3,5,1,2} 就不可能是該壓棧序列的彈出序列。

func validateStackSequences(pushed []int, popped []int) bool {
    //1、陣列模擬棧
    //indexPopped := 0
    //tempPushed := make([]int, 0)
    //for i := 0; i < len(pushed); i++ {
    //    tempPushed = append(tempPushed, pushed[i])
    //    for len(tempPushed)>0 && tempPushed[len(tempPushed)-1] == popped[indexPopped] { //比較棧頂值是否等於popped的當前值
    //        tempPushed = tempPushed[:len(tempPushed)-1]
    //        indexPopped++
    //    }
    //}
    //if len(tempPushed) == 0 {
    //    return true
    //}
    //return false
    //2、棧實現
    indexPopped := 0
    stack := NewStack()
    for i := 0; i < len(pushed); i++ {
        stack.Push(pushed[i])
        for !stack.IsEmpty() && stack.Peek() == popped[indexPopped] { //比較棧頂值是否等於popped的當前值
            stack.Pop()
            indexPopped++
        }
    }
    return stack.IsEmpty()
}

面試題23:從上到下列印二叉樹

leetcode-cn.com/problems/cong-shan...
從上到下列印出二叉樹的每個節點,同一層的節點按照從左到右的順序列印。

func levelOrder(root *TreeNode) []int {
    // 陣列模擬佇列
    result := make([]int, 0)
    if root == nil {
        return result
    }
    //層次遍歷,想明白還是很簡單的
    level := make([]*TreeNode, 0) //記錄每一層的元素,方便尋找每一層的子節點
    level = append(level, root)   //首先新增第一層
    for len(level) > 0 { //一層一個資料都沒有跳出迴圈
        for i := 0; i < len(level); i++ { //遍歷當前層,新增子節點層
            result = append(result, level[i].Val)
        }
        temp := level
        for i := 0; i < len(temp); i++ {//模擬雙端佇列,pushFront資料
            if temp[i].Left != nil {
                level = append(level, temp[i].Left)
            }
            if temp[i].Right != nil {
                level = append(level, temp[i].Right)
            }
        }
        level = level[len(temp):]//popBack資料
    }
    return result
}

請實現一個函式按照之字形順序列印二叉樹,即第一行按照從左到右的順序列印,第二層按照從右到左的順序列印,第三行再按照從左到右的順序列印,其他行以此類推。
升級後只需要區分層級的奇偶就行,邏輯類似

func levelOrder(root *TreeNode) [][]int {
    // 陣列模擬佇列
    result := make([][]int, 0)
    if root == nil {
        return result
    }
    //層次遍歷,想明白還是很簡單的
    level := make([]*TreeNode, 0) //記錄每一層的元素,方便尋找每一層的子節點
    level = append(level, root)   //首先新增第一層
    high := 0
    for len(level) > 0 { //一層一個資料都沒有跳出迴圈
        data := make([]int, 0)
        for i := 0; i < len(level); i++ { //遍歷第一層,新增子節點層
            data = append(data, level[i].Val)
        }
        temp := level
        high++
        for i := 0; i < len(temp); i++ {
            if temp[i].Left != nil {
                level = append(level, temp[i].Left)
            }
            if temp[i].Right != nil {
                level = append(level, temp[i].Right)
            }
        }
        level = level[len(data):]
        if high%2 == 0 {
            for i := 0; i < len(data)/2; i++ {
                data[i], data[len(data)-1-i] = data[len(data)-1-i], data[i]
            }
        }
        result = append(result, data)
    }
    return result
}

面試題24: 二叉搜尋樹的後序遍歷序列

leetcode-cn.com/problems/er-cha-so...

func verifyPostorder(postorder []int) bool {
    return verifyPostorderCheck(postorder, 0, len(postorder)-1)
}

func verifyPostorderCheck(postorder []int, left, right int) bool {
    if left >= right {
        return true
    }
    leftIndex := left
    for postorder[leftIndex] < postorder[right] {
        leftIndex++
    }
    rightIndex := leftIndex
    for i := rightIndex; i < right; i++ {
        if postorder[i] < postorder[right] {
            return false
        }
    }
    //判斷左子樹是不是二叉搜尋樹
    leftResult := verifyPostorderCheck(postorder, left, rightIndex-1)
    //判斷右子樹是不是二叉搜尋樹
    rightResult := verifyPostorderCheck(postorder, rightIndex, right-1)
    return leftResult && rightResult
}

面試題25: 二叉樹中和為某一值的路徑

leetcode-cn.com/problems/er-cha-sh...
典型的回溯 遞迴大法

func pathSum(root *TreeNode, target int) (result [][]int) {
    cachePath := make([]int, 0)
    var pathSumDfs func(root *TreeNode, sum int)
    pathSumDfs = func(root *TreeNode, sum int) {
        if root == nil {
            return
        }
        sum -= root.Val
        cachePath = append(cachePath, root.Val)
        defer func() { cachePath = cachePath[:len(cachePath)-1] }()
        //判斷是不是葉子節點,並且路徑值等於目標值
        if sum == 0 && root.Left == nil && root.Right == nil {
            //result = append(result, cachePath)
            //需要複製出一個副本,否則就是指標傳遞了
            result = append(result, append([]int(nil), cachePath...))
            return
        }
        //不是就繼續遞迴子節點
        pathSumDfs(root.Left, sum)
        pathSumDfs(root.Right, sum)
    }
    pathSumDfs(root, target)
    return
}

面試題26:複雜連結串列的複製

leetcode-cn.com/problems/er-cha-sh...
請實現 copyRandomList 函式,複製一個複雜連結串列。在複雜連結串列中,每個節點除了有一個 next 指標指向下一個節點,
還有一個 random 指標指向連結串列中的任意節點或者 null。
random 指標:當前節點的隨機指標指向的節點可能還沒建立

// 直接兩次迴圈遍歷 超出時間限制
func copyRandomList(head *Node) *Node {
    dummy := &Node{}
    tail := dummy
    copyNode := head
    nextNode := head
    for copyNode != nil {
        for nextNode != nil {
            tail.Next = copyNode
            if copyNode.Random == nextNode {
                tail.Random = nextNode
                break
            }
            nextNode = nextNode.Next
        }
    }
    return dummy.Next
}
//回溯+雜湊
func copyRandomList(head *Node) *Node {
    var clone func(node *Node) *Node
    cacheNodes := make(map[*Node]*Node)
    clone = func(node *Node) *Node {
        if node == nil {
            return nil
        }
        if n, has := cacheNodes[node]; has {
            return n
        }
        cloneNode := &Node{Val: node.Val}
        cacheNodes[node] = cloneNode
        cloneNode.Next = clone(node.Next)
        cloneNode.Random = clone(node.Random)
        return cloneNode
    }
    return clone(head)
}

分治 迭代 + 節點拆分

func copyRandomList(head *Node) *Node {
    //1、複製自身節點
    cloneNodes := func(head *Node) {
        cloneHead := head
        for cloneHead != nil {
            newNode := &Node{Val: cloneHead.Val, Next: cloneHead.Next}
            cloneHead.Next = newNode
            cloneHead = newNode.Next
        }
    }
    //2、複製隨機節點
    cloneRandom := func(head *Node) {
        cloneRandomHead := head
        for cloneRandomHead != nil {
            randomNode := cloneRandomHead.Next
            if cloneRandomHead.Random != nil {
                randomNode.Random = cloneRandomHead.Random.Next
            }
            cloneRandomHead = randomNode.Next
        }
    }
    //3、奇偶拆分連結串列
    splitNode := func(head *Node) *Node {

        return nil
    }
    cloneNodes(head)
    cloneRandom(head)
    return splitNode(head)
}

面試題27: 二叉搜尋樹與雙向連結串列

leetcode-cn.com/problems/er-cha-so...
leetcode沒有提供Go版本的語言實現這道題,所以只能是仿照編譯,本地執行,其他語言自行實現
本題我也沒搞懂,看課本答案才知道怎麼實現的,慚愧,很多寫法不是人家給出Demo我都不知道還可以這麼寫

type Node struct {
    Val   int
    left  *Node
    right *Node
}

func treeToDoublyList(root *Node) *Node {
    var lastNodeInList *Node //已經轉換好的連結串列的最後一個結點
    convertNode(root, &lastNodeInList)
    headOfList := lastNodeInList //需要返回的頭節點
    for headOfList != nil && headOfList.left != nil {
        headOfList = headOfList.left
    }
    return headOfList
}

func convertNode(root *Node, lastNodeInList **Node) {
    if root == nil {
        return
    }
    currentNode := root
    if currentNode.left != nil {
        convertNode(currentNode.left, lastNodeInList)
    }
    currentNode.left = *lastNodeInList
    if *lastNodeInList != nil {
        (*lastNodeInList).right = currentNode
    }
    *lastNodeInList = currentNode
    if currentNode.right != nil {
        convertNode(currentNode.right, lastNodeInList)
    }
}

面試題28:字串的排列

leetcode-cn.com/problems/zi-fu-chu...

func permutation(s string) []string {
    result := make([]string, 0)
    byteStr := []byte(s)
    //fmt.Println(byteStr)
    var getStrDfs func(index int)
    getStrDfs = func(index int) {
        if index == len(byteStr)-1 { //交換到最後一個字元,退出迴圈
            //fmt.Println("the end byte", byteStr)
            result = append(result, string(byteStr))
        }
        mapIsChange := make(map[byte]byte) //利用集合做剪枝,去重處理
        for i := index; i < len(byteStr); i++ {
            if _, ok := mapIsChange[byteStr[i]]; ok {
                continue //已經交換過,不再交換
            }
            mapIsChange[byteStr[i]] = byteStr[i]
            byteStr[i], byteStr[index] = byteStr[index], byteStr[i]//交換
            getStrDfs(index + 1)
            byteStr[i], byteStr[index] = byteStr[index], byteStr[i]//恢復交換
        }
    }

    getStrDfs(0)
    return result
}

總體來說,樹的考察比較多,對於樹本身就不是常用的搬磚農民工來說,就是個劣勢。雪上加霜的是,跟樹相關的,必定是遞迴。我們平時寫程式碼儘量規避遞迴的程式碼規範來說,這又是一個挑戰。總而言之,這一章確實不容易,建議知道原理的情況下寫程式碼,然後能白板寫出來才算真的懂了,否則就是浪費時間,因為不會寫程式碼實現的題,你其實還是不會。

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

相關文章