JS資料結構與演算法 - 劍指offer二叉樹演算法題彙總

發表於2020-04-18

❗❗ 必看經驗

在博主刷題期間,基本上是碰到一道二叉樹就不會碰到一道就不會,有時候一個下午都在搞一道題,看別人解題思路就算能看懂,自己寫就呵呵了。一氣之下不刷了,改而先去把二叉樹的基礎演算法給搞搞懂,然後又去把劍指offer裡所有關於二叉樹的題目挑了出來做,越不會就越把自己往奔潰的邊緣虐。還別說,這一搞,神清氣爽。也怪之前什麼基礎準備都沒有就直接去題庫裡挑戰題目了。

在這裡想說的是,在刷題之前一定得先有自己的知識儲備,比如說最起初的資料結構總得會吧,或者說基礎的資料結構裡都有些啥啥時重點之類的。別像我一樣什麼都不準備的上來就是刷題,越刷越懷疑人生,每題都是打擊。拿二叉樹的遍歷來說,你連個遍歷裡的遞迴結果怎麼的出來的都不知道,就算這個演算法背下來了也還是不懂,而且就三行程式碼,你好意思只背不理解嗎。在我刷題過程中,很多題都是萬變不離其宗重點就是遍歷的那三行程式碼。

所以,二叉樹起步第一步,先把基礎演算法在紙上圖圖畫畫吧,一件事半功倍的事。是遞迴的就從結束條件哪裡一步一步往回退,不用遞迴的就去了解二叉樹與進棧入棧的關係。傳送門 - 二叉樹的基礎演算法

目錄

傳送門 - 牛客網劍指offer題庫

二叉樹結構:

function TreeNode(x) {
    this.val = x;
    this.left = null;
    this.right = null;
}

題4:重建二叉樹

難度:♡♡

前中序

//pre:[1, 2, 4, 7, 3, 5, 6, 8]
//vin: [4, 7, 2, 1, 5, 3, 8, 6]
function reConstructBinaryTree(pre, vin) {
    let tree = null
    if (pre.length > 1) {
        const root = pre.shift() //從前序遍歷頭中取出一個的父節點
        const index = vin.indexOf(root)  //父節點位於中序遍歷中的位置
        tree = new TreeNode(root)
        tree.left = reConstructBinaryTree(pre.slice(0, index), vin.slice(0, index)) //遞迴父節點左邊的節點
        tree.right = reConstructBinaryTree(pre.slice(index), vin.slice(index + 1))  //遞迴父節點右邊的節點
    } else if (pre.length === 1) {
        tree = new TreeNode(pre[0])
    }
    return tree
}

後中序

//post:[7, 4, 2, 5, 8, 6, 3, 1]
//vin: [4, 7, 2, 1, 5, 3, 8, 6]
function reConstructBinaryTree(post, vin) {
    let tree = null
    if (post.length > 1) {
        const root = post.pop()   //從後序遍歷尾中取出一個的父節點
        const index = vin.indexOf(root)  //父節點位於中序遍歷中的位置
        tree = new TreeNode(root)
        tree.left = reConstructBinaryTree(post.slice(0, index), vin.slice(0, index)) //遞迴父節點左邊的節點
        tree.right = reConstructBinaryTree(post.slice(index), vin.slice(index + 1))  //遞迴父節點右邊的節點
    } else if (post.length == 1) {
        tree = new TreeNode(post[0])
    }
    return tree
}

題17:樹的子結構

難度:♡♡♡♡

子結構

題目:輸入兩棵二叉樹A,B,判斷B是不是A的子結構。(ps:我們約定空樹不是任意一個樹的子結構)

思路:DoesTreeHaveTree 函式有點像先序遍歷中的遞迴,得到父節點值比較,如果相等就再分別比較它們的左節點和右節點值是否相等

function HasSubtree(pRoot1, pRoot2) {
    let result = false
    if (pRoot1 != null && pRoot2 != null) {
        if (pRoot1.val == pRoot2.val) { //判斷父節點
            result = DoesTreeHaveTree(pRoot1, pRoot2)
        }
        if (!result) {//父節點不滿足,看看它左節點是否滿足
            result = HasSubtree(pRoot1.left, pRoot2)
        }
        if (!result) {//左節點不滿足,從其右節點是否滿足
            result = HasSubtree(pRoot1.right, pRoot2)
        }
    }
    return result
}
function DoesTreeHaveTree(pRoot1, pRoot2) {
    if (pRoot2 == null) { //root2比到底了,則一定是子結構
        return true
    }
    if (pRoot1 == null) { //root2還沒比完,root1就到底了,則一定不是子結構
        return false
    }
    if (pRoot1.val != pRoot2.val) { //節點值不相等
        return false
    }
    //節點值相等,繼續比較它們的左右節點值是否相等
    return DoesTreeHaveTree(pRoot1.left, pRoot2.left) && DoesTreeHaveTree(pRoot1.right, pRoot2.right) 
}

