二叉排序樹

JavaScript 數據結構篇 之 BST

前言

有段時間沒有寫文章了,整我的感受變得有點懶散了,你們可不要向我同樣哦~
今天開始 seaconch 打算開始記錄 JavaScript 數據結構的學習經歷。
至此,開始。node

課本:《學習JavaScript數據結構與算法 (第2版)》

術語:算法

  • BST (binary sort tree)
  • LST (left subtree)
  • RST (right subtree)

OUTLINE

  • 特性
  • 定義
  • 插入
  • 查找
  • 最大
  • 最小
  • 移除
  • 遍歷
  • AVL
  • 源碼

特性

BST 有以下特性:數據結構

  • 若 LST 不爲空,則 LST 全部節點值都 於它的根節點值
  • 若 RST 不爲空,則 RST 全部節點值都 於它的根節點值
  • 左右子樹也都是 BST
  • 沒有重複鍵

定義

爲了存儲 BST,咱們先定義一個 Node 類型,存儲各個節點。ide

Node 節點的構造函數默認爲其初始化 Subtree 都是 null。函數

/**
 * 節點
 */
class Node {

  constructor (key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }

}

而後是 BSTpost

BST 的類型咱們只初始化了一個根節點 root學習

/**
 * 二叉排序樹
 */
class BinarySearchTree {

  constructor() {
    this.root = null;
  }

}

插入

插入節點只要遵循一個原則就好:大與 node.key 就向 node.right 中插入,小於 node.key 就向 node.left 插入。this

首先定義私有函數。spa

const insertNode = Symbol('insertNode');
/**
   * 插入節點
   */
  insert(key) {

    let newNode = new Node(key);
    if (this.root === null) this.root = newNode;
    else this[insertNode](this.root, newNode);

  }

  /**
   * 插入節點
   * @param {當前節點} node 
   * @param {新節點} newNode 
   */
  [insertNode] (node, newNode) {

    if (newNode.key < node.key) {

      if (node.left === null) node.left = newNode;
      else this[insertNode](node.left, newNode);

    } else {

      if (node.right === null) node.right = newNode;
      else this[insertNode](node.right, newNode);

    }
  }

這裏稍微的說明一下,之因此寫成私有函數,無非就是爲了避免但願外部看到這些不必的。code

其實東西多了感受也會亂糟糟的...

接下來爲了查看一下效果,咱們來寫一個初始化 BST 的函數。

咱們的目標是初始化一個這樣的 BST。

clipboard.png

/**
 * 初始化數據
 * @param {樹} tree 
 */
function initialization(tree) {

  let treeKeys = [50, 25, 13, 5, 19, 35, 28, 49, 75, 64, 56, 74, 85, 79, 99];

  treeKeys.forEach( key => tree.insert(key) )

}

如今來試試實例化一個 BST 來看看效果。

let tree = new BinarySearchTree();

initialization(tree);

console.log(tree.root);

打印效果如圖

clipboard.png

查找

由:

  • 若 LST 不爲空,則 LST 全部節點值都 於它的根節點值
  • 若 RST 不爲空,則 RST 全部節點值都 於它的根節點值
  • 左右子樹也都是 BST

所以咱們能夠相對快速的查找元素。

定義私有函數

const searchNode = Symbol('searchNode');
/**
   * 是否存在目標 key
   */
  existKey(key) {
    return this[searchNode](this.root, key);
  }

  /**
   * 
   * @param {當前節點} node 
   * @param {key} key 
   */
  [searchNode] (node, key) {

    if (node === null) return false;
    
    if (key < node.key) return this[searchNode](node.left, key);
    
    else if (key > node.key) return this[searchNode](node.right, key);
    
    else return true;
    
  }

咱們的思路是這樣的:

若是要查找的 key值 小於 當前節點的 key值,則向 LST 繼續查找;
若是要查找的 key值 大於 當前節點的 key值,則向 RST 繼續查找;
若是要查找的 key值 等於 當前節點的 key值,則返回 true

