詳解BST(二叉排序樹)

来源:http://www.cnblogs.com/isLiu/archive/2017/12/02/7919856.html
-Advertisement-
Play Games

查找基本分類如下: 1. 線性表的查找 順序查找 折半查找 分塊查找 2. 樹表的查找 二叉排序樹 平衡二叉樹 B樹 B+樹 3. 散列表的查找 今天介紹 二叉排序樹 。 二叉排序樹 ( Binary Sort Tree ) 又稱為 二叉查找樹 ,它是一種對排序和查找都很有用的特殊二叉樹。 1. 二 ...


查找基本分類如下:

  1. 線性表的查找

    • 順序查找
    • 折半查找
    • 分塊查找
  2. 樹表的查找

    • 二叉排序樹
    • 平衡二叉樹
    • B樹
    • B+樹
  3. 散列表的查找

今天介紹二叉排序樹

二叉排序樹 ( Binary Sort Tree ) 又稱為二叉查找樹,它是一種對排序和查找都很有用的特殊二叉樹。

1. 二叉排序樹的定義


二叉排序樹是具有如下性質的二叉樹:

  1. 若它的左子樹不為空,則左子樹上所有節點的值均小於它的根節點的值。
  2. 若它的右子樹不為空,則右子樹上的所有節點的值均大於它的根節點的值。
  3. 它的左子樹、右子樹也均為二叉排序樹。

二叉排序樹是遞歸定義的。所以可以得出二叉排序樹的一個重要性質:中序遍歷一棵二叉排序樹時可以得到一個節點值遞增的有序序列

若中序遍歷上圖二叉樹,則可以得到一個按數值大小排序的遞增序列:3,12,24,37,45,53,61,78,90,100

2. 創建一個二叉排序樹


二叉樹是由節點構成,所以我們需要一個Node類,node實例保存當前節點的數據,以及保存左右節點的指針,還可以輸出當前節點數據。

  class Node {
    constructor(data, leftNode, rightNode) {
      this.data = data
      this.leftNode = leftNode
      this.rightNode = rightNode
    }
    print () {
      return this.data
    }
  }

二叉排序樹有一個根節點,根節點存儲了根節點的數據,左右子節點的地址,還有相應的實例方法,提供插入、遍歷、查找等操作。

  class BST {
    constructor() {
      this.root = null
    }
    
    insert (data) {...}
    preOrder () {...}
    inOrder () {...}
    postOrder () {...}
    ...
  }

3. 二叉排序樹的插入


我們要根據二叉排序樹樹的性質來決定insert的data的位置

  1. 若當前是一棵空樹,則將插入的數據作為根節點
  2. 若不是空樹,迴圈遍歷二叉排序樹的節點

    • 若當前遍歷的節點的data大於要插入的data,則將下一個要遍歷的節點賦值為當前遍歷的節點的左節點,進行下一層迴圈,直到葉子節點為止,將data作為葉子節點的左節點
    • 若當前遍歷的節點的data小於要插入的data,則將下一個要遍歷的節點賦值為當前遍歷的節點的右節點,進行下一層迴圈,直到葉子節點為止,將data作為葉子節點的右節點

還是代碼直觀

  function insert (data) {
    if (this.find(data)) {
        return
    }

    var node = new Node(data, null, null)
    if (this.root == null) {
      this.root = node
    } else {
      var currentNode = this.root
      var parentNode
      while (currentNode) {
        parentNode = currentNode
        if (data < currentNode.data) {
          currentNode = currentNode.leftNode
          if (currentNode == null) {
            parentNode.leftNode = node
            break
          }
        } else {
          currentNode = currentNode.rightNode
          if (currentNode == null) {
            parentNode.rightNode = node
            break
          }
        }
      }
    }
  }

4. 遞歸遍歷二叉排序樹


