05 Javascript資料結構與演算法 之 樹

zhaoyezi發表於2018-08-27

1. 樹的定義

什麼是樹?最常見的樹就是家譜公司組織架構

05 Javascript資料結構與演算法 之 樹

  • 一個樹結構,包含一系列存在父子關係的節點。除了頂層(0層:第一個節點)都有父節點。
  • 頂層元素:叫做根節點(11)
  • 每個元素:叫做節點
  • 至少有一個子節點的元素:叫做內部節點
  • 沒有子元素節點:叫做外部節點葉節點
  • 由節點和它的子元素構成:叫做子樹
  • 節點有一個屬性: 深度(由它的祖先節點數量確定)。比如3的深度是3(祖先節點有三個:11,7,8)
  • 樹的高度: 取決於所有節點深度的最大值。

05 Javascript資料結構與演算法 之 樹

2. 二叉樹/二叉搜尋樹

二叉樹中的節點最多隻能有兩個子節點:一個是左側子節點,一個是右側子節點。這些定義有助於我們寫出更高效的從樹中插入、刪除、查詢節點的方法。

二叉搜尋樹(BST[Binary Search Tree])。但是它只允許左側節點儲存比父節點的值。在右側節點儲存比父節點的值。上圖則是二叉搜尋樹的儲存方式。

3. 二叉搜尋樹例項

05 Javascript資料結構與演算法 之 樹

如圖在樹的結構中,有指向左節點的指標,有指向右節點的指標。同LinkedList一樣,我們會宣告一個變數,來控制此資料結構第一個節點(根節點)。
還有一點:以前連結串列中,我們將一個節點稱之為節點或項, 在這裡我們將其稱為

  • insert(key): 想樹中插入一個新的鍵。分為root未空節點root為非空
  • search(key): 在樹中查詢一個鍵,存在返回true,不存在返回false
  • inOrderTraverse: 中序遍歷方式(從最小最大的順序訪問所有節點,先左,再本身,再右),中序遍歷的一種應用是一種對樹的排序操作。

05 Javascript資料結構與演算法 之 樹

  • preOrderTraverse: 先序遍歷方式(優先於後代節點的順序訪問每個節點,先本身,後左,再右),先序遍歷的一種應用是列印樹的結構。

05 Javascript資料結構與演算法 之 樹

  • postOrderTraverse: 後序遍歷方式(先訪問節點的後代節點,再訪問節點本身=> 先左,後右,在本身),後續遍歷的一種應用是計算一個目錄和它的子目錄中所有檔案所佔空間的大小

05 Javascript資料結構與演算法 之 樹

  • min: 返回樹中最小的鍵。
  • max: 返回樹中最大的鍵。

05 Javascript資料結構與演算法 之 樹

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

05 Javascript資料結構與演算法 之 樹

05 Javascript資料結構與演算法 之 樹

05 Javascript資料結構與演算法 之 樹

