講透學爛二叉樹(三):二叉樹的遍歷圖解算法步驟及JS代碼


二叉樹的遍歷是指不重復地訪問二叉樹中所有結點,主要指非空二叉樹,對於空二叉樹則結束返回。

二叉樹的遍歷分為

  • 深度優先遍歷

    • 先序遍歷:根節點->左子樹->右子樹(根左右),有的叫:前序遍歷

    • 中序遍歷:左子樹->根節點->右子樹(左根右

    • 后序遍歷:左子樹->右子樹->根節點(左右根

  • 廣度優先遍歷

    • 層次遍歷

1590962-20190306170951841-1886726002.jpg

二叉樹-深度的優先遍歷-圖解

深度優先,前、中、后遍歷順序,就是組合[根左右],移動根的位置,左右左右,但是我即使代碼會寫了,還是搞不明白這個根左右與遍歷的關系毛線頭在哪里,特別是中序遍歷的左根右,

博主YuXi_0520的圖解,應該是我看過最易懂的。這里盜圖貼一下

先序遍歷(DLR)

先序遍歷可以想象成,小人從樹根開始繞着整棵樹的外圍轉一圈,經過結點的順序就是先序遍歷的順序

二叉樹先序遍歷流程圖解

讓我們來看下動畫,和小人兒一起跑兩遍就記住啦,記住是繞着外圍跑哦   

二叉樹先序列遍歷-gif動畫演示    二叉樹先序遍歷 小人動畫演示

先序遍歷結果:ABDHIEJCFKG

二叉樹先序遍歷-js代碼實現

遞歸實現—二叉樹先序遍歷

根 - 左 - 右遞歸

判斷根結點是否為空,為空則返回null;否則取結點的值,然后去左、右結點的值

/**
 * @description 前序遍歷 =>1.訪問根節點; 2.訪問左子樹; 3.訪問右子樹
 * @param node {Node} 遍歷的樹
 */
preOrderTraverse (node = this.tree) {
    // 數組存儲數遍歷值
    let backs = []

    // 遞歸,
    function tempFunction (node) {
        if (node.data !== null) {
            // 先取根結點
            backs.push(node.data)
            // 如果存在左結點,取左節點的值
            node.leftChild && tempFunction(node.leftChild)
            // 如果存在右結點,取右結點值
            node.rightChild && tempFunction(node.rightChild)
        }
    }

    tempFunction(node)
    return backs
}

 

非遞歸-二叉樹先序遍歷

取結點的值,然后將左、右結點壓如棧

preOrderTraverse2 (node = this.tree) {
    let backs = []
    if (!node) {
        return backs
    }
    let queue = [node]
    while (queue.length) {
        // 取出最后一個結點,對這個結點進行遍歷
        let root = queue.pop()
        backs.push(root.data)
        // 因為queue.pop,所以先存入右結點
        root.rightChild && queue.push(root.rightChild)
        root.leftChild && queue.push(root.leftChild)
    }
    return backs
}

 

非遞歸-二叉樹先序遍歷

下面代碼應該更容易理解

如果結點存在左結點,取值,然后存入棧中,直至沒有左結點是葉子,再取右邊

preOrderTraverse3 (node = this.tree) {
    let backs = []
    if (!node) {
        return backs
    }
    let currentNode = node
    let queue = [node]
    while (queue.length) {
        if(currentNode){
            backs.push(currentNode.data)
            queue.push(currentNode)
            currentNode=currentNode.leftChild
        }else {
            currentNode = queue.pop()
            currentNode = currentNode.rightChild
        }
    }
    return backs
}

 

中序遍歷(LDR)

中序遍歷可以想象成,按樹畫好的左右位置投影下來就可以了

二叉樹-中序遍歷-gif動圖演示

下面看下投影的過程動畫,其實就是按左右順序寫下來就行了

中序遍歷結果:HDIBEJAFKCG

二叉樹中序遍歷-JavaScript代碼實現

遞歸實現-二叉樹中序遍歷
/**
 * @description 中遍歷 =>左右根
 * @param node {Node} 遍歷的樹
 */
inOrderTraverse (node = this.tree) {
    // 數組存儲數遍歷值
    let backs = []

    // 遞歸,
    function tempFunction (node) {
        if (node.data !== null) {
            // 如果存在左結點,取左節點的值
            node.leftChild && tempFunction(node.leftChild)
            // 取根結點
            backs.push(node.data)
            // 如果存在右結點,取右結點值
            node.rightChild && tempFunction(node.rightChild)

        }
    }

    tempFunction(node)
    return backs
}

 

非遞歸實現-二叉樹中序遍歷
inOrderTraverse2(node){
    let backs = []
    if(!node){
        return backs
    }
    let stack = [node]
    let currentNode = node
    while(stack.length){
        if(currentNode){
            stack.push(currentNode)
            currentNode = currentNode.leftChild
        }else {
            currentNode = stack.pop()
            backs.push(currentNode.data)
            currentNode = currentNode.rightChild
        }
    }
}

 

后序遍歷(LRD)

后序遍歷就像是剪葡萄,我們要把一串葡萄剪成一顆一顆的(從左到右,剪最底下的葉子結點)。

就是圍着樹的外圍繞一圈,如果發現一剪刀就能剪下的葡萄(必須是一顆葡萄),就把它剪下來,組成的就是后序遍歷了

跟之前先序遍歷繞圈的路線是一樣的(先序遍歷,是遇到結點,就push到數組),但是后續遍歷是:遞歸:先取左葉結點(沒有就跳過),再取右葉子結點

 

二叉樹-后續遍歷二叉樹-后續遍歷-gif圖

后序遍歷結果:HIDJEBKFGCA

二叉樹后續遍歷-JavaScript代碼實現

遞歸實現—二叉樹后續遍歷
/**
 * @description 后序遍歷 =>左根右
 *  1.訪問左子樹。(先訪問左子樹中的左子樹,再訪問左子樹中的右子樹)
 *  2.訪問右子樹。(先訪問右子樹中的左子樹,再訪問右子樹中的右子樹)
 *  3.訪問根
 * @param node {Node} 遍歷的樹
 */
postOrderTraverse (node) {
    // 數組存儲數遍歷值
    let backs = []

    // 遞歸,
    function tempFunction (node) {
        if (node.data !== null) {
            // 如果存在左結點,取左節點的值
            node.leftChild && tempFunction(node.leftChild)
            // 如果存在右結點,取右結點值
            node.rightChild && tempFunction(node.rightChild)
            // 最后取根結點
            backs.push(node.data)
        }
    }

    tempFunction(node)
    return backs
}

非遞歸實現—二叉樹后續遍歷

postOrderTraverse2 (node){
    let backs = []
    if(!node){
        return backs
    }
    let stack  = [node]
    while(stack.length){
        let currentNode = stack.pop()
        backs.push(currentNode.data)
        currentNode.leftChild&&stack.push(currentNode.leftChild)
        currentNode.rightChild&&stack.push(currentNode.rightChild)
    }
    return  backs
}
非遞歸實現2—二叉樹后續遍歷
postOrderTraverse2 (node) {
    let backs = []
    if (!node) {
        return backs
    }
    let stack = []
    let currentNode = node
    while (stack.length||currentNode) {
        if (currentNode) {
            stack.push(currentNode)
            backs .unshift(currentNode.data)
            currentNode = currentNode.rightChild
        } else {
            let temp = stack.pop()
            currentNode = temp.leftChild

        }
    }
    return backs
}
非遞歸實現3—二叉樹后續遍歷
postOrderTraverse3 (node) {
    let backs = []
    if (!node) {
        return backs
    }
    let stack = [node]
    while (stack.length) {
        let currentNode = stack.pop()
        backs.unshift(currentNode.data)
        currentNode.leftChild && stack.push(currentNode.leftChild)
        currentNode.rightChild && stack.push(currentNode.rightChild)
    }
    return backs
}
非遞歸實現4—二叉樹后續遍歷
postOrderTraverse4 (node) {
    let backs = []
    if (!node) {
        return backs
    }
    let stack = [node]
    let currentNode = node
    let visitedNode = null
    while (stack.length) {
        if (currentNode) {
            stack.push(currentNode)
            currentNode = currentNode.leftChild
        } else {
            currentNode = stack[stack.length - 1]
            if (currentNode.rightChild && currentNode.rightChild !== visitedNode) {
                currentNode = currentNode.rightChild
            } else {
                backs.push(currentNode.data)
                visitedNode = currentNode
                stack.pop()
                currentNode = null
            }
        }
    }
    return backs
}

 

二叉樹-廣度優先遍歷-圖解

這個只有層序遍歷

層序遍歷

層序遍歷太簡單了,就是按照一層一層的順序,從左到右寫下來就行了。

二叉樹-廣度優先-層序遍歷

層序遍歷結果:ABCDEFGHIJK

二叉序層序遍歷-JavaScript代碼實現

/**
 * @description 中遍歷 =>左右根
 * @param node {Node} 遍歷的樹
 */
inOrderTraverse (node = this.tree) {
    // 數組存儲數遍歷值
    let backs = []

    // 遞歸,
    function tempFunction (node) {
        if (node.data !== null) {
            // 如果存在左結點,取左節點的值
            node.leftChild && tempFunction(node.leftChild)
            // 取根結點
            backs.push(node.data)
            // 如果存在右結點,取右結點值
            node.rightChild && tempFunction(node.rightChild)

        }
    }

    tempFunction(node)
    return backs
}

不知道通過這種方式,有沒有覺得閉着眼睛都能寫出前序、中序、后序 、層序了呀,不過這只是為了大家好理解,我想出的一種形象思維,為了用代碼實現,我們還需要具體了解一下前序、中序、后序遍歷。

真正理解三種遍歷

還記得我們先序和后序遍歷時候跑的順序么?按照這個順序再跑一次,就是圍着樹的外圍跑一整圈

讓我們來理解一下繞着外圍跑一整圈的真正含義是:遍歷所有結點時,都先往左孩子走,再往右孩子走

二叉樹-先序與后續遍歷流程圖

觀察一下,你有什么發現?

有沒有發現,除了根結點和空結點,其他所有結點都有三個箭頭指向它

  • 一個是從它的父節點指向它

  • 一個是從它的左孩子指向它

  • 一個是從它的右孩子指向它

一個結點有三個箭頭指向它,說明每個結點都被經過了三遍

  • 一遍是從它的父節點來的時候

  • 一遍是從它的左孩子返回時

  • 一遍是從它的右孩子返回時

其實我們在用遞歸算法實現二叉樹的遍歷的時候,不管是先序中序還是后序,程序都是按照上面那個順序跑遍所有結點的

先序中序和后序唯一的不同就是,在經過結點的三次中,哪次訪問(輸出或者打印或者做其他操作)了這個結點。有點像大禹治水三過家門,他會選擇一次進去。

  • 先序遍歷顧名思義,就是在第一次經過這個結點的時候訪問了它。就是從父節點來的這個箭頭的時候,訪問了它。

  • 中序遍歷也和名字一樣,就是在第二次經過這個結點的時候訪問了它。就是從左孩子返回的這個箭頭的時候,訪問了它。

  • 后序遍歷,就是在第三次經過這個結點的時候訪問了它。就是從右孩子返回的這個箭頭的時候,訪問了它。

怎么樣,這樣有沒有很好的理解?

其實不管是前序中序還是后序,在程序里跑的時候都是按照同樣的順序跑的,每個結點經過三遍,第幾遍訪問這個結點了,就叫什么序遍歷

當我們腦子里有這個概念的時候, 再去看實現代碼就很好理解了,下一篇博文我會貼出和講解具體的實現代碼。

如果遞歸分治來理解前、中、后遍歷與左右左右的關系,就是按照那個跑路圖,對每個最底層的子結點[前、中、后]對應[根左右、左根右、左右根]順序來處理。

二叉樹前序、中序、后序遍歷相互求法

已知二叉樹的廣度優先遍歷-層序遍歷數組,是可以完全還原二叉樹結構,但是

已知二叉樹前序、中序、后序中的一種排序數組,是無法求出二叉樹結構的。但是知道前序、中序、后序中的中序和前序或后序數組兩種也可以還原二叉樹,為何可以推出二叉樹的數據結構。

前序根結點在最前,后續根結點在最后,如果已知前序或者后續,則中序中根結點兩邊的元素分布為根結點的左邊結點和右邊結點元素。具體操作如下:

已知前序、中序遍歷,求后序遍歷

  • 前序遍歷=ABGDECFH

  • 中序遍歷=GBEDAFCH

構建二叉樹步驟:

  1. 根據前序遍歷的特點,我們知道根結點root為A;

  2. 觀察中序遍歷GBEDAFCH。其中root節點A的左側GBED必然是root的左子樹,右側FCH必然是root的右子樹。同時,這個也分別是左子樹和右子樹的中序遍歷的序列;

  3. 在前序遍歷遍歷完根節點后,接着執行前序遍歷左子樹,注意,是前序遍歷,什么意思?就是把左子樹當成一棵獨立的樹,執行前序遍歷,同樣先訪問左子樹的根,第2步我們已經知道左子樹是BGDE(前序遍歷序列)了,由此可以得到,左子樹的根是B,那么在這一步得到左子樹的根是B;

  4. 從第2步得到的中序遍歷的節點序列中,找到B,發現B左邊只有一個G,說明B的左子樹只有一個葉子節點,B的右邊呢?我們可以得到B的右子樹有ED,再看前序遍歷的序列,發現D在前,也就是說,D是先前序遍歷訪問的,則得到E是D的左子樹,只有一個葉子節點。到這里,我們可以得到這棵樹的根結點和左子樹的結構了,如下圖一

  5. 接着看右子樹,在第2步的時候已經知道右子樹是FCH這三個節點,那么先看前序遍歷的序列,先出現的是C,那么C就是右子樹的根結點,看右子樹的中序遍歷為FCH,所以F和H就分別是它的左子樹和右子樹,因此,右子樹的結構就出來了,如下圖二

二叉樹線序與中序,還原二叉樹

已知中序、后序遍歷,求前序遍歷

  • 中序遍歷:GBEDAFCH

  • 后序遍歷:GEDBFHCA

同理,步驟和上面的類似,還是得先找出根結點,由后序遍歷的特點,根結點root在最后,所以根結點為A,再由中序遍歷可以知道左子樹和右子樹分別為GBED、FCH;再按照上面的步驟遞歸分別求出左右子樹即可得解。

已知前序、后序遍歷,求中序遍歷

已知前序、中序或者中序、后序都可以唯一確定一棵二叉樹,但是已知前序、后序是無法唯一確定一棵二叉樹的,解不唯一。

 

關於算法相關的詳細代碼,查看https://github.com/zhoulujun/algorithm

參考文章:

理解二叉樹的三種遍歷--前序、中序、后序 +層序(簡明易懂)https://blog.csdn.net/weixin_44032878/article/details/88070556

js數據結構-二叉樹(二叉堆) https://segmentfault.com/a/1190000017761929 (推薦閱讀-理解堆排序-堆化操作

二叉樹前序、中序、后序遍歷相互求法 https://blog.csdn.net/qq_34154570/article/details/82700094

JavaScript 二叉樹遍歷專題:算法描述與實現 https://zhuanlan.zhihu.com/p/27307626

JS - 二叉樹算法實現與遍歷  (更新中...) https://www.cnblogs.com/padding1015/p/7729988.html

二叉樹的遍歷(前序、中序、后序、已知前中序求后序、已知中后序求前序) https://www.cnblogs.com/lanhaicode/p/10390147.html

面試BAT 卻被二叉樹秒殺?20 道題幫你一舉拿下二叉樹算法題 https://zhuanlan.zhihu.com/p/88361872

 

轉載本站文章《講透學爛二叉樹(三):二叉樹的遍歷圖解算法步驟及JS代碼》,
請注明出處:https://www.zhoulujun.cn/html/theory/algorithm/TreeGraph/8283.html

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM