博客 / 詳情

返回

二叉樹中的小鋼炮:🔍二叉查找樹

原文參考我的公眾號文章 梳理一波「二叉樹

二叉樹

二叉樹,顧名思義,每個節點最多有兩個“叉”,也就是兩個子節點,分別是左子節點和右子節點。不過,二叉樹並不要求每個節點都有兩個子節點,有的節點只有左子節點,有的節點只有右子節點。
  • 根節點:無父節點的節點
  • 葉子結點:無子節點的節點
  • 兄弟節點:有相同根節點的節點

關於“樹”,還有三個比較相似的概念:高度(Height)、深度(Depth)、層(Level)。它們的定義是這樣的:
高度-深度-層(來源於網絡)

高度-深度-層:示意圖(來源於網絡)

二叉查找(排序)樹

  • 也叫二叉搜索樹,支持動態數據集合的快速插入、刪除、查找操作;
  • 有序二叉樹:在樹中的任意一個節點,其左子樹中的每個節點的值,都要小於這個節點的值,而右子樹節點的值都大於這個節點的值;
  • 是一顆二叉樹,每個節點最多兩個子結點,即每一層最多 2^(level - 1)個節點;
樹結構
/** 樹節點 */
class BinaryNode {
  constructor(data) {
    this.data = data; //存儲的數據
    this.left = null; //左節點
    this.right = null; //右節點
    this.level = 1; //所屬層級
    this.count = 1; //重複數據次數
    this.isDeleted = false; //是否被刪除
  }
}

/** 二叉樹 */
class BinarySearchTree {
  constructor() {
    this.root = null; //根節點:在第一次insert時會被賦予值
    this.nodes = 0; //節點數
    this.list = []; //遍歷二叉樹時臨時存儲數據的數組
  }
}
插入節點
插入規則:新結點值與當前插入位置(從根節點開始)的節點值進行比較
  • 小於父節點值:插在父節點左節點上;
  • 大於父節點值:插在父節點右節點上;
  • 等於父節點值:父節點 count++;(這種方式看情況,如果結點還有其他數據而非只有一個 data,那可以。否則應合併到上一種情況,合併到右節點上)
  • 插入過程可增加被插入的結點所屬 level 的維護!
// class BinarySearchTree { ... }

  /**
   * 插入數據
   * @param {*} data 結點的值
   */
  insert(data) {
    this.nodes++;

    // 遞歸插入
    // let newNode = new BinaryNode(data);
    // if (this.root === null) {
    //   this.root = newNode;
    // } else {
    //   this._insert(this.root, newNode);
    // }

    // 循環插入
    this.loopInsert(data)
  }

  /**
   * 遞歸的方式插入節點:新結點值與當前插入位置(從根節點開始)的節點值進行比較
   * 小於根節點值:插在根節點左節點上;
   * 大於等於根節點值:插在根節點右節點上;
   * 增加結點所屬level的維護!
   * @param {*} theRootNode 被插入位置節點(從根節點開始)
   * @param {*} newNode 新插入的節點
   */
  _sameValAsBigInsert(theRootNode, newNode) {
    newNode.level++; //從根出發,每執行一次_sameValAsBigInsert,層級便自動+1
    if (theRootNode.data > newNode.data) {
      if (theRootNode.left === null) {
        theRootNode.left = newNode;
      } else {
        // 説明不是葉子結點,繼續向下層插入判斷
        this._sameValAsBigInsert(theRootNode.left, newNode);
      }
    } else {
      if (theRootNode.right === null) {
        theRootNode.right = newNode;
      } else {
        // 説明不是葉子結點,繼續向下層插入判斷
        this._sameValAsBigInsert(theRootNode.right, newNode);
      }
    }
  }

  /**
   * !!!值相同,則合併到當前根節點
   * @param {*} theRootNode 被插入位置節點(從根節點開始)
   * @param {*} newNode 新插入的節點
   */
  _sameValMergeInsert(theRootNode, newNode) {
    newNode.level++; //從根出發,每執行一次_sameValMergeInsert,層級便自動+1
    if (theRootNode.data > newNode.data) {
      if (theRootNode.left === null) {
        theRootNode.left = newNode;
      } else {
        // 説明不是葉子結點,繼續向下層插入判斷
        this._sameValMergeInsert(theRootNode.left, newNode);
      }
    } else if (theRootNode.data < newNode.data) {
      if (theRootNode.right === null) {
        theRootNode.right = newNode;
      } else {
        // 説明不是葉子結點,繼續向下層插入判斷
        this._sameValMergeInsert(theRootNode.right, newNode);
      }
    } else {
      theRootNode.count++; // 值相同,則合併到當前根節點
    }
  }

   /**
   * 循環的方式插入
   * @param {*} data
   * @returns
   */
  loopInsert(data) {
    if (this.root == null) {
      this.root = new BinaryNode(data);
      return;
    }

    let p = this.root;
    while (p != null) {
      if (data > p.data) {
        if (p.right == null) {
          p.right = new BinaryNode(data);
          return;
        }
        p = p.right;
      } else {
        // data <= p.data
        if (p.left == null) {
          p.left = new BinaryNode(data);
          return;
        }
        p = p.left;
      }
    }
  }