舉一反三 子樹

原題:力扣572.另一個樹的子樹

function HasSubtree(pRoot1, pRoot2) {
    let result = false
    if (pRoot1 != null && pRoot2 != null) {
        if (pRoot1.val == pRoot2.val) { //判斷父節點
            result = DoesTreeHaveTree(pRoot1, pRoot2)
        }
        if (!result) {
            result = HasSubtree(pRoot1.left, pRoot2)
        }
        if (!result) {
            result = HasSubtree(pRoot1.right, pRoot2)
        }
    }
    return result
}

function DoesTreeHaveTree(pRoot1, pRoot2) {
    //同時到達底部null,才是子樹
    if (!pRoot2 && !pRoot1) {
        return true
    }
    //此時已經排除了兩者都為null的情況,只要有一個為null則不是
    if (!pRoot2 || !pRoot1) {
        return false
    }
    //沒到達底部的時候,沒有一個為null
    if (pRoot1.val != pRoot2.val) {
        return false
    }
    //節點值相等,繼續比較它們的左右節點值是否相等
    return DoesTreeHaveTree(pRoot1.left, pRoot2.left) && DoesTreeHaveTree(pRoot1.right, pRoot2.right)
}

題18:二叉樹的映象

難度:♡♡

思路:中序遍歷,每次都交換下本輪節點的左右節點

function Mirror(root) {
    if (root === null) {
        return
    }
    const temp = root.left
    root.left = root.right
    root.right = temp
    Mirror(root.left)
    Mirror(root.right)
}

題22:從上往下列印二叉樹

難度:♡♡♡♡♡

思路:即二叉樹的層次遍歷(廣度優先遍歷,利用佇列即可)

function PrintFromTopToBottom(root) {
    // write code here
    let tempTree = []
    let rs = []
    if (root) tempTree.push(root)
    while (tempTree.length) {
        root = tempTree.shift()
        rs.push(root.val)
        if (root.left) tempTree.push(root.left)
        if (root.right) tempTree.push(root.right)
    }
    return rs
}

題23:二叉搜尋樹的後序遍歷序列

難度:♡♡♡♡

題目:輸入一個整數陣列,判斷該陣列是不是某二叉搜尋樹的後序遍歷的結果。如果是則輸出Yes,否則輸出No。假設輸入的陣列的任意兩個數字都互不相同。

思路:找規律。後序遍歷最後一個是根節點,陣列中可以分為比根節點值小的部分,與比根節點大的部分。然後遞迴。例:(3 6 5) (9) 7
重要的是遞迴的結束條件sequence.length <= 1,一開始以為只要等於1就可以了,忽略了陣列左邊或者右邊部分為空的情況,比如[6, 5, 9, 7]遞迴到[6,5]時,左邊為[],右邊為[6]

//sequence:[3, 6, 5, 9, 7]
//sequence:[6, 5, 9, 7]
//sequence:[3, 6, 4, 5, 9, 7]
function VerifySquenceOfBST(sequence) {
    if (sequence.length) {
        return helpVerify(sequence)
    }
    return false
}

function helpVerify(sequence) {
    if (sequence.length <= 1) {//此條件下,遞迴結束。
        return true
    }
    let index = 0
    const key = sequence[sequence.length - 1]  //後序遍歷最後一個是根節點
    while (sequence[index] < key) {   //在陣列中查詢比根節點小和比根節點大的分界點
        index++
    }
    const pos = index   //記錄分界點,此時分界點左邊全是小於根節點值的
    while (sequence[index] > key) {   //判斷根節點右邊是否全部大於根節點值
        index++
    }
    if (index != (sequence.length - 1)) {  //接while
        return false
    }
    //現在有左右兩個部分,遞迴執行
    return helpVerify(sequence.slice(0, pos)) && helpVerify(sequence.slice(pos, sequence.length - 1))
}

題24:二叉樹中和為某一值的路徑

難度:♡♡♡♡

題目:輸入一顆二叉樹的根節點和一個整數,列印出二叉樹中結點值的和為輸入整數的所有路徑。路徑定義為從樹的根結點開始往下一直到葉結點所經過的結點形成一條路徑。(注意: 在返回值的list中,陣列長度大的陣列靠前)

思路:萬變不離其宗——中序遍歷