簡單,貼下代碼,重點在非遞歸遍歷

  class BST {
    constructor() {
      this.data = null
    }
    
    preOrder () {
      preOrderFn(this.root)
    }
    inOrder () {
      inOrderFn(this.root)
    }
    postOrder () {
      postOrderFn(this.root)
    }
  }
  
  function preOrderFn (node) {
    if (node) {
      console.log(node.print())
      preOrderFn(node.leftNode)
      preOrderFn(node.rightNode)
    }
  }
  function inOrderFn (node) {
    if (node) {
      inOrderFn(node.leftNode)
      console.log(node.print())
      inOrderFn(node.rightNode)
    }
  }
  function postOrderFn (node) {
    postOrderFn (node.leftNode)
    postOrderFn (node.rightNode)
    console.log(node.print())
  }

5.非遞歸中序遍歷二叉排序樹


中序遍歷的非遞歸演算法最簡單,後序遍歷的非遞歸演算法最難,所以先介紹中序遍歷。

非遞歸遍歷一定要用到棧。

  class Stack {
    constructor() {
      this.arr = []
    }
    pop () {
      return this.arr.shift()
    }
    push (data) {
      this.arr.unshift(data)
    }
    isEmpty () {
      return this.arr.length == 0
    }
  }

我們一點一點寫想,中序遍歷,肯定是要先找到左子樹最下麵的節點吧?想不明白就好好想想。

  function inOrderWithoutRecursion (root) {
    var parentNode = root
    var stack = new Stack()

    // 一直遍歷到左子樹的最下麵,將一路遍歷過的節點push進棧中
    while (parentNode) {
      stack.push(parentNode)
      parentNode = parentNode.leftNode
    }
  }

這裡為什麼要先讓遍歷過的節點入棧呢?中序遍歷,先遍歷左節點,再根節點,最後是右節點,所以我們需要保存一下根節點,以便接下來訪問根節點和藉助根節點來訪問右節點。

1.現在我們已經到了左子樹的最下麵的節點了,這時它是一個葉子節點。通過遍歷,它也在棧中而且是在棧頂,所以就可以訪問它的data了,然後訪問根節點的data,最後將parentNode指向根節點的右節點,訪問右節點。

如圖

按我上面說的話,代碼應該是這個樣子的。

    parentNode = stack.pop()
    console.log(parentNode.data)
    parentNode = stack.pop()
    console.log(parentNode.data)
    parentNode = parentNode.rightNode

2.但是還有一種情況呢?如果左子樹最下麵的節點沒有左節點,只有右節點呢?也就是說如果這個節點不是葉子節點呢?那麼就直接訪問根節點的data,再將parentNode指向根節點的右節點,訪問右節點。對吧?

如圖

那現在代碼又成了這個樣子。

    parentNode = stack.pop()
    console.log(parentNode.data)
    parentNode = parentNode.rightNode

那麼怎麼統一格式呢?之前我們說到當parentNode不存在時就需要出棧了,那我們可以把左子樹最下麵的節點也就是第一種情況時的葉子節點看作一個根節點,繼續訪問它的右節點,因為它是一個葉子節點,所以右節點為null,所以就又執行了一次出棧操作。這時候代碼就可以統一了,好好想一想,有點抽象。

統一後的代碼就是情況2的代碼

    parentNode = stack.pop()
    console.log(parentNode.data)
    parentNode = parentNode.rightNode

如果上面的都理解了的話,就很簡單了,貼代碼

  function inOrderWithoutRecursion (root) {
    if (!root)
      return

    var parentNode = root
    var stack = new Stack()

    while (parentNode || !stack.isEmpty()) {

      // 一直遍歷到左子樹的最下麵,將一路遍歷過的節點push進棧中
      while (parentNode) {
        stack.push(parentNode)
        parentNode = parentNode.leftNode
      }
      // 當parentNode為空時,說明已經達到了左子樹的最下麵,可以出棧操作了
      if (!stack.isEmpty()) {
        parentNode = stack.pop()
        console.log(parentNode.data)
        // 進入右子樹,開始新一輪迴圈
        parentNode = parentNode.rightNode
      }
    }
  }

優化

  function inOrderWithoutRecursion (root) {
    if (!root)
      return

    var parentNode = root
    var stack = new Stack()

    while (parentNode || !stack.isEmpty()) {

      // 一直遍歷到左子樹的最下麵,將一路遍歷過的節點push進棧中
      if (parentNode) {
        stack.push(parentNode)
        parentNode = parentNode.leftNode
      }
      // 當parentNode為空時,說明已經達到了左子樹的最下麵,可以出棧操作了
      else {
        parentNode = stack.pop()
        console.log(parentNode.data)
        // 進入右子樹,開始新一輪迴圈
        parentNode = parentNode.rightNode
      }
    }
  }

