js數據結構-二叉樹(二叉搜索樹)

前言

可能有一部分人沒有讀過我上一篇寫的二叉堆,因此這裏把二叉樹的基本概念複製過來了,若是讀過的人能夠忽略前面針對二叉樹基本概念的介紹,另外若是對鏈表數據結構不清楚的最好先看一下本人以前寫的js數據結構-鏈表node

二叉樹

二叉樹(Binary Tree)是一種樹形結構,它的特色是每一個節點最多隻有兩個分支節點,一棵二叉樹一般由根節點,分支節點,葉子節點組成。而每一個分支節點也經常被稱做爲一棵子樹。react

圖片描述

  • 根節點:二叉樹最頂層的節點
  • 分支節點:除了根節點之外且擁有葉子節點
  • 葉子節點:除了自身,沒有其餘子節點

經常使用術語
在二叉樹中,咱們經常還會用父節點和子節點來描述,好比圖中2爲6和3的父節點,反之6和3是2子節點算法

二叉樹的三個性質

  1. 在二叉樹的第i層上,至多有2^i-1個節點小程序

    • i=1時,只有一個根節點,2^(i-1) = 2^0 = 1
  2. 深度爲k的二叉樹至多有2^k-1個節點segmentfault

    • i=2時,2^k-1 = 2^2 - 1 = 3個節點
  3. 對任何一棵二叉樹T,若是總結點數爲n0,度爲2(子樹數目爲2)的節點數爲n2,則n0=n2+1

樹和二叉樹的三個主要差異

  • 樹的節點個數至少爲1,而二叉樹的節點個數能夠爲0
  • 樹中節點的最大度數(節點數量)沒有限制,而二叉樹的節點的最大度數爲2
  • 樹的節點沒有左右之分,而二叉樹的節點有左右之分

二叉樹分類

二叉樹分爲徹底二叉樹(complete binary tree)和滿二叉樹(full binary tree)數組

  • 滿二叉樹:一棵深度爲k且有2^k - 1個節點的二叉樹稱爲滿二叉樹
  • 徹底二叉樹:徹底二叉樹是指最後一層左邊是滿的,右邊可能滿也可能不滿,而後其他層都是滿的二叉樹稱爲徹底二叉樹(滿二叉樹也是一種徹底二叉樹)

圖片描述

二叉搜索樹

二叉搜索樹知足如下的幾個性質:數據結構

  • 若任意節點的左子樹不空,則左子樹上全部節點的值均小於它的根節點的值;
  • 若任意節點的右子樹不空,則右子樹上全部節點的值均大於它的根節點的值;
  • 任意節點的左、右子樹也須要知足左邊小右邊大的性質

咱們來舉個例子來深刻理解如下post

一組數據:12,4,18,1,8,16,20
由下圖能夠看出,左邊的圖知足了二叉樹的性質,它的每一個左子節點都小於父節點,右子節點大於其父節點,同時左子樹的節點都小於根節點,右子樹的節點都大於根節點this

圖片描述

二叉搜索樹主要的幾個操做:spa

  • 查找(search)
  • 插入(insert)
  • 遍歷(transverse)

二叉樹搜索樹的鏈式存儲結構

經過下圖,能夠知道二叉搜索樹的節點一般包含4個域,數據元素,分別指向其左,右節點的指針和一個指向父節點的指針所構成,通常把這種存儲結構稱爲三叉鏈表。
圖片描述

用代碼初始化一個二叉搜索樹的結點:

  • 一個指向父親節點的指針 parent
  • 一個指向左節點的指針 left
  • 一個指向右節點的指針 right
  • 一個數據元素,裏面能夠是一個key和value
class BinaryTreeNode {
        constructor(key, value){
            this.parent = null;
            this.left = null;
            this.right = null;
            this.key = key;
            this.value = value;
        }
    }

接着咱們再用代碼去初始化一個二叉搜索樹

  • 在二叉搜索樹中咱們會維護一個root指針,這個就至關於鏈表中的head指針,在沒有任何節點插入的時候它指向空,在有節點插入之後它指向根節點。
class BinarySearchTree {
        constructor() {
            this.root = null;
        }
    }

建立節點

static createNode(key, value) {
        return new BinarySearchTree(key, value);
    }

插入操做

看下面這張圖,13是咱們要插入的節點,它插入的具體步驟:

  1. 跟根節點12作比較,比12大,因此咱們肯定了,這個節點是往右子樹插入的
  2. 而根節點的右邊已經有節點,那麼跟這個節點18作比較,結果小於18因此往18的左節點找位置
  3. 而18的左節點也已經有節點了,因此繼續跟這個節點作比較,結果小於16
  4. 恰好16的左節點是空的(left=null),因此13這個節點就插入到了16的左節點

圖片描述

經過上面的描述,咱們來看看代碼是怎麼寫的

  • 定義兩個指針,分別是p和tail,最初都指向root,p是用來指向要插入的位置的父節點的指針,而tail是用來查找插入位置的,因此最後它會指向null,用上圖舉個例子,p最後指向了6這個節點,而tail最後指向了null(tail爲null則說明已經找到了要插入的位置)
  • 循環,tail根據咱們上面分析的一步一步往下找位置插入,若是比當前節點小就往左找,大則往右找,一直到tail找到一個空位置也就是null
  • 若是當前的root爲null,則說明當前結構中並無節點,因此插入的第一個節點直接爲跟節點,即this.root = node
  • 將插入後的節點的parent指針指向父節點