運行效果以下:

clipboard.png

最大值

由:

  • 若 RST 不爲空,則 RST 全部節點值都 於它的根節點值
  • 左右子樹也都是 BST

咱們能夠求得最大值

很明顯是這樣的

clipboard.png

代碼以下

定義私有函數

const maxNode = Symbol('maxNode');
/**
   * 獲取最大節點 key
   */
  max() {
    return this[maxNode](this.root);
  }

  /**
   * 獲取最大節點 key
   * @param {節點} node 
   */
  [maxNode] (node) {

    if (node) {
    
      while (node && node.right !== null) {
      
        node = node.right;
      }
      return node.key;
    }
    return null;
  }

輸出結果爲:99

最小值

獲取最小值的方法與最大值相似,只是方向變了一下

clipboard.png

定義私有函數

const minNode = Symbol('minNode');
/**
   * 獲取最小節點 key
   */
  min() {
    return this[minNode](this.root);
  }

  /**
   * 獲取最小節點 key
   * @param {節點} node 
   */
  [minNode] (node) {

    if (node) {
    
      while (node && node.left !== null) {
      
        node = node.left;
      }
      return node.key;
    }
    return null;
  }

運行結果天然是:5

移除

移除相對來講複雜一點,由於假如咱們要移除的是一個父節點,那他們的子節點怎麼辦?

固然也是有相應的應對措施的。

  • 對於沒有 subtree 的 node 來講,只須要把他們修改成 null 便可
  • 對於存在 subtree 的 node 來講,就要考慮全部狀況分別處理

    • 當 LST === null 時 => RST 上前來頂替待刪除節點的位置
    • 當 RST === null 時 => LST 上前來頂替待刪除節點的位置
    • 當 LST && RST 都不是 null 時,由 RST 中最小節點上前來頂起待刪除節點的位置

圖例說明:

1. LST 等於 null

clipboard.png

2. RST 等於 null

clipboard.png

3. LST 和 RST 都不等於 null

clipboard.png

定義私有函數

const removeNode = Symbol('removeNode');
const findMinNode = Symbol('findMinNode');
/**
   * 刪除節點
   */
  remove(key) {
    this[removeNode](this.root, key);
  }

  /**
   * 刪除節點,返回刪除後的 tree
   * @param {當前節點} node 
   * @param {key} key 
   */
  [removeNode] (node, key) {

    if (node === null) /** the tree is empty or does not have this key who you want to remove. */ return null;

    if (key < node.key) /** the key of currrent node is bigger than target key. */ {

      node.left = this[removeNode](node.left, key);
      return node;

    } else if (key > node.key) /** smaller */ {

      node.right = this[removeNode](node.right, key);
      return node;

    } else /** 相等 */ {

      if (node.left === null && node.right === null) {
        /**
         * 當前節點沒有左右節點,能夠放心刪除
         */
        node = null;
        return node;
      }

      /**
       * 當前節點有一個節點,讓子節點`頂`上來
       */
      if (node.left === null) {
        
        node = node.right;
        return node

      } else if (node.right === null) {

        node = node.left;
        return node;

      }

      /**
       * 來到這裏表明當前節點有兩個子節點
       */
      let aux = this[findMinNode](node.right); // 找到右節點的最小節點
      node.key = aux.key; // 把要刪除的節點的 key 覆蓋爲右側最小節點 key
      node.right = this[removeNode](node.right, aux.key); // 重構 right side tree (刪除上面找到的 aux 節點)
      return node;
    }
  }

  /**
   * 返回目標節點分支下最小節點
   * @param {目標節點} node 
   */
  [findMinNode] (node) {

    while (node && node.left !== null) {
    
      node = node.left;
    }
    return node;
  }

好了,如今來一塊兒運行一下,看一下效果吧

clipboard.png

遍歷

遍歷 BST 通常有三種方式:

- 先序
- 中序
- 後序

seaconch 畫了 3 張圖幫助理解:

先序遍歷
圖片描述