6.非遞歸先序遍歷二叉排序樹


有了中序遍歷的基礎,掌握先序遍歷就不難了吧?先序就是到了根節點就列印出來,然後將節點入棧,然後左子樹,基本與中序類似,想想就明白。

直接貼最終代碼

  function PreOrderWithoutRecursion (root) {
    if (!root)
      return

    var parentNode = root
    var stack = new Stack()

    while (parentNode || !stack.isEmpty()) {

      // 一直遍歷到左子樹的最下麵,一邊列印data,將一路遍歷過的節點push進棧中
      if (parentNode) {
        console.log(parentNode.data)
        stack.push(parentNode)
        parentNode = parentNode.leftNode
      }
      // 當parentNode為空時,說明已經達到了左子樹的最下麵,可以出棧操作了
      else {
        parentNode = stack.pop()
        // 進入右子樹,開始新一輪迴圈
        parentNode = parentNode.rightNode
      }
    }
  }

7.非遞歸後序遍歷二叉排序樹


後序遍歷中,一個根節點被訪問的前提是,右節點不存在或者右節點已經被訪問過

後序遍歷難點在於:判斷右節點是否被訪問過。

  • 如果右節點不存在或者右節點已經被訪問過,則訪問根節點

  • 如果不符合上述條件,則跳過根節點,去訪問右節點

我們可以使用一個變數來保存上一個訪問的節點,如果是當前訪問的節點的右節點就是上一個訪問過的節點,證明右節點已經被訪問過了,可以去訪問根節點了。

這裡需要註意的一點是:節點Node是一個對象,如果用==比較的話,返回的永遠是false,所以我們比較的是node的data屬性。

代碼在這裡

function PostOrderWithoutRecursion (root) {
    if (!root)
      return

    var parentNode = root
    var stack = new Stack()
    var lastVisitNode = null

    while (parentNode || !stack.isEmpty()) {
      if (parentNode) {
        stack.push(parentNode)
        parentNode = parentNode.leftNode
      }
      else {
        parentNode = stack.pop()
        // 如果當前節點沒有右節點或者是右節點被訪問過,則訪問當前節點
        if (!parentNode.rightNode || parentNode.rightNode.data == lastVisitNode.data) {
          console.log(parentNode.data)
          lastVisitNode = parentNode
        }
        // 訪問右節點
        else {
          stack.push(parentNode)
          parentNode = parentNode.rightNode
          while (parentNode) {
            parentNode = parentNode.leftNode
          }
        }
      }
    }
  }

8.二叉排序樹的查找


寫查找是為了刪除節點做準備。

1.查找給定值

很簡單,根據要查找的數據和根節點對比,然後遍歷左子樹或者右子樹就好了。

  find (data) {
    var currentNode = this.root
    while (currentNode) {
      if (currentNode.data == data) {
        return currentNode
      } else if (currentNode.data > data) {
        currentNode = currentNode.leftNode
      } else {
        currentNode = currentNode.rightNode
      }
    }
    return null
  }

2.查找最大值

很簡單,直接找到最右邊的節點就是了

  getMax () {
    var currentNode = this.root
    while (currentNode.rightNode) {
      currentNode = currentNode.rightNode
    }
    return currentNode.data
  }

3.查找最小值

一樣

  getMax () {
    var currentNode = this.root
    while (currentNode.leftNode) {
      currentNode = currentNode.leftNode
    }
    return currentNode.data
  }

9.二叉排序樹的刪除


刪除很重要,說下邏輯:

首先從二叉排序樹的根節點開始查找關鍵字為key的待刪節點,如果樹中不存在此節點,則不做任何操作;

否則,假設待刪節點為delNode,其父節點為delNodeParentdelNodeLeftdelNodeRight分別為待刪節點的左子樹、右子樹。

可設delNodedelNodeParent的左子樹(右子樹情況類似)。 分下麵三種情況考慮