二叉樹結構

遍歷樹(前序、中序、後續)
遍歷樹採取遞歸的方式,出遞歸的條件是 node=null
  /** 遍歷樹: [type] 遍歷方式 */
  print(type) {
    this.list = [];
    let callee = `${type}Order`;
    this[callee](this.root);
  }
  • 前序遍歷
prevOrder(node){
    if(node==null){
        return
    }
    console.log(node.data)
    this.prevOrder(node.left)
    this.prevOrder(node.right)
}
  • 中序遍歷(遍歷結果正好是有序的)
inOrder(node){
    if(node==null){
        return
    }
    this.inOrder(node.left)
    console.log(node.data)
    this.inOrder(node.right)
}
  • 後序遍歷
postOrder(node){
    if(node==null){
        return
    }
    this.postOrder(node.left)
    this.postOrder(node.right)
    console.log(node.data)
}

三序遍歷結果

查找節點
先取根節點,如果它等於我們要查找的數據,那就返回。如果要查找的數據比根節點的值小,那就在左子樹中遞歸查找;如果要查找的數據比根節點的值大,那就在右子樹中遞歸查找。
  /**
   * 查找一個節點
   * @param {*} data 要查找的值
   * @returns
   */
  find(data) {
    let node = this.root;
    while (node != null) {
      if (data < node.data) {
        node = node.left;
      } else if (data > node.data) {
        node = node.right;
      } else {
        return node;
      }
    }

    return null;
  }
刪除節點
物理刪除:刪除操作比較複雜,一般要分以下三種情況分別處理。
  • 被刪除節點沒有子節點:直接將父節點中,指向要刪除節點的指針置為 null;
  • 被刪除節點只有一個子節點:只需要更新父節點中,指向要刪除節點的指針,讓它指向要刪除節點的子節點(左或右)就可以了;
  • 被刪除節點右兩個子節點(最複雜):我們需要找到這個節點的右子樹中的最小節點,把它替換到要刪除的節點上。然後再刪除掉這個最小節點,因為最小節點肯定沒有左子節點(如果有左子結點,那就不是最小節點了),所以,我們可以應用上面兩條規則來刪除這個最小節點;
邏輯刪除節點(簡易版)
  /**
   * 刪除節點(簡易版)
   * 對要刪除的節點做標記,表示已刪除
   * @param {*} data
   */
  delete(data){
    let targetNode = this.find(data);
    // 未找到要刪除的數據
    if(targetNode == null){
        return null
    }
    // 標記為“已刪除”
    targetNode.isDeleted = true;
  }
}
邏輯刪除對應的樹遍歷方式修改!
  prevOrder(node) {
    if (node == null) {
      return;
    }
    /**+++ 此處增加判斷,inOrder和postOrder同樣處理! */
    if (node.isDeleted != true) {
      this.list.push(node.data);
      console.log(node.data);
    }
    this.prevOrder(node.left);
    this.prevOrder(node.right);
  }

刪除節點

測試有序二叉樹
// 創建一顆有序二叉樹
let bsTree = new BinarySearchTree();

// 插入數據
let arr = [6,8,7,6,3,5,9,4]
arr.map(item=>{
    bsTree.insert(item)
})

// 二叉樹結構
console.log(bsTree);

// 三種方式遍歷整棵樹!
console.log('前序遍歷');
bsTree.prevOrder(bsTree.root)
console.log('\n\n')
console.log('中序遍歷(有序輸出)');
bsTree.inOrder(bsTree.root)
console.log('\n\n')
console.log('後序遍歷');
bsTree.postOrder(bsTree.root)
console.log('\n\n')

// 查找某個節點
console.log("\n查找值6");
console.log(bsTree.find(6));

// 刪除某個節點
console.log("\n刪除6");
console.log(bsTree.delete(6));
console.log("\n\n");

console.log("刪除後-前序遍歷");
bsTree.print("prev");

總結(來自極客時間課程總結,總歸比我自己總結的好 🤷‍♂️)

這篇文章學習了一種特殊的二叉樹,二叉查找樹。它支持快速地查找、插入、刪除操作。

二叉查找樹中,每個節點的值都大於左子樹節點的值,小於右子樹節點的值。不過,這只是針對沒有重複數據的情況。對於存在重複數據的二叉查找樹,我介紹了兩種構建方法,

  • 一種是讓每個節點存儲多個值相同的數據;
  • 另一種是,每個節點中存儲一個數據。針對這種情況,只需要稍加改造原來的插入、刪除、查找操作即可。

在二叉查找樹中,查找、插入、刪除等很多操作的時間複雜度都跟樹的高度成正比。兩個極端情況的時間複雜度分別是 O(n) 和 O(logn),分別對應二叉樹退化成鏈表的情況和完全二叉樹。

參考鏈接

js 二叉樹

數據結構與算法之美-二叉樹基礎下

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.