【譯】JavaScript數據結構(4):樹

翻譯:瘋狂的技術宅
英文https://code.tutsplus.com/art...
說明:本文翻譯自系列文章《Data Structures With JavaScript》,總共爲四篇,原做者是在美國硅谷工做的工程師 Cho S. Kim。這是本系列的第四篇。javascript

說明:本專欄文章首發於公衆號:jingchengyideng 。html

樹是 web 開發中最經常使用的數據結構之一。 這種說法對開發者和用戶都是正確的。每一個編寫HTML的開發者,只要把網頁載入瀏覽器就會建立一個樹,樹一般被稱爲文檔對象模型(DOM)。相應地,每一個在互聯網上瀏覽信息的人,也都是以DOM樹的形式接受信息。 每一個編寫HTML而且將其加載到Web瀏覽器的Web開發人員都建立了一個樹,這被稱爲文檔對象模型(DOM)。互聯網上的全部用戶,在獲取信息時,都是以樹的形式收——即DOM。 java

如今,高潮來了:你正在讀的本文在瀏覽器中就是以樹的形式進行渲染的。文字由<p>元素進行表示;<p>元素又嵌套在<body>元素中;<body>元素又嵌套在<html>元素中。 您正在閱讀的段落表示爲<p>元素中的文本;<p>元素嵌套在<body>元素中;<body>元素嵌套在<html>元素中。node

這些嵌套數據和家族數相似。 <heml>是父元素,<body>是子元素,<p>又是<body>的子元素 若是這個比喻對你有點用的話,你將會發如今咱們介紹樹的時候會用到更多的類比。web

在本文中,咱們將會經過兩種不一樣的遍歷方式來建立一個樹:深度優先(DFS)和廣度優先(BFS)。 (若是你對遍歷這個詞感到比較陌生,不妨將他想象成訪問樹中的每個節點。) 這兩種類型的遍歷強調了與樹交互的不一樣方式, DFS和BFS分別用棧和隊列來訪問節點。 這聽起來很酷!瀏覽器

樹(深度搜索和廣度搜索)

在計算機科學中,樹是一種用節點來模擬分層數據的數據結構。每一個樹節點都包含他自己的數據及指向其餘節點的指針。數據結構

節點和指針這些術語可能對一些讀者來講比較陌生,因此讓咱們用類比來進一步描述他們。 讓咱們將樹與組織圖結構圖進行比較。 這個結構圖有一個頂級位置(根節點),好比CEO。 在這個節點下面還有一些其餘的節點,好比副總裁(VP)。app

爲了表示這種關係,咱們用箭頭從CEO指向VP。 一個位置,好比CEO,是一個節點;咱們建立的CEO到VP的關係是一個指針。 在咱們的組織結構圖中去建立更多的關係,咱們只要重複這些步驟便可---咱們讓一個節點指向另外一個節點。ide

在概念層次上,我但願節點和指針有意義。 在實際中,咱們能從更科學的實例中獲取收益。 讓咱們來思考DOM。 DOM有<html>元素做爲其頂級位置(根節點)。 這個節點指向<head>元素和<body>元素。 這些步驟在DOM的全部節點中重複。函數

這種設計的一個優勢是可以嵌套節點:例如:一個<ul>元素可以包含不少個<li>元素;此外,每一個<li>元素能擁有兄弟<li>元素。這很怪異,可是確實真實有趣!

操做樹

因爲每一個樹都包含節點,其能夠是來自樹的單獨構造器,咱們將概述兩個構造函數的操做:NodeTree

節點

  • data 存儲值。

  • parent 指向節點的父節點。

  • children 指向列表中的下一個節點。

  • _root 指向一個樹的根節點。

  • traverseDF(callback) 對樹進行DFS遍歷。

  • traverseBF(callback) 對樹進行BFS遍歷。

  • contains(data, traversal) 搜索樹中的節點。

  • add(data, toData, traverse) 向樹中添加節點。

  • remove(child, parent) 移除樹中的節點。