function FindPath(root, expectNumber) {
    // write code here
    let result = []   //存放所有滿足條件的路徑
    if (root) {
        let path = []    //記錄當前路徑,噹噹前路勁滿足條件的時候,push進result,
        let currentSum = 0   //記錄當前路徑的和
        isPath(root, expectNumber, path, result, currentSum)
    }
    return result
}

function isPath(root, expectNumber, path, result, currentSum) {
    currentSum += root.val
    path.push(root.val)

    if (currentSum == expectNumber && !root.left && !root.right) { //根結點開始往下一直到葉結點,當前sum等於目標數
        result.push(path.slice(0))  //注意:這裡不能直接push(path),陣列是引用型別。也可ES6用法:push([...path])
    }

    if (root.left) { //當前root有左節點
        isPath(root.left, expectNumber, path, result, currentSum)
    }

    if (root.right) { //當前root有右節點
        isPath(root.right, expectNumber, path, result, currentSum)
    }

    // 走到底(葉子)了,無論當前路徑滿不滿足條件,都要回退到父節點繼續搜尋
    path.pop()
}

舉一反三

如果不是從樹的根結點開始往下一直到葉結點,而是任意路徑呢?

參考子樹與子結構

題26:二叉搜尋樹與雙向連結串列

難度:♡♡♡

思路:重點就是用指標p記錄上一個的節點。畫個圖就很好理解了。還是以中序遍歷為順序

function Convert(pRootOfTree) {
    if (!pRootOfTree) return null
    let p = null //指標,記錄前一個結點
    p = ConvertSub(pRootOfTree, p)
    let re = p
    while (re.left) {
        re = re.left
    }
    return re
}

function ConvertSub(pNode, p) {
    if (pNode.left) p = ConvertSub(pNode.left, p);

    if (p == null) {
        p = pNode //找到最左端
    } else {
        p.right = pNode
        pNode.left = p
        p = pNode
    }

    if (pNode.right) p = ConvertSub(pNode.right, p);
    return p
}

題38:二叉樹的深度

難度:♡♡

樹的深度是從根節點開始(其深度為1)自頂向下逐層累加。高度是從葉節點開始(其高度為1)自底向上逐層百累加的。雖然樹的深度和高度一樣,但是具體到樹的某個節點,其深度和高度是不一樣的。

方法一:

function TreeDepth(pRoot) {
    if (!pRoot) return 0;
    var left = 1 + TreeDepth(pRoot.left);
    var right = 1 + TreeDepth(pRoot.right);
    return Math.max(left, right)
}

方法二:

該方法從根路徑開始,是題24的學以致用,都是找個陣列記錄路徑,每走到一個葉子節點就計算當前路徑長,和上一次的長度做比較。然後pop退回父節點計算別的路徑的長度。

function TreeDepth(pRoot) {
    // write code here
    let longest = 0
    if (pRoot) {
        let path = []
        longest = getTreeDepth(pRoot, path, longest)
    }
    return longest
}

function getTreeDepth(pRoot, path, longest) {
    path.push(pRoot.val)
    if (!pRoot.left && !pRoot.right && path.length > longest) {
        longest = path.length
    }
    if (pRoot.left) {
        longest = getTreeDepth(pRoot.left, path, longest)
    }
    if (pRoot.right) {
        longest = getTreeDepth(pRoot.right, path, longest)
    }
    path.pop()
    return longest
}

題39:平衡二叉樹

難度:♡♡♡

是一空樹或它的左右兩個子樹的高度差(稱為平衡因子)不大於1的二叉排序樹。並且左右兩個子樹都是一棵平衡二叉樹。

思路:牢牢抓住平衡二叉樹定義的重點,左右兩個子樹都是一棵平衡二叉樹

function IsBalanced_Solution(pRoot) {
    if (pRoot == null) {
        return true
    }
    if (Math.abs(TreeDepth(pRoot.left) - TreeDepth(pRoot.right)) > 1) { 
        return false;
    } else { //當前節點的左右高度差不大於1
        return IsBalanced_Solution(pRoot.left) && IsBalanced_Solution(pRoot.right);//判斷左右兩個子樹都是一棵平衡二叉樹嗎
    }
}

function TreeDepth(pRoot) {
    if (!pRoot) return 0;
    var left = 1 + TreeDepth(pRoot.left);
    var right = 1 + TreeDepth(pRoot.right);
    return Math.max(left, right)
}

題57:二叉樹的下一個結點

難度:♡♡♡

