用JavaScript來學習樹「譯」

樹可謂是web開發者最常碰到的數據結構之一了. 要知道, 整張網頁就是一棵DOM樹啊 (Document Object Model ). 因此咱們就來學習樹這一數據結構吧 !javascript

在這篇文章中, 咱們將建立一棵樹而且用兩種不一樣的方法來遍歷它: Depth-First Search ( DFS, 深度優先遍歷 ), 和 Breadth-First Search ( BFS, 寬度/廣度優先遍歷 ). DFS方法使用藉助棧 ( stack ) 這一數據結構來訪問樹的每一個節點, BFS則藉助了隊列 ( queue ).html

在計算機科學裏, 樹是一種分層的數據結構, 用節點來描述數據. 每一個節點都保存有本身的數據和指向其餘節點的指針.java

用咱們熟悉的DOM來解釋一下節點 ( node ) 和 指針 ( pointer ) . 在一張網頁的DOM裏, <html> 標籤被稱爲根節點/根元素 ( root node ), 那麼, 表明html 的這個節點就有指向它的子節點們的指針. 具體到下面的代碼:node

var rootNode = document.getElementsByTagName('html')[0];
//  rootNode.childNodes能夠粗暴地認爲就是指針啦
var childNodes = rootNode.childNodes;
console.log(childNodes);

嗯, 因此一張網頁就是節點有它的子節點們, 每一個子節點又有可能有各自的子節點, 這樣一直嵌套下去, 就構成了一棵DOM樹.web

對樹的操做

由於每棵樹都包含節點, 因此咱們有理由抽象出兩個構造函數: NodeTree. 下面列出的是他們的屬性和方法. 掃一眼就好, 到具體實現能夠再回來看有什麼.編程

Node

  • data 屬性用來保存節點自身的值, 簡單起見, 先假設它保存的是一個基本類型的值, 如字符串one
  • parent 指向它的父節點
  • children 指向它的子節點們所組成的數組

Tree

  • _root 表明一棵樹的根節點
  • traverseDF(callback) 用DFS遍歷一棵樹
  • traverseBF(callback) 用BFS遍歷一棵樹
  • contains(callback, traversal)DFSBFS 在樹裏遍歷搜索一個節點
  • add(data, toData, traverse) 往樹裏添加一個節點
  • remove(child, parent) 從樹裏移除一個節點

具體的代碼實現

來開始寫代碼吧 !數組

Node構造函數

function Node(data) {
    this.data = data;
    this.parent = null;
    this.children = [];
}

Tree構造函數

function Tree(data) {
    var node = new Node(data);
    this._root = node;
}

Tree 構造函數裏只有兩行代碼, 第一行先是建立了一個節點, 第二行是把這個節點設爲樹的根節點.數據結構

雖然NodeTree 的代碼只有那麼幾行, 可是這就足以讓咱們描述一棵樹了. 不信 ? 用下面的代碼建立一棵樹看看:app

var tree = new Tree ('CEO');  // 根節點就像是CEO老總
console.log(tree._root);  // Node {data: "CEO", parent: null, children: Array(0)}

幸虧有parentchildren 這兩個屬性的存在, 咱們能夠children 給根節點_root 添加子節點, 也能夠用parent 把其餘節點的父節點設置成_root . 反正你開心就好.函數

Tree的方法

方法上面已經列舉過啦.

1. traverseDF(callback) 深度優先遍歷

Tree.prototype.traverseDF = function (callback) {
    (function recurse(currentNode) {
        // step2 遍歷當前節點的子節點們
        for (var i = 0, length = currentNode.children.length; i < length; i++) {
            // step3, 遞歸調用遍歷每一個子節點的子節點們
            recurse(currentNode.children[i]);
        }

        // step4 能夠在這裏寫你處理每個節點的回調函數
        callback(currentNode);

        // step1, 把根節點傳進來
    })(this._root);
};

traverseDF(callback) 有一個callback 參數, 是一個函數, 等到你須要調用的時候調用. 除此以外, 還有一個叫recurse 的遞歸函數. 說一下詳細的步驟吧:

  1. 首先, 利用當即執行函數表達式把根節點傳進recurse 函數, 此時, currentNode 就是根節點
  2. 進入for 循環後, 依次遍歷當前節點的每個子節點
  3. for 循環體裏, 遞歸地調用recurse 函數再遍歷子節點的子節點
  4. currentNode 再也不有子節點了, 就會退出for 循環, 而後調用callback 回調函數後, 就一層層地返回了

開頭咱們說DFS 方法藉助了棧來實現, 是的, 咱們確實借用了棧, 就是recurse遞歸函數的函數調用棧. 任何函數的調用都會涉及到進棧和出棧.

