前端學習數據結構1 二分排序樹(BST)

對於我這樣一個畢業了一年半左右的前端來講,很摸着良心的講,平時工做中,沒有遇到過寫什麼樹。可是有大塊時間的時候,以爲這些仍是得好好複習一下,或者說預習。今天的主角是二分搜索樹。

二分搜索樹

二分搜索樹也是二叉樹,和二叉樹長的同樣,就是有個特色,每一個節點的值比他的左子樹的值大,比他的右子樹的值小。以下圖所示:前端

那麼接下來就來實現一下這個樹:

// 聲明節點構造函數 當前節點的值,左節點,右節點
class Node {
  constructor(value) {
    this.value = value
    this.left = null
    this.right = null
  }
}
// 二分搜索樹構造函數
class BST {
  constructor() {
    this.root = null
    this.size = 0
  }
  getSize() {
    return this.size
  }
  isEmpty() {
    return this.size === 0
  }
  addNode(v) {
    // 每次添加子節點後,更新樹
    this.root = this._addChild(this.root, v)
  }
  _addChild(node, v) {
    if (!node) {
      this.size++
      return new Node(v)
    }
    if (node.value > v) {
      console.log(`在${node.value}節點,添加左節點${v}`)
      node.left = this._addChild(node.left, v)
    } else if (node.value < v) {
      console.log(`在${node.value}節點,添加右節點${v}`)
      node.right = this._addChild(node.right, v)
    }
    return node
  }    
}

let bst = new BST()
// 第一個節點
bst.addNode(10)
// 後續節點
bst.addNode(8)
bst.addNode(6)
bst.addNode(3)
bst.addNode(7)
bst.addNode(9)
bst.addNode(12)
bst.addNode(11)
bst.addNode(15)
bst.addNode(14)

console.log(bst)
複製代碼

運行結果以下:node

左節點值是8 右節點值12 中間節點值是10。函數

再展開左節點看一下this

右邊就再也不展開贅述了spa

二分搜索樹--遍歷

二叉樹遍歷 分爲深度遍歷(先序遍歷、中序遍歷、後序遍歷,三種遍歷的區別在於什麼時候訪問節點), 廣度遍歷(一層層地遍歷)3d

先序遍歷

繞不開的遞歸又出現了,想起有一天右邊同事妹子很開心的和我說她知道了遞歸的終極奧義:「遞歸的終極奧義就是:不要想遞歸是怎麼具體一步步實現的」code

那我先來實現一下先序遍歷cdn

class BST { 
    ...
    // 添加先序遍歷實現,其實就是很簡單的幾行代碼
    preTraversal() {
      this._pre(this.root)
    }
    _pre(node) {
      if (node) {
        // 訪問節點的值
        console.log(node.value)
        // 遞歸左右子樹
        this._pre(node.left)
        this._pre(node.right)
      }
    }
}
// 用上面生成的bst實例執行一下,結果以下圖
bst.preTraversal()
複製代碼

那麼這個結果是如何生成的呢?blog

  1. 先是打印10,這個毫無爭議 而後 this._pre(node.left),this._pre(node.right)這兩個方法看似兩行,其實左子樹沒有遍歷完結的話是不會去遍歷右子樹的
  2. 此時this._pre(node.left)中參數是以下圖部分,一樣,會對這部分執行那3行代碼,首先會打印8 , 而後以8那個節點做爲根節點,去遍歷左右子樹

3. 打印8以後,遍歷左子樹的參數以下圖部分,一樣,會對這部分執行那3行代碼,首先會打印6 , 而後以6那個節點做爲根節點,去遍歷左右子樹