中序遍歷
圖片描述

後序遍歷
圖片描述

這裏咱們只演示中序遍歷的代碼

中序遍歷

所謂前序,中序,後序通常都是指具體操做的位置,在這裏表示回調函數的位置

定義私有函數

const inOrderTraverseNode = Symbol('inOrderTraverseNode');
/**
   * 中序遍歷,標準名稱爲: `inOrderTraverse`
   */
  middleOrderTraverse(cb) {
    this[inOrderTraverseNode](this.root, cb);
  }

  /**
   * 
   * @param {當前節點} node 
   * @param {回調} cb 
   */
  [inOrderTraverseNode] (node, cb) {
    if (node !== null) {
      this[inOrderTraverseNode](node.left, cb);
      cb(node.key); // 回調在中間就是中序
      this[inOrderTraverseNode](node.right, cb);
    }
  }

結果是按照順序輸出了各個節點的 key:
圖片描述

AVL

Adelson-Velskii-Landi(AVL) 自平衡樹

BST 有必定的問題,好比當你添加了不少 大於 root 的元素,而只添加了不多的小於 root 的元素,那麼 BST 將嚴重失衡,最直觀的一個說明就是,獲取最大值的速度明顯沒有獲取最小值的速度快。

圖片描述

AVL 樹就是爲了解決 BST 失衡的問題

AVL 在每次 添加 或 刪除 元素的時候,嘗試自平衡,使左右子樹高度差 >= 1

(hr(右子樹高度) - hl(左子樹高度) in (-1, 0, 1))

源碼

源碼以下:

/**
 * 節點
 */
class Node {

  constructor (key) {
    this.key = key;
    this.left = null;
    this.right = null;
  }
}

const insertNode = Symbol('insertNode');
const removeNode = Symbol('removeNode');
const findMinNode = Symbol('findMinNode');
const minNode = Symbol('minNode');
const maxNode = Symbol('maxNode');
const searchNode = Symbol('searchNode');
const inOrderTraverseNode = Symbol('inOrderTraverseNode');
const preOrderTraverseNode = Symbol('preOrderTraverseNode');
const postOrderTraverseNode = Symbol('postOrderTraverseNode');
/**
 * 二叉排序樹
 */
class BinarySearchTree {

  constructor() {
    this.root = null;
  }

  /**
   * 插入節點
   */
  insert(key) {

    let newNode = new Node(key);
    if (this.root === null) this.root = newNode;
    else this[insertNode](this.root, newNode);

  }

  /**
   * 插入節點
   * @param {當前節點} node 
   * @param {新節點} newNode 
   */
  [insertNode] (node, newNode) {

    if (newNode.key < node.key) {

      if (node.left === null) node.left = newNode;
      else this[insertNode](node.left, newNode);

    } else {

      if (node.right === null) node.right = newNode;
      else this[insertNode](node.right, newNode);

    }
  }

  /**
   * 刪除節點
   */
  remove(key) {
    this[removeNode](this.root, key);
  }

  /**
   * 刪除節點,返回刪除後的 tree
   * @param {當前節點} node 
   * @param {key} key 
   */
  [removeNode] (node, key) {

    if (node === null) /** the tree is empty or does not have this key who you want to remove. */ return null;

    if (key < node.key) /** the key of currrent node is bigger than target key. */ {

      node.left = this[removeNode](node.left, key);
      return node;

    } else if (key > node.key) /** smaller */ {

      node.right = this[removeNode](node.right, key);
      return node;

    } else /** 相等 */ {

      if (node.left === null && node.right === null) {
        /**
         * 當前節點沒有左右節點,能夠放心刪除
         */
        node = null;
        return node;
      }

      /**
       * 當前節點有一個節點,讓子節點`頂`上來
       */
      if (node.left === null) {
        
        node = node.right;
        return node

      } else if (node.right === null) {

        node = node.left;
        return node;

      }

      /**
       * 來到這裏表明當前節點有兩個子節點
       */
      let aux = this[findMinNode](node.right); // 找到右節點的最小節點
      node.key = aux.key; // 把要刪除的節點的 key 覆蓋爲右側最小節點 key
      node.right = this[removeNode](node.right, aux.key); // 重構 right side tree (刪除上面找到的 aux 節點)
      return node;
    }
  }

