資料結構☞二叉搜尋樹BST

guojikun發表於2021-04-20

二叉查詢樹(Binary Search Tree),(又:二叉搜尋樹,二叉排序樹)它可以是一棵空樹,也可以是具有下列性質的二叉樹: 若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值; 它的左、右子樹也分別為二叉排序樹。二叉搜尋樹作為一種經典的資料結構,它既有連結串列的快速插入與刪除操作的特點,又有陣列快速查詢的優勢;所以應用十分廣泛,例如在檔案系統和資料庫系統一般會採用這種資料結構進行高效率的排序與檢索操作

樹的定義

樹由一組以邊連線的節點組成。公司的組織結構圖就是一個樹的例子,參見下圖

組織結構圖是用來描述一個組織的架構。在圖 10-1 中,每個方框都是一個節點,連線方框
的線叫做邊。節點代表了該組織中的各個職位,邊描述了各職位間的關係。比如,CIO 直
接彙報給 CEO,那麼兩者就用一條邊連線起來。開發經理向 CIO 彙報,也用一條邊連線
起來。銷售副總監和開發經理沒有直接的聯絡,因此兩個節點間沒有用一條邊相連。

下圖的樹展示了更多有關樹的術語,在後續討論中將會提到。一棵樹最上面的節點稱為
根節點,如果一個節點下面連線多個節點,那麼該節點稱為父節點,它下面的節點稱為子
節點。一個節點可以有 0 個、1 個或多個子節點。沒有任何子節點的節點稱為葉子節點。

2.png

沿著一組特定的邊,可以從一個節點走到另外一個與它不直接相連的節點。從一個節點到另一個節點的這一組邊稱為路徑,在圖中用虛線表示。以某種特定順序訪問樹中所有的節點稱為樹的遍歷。

樹可以分為幾個層次,根節點是第 0 層,它的子節點是第 1 層,子節點的子節點是第2層,以此類推。樹中任何一層的節點可以都看做是子樹的根,該子樹包含根節點的子節點,子節點的子節點等。我們定義樹的層數就是樹的深度。

這種自上而下的樹與人們的直覺相反。現實世界裡,樹的根是在底下的。在電腦科學裡,自上而下的樹則是個由來已久的習慣。事實上,電腦科學家高德納曾經試圖改變這個習慣,但沒幾個月他就發現,大多數電腦科學家都不願用自然的、自下而上的方式描述樹,於是,這件事也就只好不了了之。最後,每個節點都有一個與之相關的值,該值有時被稱為鍵。

二叉樹和二叉查詢樹

叉排序樹的查詢過程和次優二叉樹類似,通常採取二叉連結串列作為二叉排序樹的儲存結構。中序遍歷二叉排序樹可得到一個關鍵字的有序序列,一個無序序列可以通過構造一棵二叉排序樹變成一個有序序列,構造樹的過程即為對無序序列進行排序的過程。每次插入的新的結點都是二叉排序樹上新的葉子結點,在進行插入操作時,不必移動其它結點,只需改動某個結點的指標,由空變為非空即可。搜尋,插入,刪除的複雜度等於樹高,O(log(n))

二叉樹每個節點的子節點不允許超過兩個。通過將子節點的個數限定為 2,可以寫出高效的程式在樹中插入、查詢和刪除資料。
下圖展示了一棵二叉樹

3.png

當考慮某種特殊的二叉樹,比如二叉查詢樹時,確定子節點非常重要。二叉查詢樹是一種特殊的二叉樹,相對較小的值儲存在左節點中,較大的值儲存在右節點中。這一特性使得查詢的效率很高,對於數值型和非數值型的資料,比如單詞和字串,都是如此。

實現二叉查詢樹

實現Node

二叉查詢樹由節點組成,所以我們要定義的第一個物件就是 Node,該物件和連結串列類似。Node 類的定義如下:

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

Node 物件既儲存資料,也儲存和其他節點的連結(left 和 right)。

實現BST

現在可以建立一個類,用來表示二叉查詢樹(BST)。我們讓類只包含一個資料成員:一個表示二叉查詢樹根節點的 Node 物件。該類的建構函式將根節點初始化為 null,以此建立一個空節點。BST 先要有一個 insert() 方法,用來向樹中加入新節點。這個方法有點複雜,需要著重講解。首先要建立一個 Node 物件,將資料傳入該物件儲存。
其次檢查 BST 是否有根節點,如果沒有,那麼這是棵新樹,該節點就是根節點,這個方法到此也就完成了;否則,進入下一步。
如果待插入節點不是根節點,那麼就需要準備遍歷 BST,找到插入的適當位置。該過程類似於遍歷連結串列。用一個變數儲存當前節點,一層層地遍歷 BST。
進入 BST 以後,下一步就要決定將節點放在哪個地方。找到正確的插入點時,會跳出迴圈。查詢正確插入點的演算法如下。

  1. 設根節點為當前節點。
  2. 如果待插入節點儲存的資料小於當前節點,則設新的當前節點為原節點的左節點;反
  3. 之,執行第 4 步。
  4. 如果當前節點的左節點為 null,就將新的節點插入這個位置,退出迴圈;反之,繼續
  5. 執行下一次迴圈。
  6. 設新的當前節點為原節點的右節點。
  7. 如果當前節點的右節點為 null,就將新的節點插入這個位置,退出迴圈;反之,繼續
  8. 執行下一次迴圈。
    有了上面的演算法,就可以開始實現 BST 類了。
