js 中二叉樹的深度遍歷與廣度遍歷(遞歸實現與非遞歸實現)

樹的簡介

棧、隊列、鏈表等數據結構,都是順序數據結構。而樹是非順序數據結構。樹型結構是一類很是重要的非線性結構。直觀地,樹型結構是以分支關係定義的層次結構。
樹型結構javascript

樹在計算機領域中也有着普遍的應用,例如在編譯程序中,用樹來表示源程序的語法結構;在數據庫系統中,可用樹來組織信息;在分析算法的行爲時,可用樹來描述其執行過程等等。html

  1. 樹(Tree)是n(n>=0)個結點的有限集。在任意一棵非空樹中:
  • 有且僅有一個特定的稱爲根(Root)的結點
  • 當n>1時,其他結點可分爲m(m>0)個互不相交的有限集T1,T2,T3,...Tm,其中每個集合自己又是一棵樹,而且稱爲根的子樹(Subtree)

例如,(a)是隻有一個根結點的樹;(b)是有13個結點的樹,其中A是根,其他結點分紅3個互不相交的子集:T1={B,E,F,K,L},t2={D,H,I,J,M};T1,T2和T3都是根A的子樹,且自己也是一棵樹。java

  1. 樹的結點包含一個數據元素及若干指向其子樹的分支。

結點擁有的子樹數稱爲結點的度(Degree)。例如,(b)中A的度爲3,C的度爲1,F的度爲0。度爲0的結點稱爲葉子(Leaf)或者終端結點。度不爲0的結點稱爲非終端結點或分支結點node

樹的度是樹內各結點的度的最大值。(b)的樹的度爲3。結點的子樹的根稱爲該結點的孩子(Child)。相應的,該結點稱爲孩子的雙親(Parent)。同一個雙親的孩子之間互稱兄弟(Sibling)。結點的祖先是從根到該結點所經分支上的全部結點。反之,以某結點爲根的子樹中的任一結點都稱爲該結點的子孫。git

  1. 結點的層次(Level)從根開始定義起,根爲第一層,跟的孩子爲第二層。若某結點在第層,則其子樹的根就在第l+1層。其雙親在同一層的結點互爲堂兄弟。例如,結點G與E,F,H,I,J互爲堂兄弟。樹中結點的最大層次稱爲樹的深度(Depth)或高度。(b)的樹的深度爲4。

求樹的深度:看這篇:https://www.jianshu.com/p/9fc...程序員

  1. 若是將樹中結點的各子樹當作從左至右是有次序的(即不能交換),則稱該樹爲有序樹,不然稱爲無序樹。在有序樹中最左邊的子樹的根稱爲第一個孩子,最右邊的稱爲最後一個孩子。
  2. 森林(Forest)是m(m>=0)棵互不相交的樹的集合。對樹中每一個結點而言,其子樹的集合即爲森林。

二叉樹

二叉樹(Binary Tree)是另外一種樹型結構,它的特色是每一個結點至多隻有兩棵子樹(即二叉樹中不存在度大於2的結點),而且,二叉樹的子樹有左右之分(其次序不能任意顛倒。)github

  1. 二叉樹的性質:
  • 在二叉樹的第 i 層上至多有$2^{i-1}$個結點(i>=1)。
  • 深度爲k的二叉樹至多有$2^k - 1$個結點,(k>=1)。
  • 對任何一棵二叉樹T,若是其終端結點數爲n0,度爲2的結點數爲n2,則n0 = n2 + 1;
  • 一棵深度爲k且有$2^k$ - 1個結點的二叉樹稱爲滿二叉樹
  • 深度爲k的,有n個結點的二叉樹,當且僅當其每個結點都與深度爲k的滿二叉樹中編號從1至n的結點一一對應時,稱之爲徹底二叉樹
  1. 徹底二叉樹的兩個特性:
  • 具備n個結點的徹底二叉樹的深度爲$Math.floor(log_2 n) + 1$;
  • 若是對一棵有n個結點的徹底二叉樹(其深度爲$Math.floor(log_2 n) + 1$)的結點按層序編號(從第1層到第$Math.floor(log_2 n) + 1$,每層從左到右),則對任一結點(1<=i<=n)有:
