學習JavaScript數據結構與算法 — 樹

定義

樹同散列表同樣,是一種非順序數據結構。現實中樹的例子有家譜、公司組織架構圖及其它樹形結構關係。
樹由一系列節點構成,每一個節點都有一個父節點(除根節點外)以及零個或多個子節點,如圖:javascript

clipboard.png

樹中的每個元素叫做節點,最頂部的節點叫做根節點。至少有一個子節點的節點稱爲內部節點(如圖中的七、九、1五、1三、20),沒有子節點的節點稱爲外部節點或葉節點(如圖中的三、 六、 八、 十、 十二、 14等)。一個節點能夠包括祖先及後代,祖先包括父節點、祖父節點等,後代包括子節點、孫節點等。一個節點及其後代能夠組成一個子樹(如圖中的1三、十二、14)。節點的深度指節點祖先節點的數量(如圖中的節點6的深度爲3)。樹的高度指樹中節點深度的最大值。上圖中,節點中的數字稱爲鍵。java

  • 二叉樹:二叉樹指節點最多隻能包含兩個子節點的樹,即左側子節點與右側子節點。二叉樹有利於高效地插入、查找、刪除節點。node

  • 二叉搜索樹:二叉搜索樹是二叉樹的一種,它的左側節點的值必須大於右側節點的值,上圖就是一個二叉搜索樹。優化的二叉搜索樹包括AVL樹、紅黑樹(待完成)等segmentfault

方法

  • insert(key):向樹中插入一個新的鍵。數據結構

  • search(key):在樹中查找一個鍵,若是節點存在,則返回true;若是不存在,則返回false。架構

  • inOrderTraverse:經過中序遍歷方式遍歷全部節點。函數

  • preOrderTraverse:經過先序遍歷方式遍歷全部節點。post

  • postOrderTraverse:經過後序遍歷方式遍歷全部節點。優化

  • min:返回樹中最小的值/鍵。this

  • max:返回樹中最大的值/鍵。

  • remove(key):從樹中移除某個鍵。

實現

首選實現二叉查找樹類的骨架

// 二叉查找樹類
function BinarySearchTree() {
    // 用於實例化節點的類
    var Node = function(key){
        this.key = key; // 節點的健值
        this.left = null; // 指向左節點的指針
        this.right = null; // 指向右節點的指針
    };
    var root = null; // 將根節點置爲null
}

insert方法,向樹中插入一個新的鍵。遍歷樹,將插入節點的鍵值與遍歷到的節點鍵值比較,若是前者大於後者,繼續遞歸遍歷右子節點,反之,繼續遍歷左子節點,直到找到一個空的節點,在該位置插入。

this.insert = function(key){
    var newNode = new Node(key); // 實例化一個節點
    if (root === null){
        root = newNode; // 若是樹爲空,直接將該節點做爲根節點
    } else {
        insertNode(root,newNode); // 插入節點(傳入根節點做爲參數)
    }
};
// 插入節點的函數
var insertNode = function(node, newNode){
    // 若是插入節點的鍵值小於當前節點的鍵值
    // (第一次執行insertNode函數時,當前節點就是根節點)
    if (newNode.key < node.key){
        if (node.left === null){
            // 若是當前節點的左子節點爲空,就直接在該左子節點處插入
            node.left = newNode;
        } else {
            // 若是左子節點不爲空,須要繼續執行insertNode函數,
            // 將要插入的節點與左子節點的後代繼續比較,直到找到可以插入的位置
            insertNode(node.left, newNode);
        }
    } else {
        // 若是插入節點的鍵值大於當前節點的鍵值
        // 處理過程相似,只是insertNode函數繼續比較的是右子節點
        if (node.right === null){
            node.right = newNode;
        } else {
            insertNode(node.right, newNode);
        }
    }
}

在下圖的樹中插入健值爲6的節點,過程以下:

clipboard.png

樹的遍歷

樹的遍歷指訪問樹的每個節點,並對節點作必定操做。遍歷樹主要有三種方式:中序、先序、後序。

中序遍歷

中序遍歷嚴格按照從小到大的方式遍歷樹,也就是優先訪問左子節點,至關於對樹進行了排序。

this.inOrderTraverse = function(callback){
    // callback用於對遍歷到的節點作操做
    inOrderTraverseNode(root, callback);
};
var inOrderTraverseNode = function (node, callback) {
    // 遍歷到node爲null爲止
    if (node !== null) {
        // 優先遍歷左邊節點,保證從小到大遍歷
        inOrderTraverseNode(node.left, callback);
        // 處理當前的節點
        callback(node.key);
        // 遍歷右側節點
        inOrderTraverseNode(node.right, callback);
    }
};

對下圖的樹作中序遍歷,並輸出各個節點的鍵值:

tree.inOrderTraverse(function(value){
        console.log(value);
});

依次輸出:3 5 6 7 8 9 10 11 12 13 14 15 18 20 25,
遍歷過程如圖:

clipboard.png

先序遍歷