class BinarySearchTree {
  constructor() {
    this.root = null;
  }
  insert(key) {
    // 插入
    const newNode = new Node(key);
    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, 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);
      }
    }
  }
}

有三種遍歷 BST 的方式:中序、先序和後序。中序遍歷按照節點上的鍵值,以升序訪問BST 上的所有節點。先序遍歷先訪問根節點,然後以同樣方式訪問左子樹和右子樹。後序遍歷先訪問葉子節點,從左子樹到右子樹,再到根節點
4.png
5.png
6.png

增加中序、先序和後序遍歷

class BinarySearchTree {
  ...
  inOrderTraverse(callback) {
    // 中序查詢
    this.inOrderTraverseNode(this.root, callback);
  }
  preOrderTraverse(callback) {
    // 先序查詢
    this.preOrderTraverseNode(this.root, callback);
  }
  postOrderTraverse(callback) {
    // 後序查詢
    this.postOrderTraverseNode(this.root, callback);
  }
  inOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.key);
      this.inOrderTraverseNode(node.right, callback);
    }
  }
  preOrderTraverseNode(node, callback) {
    if (node !== null) {
      callback(node.key);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
  }
  postOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left, callback);
      this.postOrderTraverseNode(node.right, callback);
      callback(node.key);
    }
  }
}

在二叉查詢樹上進行查詢

對 BST 通常有下列三種型別的查詢:

  1. 查詢給定值;
  2. 查詢最小值;
  3. 查詢最大值。
class BinarySearchTree {
  ...
  min() {
    // 最小值
    return this.minNode(this.root);
  }
  max() {
    // 最大值
    return this.maxNode(this.root);
  }
  search(key) {
    // 查詢
    this.searchNode(this.root, key);
  }
  minNode(node) {
    if (node) {
      while (node && node.left !== null) {
        node = node.left;
      }
      return node.key;
    }
    return null;
  }
  maxNode(node) {
    if (node) {
      while (node && node.right !== null) {
        node = node.right;
      }
      return node.key;
    }
    return null;
  }
  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;
    }
  }
  findMinNode(node) {
    if (node) {
      while (node && node.left !== null) {
        node = node.left;
      }
      return node.key;
    }
    return null;
  }
}

從二叉查詢樹上刪除節點

class BinarySearchTree {
  ...
  remove(key) {
    //移除樹節點
    this.removeNode(this.root, key);
  }
  removeNode(node, key) {
    if (node === null) return null;

    if (key < node.key) {
      node.left = this.removeNode(node.left, key);
      return node;
    } else if (key > node.key) {
      node.right = this.removeNode(node.right, key);
      return node;
    } else {
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      } else if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }

      const aux = this.findMinNode(node.right);
      node.key = aux.key;
      node.right = this.removeNode(node.right, aux.key);
      return node;
    }
  }
}

完整程式碼

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

class BinarySearchTree {
  constructor() {
    this.root = null;
  }
  insert(key) {
    // 插入
    const newNode = new Node(key);
    if (this.root === null) {
      this.root = newNode;
    } else {
      this.insertNode(this.root, newNode);
    }
    console.log(this.root);
  }
  inOrderTraverse(callback) {
    // 中序查詢
    this.inOrderTraverseNode(this.root, callback);
  }
  preOrderTraverse(callback) {
    // 先序查詢
    this.preOrderTraverseNode(this.root, callback);
  }
  postOrderTraverse(callback) {
    // 後序查詢
    this.postOrderTraverseNode(this.root, callback);
  }
  min() {
    // 最小值
    return this.minNode(this.root);
  }
  max() {
    // 最大值
    return this.maxNode(this.root);
  }
  search(key) {
    // 查詢
    this.searchNode(this.root, key);
  }
  remove(key) {
    //移除樹節點
    this.removeNode(this.root, key);
  }
  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);
      }
    }
  }
  inOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.inOrderTraverseNode(node.left, callback);
      callback(node.key);
      this.inOrderTraverseNode(node.right, callback);
    }
  }
  preOrderTraverseNode(node, callback) {
    if (node !== null) {
      callback(node.key);
      this.preOrderTraverseNode(node.left, callback);
      this.preOrderTraverseNode(node.right, callback);
    }
  }
  postOrderTraverseNode(node, callback) {
    if (node !== null) {
      this.postOrderTraverseNode(node.left, callback);
      this.postOrderTraverseNode(node.right, callback);
      callback(node.key);
    }
  }
  minNode(node) {
    if (node) {
      while (node && node.left !== null) {
        node = node.left;
      }
      return node.key;
    }
    return null;
  }
  maxNode(node) {
    if (node) {
      while (node && node.right !== null) {
        node = node.right;
      }
      return node.key;
    }
    return null;
  }
  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;
    }
  }
  removeNode(node, key) {
    if (node === null) return null;

    if (key < node.key) {
      node.left = this.removeNode(node.left, key);
      return node;
    } else if (key > node.key) {
      node.right = this.removeNode(node.right, key);
      return node;
    } else {
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      } else if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }

      const aux = this.findMinNode(node.right);
      node.key = aux.key;
      node.right = this.removeNode(node.right, aux.key);
      return node;
    }
  }
  findMinNode(node) {
    if (node) {
      while (node && node.left !== null) {
        node = node.left;
      }
      return node.key;
    }
    return null;
  }
}

參考書籍

資料結構與演算法JavaScript描述

相關文章