遞歸是一個編程上很重要的思想, 要想講清楚也不是一時半會的事. 在這裏咱們把重點放到樹上, 對遞歸不太理解的童鞋們能夠自行搜索一下, 但在這裏建議你們把這個traverseDF 的代碼敲一下, 相信你起碼能理解其中的一些奧妙.

接下來的例子只用上面說起到的代碼建立了一棵樹, 並用traverseDF 遍歷, 雖然不夠優雅, 但好歹能正常工做. 在後面實現 add(value) 這個方法後, 咱們的實現看起來就不會那麼傻逼了

var tree = new Tree('one');
 
tree._root.children.push(new Node('two'));
tree._root.children[0].parent = tree;
 
tree._root.children.push(new Node('three'));
tree._root.children[1].parent = tree;
 
tree._root.children.push(new Node('four'));
tree._root.children[2].parent = tree;
 
tree._root.children[0].children.push(new Node('five'));
tree._root.children[0].children[0].parent = tree._root.children[0];
 
tree._root.children[0].children.push(new Node('six'));
tree._root.children[0].children[1].parent = tree._root.children[0];
 
tree._root.children[2].children.push(new Node('seven'));
tree._root.children[2].children[0].parent = tree._root.children[2];
 
/*
 
creates this tree
 
 one
 ├── two
 │   ├── five
 │   └── six
 ├── three
 └── four
     └── seven
 
*/

traverseDF(callback) 遍歷:

tree.traverseDF(function(node) {
    console.log(node.data)
});
 
/*
 
logs the following strings to the console
 
'five'
'six'
'two'
'three'
'seven'
'four'
'one'
 
*/

2. traverseBF(callback)

接下來來看看寬度優先遍歷BFS吧 !

DFS和BFS 的不一樣, 在於遍歷順序的不一樣. 爲了體現這點, 咱們再次使用以前DFS用過的那棵樹, 這樣就比如較異同了

/*
 
 tree
 
 one (depth: 0)
 ├── two (depth: 1)
 │   ├── five (depth: 2)
 │   └── six (depth: 2)
 ├── three (depth: 1)
 └── four (depth: 1)
     └── seven (depth: 2)
 
 */

先僞裝咱們已經實現了traverseBF(callback) , 而且使用了和traverseDF(callback) 相同的回調函數, 看看輸出的結果, 畢竟有時候從結果推過程很重要

tree.traverseBF(function(node) {
    console.log(node.data)
});
 
/*
 
logs the following strings to the console
 
'one'
'two'
'three'
'four'
'five'
'six'
'seven'
 
*/

哦吼, 就是先從depth = 0, depth = 1...這樣按照每一層去遍歷嘛. 既然咱們已經有了個大體的概念, 那就又來愉快地敲代碼吧:

Tree.prototype.traverseBF = function (callback) {
    var queue = [];

    queue.push(this._root);

    var currentNode = queue.shift();

    while (currentNode) {
        for (var i = 0, length = currentNode.children.length; i < length; i++) {
            queue.push(currentNode.children[i]);
        }

        callback(currentNode);
        currentNode = queue.shift();
    }
};

// 注: 此處原文的隊列做者用了 `var queue = new Queue();`, 多是他以前封裝的構造函數
// 咱們這裏用數組來就好, push()表示進隊列, shift()表示出隊列

這裏的概念稍微有點多, 讓咱們先來梳理一下:

  1. 建立一個空數組, 表示隊列queue
  2. 把根節點_root 壓入隊列
  3. 聲明currentNode 變量, 並用根節點_root 初始化
  4. currentNode 表示一個節點, 轉換成布爾值不爲false 時, 進入while 循環
  5. for 循環來取得currentNode 的每個子節點, 並把他們逐個壓入queue
  6. currentNode 調用回調函數 callback
  7. queue 的隊頭出隊列, 將其賦值給currentNode
  8. 就這樣一直重複, 直到沒有隊列中沒有節點賦值給currentNode , 程序結束

你可能會對上述步驟2, 3的對應兩行代碼有些疑惑:

queue.push(this._root);
var currentNode = queue.shift();
// 先進隊列又出隊列好像顯得有些屢次一舉? 
// 實際上直接 var currentNode = this._root也是能夠的
// 但在這裏仍是建議像這樣寫, 以保持和while循環體內代碼格式的統一

到了這裏, 是否是感受到棧和隊列的神奇之處? 後進先出 ( LIFO, Last In First Out) 和 先進先出 ( FIFO, First In First Out ) 就讓能讓咱們的訪問順序大相徑庭

3. contains(callback, traversal)

下面咱們來定義contains 方法:

Tree.prototype.contains = function (callback, traversal) {
    traversal.call(this, callback);
};

它是這樣被調用的:

tree.contains(function (node) {
    if (node.data === 'two') {
        console.log(node);
    }
}, tree.traverseBF);

能夠看到, contains 方法實際上只是對樹的遍歷方法包裹多了一層而已:

  1. traversal 讓你決定定是遍歷方法是DFS, 仍是BFS
  2. callback 讓你指定的就是以前咱們定義traverseDF(callback) 或者 traverseBF(callback) 裏的callback 函數
  3. 函數體內 traversal.call(this, callback) , this 綁定到當前函數的執行環境對象, 在這裏來講tree.contains()... 的話, tree 就是 this

這和你直接調用traverseDF(callback) 或者 traverseBF(callback) 並無什麼不一樣, 只是提供了一個更一致的對外接口

4. add(data, toData, traversal)

通過前面的步驟咱們已經知道如何在一棵樹搜索一個節點了, 那麼咱們就能夠給某個特定的節點來添加子節點啦

Tree.prototype.add = function (data, toData, traversal) {
    var child = new Node(data),
        parent = null,
        callback = function (node) {
            if (node.data === toData) {
                parent = node;
            }
        };

    this.contains(callback, traversal);

    if (parent) {
        parent.children.push(child);
        child.parent = parent;
    } else {
        throw new Error('Cannot add node to a non-existent parent.');
    }
};

var tree = new Tree('CEO');

tree.add('VP of Happiness', 'CEO', tree.traverseBF);
tree.add('VP of Finance', 'CEO', tree.traverseBF);
tree.add('VP of Sadness', 'CEO', tree.traverseBF);

tree.add('Director of Puppies', 'VP of Finance', tree.traverseBF);
tree.add('Manager of Puppies', 'VP of Finance', tree.traverseBF);

/*

 tree

 'CEO'
 ├── 'VP of Happiness'
 ├── 'VP of Finance'
 │   ├── 'Director of Puppies'
 │   └── 'Manager of Puppies'
 └── 'VP of Sadness'

 */
 
 // 注: 原文此處的樹圖和代碼有點不對應, 應該是做者畫錯了, 這裏改了一下

感受不用再囉嗦了, 就是遍歷搜索節點, 找到的話new 一個Node設定好相互間的父子關係, 找不到這個特定的節點就拋出異常 : )

5. remove(data, fromData, traversal)

有了添加, 那就要有刪除嘛:

Tree.prototype.remove = function (data, fromData, traversal) {
    var childToRemove = null,
        parent = null,
        index;

    var callback = function (node) {
        if (node.data === fromData) {
            parent = node;
        }
    };

    this.contains(callback, traversal);

    if (parent) {
        index = findIndex(parent.children, data);

        if (index === undefined) {
            throw new Error('Node to removes not exist.')
        } else {
            childToRemove = parent.children.splice(index, 1);
        }
    } else {
        throw new Error('Parent does not exist.');
    }
};

function findIndex(arr, data) {
    var index;

    for (var i = 0; i < arr.length; i++) {
        if (arr[i].data === data) {
            index = i;
        }
    }

    return index;
}

tree.remove('Manager of Puppies', 'VP of Finance', tree.traverseDF);
tree.remove('VP of Sadness', 'CEO', tree.traverseDF);

/*

 tree

 'CEO'
 ├── 'VP of Happiness'
 └── 'VP of Finance'
    ├── 'Director of Puppies'
    └── 'Manager of Puppies'

 */

其實都是相似的套路, 另外, 數組的findIndex 方法已經存在於ES6的標準裏, 咱們大能夠直接使用而不用再次定義一個相似的方法.

這篇文章重點是如何創建一棵樹, 和遍歷方法DFS, BFS 的思想, 至於那些增刪改查, 只要懂得遍歷, 那都好辦, 具體狀況具體分析

好啦, 到這裏這些方法已經所有都實現了. 本文沒有逐字翻譯, 大部分是意譯, 和原文是有些出入的, 此外代碼也有一些邊角的改動, 並無一一指明.

原文連接: Data Structures With JavaScript: Tree

完整代碼, 或者訪問相應的JS Bin