實現樹

如今開始寫樹的代碼!

節點的屬性

在實現中,咱們首先定義一個叫作Node的函數,而後構造一個Tree

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

每個Node的實例都包含三個屬性:dataparant,和children。 第一個屬性保存與節點相關聯的數據。 第二個屬性指向一個節點。 第三個屬性指向許多子節點。

樹的屬性

如今讓咱們來定義Tree的構造函數,其中包括Node構造函數的定義:

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

Tree包含兩行代碼。 第一行建立了一個Node的新實例;第二行讓node等於樹的根節點。

TreeNode的定義只須要幾行代碼。 可是,經過這幾行足以幫助咱們模擬分層數據。 爲了證實這一點,讓咱們用一些示例數據去建立Tree的示例(和間接的Node)。

var tree = new Tree('CEO');
 
// {data: 'CEO', parent: null, children: []}
tree._root;

幸虧有parentchildren的存在,咱們能夠爲_root添加子節點和讓這些子節點的父節點等於_root。 換一種說法,咱們能夠模擬分層數據的建立。

Tree的方法

接下來咱們將要建立如下五種方法。

  1. traverseDF(callback)

  2. traverseBF(callback)

  3. contains(data, traversal)

  4. add(child, parent)

  5. remove(node, parent)

由於每種方法都須要遍歷一個樹,因此咱們首先要實現一個方法去定義不一樣的樹遍歷。 (遍歷樹是訪問樹的每一個節點的正式方式。)

方法1/5: traverseDF(callback)

這種方法以深度優先方式遍歷樹。

Tree.prototype.traverseDF = function(callback) {
 
    // this is a recurse and immediately-invoking function 
    (function recurse(currentNode) {
        // step 2
        for (var i = 0, length = currentNode.children.length; i < length; i++) {
            // step 3
            recurse(currentNode.children[i]);
        }
 
        // step 4
        callback(currentNode);
         
        // step 1
    })(this._root);
 
};

traverseDF(callback)有一個參數callback。 若是對這個名字不明白,callback被假定是一個函數,將在後面被traverseDF(callback)調用。

traverseDF(callback)的函數體含有另外一個叫作recurse的函數。 這個函數是一個遞歸函數! 換句話說,它是自我調用和自我終止。 使用recurse的註釋中提到的步驟,我將描述遞歸用來recurse整個樹的通常過程。

這裏是步驟:

  1. 當即使用樹的根節點做爲其參數調用recurse。 此時,currentNode指向當前節點。

  2. 進入for循環而且從第一個子節點開始,每個子節點都迭代一次currentNode函數。

  3. for循環體內,使用currentNode的子元素調用遞歸。 確切的子節點取決於當前for循環的當前迭代。

  4. currentNode不存在子節點時,咱們退出for循環並callback咱們在調用traverseDF(callback)期間傳遞的回調。

步驟2(自終止),3(自調用)和4(回調)重複,直到咱們遍歷樹的每一個節點。

遞歸是一個很是困難的話題,須要一個完整的文章來充分解釋它。因爲遞歸的解釋不是本文的重點 —— 重點是實現一棵樹 —— 我建議任何讀者沒有很好地掌握遞歸作如下兩件事。

首先,實驗咱們當前的traverseDF(callback)實現,並嘗試必定程度上理解它是如何工做的。 第二,若是你想要我寫一篇關於遞歸的文章,那麼請在本文的評論中請求它。

如下示例演示如何使用traverseDF(callback)遍歷樹。要遍歷樹,我將在下面的示例中建立一個。我如今使用的方法不是罪理想的,但它能很好的工做。 一個更好的方法是使用add(value),咱們將在第4步和第5步中實現。

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/5: traverseBF(callback)

這個方法使用深度優先搜索去遍歷樹

深度優先搜索和廣度優先搜索之間的差異涉及樹的節點訪問的序列。 爲了說明這一點,讓咱們使用traverseDF(callback)建立的樹。

