日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

[译文] 初学者应该了解的数据结构: Tree

發布時間:2024/4/13 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 [译文] 初学者应该了解的数据结构: Tree 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

原文鏈接:Tree Data Structures for Beginners

眾成翻譯地址:初學者應該了解的數據結構: Tree

系列文章,建議不了解樹的同學慢慢閱讀一下這篇文章,希望對你有所幫助~至于系列的最后一篇自平衡二叉搜索樹就不再翻譯了(主要是原文坑太多,很難填),以下是譯文正文:

Tree 是很多(上層的)數據結構(如 Map、Set 等)的基礎。同時,在數據庫中快速搜索(元素)也用到了樹。HTML 的 DOM 節點也通過樹來表示對應的層次結構。以上僅僅是樹在實際應用中的一小部分例子。在這篇文章中,我們將探討不同類型的樹,如二叉樹、二叉搜索樹以及如何實現它們。

在上一篇文章(譯文)中,我們探討了數據結構:圖,它是樹一般化的情況。讓我們開始學習樹吧!


本篇是以下教程的一部分(譯者注:如果大家覺得還不錯,我會翻譯整個系列的文章):

初學者應該了解的數據結構與算法(DSA)

  • 算法的時間復雜性與大 O 符號
  • 每個程序員應該知道的八種時間復雜度
  • 初學者應該了解的數據結構:Array、HashMap 與 List (譯文)
  • 初學者應該了解的數據結構: Graph (譯文)
  • 初學者應該了解的數據結構:Tree ? 即本文
  • 自平衡二叉搜索樹
  • 附錄 I:遞歸算法分析

  • 樹的基本概念

    在樹中,每個節點可有零個或多個子節點,每個節點都包含一個。和圖一樣,節點之間的連接被稱為。樹是圖的一種,但并不是所有圖都是樹(只有無環無向圖才是樹)。

    這種數據類型之所以被稱為樹,是因為它長得很像一棵(倒置的)樹 ?。它從節點出發,它的子節點是它的分支,沒有任何子節點的節點就是樹的葉子(即葉節點)。

    以下是樹的一些屬性:

    • 最頂層的節點被稱為(root)節點(譯者注:即沒有任何父節點的節點)。
    • 沒有任何子節點的節點被稱為節點(leaf node)或者終端節點(terminal node)。
    • 樹的(Height)是最深的葉節點與根節點之間的距離(即邊的數量)。
      • A 的度是 3。
      • I 的度是 0(譯者注:子樹也是樹,I 的度是指 I 為根節點的子樹的度)。
    • 深度(Depth)或者層次(level)是節點與根節點的距離。
      • H 的層次是 2。
      • B 的層次是 1。

    樹的簡單實現

    正如此前所見,樹的節點有一個值,且存有它每一個子節點的引用。

    以下是節點的例子:

    class TreeNode {constructor(value) {this.value = value;this.descendents = [];} } 復制代碼

    我們可以創建一棵樹,它有三個葉節點:

    // create nodes with values const abe = new TreeNode('Abe'); const homer = new TreeNode('Homer'); const bart = new TreeNode('Bart'); const lisa = new TreeNode('Lisa'); const maggie = new TreeNode('Maggie'); // associate root with is descendents abe.descendents.push(homer); homer.descendents.push(bart, lisa, maggie); 復制代碼

    這樣就完成啦,我們有了一棵樹!

    Simpson tree data structure

    節點 abe 是節點,而節點 bart、lisa 和 maggie 則是這棵樹的 節點。注意,樹的節點的子節點可以是任意數量的,無論是 0 個、1 個、3 個或是多個均可。

    二叉樹

    樹的節點可以有 0 個或多個子節點。然而,當一棵樹(的所有節點)最多只能有兩個子節點時,這樣的樹被稱為二叉樹。

    二叉樹是樹中最常見的形式之一,它應用廣泛,如:

    • Maps
    • Sets
    • 數據庫
    • 優先隊列
    • 在 LDAP(Lightweight Directory Access Protocol)中查找相應信息。
    • 在 XML/HTML 文件中,使用文檔對象模型(DOM)接口進行搜索。

    完滿二叉樹、完全二叉樹、完美二叉樹

    取決于二叉樹節點的組織方式,一棵二叉樹可以是完滿二叉樹、完全二叉樹完美二叉樹

    • 完滿二叉樹(Full binary tree):除去葉節點,每個節點都有兩個子節點。
    • 完全二叉樹(Complete binary tree):除了最深一層之外,其余所有層的節點都必須有兩個子節點(譯者注:其實還需要最深一層的節點均集中在左邊,即左對齊)。
    • 完美二叉樹(Perfect binary tree):滿足完全二叉樹性質,樹的葉子節點均在最后一層(也就是形成了一個完美的三角形)。

    (譯者注:國內外的定義是不同的,此處根據原文與查找的資料,作了一定的修改,用的是國外的標準)

    下圖是上述概念的例子:

    Full vs. Complete vs. Perfect Binary Tree

    完滿二叉樹、完全二叉樹與完美二叉樹并不總是互斥的:

    • 完美二叉樹必然是完滿二叉樹和完全二叉樹。
      • 完美的二叉樹正好有 2 的 k 次方 減 1 個節點,其中 k 是樹的最深一層(從1開始)。.
    • 完全二叉樹并不總是完滿二叉樹。
      • 正如上面的完全二叉樹例子,最右側的灰色節點是它父子點僅有的一個子節點。如果移除掉它,這棵樹就既是完全二叉樹,也是完滿二叉樹。(譯者注:其實有了那個灰色節點的話,這顆樹不能算是完全二叉樹的,因為完滿二叉樹需要左對齊)
    • 完滿二叉樹并不一定是完全二叉樹與完美二叉樹。

    二叉搜索樹 (BST)

    二叉搜索樹(Binary Search Tree,簡寫為:BST)是二叉樹的特定應用。BST 的每個節點如二叉樹一樣,最多只能有兩個子節點。然而,BST 左子節點的值必須小于父節點的值,而右子節點的值則必須大于父節點的值。

    強調一下:一些 BST 并不允許重復值的節點被添加到樹中,如若允許,那么重復值的節點將作為右子節點。有些二叉搜索樹的實現,會記錄起重復的情況(這也是接下來我們需要實現的)。

    一起來實現二叉搜索樹吧!

    BST 的實現

    BST 的實現與上文樹的實現相像,然而有兩點不同:

    • 節點最多只能擁有兩個子節點。
    • 節點的值滿足以下關系:左子節點 < 父節點 < 右子節點。

    以下是樹節點的實現,與之前樹的實現類似,但會為左右子節點添加 getter 與 setter。請注意,實例中會保存父節點的引用,當添加新的子節點時,將更新(子節點中)父節點的引用。

    const LEFT = 0; const RIGHT = 1; class TreeNode {constructor(value) {this.value = value;this.descendents = [];this.parent = null;//譯者注:原文并沒有以下兩個屬性,但不加上去話下文的實現會報錯this.newNode.isParentLeftChild = false;this.meta = {}; }get left() {return this.descendents[LEFT];}set left(node) {this.descendents[LEFT] = node;if (node) {node.parent = this;}}get right() {return this.descendents[RIGHT];}set right(node) {this.descendents[RIGHT] = node;if (node) {node.parent = this;}} } 復制代碼

    OK,現在已經可以添加左右子節點。接下來將編寫 BST 類,使其滿足 左子節點 < 父節點 < 右子節點。

    class BinarySearchTree {constructor() {this.root = null;this.size = 0;}add(value) { /* ... */ }find(value) { /* ... */ }remove(value) { /* ... */ }getMax() { /* ... */ }getMin() { /* ... */ } } 復制代碼

    下面先編寫插入新節點相關的的代碼。

    BST 節點的插入

    要將一個新的節點插入到二叉搜索樹中,我們需要以下三步:

  • 如果樹中沒有任何節點,第一個節點當成為根節點。
  • (將新插入節點的值)與樹中的根節點或樹節點進行對比,如果值 更大,則放至右子樹(進行下一次對比),反之放到左子樹(進行對比) 。如果值一樣,則說明被重復添加,可增加重復節點的計數。
  • 重復第二點操作,直至找到空位插入新節點。
  • 讓我們通過以下例子來說明,樹中將依次插入30、40、10、15、12、50:

    Inserting nodes on a Binary Search Tree (BST)

    代碼實現如下:

    add(value) {const newNode = new TreeNode(value);if (this.root) {const { found, parent } = this.findNodeAndParent(value);if (found) { // duplicated: value already exist on the treefound.meta.multiplicity = (found.meta.multiplicity || 1) + 1;} else if (value < parent.value) {parent.left = newNode;//譯者注:原文并沒有這行代碼,但不加上去的話下文實現會報錯newNode.isParentLeftChild = true;} else {parent.right = newNode;}} else {this.root = newNode;}this.size += 1;return newNode; } 復制代碼

    我們使用了名為 findNodeAndParent 的輔助函數。如果(與新插入節點值相同的)節點已存在于樹中,則將節點統計重復的計數器加一??纯催@個輔助函數該如何實現:

    findNodeAndParent(value) {let node = this.root;let parent;while (node) {if (node.value === value) {break;}parent = node;node = ( value >= node.value) ? node.right : node.left;}return { found: node, parent }; } 復制代碼

    findNodeAndParent 沿著樹的結構搜索值。它從根節點出發,往左還是往右搜索取決于節點的值。如果已存在相同值的節點,函數返回找到的節點(即相同值的節點)與它的父節點。如果沒有相同值的節點,則返回最后找到的節點(即將變為新插入節點父節點的節點)。

    BST 節點的刪除

    我們已經知道如何(在二叉搜索樹中)插入與查找值,現在將實現刪除操作。這比插入而言稍微麻煩一點,讓我們用下面的例子進行說明:

    刪除葉節點(即沒有任何子節點的節點)

    30 30/ \ remove(12) / \ 10 40 ---------> 10 40\ / \ \ / \15 35 50 15 35 50/ 12* 復制代碼

    只需要刪除父節點(即節點 #15)中保存著的 節點 #12 的引用即可。

    刪除有一個子節點的節點

    30 30/ \ remove(10) / \ 10* 40 ---------> 15 40\ / \ / \15 35 50 35 50 復制代碼

    在這種情況中,我們將父節點 #30 中保存著的子節點 #10 的引用,替換為子節點的子節點 #15 的引用。

    刪除有兩個子節點的節點

    30 30/ \ remove(40) / \ 15 40* ---------> 15 50/ \ /35 50 35 復制代碼

    待刪除的節點 #40 有兩個子節點(#35 與 #50)。我們將待刪除節點替換為節點 #50。待刪除的左子節點 #35 將在原位不動,但它的父節點已被替換。

    另一個刪除節點 #40 的方式是:將左子節點 #35 移到節點 #40 的位置,右子節點位置保持不變。

    30/ \ 15 35\50 復制代碼

    兩種形式都可以,這是因為它們都遵循了二叉搜索樹的原則:左子節點 < 父節點 < 右子節點。

    刪除根節點

    30* 50/ \ remove(30) / \ 15 50 ---------> 15 35/35 復制代碼

    刪除根節點與此前討論的機制情況差不多。唯一的區別是需要更新二叉搜索樹實例中根節點的引用。

    以下的動畫是上述操作的具體展示:

    Removing a node with 0, 1, 2 children from a binary search tree

    在動畫中,被移動的節點是左子節點或者左子樹,右子節點或右子樹位置保持不變。

    關于刪除節點,已經有了思路,讓我們來實現它吧:

    remove(value) {const nodeToRemove = this.find(value);if (!nodeToRemove) return false;// Combine left and right children into one subtree without nodeToRemoveconst nodeToRemoveChildren = this.combineLeftIntoRightSubtree(nodeToRemove);if (nodeToRemove.meta.multiplicity && nodeToRemove.meta.multiplicity > 1) {nodeToRemove.meta.multiplicity -= 1; // handle duplicated} else if (nodeToRemove === this.root) {// Replace (root) node to delete with the combined subtree.this.root = nodeToRemoveChildren;this.root.parent = null; // clearing up old parent} else {const side = nodeToRemove.isParentLeftChild ? 'left' : 'right';const { parent } = nodeToRemove; // get parent// Replace node to delete with the combined subtree.parent[side] = nodeToRemoveChildren;}this.size -= 1;return true; } 復制代碼

    以下是實現中一些要注意的地方:

    • 首先,搜索待刪除的節點是否存在。如果不存在,返回 false。
    • 如果待刪除的節點存在,則將它的左子樹合并到右子樹中,組合為一顆新子樹。
    • 替換待刪除的節點為組合好的子樹。

    將左子樹組合到右子樹的函數如下:

    combineLeftIntoRightSubtree(node) {if (node.right) {//譯者注:原文是 getLeftmost,尋找左子樹最大的節點,這肯定有問題,應該是找最小的節點才對const leftLeast = this.getLefLeast(node.right); leftLeast.left = node.left;return node.right;}return node.left; } 復制代碼

    正如下面例子所示,我們想把節點 #30 刪除,將待刪除節點的左子樹整合到右子樹中,結果如下:

    30* 40/ \ / \ 10 40 combine(30) 35 50\ / \ -----------> /15 35 50 10\15 復制代碼

    現在把新的子樹的根節點作為整個二叉樹的根節點,節點 #30 將不復存在!

    二叉樹的遍歷

    根據遍歷的順序,二叉樹的遍歷有若干種形式:中序遍歷、先序遍歷與后序遍歷。同時,我們也可以使用在《初學者應該了解的數據結構: Graph》 (譯文一文中學到的 DFS 或 BFS 來遍歷整棵樹。以下是具體的實現:

    中序遍歷(In-Order Traversal)

    中序遍歷訪問節點的順序是:左子節點、節點本身、右子節點。

    * inOrderTraversal(node = this.root) {if (node.left) { yield* this.inOrderTraversal(node.left); }yield node;if (node.right) { yield* this.inOrderTraversal(node.right); }} 復制代碼

    用以下這棵樹作為例子:

    10/ \5 30/ / \4 15 40/ 3 復制代碼

    中序遍歷將按照以下順序輸出對應的值:3、4、5、10、15、30、40。也就是說,如果待遍歷的樹是一顆二叉搜索樹,那輸出值的順序將是升序的。

    后序遍歷(Post-Order Traversal)

    后序遍歷訪問節點的順序是:左子節點、右子節點、節點本身。

    * postOrderTraversal(node = this.root) {if (node.left) { yield* this.postOrderTraversal(node.left); }if (node.right) { yield* this.postOrderTraversal(node.right); }yield node;} 復制代碼

    后序遍歷將按照以下順序輸出對應的值:3、4、5、15、40、30、10。

    先序遍歷與 DFS(Pre-Order Traversal)

    先序遍歷訪問節點的順序是:節點本身、左子節點、右子節點。

    * preOrderTraversal(node = this.root) {yield node;if (node.left) { yield* this.preOrderTraversal(node.left); }if (node.right) { yield* this.preOrderTraversal(node.right); }} 復制代碼

    先序遍歷將按照以下順序輸出對應的值:10、5、4、3、30、15、40。與深度優先搜索(DPS)的順序是一致的。

    廣度優先搜索 (BFS)

    樹的 BFS 可以通過隊列來實現:

    * bfs() {const queue = new Queue();queue.add(this.root);while (!queue.isEmpty()) {const node = queue.remove();yield node;node.descendents.forEach(child => queue.add(child));}} 復制代碼

    BFS 將按照以下順序輸出對應的值:10、5、30、4、15、40、3。

    平衡樹 vs. 非平衡樹

    目前,我們已經討論了如何新增、刪除與查找元素。然而,我們并未談到(相關操作的)時間復雜度,先思考一下最壞的情況。

    假設按升序添加數字:

    Inserting values in ascending order in a Binary Search Tree

    樹的左側沒有任何節點!在這顆非平衡樹( Non-balanced Tree)中進行查找元素并不比使用鏈表所花的時間短,都是 O(n)。 ?

    在非平衡樹中查找元素,如同以逐頁翻看的方式在字典中尋找一個單詞。但如果樹是平衡的,將類似于對半翻開字典,視乎該頁的字母,選擇左半部分或右半部分繼續查找(對應的詞)。

    需要找到一種方式使樹變得平衡!

    如果樹是平衡的,查找元素不在需要遍歷全部元素,時間復雜度降為 O(log n)。讓我們探討一下平衡樹的意義。

    Balanced vs unbalanced Tree

    如果在非平衡樹中尋找值為 7 的節點,就必須從節點 #1 往下直到節點 #7。然而在平衡樹中,我們依次訪問 #4、#6 后,下一個節點將到達 #7。隨著樹規模的增大,(非平衡樹的)表現會越來越糟糕。如果樹中有上百萬個節點,查找一個不存在的元素需要上百萬次訪問,而平衡樹中只要20次!這是天壤之別!

    我們將在下一篇文章中通過自平衡樹來解決這個問題。

    總結

    我們討論了不少樹的基礎,以下是相關的總結:

    • 樹是一種數據結構,它的節點有 0 個或多個子節點。
    • 樹并不存在環,圖才存在。
    • 在二叉樹中,每個節點最多只有兩個子節點。
    • 當一顆二叉樹中,左子節點的值小于節點的值,而右子節點的值大于節點的值時,這顆樹被稱為二叉搜索樹。
    • 可以通過先序、后續和中序的方式訪問一棵樹。
    • 在非平衡樹中查找的時間復雜度是 O(n)。 ??
    • 在平衡樹中查找的時間復雜度是 O(log n)。 ?

    總結

    以上是生活随笔為你收集整理的[译文] 初学者应该了解的数据结构: Tree的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。