function BinarySearchTree() {
    // 節點類
    function Node(key) {
        this.key = key;
        this.left = null;
        this.right = null;
    }

    // root 根節點: 存放頂層節點
    let root = null;

    // 插入節點
    // 1. 建立新節點Node例項
    // 2. 例項是樹中第一個節點:直接賦值給 root 物件
    // 3. 例項是非根節點位置: 使用輔助函式 insertNode 來幫助插入
    this.insert =  (key) => {
        let node = new Node(key);
        if (!root) {
            root = node;
        } else {
            insertNode(root, node);
        }
    };

    // 輔助函式 insertNode
    // 1. 樹非空,需要查詢到新節點的位置。因此需要傳入樹的 根節點 和 插入的新節點
    // 2. 新節點的鍵 < 當前節點的鍵(當前節點就是根節點),檢查當前節點左節點是否為空,為空則插入新節點。如果有左側子節點,則遞迴呼叫insertNode,繼續找樹的下一層,下一次比較的將會是當前節點的左側子節點
    // 3. 新節點 > 當前節點,當前節點右側節點為空,則插入到右側節點,不為空,繼續遞迴呼叫insertNode方法,下一次要比較的節點是右側子節點
    let insertNode = (node, newNode) => {
        if (newNode.key < node.key) {
            // 如果current node 節點沒有左節點,直接將newNode賦值
            if (!node.left) {
                node.left = newNode;
            } else {
                // 遞迴處理新node和node.left
                insertNode(node.left, newNode);
            }
        } else {
            if (!node.right) {
                node.right = newNode;
            } else {
                insertNode(node.right, newNode);
            }
        }
    };

    // 從最小到最大的訪問順序,通過內部方法 inOrderTraverseNode 實現
    this.inOrderTraverse = (callback) => {
        inOrderTraverseNode(root, callback);
    };

    // 如果node不為空
    // 1. 先遞迴node的左側節點,深度遞迴下去,會從最小的值執行
    // 2. 呼叫callback值
    // 3. 再遞迴處理node的右側節點
    let inOrderTraverseNode = (node, callback) => {
        if (node) {
            inOrderTraverseNode(node.left, callback);
            callback(node.key);
            inOrderTraverseNode(node.right, callback);
        }
    };

    // 先序遍歷
    this.preOrderTraverse = (callback) => {
        preOrderTraverseNode(root, callback);
    };
    let preOrderTraverseNode = (node, callback) => {
        // 先列印節點,再遞迴處理左節點,遞迴處理右節點
        if (node) {
            callback(node.key);
            preOrderTraverseNode(node.left, callback);
            preOrderTraverseNode(node.right, callback);
        }
    };

    // 後續遍歷
    this.postOrderTraverse = (callback) => {
        postOrderTraverseNode(root, callback);
    };
    let postOrderTraverseNode = (node, callback) => {
        // 先左,再後,再自身
        if (node) {
            postOrderTraverseNode(node.left, callback);
            postOrderTraverseNode(node.right, callback);
            callback(node.key);
        }
    };

    // 最小值
    this.min = () => {
    return findMinNode(root).key;        
    };
    let findMinNode = (node) => {
        if (node) {
            while(node && node.left) {  
                node = node.left;
            };
            return node;
        } else {
            return null;
        }
    }
    // 最大值
    this.max = () => {
        let node = root;
        if (node) {
            while(node && node.right) {
                node = node.right;
            }
            return node.key;
        } else {
            return null;
        }
    };

    // 搜尋指定值
    this.search = (key) => {
        return searchNode(root, key);
    };
    let searchNode = (node, key) => {
        if (node === null) {
            return false;
        }
        // 查詢的key大於當前key,則從右邊查詢
        if (node.key < key) {
            return searchNode(node.right, key);
        } else if (node.key > key) {
            return searchNode(node.left, key);
        } else {
            return true;
        }
    };

    // 移除節點
    this.remove = (key) => {
        root = removeNode(root, key);
    };
    let removeNode = (node, key) => {
        if (node === null) {
            return null
        }
        // 每一次對比,都會將處理完後的node返回,方便將更新後的node賦值到父節點的指標上(也可以將父節點傳入,在內部進行賦值)
       if (key < node.key) {
           node.left = removeNode(node.left, key);
           return node;
       } else if (key > node.key) {
           node.right = removeNode(node.right, key);
           return node;
       } else {
           // 當key相等時,分為三種情況(currentNode)
           // 1. 葉子節點,無任何子節點,需要將currentNode移除【設定為null,並返回】
           if (node.left === null && node.right === null) {
               node = null;
               return node;
           }
           // 2. 節點只有一個子節點【左節點或右節點】,將父節點指向 curretNode的left(左節點存在指向左節點) / right(右節點存在指向右節點)
           if (node.left === null) {
               node = node.right;
               return node;
           } else if (node.right === null) {
               node = node.left;
               return node;
           }
           // 3. curretNode節點存在兩個節點
             // 3-1. 查詢node.right 最小的節點
             // 3-2. 將最小節點賦值給currentNode
             // 3-3. 移除之前的currentNode(將修改後返回,currentNode的父節點的指向會重新指向修改後的currentNode )
           let aux = findMinNode(node.right);
           // 將最小值更改到currentNode
           node.key = aux.key;
           // 從node.right中移除掉最小值(因為已經複製了被刪除的node中)
           node.right = removeNode(node.right, aux.key);
           // 返回最新的node
           return node;
       }
    }
}
複製程式碼

想要插入出上面途中的樹來,可以通過下面的插入方式:

let tree = new BinarySearchTree();
// 建立頂層root節點
tree.insert(11);
// 存放root節點left
tree.insert(7);
// 存放root節點right
tree.insert(15);
// 第一次11比較,第二次7比較,存放7的left
tree.insert(5);
// 第一次11比較,第二次7比較,第三次5比較,存放5的left
tree.insert(3);
// 第一次11比較,第二次7比較,第三次5比較,存放5的right
tree.insert(6);
// 第一次11比較,第二次7比較,存放7的 right
tree.insert(9);
// 第一次11比較,第二次7比較,第三次9比較,存放9的 left
tree.insert(8);
// 第一次11比較,第二次7比較,第三次9比較,存放9的 right
tree.insert(10);
// 第一次11比較,第二次15比較,存放15的left
tree.insert(13);
// 第一次11比較,第二次15比較,第三次13比較,存放13的left
tree.insert(12);
// 第一次11比較,第二次15比較,第三次13比較,存放13的right
tree.insert(14);
// 第一次11比較,第二次15比較,存放15的right
tree.insert(20);
// 第一次11比較,第二次15比較,第三次20比較,存放20的left
tree.insert(18);
// 第一次11比較,第二次15比較,第三次20比較,存放20的right
tree.insert(25);


function printNode(value){
console.log(value);
}


// 中序遍歷
tree.inOrderTraverse(printNode);

// 先序遍歷
tree.preOrderTraverse(printNode);

// 後續遍歷
tree.postOrderTraverse(printNode);

console.log(tree.min());
console.log(tree.max());
console.log(tree.search(15));

// 移除節點
// 移除不帶子節點的節點
tree.remove(6);
tree.inOrderTraverse(printNode);
// 移除帶有一個子節點的節點
tree.remove(5);
tree.inOrderTraverse(printNode);
// 移除帶有2個子節點的節點
tree.remove(15);
tree.inOrderTraverse(printNode);
複製程式碼

相關文章