1.若delNode為葉子節點,即delNodeLeftdelNodeRight均為空。刪除葉子節點不會破壞整棵樹的結構,則只需修改delNodeParent的指向即可。

delNodeParent.leftNode = null

2.若delNode只有左子樹delNodeLeft或者只有右子樹delNodeRight,此時只要令delNodeLeft或者delNodeRight直接成為待刪節點的父節點的左子樹即可。

delNodeParent.leftNode = delNode.leftNode

(或者delNodeParent.leftNode = delNode.rightNode)

3.若delNode左子樹和右子樹均不為空,刪除delNode之後,為了保持其他元素之間的相對位置不變,可以有兩種處理辦法

  • delNode的左子樹為delNodeParent的左子樹,而delNode的右子樹為delNode的左子樹中序遍歷的最後一個節點(令其為leftBigNode,即左子樹中最大的節點,因為要符合二叉樹的性質,仔細想一想)的右子樹

    delNodeParent.leftNode = delNode.leftNode

    leftBigNode.rightNode = delNode.rightNode

  • delNode的直接前驅(也就是左子樹中最大的節點,令其為leftBigNode)替代delNode,然後再從二叉排序樹中刪除它的直接前驅(或直接後繼,原理類似)。當以直接前驅替代delNode時,由於leftBigNode只有左子樹(否則它就不是左子樹中最大的節點),則在刪除leftBigNode之後,只要令leftBigNode的左子樹為雙親leftBigNodeParent的右子樹即可。

    delNode.data = leftBigNode.data

    leftBigNodeParent.rightNode = leftBigNode.leftNode

畫了三張圖片來理解下:

刪除節點P之前:

第一種方式刪除後:

第二種方式刪除後:

顯然,第一種方式可能增加數的深度,而後一種方法是以被刪節點左子樹中最大的節點代替被刪的節點,然後從左子樹中刪除這個節點。此節點一定沒有子樹(同上,否則它就不是左子樹中最大的節點),這樣不會增加樹的高度,所以常採用這種方案,下麵的演算法也使用這種方案。

代碼註釋很清除,好好理解下,這塊真的不好想

  deleteNode (data) {
    /********************** 初始化 **************************/
    var delNode = this.root,
        delNodeParent = null
    /************ 從根節點查找關鍵字為data的節點 ***************/
    while (delNode) {
      if (delNode.data == data) break
      delNodeParent = delNode // 記錄被刪節點的雙親節點
      if (delNode.data > data) delNode = delNode.leftNode // 在被刪節點左子樹中繼續查找
      else delNode = delNode.rightNode  // 在被刪節點的右子樹中繼續查找
    }
    if (!delNode) { // 沒找到
      return 
    }
    /**
     * 三種情況
     * 1.被刪節點既有左子樹,又有右子樹
     * 2.被刪節點只有右子樹
     * 3.被刪節點只有左子樹
    **/
    var leftBigNodeParent = delNode 
    if (delNode.leftNode && delNode.rightNode) { // 被刪節點左右子樹都存在
      var leftBigNode = delNode.leftNode
      while (leftBigNode.rightNode) { // 在被刪節點的左子樹中尋找其前驅節點,即最右下角的節點,也就是左子樹中數值最大的節點
        leftBigNodeParent = leftBigNode
        leftBigNode = leftBigNode.rightNode // 走到右盡頭
      }
      delNode.data = leftBigNode.data // 令被刪節點的前驅替代被刪節點
      if (leftBigNodeParent.data != delNode.data) {
        leftBigNodeParent.rightNode = leftBigNode.leftNode // 重接被刪節點的前驅的父節點的右子樹
      } else {
        leftBigNodeParent.leftNode = leftBigNode.leftNode // 重接被刪節點的前驅的父節點的左子樹
      }
    } else if (!delNode.leftNode) {
      delNode = delNode.rightNode // 若被刪節點沒有左子樹,只需重接其右子樹
    } else if (!delNode.rightNode) {
      delNode = delNode.leftNode // 若被刪節點沒有右子樹,只需重接其左子樹
    }
    /********* 將被刪節點的子樹掛接到其父節點的相應位置 **********/
    if (!delNodeParent) { 
      this.root = delNode // 若被刪節點是根節點
    } else if (leftBigNodeParent.data == delNodeParent.data) {
      delNodeParent.leftNode = delNode // 掛接到父節點的左子樹位置
    } else {
      delNodeParent.rightNode = delNode // 掛接到父節點的右子樹位置
    }
  }