insert(node){
        let p = this.root;
        let tail = this.root;
        
        // 循環遍歷,去找到對應的位置
        while(tail) {
            p = tail;
            // 要插入的節點key比當前節點小
            if (node.key < tail.key){
                tail = tail.left;
            }
            // 要插入的節點key比當前節點大
            else {
                tail = tail.right;
            }
        }
        
        // 沒有根節點,則直接做爲根節點插入
        if(!p) {
            this.root = node;
            return;
        }
        
        // p是最後一個節點,也就是咱們要插入的位置的父節點
        // 比父節點大則往右邊插入
        if(p.key < node.key){
            p.right = node;
        }
        // 比父節點小則往左邊插入
        else {
            p.left = node;
        }
        
        // 指向父節點
        node.parent = p;

    }

查找

查找就很簡單了,其實和插入差多,都是去別叫左右節點的大小,而後往下找

  • 若是root = null, 則二叉樹中沒有任何節點,直接return,或者報個錯什麼的。
  • 循環查找
search(key) {
        let p = this.root;
        if(!p) {
            return;
        }
        
        while(p && p.key !== key){
            if(p.key<key){
                p = p.right;
            }else{
                p = p.left;
            }
        }
        
        return p;
    }

遍歷

  • 中序遍歷(inorder):先遍歷左節點,再遍歷本身,最後遍歷右節點,輸出的恰好是有序的列表
  • 前序遍歷(preorder):先本身,再遍歷左節點,最後遍歷右節點
  • 後序遍歷(postorder):先左節點,再右節點,最後本身

最經常使用的通常是中序遍歷,由於中序遍歷能夠獲得一個已經排好序的列表,這也是爲何會用二叉搜索樹排序的緣由

根據上面對中序遍歷的解釋,那麼代碼就變的很簡單,就是一個遞歸的過程,遞歸中止的條件就是節點爲null

  • 先遍歷左節點-->yield* this._transverse(node.left)
  • 遍歷本身 --> yield* node
  • 遍歷左節點 --> yield* this._transverse(node.right)
transverse() {
        return this._transverse(this.root);
    }
    
    *_transverse(node){
        if(!node){
            return;
        }
        yield* this._transverse(node.left);
        yield node;
        yield* this._transverse(node.right)
    }

圖片描述
看上面這張圖,咱們簡化的來看一下,先訪問左節點4,再本身12,而後右節點18,這樣輸出的就恰好是一個4,12,18

補充:這個地方用了generater,因此返回的一個迭代器。能夠經過下面這種方式獲得一個有序的數組,這裏的前提就當是已經有插入的節點了

const tree = new BinaryTree();
   //...中間省略插入過程
    
   // 這樣就返回了一個有序的數組 
   var arr = [...tree.transverse()].map(item=>item.key);

完整代碼

class BinaryTreeNode {
  constructor(key, value) {
    // 指向父節點
    this.p = null;

    // 左節點
    this.left = null;

    // 右節點
    this.right = null;

    // 鍵
    this.key = key;

    // 值
    this.value = value;
  }
}

class BinaryTree {
  constructor() {
    this.root = null;
  }

  static createNode(key, value) {
    return new BinaryTreeNode(key, value);
  }

  search(key) {
    let p = this.root;
    if (!p) {
      return;
    }

    while (p && p.key !== key) {
      if (p.key < key) {
        p = p.right;
      } else {
        p = p.left;
      }
    }

    return p;
  }

  insert(node) {
    // 尾指針的父節點指針
    let p = this.root;

    // 尾指針
    let tail = this.root;

    while (tail) {
      p = tail;
      if (node.key < tail.key) {
        tail = tail.left;
      } else {
        tail = tail.right;
      }
    }

    if (!p) {
      this.root = node;
      return;
    }

    // 插入
    if (p.key < node.key) {
      p.right = node;
    } else {
      p.left = node;
    }

    node.p = p;
  }

  transverse() {
    return this.__transverse(this.root);
  }

  *__transverse(node) {
    if (!node) {
      return;
    }
    yield* this.__transverse(node.left);
    yield node;
    yield* this.__transverse(node.right);
  }
}

總結

二叉查找樹就講完了哈,其實這個和鏈表很像的,仍是操做那麼幾個指針,既然叫查找樹了,它主要仍是用來左一些搜索,還有就是排序了,另外補充一下,二叉查找樹裏找最大值和最小值也很方即是不是,若是你大體讀懂了的話。
這篇文章我寫的感受有點亂誒,由於總感受哪裏介紹的不到位,讓一些基礎差的人會看不懂,若是有不懂或者文章哪裏寫錯了,歡迎評論留言哈

後續

後續寫什麼呢,這個問題我也在想,排序算法,react第三方的一些模擬實現?,作個小程序組件庫?仍是別的,容我再想幾個小時,由於能夠,有建議的朋友們也能夠留言說一下哈。最後最後,最重要的請給個贊,請粉一個呢,謝謝啦

相關文章
相關標籤/搜索