function GetNext(pNode) {
    // write code here
    if (!pNode) {
        return null
    }
    //有右子樹的
    if (pNode.right) {
        pNode = pNode.right;
        while (pNode.left) { //下個結點就是其右子樹最左邊的點
            pNode = pNode.left
        }
        return pNode
    }
    // 沒有右子樹
    while (pNode.next) { //有父節點
        let p = pNode.next //p指向當前節點的父節點
        if (p.left == pNode) { //直到當前結點是其父節點的左孩子為止
            return p
        }
        pNode = pNode.next
    }
    return null //尾節點
}

題58:對稱的二叉樹

難度:♡♡♡♡♡

思路:之前做過的遞迴都是一棵樹的遞迴,現在分別將這棵樹的左右子樹遞迴

function isSymmetrical(pRoot) {
    // write code here
    if (pRoot == null) {
        return true
    }
    return judge(pRoot.left, pRoot.right)
}

function judge(left, right) {
    // 以下判斷是否都走到底
    if (left == null) {
        return right == null
    }
    if (right == null) {
        return false
    }
    // 都未走到底
    if (left.val != right.val)
        return false
    return judge(left.left, right.right) && judge(left.right, right.left)
}

題59:按之字形順序列印二叉樹

難度:♡♡♡♡

這道題的解題方法妙就妙在還是按層數從左到右儲存節點值,有些人(對,就是我)在層次遍歷的程式碼上加工,對push這一步分類討論,想著這裡是push左邊的還是右邊的,最後把自己繞暈了。

層次遍歷是shift出一個,push進它的左右節點值。這裡在while裡面加了個for迴圈,妙的是對同一層的節點進行處理,就算是偶數層要求倒著輸出,我們只要有了該層的順序陣列,只要對該陣列進行reverse就行了。誰還想去倒著額遍歷偶數層的節點,瘋了嗎嗎嗎

function TreeNode(x) {
    this.val = x;
    this.left = null;
    this.right = null;
}

function Print(pRoot) {
    if (!pRoot) return []

    let queue = []
    let result = []
    let flag = true //true奇數

    queue.push(pRoot)
    while (queue.length) {
        let tempArr = [] //用來存放當前層所有節點的值
        const len = queue.length //存放當前佇列的長度
        for (let i = 0; i < len; i++) {
            let temp = queue.shift();
            tempArr.push(temp.val);
            if (temp.left) {
                queue.push(temp.left);
            }
            if (temp.right) {
                queue.push(temp.right);
            }
        }
        if (!flag) {
            tempArr.reverse();
        }
        flag = !flag;
        result.push(tempArr);
    }
    return result
}

題60:把二叉樹列印成多行

難度:♡♡♡

題目:從上到下按層列印二叉樹,同一層結點從左至右輸出。每一層輸出一行。

把上面那一題關於倒序某一層所有值的程式碼去掉就行了。

題61:序列化二叉樹

難度:♡♡♡♡

此題想吐槽,重點還是看第4題的重建二叉樹吧

function Serialize(pRoot) { 
    if (!pRoot) {
        res.push('#');
    } else {
        res.push(pRoot.val);
        Serialize(pRoot.left);
        Serialize(pRoot.right);
    }
}

function Deserialize(s) {
    if (res.length < 1) return null;
    let node = null;

    let cur = res.shift();
    if (typeof cur == 'number') {
        node = new TreeNode(cur);
        node.left = Deserialize(s);
        node.right = Deserialize(s);
    }
    return node;
}

題62:二叉搜尋樹的第k小的結點

難度:♡♡♡♡

思路:第k小即是中序遍歷的第K個節點。

程式碼需要注意的地方,一開始我將KthNodeCore(pRoot,<u>k</u>)放在KthNode外,明明和書本里C的程式碼一樣卻通不過。
後來發現還是因為JavaScript基本資料型別的傳參問題,每次的p值改變必須得return回上一輪遞迴才能在上一輪遞迴中取得最新p值,但是該函式中我們還需要返回目標節點,因此最好的解決辦法就是將k放於遞迴函式的上一級作用域中。

佔個坑:用非遞迴寫一下
佔個坑:第K大呢?

function KthNode(pRoot, k) {
    // write code here
    if (!pRoot || k <= 0)
        return null

    // 為了能追蹤k,應該把KthNodeCore函式定義在這裡面,k應該在KthNodeCore函式外面
    function KthNodeCore(pRoot) {
        let target = null
        if (pRoot.left) target = KthNodeCore(pRoot.left)
        if (!target) {
            if (k == 1) target = pRoot
            k--
        }
        if (!target && pRoot.right) target = KthNodeCore(pRoot.right)
        return target
    }
    return KthNodeCore(pRoot)
}

相關文章