先序遍歷先訪問節點自己,再遍歷其後代節點,最後遍歷其兄弟節點。它的一種應用是打印一個結構化的文檔。

this.preOrderTraverse = function(callback){
    // 一樣的,callback用於對遍歷到的節點作操做
    preOrderTraverseNode(root, callback);
};
var preOrderTraverseNode = function (node, callback) {
    // 遍歷到node爲null爲止
    if (node !== null) {
        callback(node.key); // 先處理當前節點
        preOrderTraverseNode(node.left, callback); // 再繼續遍歷左子節點
        preOrderTraverseNode(node.right, callback); // 最後遍歷右子節點
    }
};

用先序遍歷遍歷下圖所示的樹,並打印節點鍵值,輸出結果:11 7 5 3 6 9 8 10 15 13 12 14 20 18 25,
遍歷過程如圖:

clipboard.png

後序遍歷

後序遍歷先訪問節點的後代節點,再訪問節點自己。它的一種應用是計算一個目錄和它的子目錄中全部文件的大小。

this.postOrderTraverse = function(callback){
    postOrderTraverseNode(root, callback);
};
var postOrderTraverseNode = function (node, callback) {
    if (node !== null) {
        postOrderTraverseNode(node.left, callback); //{1}
        postOrderTraverseNode(node.right, callback); //{2}
        callback(node.key); //{3}
    }
};

能夠看到,中序、先序、後序遍歷的實現方式幾乎如出一轍,只是{1}、{2}、{3}行代碼的執行順序不一樣。
對下圖的樹進行後序遍歷,並打印鍵值:3 6 5 8 10 9 7 12 14 13 18 25 20 15 11,
遍歷過程如圖:

clipboard.png

樹的搜索

搜索最小值

在二叉搜索樹裏,不論是整個樹仍是其子樹,最小值必定在樹最左側的最底層。所以給定一顆樹或其子樹,只須要一直向左節點遍歷到底就好了。

this.min = function(node) {
    // min方法容許傳入子樹
    node = node || root;
    // 一直遍歷左側子節點,直到底部
    while (node && node.left !== null) {
        node = node.left;
    }
    return node;
};

搜索最大值

搜索最大值與搜索最小值相似,只是沿着樹的右側遍歷。

this.max = function(node) {
    // min方法容許傳入子樹
    node = node || root;
    // 一直遍歷左側子節點,直到底部
    while (node && node.right !== null) {
        node = node.right;
    }
    return node;
};

搜索特定值

搜索特定值的處理與插入值的處理相似。遍歷樹,將要搜索的值與遍歷到的節點比較,若是前者大於後者,則遞歸遍歷右側子節點,反之,則遞歸遍歷左側子節點。

this.search = function(key, node){
    // 一樣的,search方法容許在子樹中查找值
    node = node || root;
    return searchNode(key, node);
};
var searchNode = function(key, node){
    // 若是node是null,說明樹中沒有要查找的值,返回false
    if (node === null){
        return false;
    }
    if (key < node.key){
        // 若是要查找的值小於該節點,繼續遞歸遍歷其左側節點
        return searchNode(node.left, key);
    } else if (key > node.key){
        // 若是要查找的值大於該節點,繼續遞歸遍歷其右側節點
        return searchNode(node.right, key);
    } else {
        // 若是要查找的值等於該節點,說明查找成功,返回改節點
        return node;
    }
};

移除節點

移除節點,首先要在樹中查找到要移除的節點,再判斷該節點是否有子節點、有一個子節點或者有兩個子節點,最後分別處理。

this.remove = function(key, node){
    // 一樣的,容許僅在子樹中刪除節點
    node = node || root;
    removeNode(key, node);
};
var removeNode = function (node, key) {
    // 找到要刪除的節點
    var delete_node = this.search(node, key);
    // 若是node不存在,直接返回
    if (node === false) {
        return null;
    }
    // 第一種狀況,該節點沒有子節點
    if (node.left === null && node.right === null) {
        node = null;
        return node;
    }
    // 第二種狀況,該節點只有一個子節點的節點
    if (node.left === null) { // 只有右節點
        node = node.right;
        return node;
    } else if (node.right === null) { // 只有左節點
        node = node.left;
        return node;
    }
    // 第三種狀況,有有兩個子節點的節點
    // 將右側子樹中的最小值,替換到要刪除的位置
    // 找到最小值
    var aux = this.min(node.right);
    // 替換
    node.key = aux.key;
    // 刪除最小值
    node.right = removeNode(node.right, aux.key);
    return node;
}

第三種狀況的處理過程,以下圖所示。當要刪除的節點有兩個子節點時,爲了避免破壞樹的結構,刪除後要替補上來的節點的鍵值大小必須在已刪除節點的左、右子節點的鍵值之間,且替補上來的節點不該該有子節點,不然會產生一個節點有多個字節點的狀況,所以,找右側子樹的最小值替換上來。同理,找左側子樹的最大值替換上來也能夠。
clipboard.png

相關文章
相關標籤/搜索