  /**
   * 返回目標節點分支下最小節點
   * @param {目標節點} node 
   */
  [findMinNode] (node) {

    while (node && node.left !== null) {

      node = node.left;
    }
    return node;
  }

  /**
   * 獲取最小節點 key
   */
  min() {
    return this[minNode](this.root);
  }

  /**
   * 獲取最小節點 key
   * @param {節點} node 
   */
  [minNode] (node) {

    if (node) {

      while (node && node.left !== null) {

        node = node.left;
      }
      return node.key;
    }
    return null;
  }

  /**
   * 獲取最大節點 key
   */
  max() {
    return this[maxNode](this.root);
  }

  /**
   * 獲取最大節點 key
   * @param {節點} node 
   */
  [maxNode] (node) {

    if (node) {

      while (node && node.right !== null) {

        node = node.right;
      }
      return node.key;
    }
    return null;
  }

  /**
   * 是否存在目標 key
   */
  existKey(key) {
    return this[searchNode](this.root, key);
  }

  /**
   * 
   * @param {當前節點} node 
   * @param {key} key 
   */
  [searchNode] (node, key) {

    if (node === null) return false;

    if (key < node.key) return this[searchNode](node.left, key);

    else if (key > node.key) return this[searchNode](node.right, key);

    else return true;

  }

  /**
   * 中序遍歷,標準名稱爲: `inOrderTraverse`
   */
  middleOrderTraverse(cb) {
    this[inOrderTraverseNode](this.root, cb);
  }

  /**
   * 
   * @param {當前節點} node 
   * @param {回調} cb 
   */
  [inOrderTraverseNode] (node, cb) {
    if (node !== null) {
      this[inOrderTraverseNode](node.left, cb);
      cb(node.key); // 回調在中間就是中序
      this[inOrderTraverseNode](node.right, cb);
    }
  }

  preOrderTraverse(cb) {
    this[preOrderTraverseNode](this.root, cb);
  }

  /**
   * 
   * @param {*} node 
   * @param {*} cb 
   */
  [preOrderTraverseNode] (node, cb) {
    if (node !== null) {
      cb(node.key); // 回調在前
      this[preOrderTraverseNode](node.left, cb);
      this[preOrderTraverseNode](node.right, cb);
    }
  }

  postOrderTraverse(cb) {
    this[postOrderTraverseNode](this.root, cb);
  }

  /**
   * 
   * @param {*} node 
   * @param {*} cb 
   */
  [postOrderTraverseNode] (node, cb) {
    if (node !== null) {
      this[postOrderTraverseNode](node.left, cb);
      this[postOrderTraverseNode](node.right, cb);
      cb(node.key); // 回調在後
    }
  }
}


/**
 * 初始化數據
 * @param {樹} tree 
 */
function initialization(tree) {

  let treeKeys = [50, 25, 13, 5, 19, 35, 28, 49, 75, 64, 56, 74, 85, 79, 99];

  treeKeys.forEach( key => tree.insert(key) )

}

let tree = new BinarySearchTree();

initialization(tree);

// tree.preOrderTraverse(v => console.log(v));
tree.middleOrderTraverse(v => console.log(v));
// tree.postOrderTraverse(v => console.log(v));

// console.log('the min node.key is: ', tree.min());
// console.log('the max node.key is: ', tree.max());

// let tempKey = 49;
// console.log('the key of [', tempKey, ']', tree.existKey(tempKey) ? 'real' : 'not', 'exist.');

// tree.remove(49)
// console.log('remove key [', tempKey, ']');

// console.log('the key of [', tempKey, ']', tree.existKey(tempKey) ? 'real' : 'not', 'exist.');

continue...

相關文章
相關標籤/搜索