/* 
 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'
 
*/

來自控制檯的日誌和咱們的樹的圖顯示了關於廣度優先搜索的模式。從根節點開始;而後行進一個深度並訪問該深度從左到右的每一個節點。重複此過程,直到沒有更多的深度要移動。

因爲咱們有一個廣度優先搜索的概念模型,如今讓咱們實現使咱們的示例工做的代碼。

Tree.prototype.traverseBF = function(callback) {
    var queue = new Queue();
     
    queue.enqueue(this._root);
 
    currentTree = queue.dequeue();
 
    while(currentTree){
        for (var i = 0, length = currentTree.children.length; i < length; i++) {
            queue.enqueue(currentTree.children[i]);
        }
 
        callback(currentTree);
        currentTree = queue.dequeue();
    }
};

咱們對traverseBF(callback)的定義包含了不少邏輯。 所以,我會用下面的步驟解釋這些邏輯:

  1. 建立 Queue的實例。

  2. 調用traverseBF(callback)產生的節點添加到Queue的實例。

  3. 定義一個變量currentNode而且將其值初始化爲剛纔添加到隊列裏的node

  4. currentNode指向一個節點時,執行wille循環裏面的代碼。

  5. for循環去迭代currentNode的子節點。

  6. for循環體內,將每一個子元素加入隊列。

  7. 獲取currentNode並將其做爲callback的參數傳遞。

  8. currentNode從新分配給正從隊列中刪除的節點。

  9. 直到currentNode再也不指向任何節點——也就是說樹中的每一個節點都訪問過了——重複4-8步。

方法3/5 contains(callback, traversal)

讓咱們定義一個方法,能夠在樹中搜索一個特定的值。去使用咱們建立的任意一種樹的遍歷方法,咱們已經定義了contains(callback, traversal)接收兩個參數:搜索的數據和遍歷的類型。

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

contains(callback, traversal)函數體內,咱們用call方法去傳遞thiscallback。 第一個參數將traversal綁定到被調用的樹contains(callback,traversal);第二個參數是在樹中每一個節點上調用的函數。

想象一下,咱們要將包含奇數數據的任何節點記錄到控制檯,並使用BFS遍歷樹中的每一個節點。 咱們能夠這麼寫代碼:

// tree is an example of a root node
tree.contains(function(node){
    if (node.data === 'two') {
        console.log(node);
    }
}, tree.traverseBF);
add(data, toData, traversal)

方法4/5: 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.');
    }
};

add(data, toData, traversal)定義了三個參數。 第一個參數data用來建立一個Node的新實例。 第二個參數toData用來比較樹中的每一個節點。 第三個參數traversal,是這個方法中用來遍歷樹的類型。

add(data, toData, traversal)函數體內,咱們聲明瞭三個變量。 第一個變量child表明初始化的Node實例。 第二個變量parent初始化爲null;可是未來會指向匹配toData值的樹中的任意節點。parent的從新分配發生在咱們聲明的第三個變量,這就是callback

callback是一個將toData和每個節點的data屬性作比較的函數。 若是if語句的值是true,那麼parent將被賦值給if語句中匹配比較的節點。

每一個節點的toDatacontains(callback, traversal)中進行比較。遍歷類型和callback必須做爲contains(callback, traversal)的參數進行傳遞。

最後,若是parent不存在於樹中,咱們將child推入parent.children; 同時也要將parent賦值給child的父級。不然,將拋出錯誤。

讓咱們用add(data, toData, traversal)作個例子:

var tree = new Tree('CEO');
 
tree.add('VP of Happiness', 'CEO', tree.traverseBF);
 
/*
 
our tree
 
'CEO'
└── 'VP of Happiness'
 
*/

這裏是add(addData, toData, traversal)的更加複雜的例子:

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', 'Director of Puppies', tree.traverseBF);
 
