請啟用 Javascript 查看內容

JavaScript 學演算法(十九)- 樹堆

 ·  ☕ 3 分鐘

這週是六角鼠年鐵人賽第二十七週。

我們來看相較於 紅黑樹,可以說是非常簡單的平衡樹:樹堆(Treap)

樹堆

樹堆(Treap) 其名稱是 Tree 和 Heap 的結合。顧名思義,Treap 同時滿足二元搜尋樹和 Heap(堆積)的性質。屬於 笛卡爾樹(Cartesian Tree) 的一種,兩者在結構上是相同的,只是應用不同。

Treap 結構相當於以隨機資料插入的二元搜尋樹,相較於其它平衡樹,實現可以說是非常簡單直觀。由於樹的結構並不等於添加元素順序,因此可有效避免一般的二元搜尋樹所出現的最糟情況。

1. 性質

Treap 在二元搜尋樹的基礎上,每個節點都有一個隨機的 優先順序(priority)

  1. Treap 為一棵二元搜尋樹;
  2. 左、右子樹也是 Treap;
  3. 每個節點都有一個隨機的 優先順序
  4. 優先順序 保持著 Heap 性質。
    • Max Heap(最大堆積),所有的父節點都比子節點要大;
    • Min Heap(最小堆積),所有的父節點都比子節點要小。

需要注意的兩點:

  • Treap 相較於 Heap,不需要滿足 Complete binary tree(完全二元樹)的條件;
  • Treap 相較於 AVL-Tree,屬於非嚴格平衡樹。

2. 新增操作

新增時,會給節點隨機分配一個優先順序,新增操作和一般二元搜尋樹相同,新增節點後,需要維護 Heap 的性質,會用到旋轉操作

Max Heap 為例:

  1. 新增節點到一個葉子節點上;
  2. 當前節點的優先順序比其根大,對根旋轉:
    • 如果當前節點是根的左子節點,右旋轉;
    • 如果當前節點是根的右子節點,左旋轉。

3. 刪除操作

因為 Treap 滿足 Heap 性質,所以只需要把要刪除的節點旋轉到葉子節點上,然後直接刪除就可以了。

具體的方法:

  1. 比較被刪除節點的左、右子節點優先順序
     - 左子樹優先順序大,執行右旋轉;
     - 右子樹優先順序大,執行左旋轉。
  2. 直到被刪除節點被旋轉到了葉子節點,然後直接刪除。

JavaScript 實作 Treap

1. 基本結構

我們簡單的寫一個隨機函式,用來模擬隨機分配的優先順:

1
2
3
function random() {
  return Math.floor(Math.random() * 1000);
}

節點:

1
2
3
4
5
6
7
8
class TreapNode {
  constructor(data, priority = random()) {
    this.data = data;
    this.priority = priority;
    this.left = null;
    this.right = null;
  }
}

節點比一般二元樹多一個優先順序

本體:

1
2
3
4
5
6
class Treap {
  constructor() {
    this.root = null;
  }
  // methods
}

2. 旋轉操作

與 AVL-Tree 相同。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
rightRotation(node) {
  const temp = node.left;
  node.left = temp.right;
  temp.right = node;
  return temp;
}

leftRotation(node) {
  const temp = node.right;
  node.right = temp.left;
  temp.left = node;
  return temp;
}

3. 新增操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
insert(data) {
  const insertHelper = (node) => {
    let curNode = node;
    if (!curNode) {
      return new TreapNode(data);
    }
    if (data < curNode.data) {
      curNode.left = insertHelper(curNode.left);
      if (curNode.left.priority > curNode.priority) {
        curNode = this.rightRotation(curNode);
      }
    } else if (data > curNode.data) {
      curNode.right = insertHelper(curNode.right);
      if (curNode.right.priority > curNode.priority) {
        curNode = this.leftRotation(curNode);
      }
    }
    return curNode;
  };
  this.root = insertHelper(this.root);
}

4. 刪除操作

因為節點是單向的,所以我們要記錄父節點和一個 key 紀錄當前節點是左、右子樹。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
remove(data) {
  let curNode = this.root;
  let parentNode = null;
  let key = '';

  // 尋找刪除節點
  while (curNode) {
    if (data < curNode.data) {
      parentNode = curNode;
      key = 'left';
      curNode = curNode.left;
    } else if (data > curNode.data) {
      parentNode = curNode;
      key = 'right';
      curNode = curNode.right;
    } else {
      break;
    }
  }

  // 旋轉刪除節點,直到最底下
  while (curNode) {
    // 判斷節點狀態
    const result = this.comparison(curNode);

    if (result === -1) {
      this.setNode(parentNode, null, key);
      return;
    }
    if (result === 1) {
      curNode = this.rightRotation(curNode);
      this.setNode(parentNode, curNode, key);
      key = 'right';
      parentNode = curNode;
      curNode = curNode.right;
    } else {
      curNode = this.leftRotation(curNode);
      this.setNode(parentNode, curNode, key);
      key = 'left';
      parentNode = curNode;
      curNode = curNode.left;
    }
  }
}

輔助函式:

1
2
3
4
5
6
7
setNode(parent, node, key) {
  if (!parent) {
    this.root = node;
  } else {
    parent[key] = node;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
comparison(node) {
  if (!node.left && !node.right) {
    return -1;
  }
  if (node.left && !node.right) {
    return 1;
  }
  if (!node.left && node.right) {
    return 0;
  }
  if (node.left.priority > node.right.priority) {
    return 1;
  }
  return 0;
}

竹白
作者
竹白
前端筆記

文章目錄