更新(9/18): 若是你想看二叉樹的實現

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Tree in JS</title>
</head>
<body>
<script>
    /********************************** 構造函數 ********************************/
    function Node(data) {
        this.data = data;
        this.parent = null;
        this.children = [];
    }

    function Tree(data) {
        var node = new Node(data);
        this._root = node;
    }

    var tree = new Tree ('CEO');
    console.log(tree._root);

    /********************************** 1. traverseDF ********************************/
    Tree.prototype.traverseDF = function (callback) {
        (function recurse(currentNode) {
            // step2 遍歷當前節點的子節點們
            for (var i = 0, length = currentNode.children.length; i < length; i++) {
                // step3, 遞歸調用遍歷每一個子節點的子節點們
                recurse(currentNode.children[i]);
            }

            // step4 能夠在這裏寫你處理每個節點的回調函數
            callback(currentNode);

            // step1, 把根節點傳進來
        })(this._root);
    };

    var tree = new Tree('one');

    tree._root.children.push(new Node('two'));
    tree._root.children[0].parent = tree;

    tree._root.children.push(new Node('three'));
    tree._root.children[1].parent = tree;

    tree._root.children.push(new Node('four'));
    tree._root.children[2].parent = tree;

    tree._root.children[0].children.push(new Node('five'));
    tree._root.children[0].children[0].parent = tree._root.children[0];

    tree._root.children[0].children.push(new Node('six'));
    tree._root.children[0].children[1].parent = tree._root.children[0];

    tree._root.children[2].children.push(new Node('seven'));
    tree._root.children[2].children[0].parent = tree._root.children[2];

    /*

    creates this tree

     one
     ├── two
     │   ├── five
     │   └── six
     ├── three
     └── four
         └── seven

    */


    tree.traverseDF(function(node) {
        console.log(node.data)
    });

    /*

    logs the following strings to the console

    'five'
    'six'
    'two'
    'three'
    'seven'
    'four'
    'one'

    */

    /********************************** 2. traverseBF ********************************/
    Tree.prototype.traverseBF = function (callback) {
        var queue = [];

        queue.push(this._root);

        var currentNode = queue.shift();

        while (currentNode) {
            for (var i = 0, length = currentNode.children.length; i < length; i++) {
                queue.push(currentNode.children[i]);
            }

            callback(currentNode);
            currentNode = queue.shift();
        }
    };

    tree.traverseBF(function(node) {
        console.log(node.data)
    });

    /*

    logs the following strings to the console

    'one'
    'two'
    'three'
    'four'
    'five'
    'six'
    'seven'

    */

    /********************************** 3. contains ********************************/
    Tree.prototype.contains = function (callback, traversal) {
        traversal.call(this, callback);
    };

    tree.contains(function (node) {
        if (node.data === 'two') {
            console.log(node);
        }
    }, tree.traverseBF);

    /********************************** 4. add ********************************/
    Tree.prototype.add = function (data, toData, traversal) {
        var child = new Node(data),
            parent = null,
            callback = function (node) {
                if (node.data === toData) {
                    parent = node;
                }
            };

        this.contains(callback, traversal);

        if (parent) {
            parent.children.push(child);
            child.parent = parent;
        } else {
            throw new Error('Cannot add node to a non-existent parent.');
        }
    };

    var tree = new Tree('CEO');

    tree.add('VP of Happiness', 'CEO', tree.traverseBF);
    tree.add('VP of Finance', 'CEO', tree.traverseBF);
    tree.add('VP of Sadness', 'CEO', tree.traverseBF);

    tree.add('Director of Puppies', 'VP of Finance', tree.traverseBF);
    tree.add('Manager of Puppies', 'VP of Finance', tree.traverseBF);

    /*

     tree

     'CEO'
     ├── 'VP of Happiness'
     ├── 'VP of Finance'
     │   ├── 'Director of Puppies'
     │   └── 'Manager of Puppies'
     └── 'VP of Sadness'

     */

    /********************************** 5. remove ********************************/
    Tree.prototype.remove = function (data, fromData, traversal) {
        var childToRemove = null,
            parent = null,
            index;

        var callback = function (node) {
            if (node.data === fromData) {
                parent = node;
            }
        };

        this.contains(callback, traversal);

        if (parent) {
            index = findIndex(parent.children, data);

            if (index === undefined) {
                throw new Error('Node to removes not exist.')
            } else {
                childToRemove = parent.children.splice(index, 1);
            }
        } else {
            throw new Error('Parent does not exist.');
        }
    };

    function findIndex(arr, data) {
        var index;

        for (var i = 0; i < arr.length; i++) {
            if (arr[i].data === data) {
                index = i;
            }
        }

        return index;
    }

    tree.remove('Manager of Puppies', 'VP of Finance', tree.traverseDF);
    tree.remove('VP of Sadness', 'CEO', tree.traverseDF);

    /*

     tree

     'CEO'
     ├── 'VP of Happiness'
     └── 'VP of Finance'
        ├── 'Director of Puppies'
        └── 'Manager of Puppies'

     */

</script>
</body>
</html>
相關文章
相關標籤/搜索