/*
 
 tree
 
 'CEO'
 ├── 'VP of Happiness'
 ├── 'VP of Finance'
 │   ├── 'Director of Puppies'
 │   └── 'Manager of Puppies'
 └── 'VP of Sadness'
 
 */

方法5/5:remove(data, fromData, traversal)

爲了完成Tree的實現,咱們將添加一個叫作remove(data, fromData, traversal)的方法。 跟從DOM裏面移除節點相似,這個方法將移除一個節點和他的全部子級。

Tree.prototype.remove = function(data, fromData, traversal) {
    var tree = this,
        parent = null,
        childToRemove = 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 remove does not exist.');
        } else {
            childToRemove = parent.children.splice(index, 1);
        }
    } else {
        throw new Error('Parent does not exist.');
    }
 
    return childToRemove;
};

add(data, toData, traversal)相似,移除將遍歷樹以查找包含第二個參數的節點,如今爲fromData。 若是這個節點被發現了,那麼parent將指向它。

在這時候,咱們到達了第一個if語句。 若是parent不存在,將拋出錯誤。 若是parent不存在,咱們使用parent.children調用findIndex()和咱們要從parent節點的子節點中刪除的數據 (findIndex()是一個幫助方法,我將在下面定義。)

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

findIndex()裏面,如下邏輯將發生。 若是parent.children中的任意一個節點包含匹配data值的數據,那麼變量index賦值爲一個整數。 若是沒有子級的數值屬性匹配data,那麼index保留他的默認值undefined。 在最後一行的findIndex()方法,咱們返回一個index。

咱們如今去remove(data, fromData, traversal) 若是index的值是undefined,將會拋出錯誤。 若是index的值存在,咱們用它來拼接咱們想從parent的子節點中刪除的節點。一樣咱們給刪除的子級賦值爲childToRemove

最後,咱們返回childToRemove

樹的的完整實現

到此爲止Tree已經徹底實現。回過頭看看,咱們到底完成了多少工做:

function Node(data) {
    this.data = data;
    this.parent = null;
    this.children = [];
}
 
function Tree(data) {
    var node = new Node(data);
    this._root = node;
}
 
Tree.prototype.traverseDF = function(callback) {
 
    // this is a recurse and immediately-invoking function
    (function recurse(currentNode) {
        // step 2
        for (var i = 0, length = currentNode.children.length; i < length; i++) {
            // step 3
            recurse(currentNode.children[i]);
        }
 
        // step 4
        callback(currentNode);
 
        // step 1
    })(this._root);
 
};
 
Tree.prototype.traverseBF = function(callback) {
    var queue = new Queue();
 
    queue.enqueue(this._root);
 
    currentTree = queue.dequeue();
 
    while(currentTree){
        for (var i = 0, length = currentTree.children.length; i < length; i++) {
            queue.enqueue(currentTree.children[i]);
        }
 
        callback(currentTree);
        currentTree = queue.dequeue();
    }
};
 
Tree.prototype.contains = function(callback, traversal) {
    traversal.call(this, callback);
};
 
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.');
    }
};
 
Tree.prototype.remove = function(data, fromData, traversal) {
    var tree = this,
        parent = null,
        childToRemove = 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 remove does not exist.');
        } else {
            childToRemove = parent.children.splice(index, 1);
        }
    } else {
        throw new Error('Parent does not exist.');
    }
 
    return childToRemove;
};
 
function findIndex(arr, data) {
    var index;
 
    for (var i = 0; i < arr.length; i++) {
        if (arr[i].data === data) {
            index = i;
        }
    }
 
    return index;
}

總結

樹能夠用來模擬分層數據。咱們周圍有許多相似這種類型的層次結構,例如網頁和族譜。當你發現本身須要使用層次結構來結構化數據時,能夠考慮使用樹。

歡迎掃描二維碼關注公衆號,天天推送我翻譯的技術文章。

圖片描述

相關文章
相關標籤/搜索