若是i=1,則結點i是二叉樹的根,無雙親;若是i>1,則其雙親parent(i)是結點Math.floor(i/2)。
若是2i > n,則結點i無左孩子(結點i爲葉子結點);不然其左孩子LChild(i)是結點2i.
若是2i + 1 > n,則結點i無右孩子;不然其右孩子RChild(i)是結點2i + 1;

二叉樹
徹底二叉樹

  1. 二叉樹的存儲結構
  • 順序存儲結構

用一組連續的存儲單元依次自上而下,自左至右存儲徹底二叉樹上的結點元素,即將二叉樹上編號爲i的結點元素存儲在加上定義的一維數組中下標爲i-1的份量中。「0」表示不存在此結點。這種順序存儲結構僅適用於徹底二叉樹。由於,在最壞狀況下,一個深度爲k且只有k個結點的單支樹(樹中不存在度爲2的結點)卻須要長度爲2的n次方-1的一維數組。web

順序:[1, 2, 3, 4, 5, , 6, , , 7]算法

  • 鏈式存儲結構

二叉樹的結點由一個數據元素和分別指向其左右子樹的兩個分支構成,則表示二叉樹的鏈表中的結點至少包含三個域:數據域和左右指針域。有時,爲了便於找到結點的雙親,還可在結點結構中增長一個指向其雙親結點的指針域。利用這兩種結構所得的二叉樹的存儲結構分別稱之爲二叉鏈表和三叉鏈表。 在含有n個結點的二叉鏈表中有n+1個空鏈域,咱們能夠利用這些空鏈域存儲其餘有用信息,從而獲得另外一種鏈式存儲結構---線索鏈表。數據庫

鏈式:{ data, left, right}

二叉樹的遍歷

遍歷二叉樹(Traversing Binary Tree):是指按指定的規律對二叉樹中的每一個結點訪問一次且僅訪問一次。

二叉樹有深度遍歷和廣度遍歷, 深度遍歷有前序、 中序和後序三種遍歷方法。二叉樹的前序遍歷能夠用來顯示目錄結構等;中序遍歷能夠實現表達式樹,在編譯器底層頗有用;後序遍歷能夠用來實現計算目錄內的文件及其信息等。

二叉樹是很是重要的數據結構, 其中二叉樹的遍歷要使用到棧和隊列還有遞歸等,不少其它數據結構也都是基於二叉樹的基礎演變而來的。熟練使用二叉樹在不少時候能夠提高程序的運行效率,減小代碼量,使程序更易讀

二叉樹不只是一種數據結構,也是一種編程思想。學好二叉樹是程序員進階的一個必然進程。

前序遍歷:訪問根–>遍歷左子樹–>遍歷右子樹;
中序遍歷:遍歷左子樹–>訪問根–>遍歷右子樹;
後序遍歷:遍歷左子樹–>遍歷右子樹–>訪問根;
廣度遍歷:按照層次一層層遍歷;
  • 例如(a+b*c)-d/e,該表達式用二叉樹表示如圖:

(a+b*c)-d/e

對該二叉樹進行深度和廣度遍歷爲:

前序遍歷:- + a b c / d e
中序遍歷:a + b * c - d / e
後序遍歷:a b c + d e / -
廣度遍歷:- + / a * d e b c

1. js中的二叉樹

上述二叉樹(a+b*c)-d/e在js中能夠用對象的形式表示出來:

var tree = {
    value: "-",
    left: {
        value: '+',
        left: {
            value: 'a',
        },
        right: {
            value: '*',
            left: {
                value: 'b',
            },
            right: {
                value: 'c',
            }
        }
    },
    right: {
        value: '/',
        left: {
            value: 'd',
        },
        right: {
            value: 'e',
        }
    }
}

2. js中二叉樹的深度遍歷

深度遍歷也可稱爲深度優先遍歷(Depth-First Search,DFS),由於它老是優先往深處訪問。

先序遍歷
  • 遞歸遍歷
let result = [];
let dfs = function (node) {
    if(node) {
        result.push(node.value);
        dfs(node.left);
        dfs(node.right);
    }
}

dfs(tree);
console.log(result); 
// ["-", "+", "a", "*", "b", "c", "/", "d", "e"]

先序遞歸遍歷思路:

先遍歷根結點,將值存入數組,而後遞歸遍歷:先左結點,將值存入數組,繼續向下遍歷;直到(二叉樹爲空)子樹爲空,則遍歷結束;
而後再回溯遍歷右結點,將值存入數組,這樣遞歸循環,直到(二叉樹爲空)子樹爲空,則遍歷結束。

  • 非遞歸遍歷(利用棧:將遍歷到的結點都依次存入棧中,拿結果時從棧中訪問
let dfs = function (nodes) {
    let result = [];
    let stack = [];
    stack.push(nodes);
    while(stack.length) { // 等同於 while(stack.length !== 0) 直到棧中的數據爲空
        let node = stack.pop(); // 取的是棧中最後一個j
        result.push(node.value);
        if(node.right) stack.push(node.right); // 先壓入右子樹
        if(node.left) stack.push(node.left); // 後壓入左子樹
    }
    return result;
}
dfs(tree);

先序非遞歸遍歷思路:

  1. 初始化一個棧,將根節點壓入棧中;
  2. 當棧爲非空時,循環執行步驟3到4,不然執行結束;
  3. 從隊列取得一個結點(取的是棧中最後一個結點),將該值放入結果數組;
  4. 若該結點的右子樹爲非空,則將該結點的右子樹入棧,若該結點的左子樹爲非空,則將該結點的左子樹入棧;(注意:先將右結點壓入棧中,後壓入左結點,從棧中取得時候是取最後一個入棧的結點,而先序遍歷要先遍歷左子樹,後遍歷右子樹)
中序遍歷
  • 遞歸遍歷
let result = [];
let dfs = function (node) {
     if(node) {
        dfs(node.left);
        result.push(node.value); // 直到該結點無左子樹 將該結點存入結果數組 接下來並開始遍歷右子樹
        dfs(node.right);
    }
}

dfs(tree);
console.log(result);
//  ["a", "+", "b", "*", "c", "-", "d", "/", "e"]

中序遞歸遍歷的思路:

先遞歸遍歷左子樹,從最後一個左子樹開始存入數組,而後回溯遍歷雙親結點,再是右子樹,這樣遞歸循環。

  • 非遞歸遍歷
function dfs(node) {
    let result = [];
    let stack = [];
    while(stack.length || node) { // 是 || 不是 &&
        if(node) {
            stack.push(node);
            node = node.left;
        } else {
            node = stack.pop();
            result.push(node.value);
            //node.right && stack.push(node.right);
            node = node.right; // 若是沒有右子樹 會再次向棧中取一個結點即雙親結點
        }
    }
    return result;
}

dfs(tree);
// ["a", "+", "b", "*", "c", "-", "d", "/", "e"]

一種利用回溯法思想的代碼:
看這裏:https://zhuanlan.zhihu.com/p/... 可是他的代碼有些問題。。。

非遞歸遍歷的思路:

將當前結點壓入棧,而後將左子樹當作當前結點,若是當前結點爲空,將雙親結點取出來,將值保存進數組,而後將右子樹當作當前結點,進行循環。

後序遍歷
  • 遞歸遍歷
result = [];
function dfs(node) {
    if(node) {
        dfs(node.left);
        dfs(node.right);
        result.push(node.value);
    }
}
dfs(tree);
console.log(result);
// ["a", "b", "c", "*", "+", "d", "e", "/", "-"]

寫到這,深深的被遞歸折服。。。。服

先走左子樹,當左子樹沒有孩子結點時,將此結點的值放入數組中,而後回溯遍歷雙親結點的右結點,遞歸遍歷。

  • 非遞歸遍歷

(含大量註釋代碼的)

function dfs(node) {
    let result = [];
    let stack = [];
    stack.push(node);
    while(stack.length) {
        // 不能用node.touched !== 'left' 標記‘left’作判斷,
        // 由於回溯到該結點時,遍歷右子樹已經完成,該結點標記被更改成‘right’ 若用標記‘left’判斷該if語句會一直生效致使死循環
        if(node.left && !node.touched) { // 不要寫成if(node.left && node.touched !== 'left')
            // 遍歷結點左子樹時,對該結點作 ‘left’標記;爲了子結點回溯到該(雙親)結點時,便再也不訪問左子樹
            node.touched = 'left';
            node = node.left;
            stack.push(node);
            continue;
        }
        if(node.right && node.touched !== 'right') { // 右子樹同上
            node.touched = 'right';
            node = node.right;
            stack.push(node);
            continue;
        }
        node = stack.pop(); // 該結點無左右子樹時,從棧中取出一個結點,訪問(並清理標記)
        node.touched && delete node.touched; // 能夠不清理不影響結果 只是第二次對同一顆樹再執行該後序遍歷方法時,結果就會出錯啦由於你對這棵樹作的標記還留在這棵樹上
        result.push(node.value);
        node = stack.length ? stack[stack.length - 1] : null;
        //node = stack.pop(); 這時當前結點再也不從棧中取(彈出),而是不改變棧數據直接訪問棧中最後一個結點
        //若是這時當前結點去棧中取(彈出)會致使回溯時當該結點左右子樹都被標記過期 當前結點又變成從棧中取會漏掉對結點的訪問(存入結果數組中)
    }
    return result; // 返回值
}

dfs(tree);

後序遍歷非遞歸遍歷思路:先把根結點和左樹推入棧,而後取出左樹,再推入右樹,取出,最後取根結點。

步驟:

  1. 初始化一個棧,將根節點壓入棧中,並標記爲當前節點(node);
  2. 當棧爲非空時,執行步驟3,不然執行結束;
  3. 若是當前節點(node)有左子樹且沒有被 touched,則執行4;若是當前結點有右子樹,被 touched left 但沒有被 touched right 則執行5 不然執行6;
  4. 對當前節點(node)標記 touched left,將當前節點的左子樹賦值給當前節點(node=node.left) 並將當前節點(node)壓入棧中,回到3;
  5. 對當前節點(node)標記 touched right,將當前節點的右子樹賦值給當前節點(node=node.right) 並將當前節點(node)壓入棧中,回到3;
  6. 清理當前節點(node)的 touched 標記,彈出棧中的一個節點並訪問,而後再將棧頂節點標記爲當前節點(item),回到3;

3. js中二叉樹的廣度遍歷

廣度優先遍歷二叉樹(層序遍歷)是用隊列來實現的,廣度遍歷是從二叉樹的根結點開始,自上而下逐層遍歷;在同一層中,按照從左到右的順序對結點逐一訪問。

  • 遞歸遍歷
let result = [];
let stack = [tree]; // 先將要遍歷的樹壓入棧
let count = 0; // 用來記錄執行到第一層
let bfs = function () {
    let node = stack[count];
    if(node) {
        result.push(node.value);
        if(node.left) stack.push(node.left);
        if(node.right) stack.push(node.right);
        count++;
        bfs();
    }
}
dfc();
console.log(result);
//  ["-", "+", "/", "a", "*", "d", "e", "b", "c"]
  • 非遞歸算法
function bfs(node) {
    let result = [];
    let queue = [];
    queue.push(node);
    let pointer = 0;
    while(pointer < queue.length) {
        let node = queue[pointer++]; // // 這裏不使用 shift 方法(複雜度高),用一個指針代替
        result.push(node.value);
        node.left && queue.push(node.left);
        node.right && queue.push(node.right);
    }
    return result;
}

bfs(tree);
// ["-", "+", "/", "a", "*", "d", "e", "b", "c"]

另一種比較消耗性能的方法:不額外定義一個指針變量 pointer,使用數組的shift()方法,每次改變 queue 的數據(入棧、出棧),來讀取數據,直到棧 queue 中數據爲空,執行結束。(頻繁的改變數組,由於數組是引用類型,要改變它,要新開闢一個地址,因此比較消耗空間)

function bfs (node) {
    let result = [];
    let queue = [];
    queue.push(node);
    while(queue.length) {
        node = queue.shift();
        result.push(node.value); // 不要忘記訪問
        // console.log(node.value);
        node.left && queue.push(node.left);
        node.right && queue.push(node.right);
    }
    return result;
}
bfs(tree);
//  ["-", "+", "/", "a", "*", "d", "e", "b", "c"]

References

二叉樹與JavaScript
JavaScript與簡單算法
javascript實現數據結構: 樹和二叉樹,二叉樹的遍歷和基本操做
圖的基本算法(BFS和DFS)
搜索思想——DFS & BFS(基礎基礎篇)

相關文章
相關標籤/搜索