10.其他方法


1.複製二叉排序樹

這一塊我先用了遞歸,後來想到,BST是個對象,直接深度克隆就好了。。。不說了

2.二叉排序樹深度

遞歸遞歸遞歸

  class BST {
    constructor() {
      this.root = null
    }
    depth () {
      return depthFn(this.root)
    }
  }

  function depthFn (node) {
    if (!node) {
      return 0
    } else {
      var leftDepth = depthFn(node.leftNode)
      var rightDepth = depthFn(node.rightNode)
      if (leftDepth > rightDepth)
        return (leftDepth + 1)
      else
        return (rightDepth + 1)
    }
  }

3.二叉排序樹節點個數

遞歸遞歸遞歸

  class BST {
    constructor() {
      this.root = null
    }
    nodeCount () {
      return nodeCount(this.root)
    }
  }
  function nodeCount(node) {
    if (!node) {
      return 0
    } else {
      return nodeCount(node.leftNode) + nodeCount(node.rightNode) + 1
    }
  }

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 文章目錄 ★引子 ★求導 ★最初的想法 ★初步的想法 ★後來的想法 ★最後的想法 ★編程範式 ★結尾 首先聲明一點,本文主要介紹的是面向對象(OO)的思想,順便談下函數式編程,而不是教你如何準確地、科學地用java求出函數在一點的導數。 ★引子 首先,直接上一段python代碼,請大家先分析下上面代 ...
  • 熔斷與降級 為什麼在RPC環節中有熔斷以及降級的需求,詳細的原因這裡不多解釋,從網上搜索一張圖做示意。 熔斷 我理解熔段主要解決如下幾個問題: 當所依賴的對象不穩定時,能夠起到快速失敗的目的 快速失敗後,能夠根據一定的演算法動態試探所依賴對象是否恢復 比如產品詳細頁獲取產品的好評總數時,由於後端服務異 ...
  • 你可能會疑惑為什麼我們使用6位數來表示一種顏色而不是只用一位或二位,答案是使用6位數可提供給我們巨大數量的顏色變化。 會有多少種可能的顏色?16 個值(0~F)和 6 個位置意味著我們有 16 的 6 次方,或者說超過 1600 萬種可能的顏色。 Hex code 遵循 red-green-blue ...
  • 很多情況下,你會使用 CSS 庫,這些庫可能會意外覆蓋掉你自己的 CSS。所以當你需要確保某元素具有指定的 CSS 時,你可以使用 !important。 Hello World!是粉色的 ...
  • 1、js的簡介 (1)js是什麼? js是可以嵌入到html中,是基於對象和事件驅動的腳本語言。 特點: 交互性 安全性:js不能訪問本地磁碟 跨平臺:瀏覽器中都具備js解析器 (2)js能做什麼 js能動態的修改(增刪)html和css的代碼 能動態的校驗數據 (3)js的歷史及組成 BOM(瀏覽 ...
  • 內容為整理博主文章: "https://juejin.im/user/58870f04128fe10065efc8d9/article" 個人覺得他對Operators的解說較容易理解和全面,顧把它們整理在一起,也方面查找。 Operators: Observable 的 Operators 是實例 ...
  • s中的this總是讓人,是js眾所周知的坑之一。 總結一下,this的坑分為5種。 1.全局代碼中的this。 alert(this);//window 全局範圍內this將會全局,在瀏覽器window 2.作為單純函數調用 這裡this指向全局對象,就是window。在嚴格模式中,則undefie ...
  • 一般認為:嚴格模式下this不允許指向全局對象。 如:http://www.ruanyifeng.com/blog/2013/01/javascript_strict_mode.html 需要說明的是:本身指向全局的this是沒有問題的。 示例代碼: 控制台輸出為window對象(全局對象): 嚴格 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...