4. 這個時候遍歷左子樹時候 this_pre(node.left)的參數只是3節點,3節點沒有子樹,那麼在執行上面那3行代碼只是打印3 ,this._pre(node.left)和this._pre(node.right)執行不下去了。

  1. 上面第三步驟時候,打印6以後,先遍歷左子樹,後遍歷右子樹。而此時的遍歷左子樹只是打印3。因而要去遍歷6的右子樹,也就是打印7。
  2. 打印7以後,本例中,做爲節點8的左節點已經遍歷完畢。遍歷8的右節點,也就是打印9,以後8的右節點也遍歷完畢。
  3. 再往回退,打印9以後,也就是10節點的左節點已經所有遍歷完畢。 因此打印的結果是 10 8 6 3 7 9
  4. 一樣的邏輯此時該去遍歷10節點的右節點了。依次打印12 11 15 14 ,因此最終結果就是 10 8 6 3 7 9 12 11 15 14

一步步的推導遞歸的具體實現後,還真的覺的上面所說遞歸的奧義那句話總結的是頗有意思的。排序

中序遍歷

class BST { 
    ...
    // 添加中序遍歷實現,其實就是很簡單的幾行代碼
    midTraversal() {
      this._mid(this.root)
    }
    _mid(node) {
      if (node) {
        // 1語句 後面講解時候說'1語句'就指代下面這句
        this._mid(node.left)
        // 2語句
        console.log(node.value)
        // 3語句
        this._mid(node.right)
      }
    }
}
// 用上面生成的bst實例執行一下,結果以下圖
bst.midTraversal()
複製代碼

  1. 上面的逐條分析以後,中序遍歷就很容易理解了。上面的循環體主要三條語句,在第一次執行 2語句以前,咱們要想一下,此時的參數node,是什麼?換個問法,在參數node是樹中的哪一個節點的時候,纔會第一次的執行2語句? 我來截圖吧

只有當node是這個3節點的時候,纔會第一次觸發這個console.log(node.value),而 此時此刻3語句也至關因而一條廢語句。

  1. 如今回想一下,當node參數是這個3節點的時候,回退一下,他的上一步執行函數中node是什麼?我來截圖

node是圖中這個6節點的時候, 1語句執行完畢,纔會執行 此時此刻的console.log(node.value),也就是打印出了6,而後執行 3語句 ,以7節點做爲node參數,再去執行123語句,此時此刻13語句都是廢語句,也就是打印出7 。也就是打印 3 6 7

  1. 那麼再回退一下,打印出7以後,也就是node.left 是6這個節點的遞歸執行完畢了,此時此刻的node是8,而後執行console.log(node.value),再去執行3語句,此時此刻的3語句的參數是node.left,也就是9節點,對這個9節點,依次執行123語句,9節點沒有子節點,因此只是打印出9 ,至此,整個8節點遍歷完畢。

  2. 打印出了9以後, 8節點徹底遍歷完畢了,8節點做爲node.left,那麼此時的node是10節點,執行2語句,打印出10 ,那麼後續的,相信不用我再說了吧

  3. 這就是爲何二分排序樹的中序遍歷的結果是排序好的。

後序遍歷

class BST { 
    ...
    // 添加後序遍歷實現,其實就是很簡單的幾行代碼
    backTraversal() {
      this._back(this.root)
    }
    _back(node) {
      if (node) {
        // 1語句 後面講解時候說'1語句'就指代下面這句
        this._back(node.left)
        // 2語句
        this._back(node.right)
        // 3語句
        console.log(node.value)
      }
    }
}
// 用上面生成的bst實例執行一下,結果以下圖
bst.backTraversal()
複製代碼

  1. 遞歸的奧義:就是不要想遞歸是怎麼一步步具體實現的。
  2. 後續遍歷與以前遍歷方式的區別是的3語句是打印,那麼可否根據上面的前序推導和中序推導加上奧義,直接看着樹的圖,寫出後續遍歷的結果呢?

廣度遍歷

未完待續

(以上參考掘金小冊,融入本身的實操和思考,由於是收費小冊,參考的地址原文沒辦法貼出來,yck老師很贊,建議你們都去看看他的小冊)

相關文章
相關標籤/搜索