# 【資工考研】DS: Binary Tree (1) Binary Tree + Binary Search Tree + Heap ## Tree Basic Definition - 不可為空 (node 數 $\gt 0$) - 至少一個特定節點稱為 root - 其餘的節點可分為 $k$ 個集合 ($T_1,T_2,\cdots ,T_k$) 稱為 subtree of a root - subtree 本身沒有方向性 ## Tree Representations 1. linked list 2. transform into the binary tree 3. child-sibling 4. parentheses 如何表示一棵樹呢?有上述四種方法,不過這邊我們只細談第一種 假設有下面這棵樹: ```graphviz graph linked_list{ rankdir=TD; graph [fontname="DFKai-SB"]; node [fontname="DFKai-SB"]; edge [fontname="DFKai-SB"]; label="Use the Linked List to Represent a Tree" {rank=same A} {rank=same B C D} {rank=same E F} A--B A--C A--D C--E C--F } ``` 我們可以這樣表示樹的節點,然後連接節點 ```graphviz digraph structure { rankdir=TD; graph [fontname="DFKai-SB"]; node [fontname="DFKai-SB" shape=record, width=1.2, height=0.6]; edge [fontname="DFKai-SB"]; A [label="{ <data> A | <ref1> | <ref2> | <ref3> | <ref4> }"]; B [label="{ <data> B | <ref1> | <ref2> | <ref3> | <ref4> }"]; C [label="{ <data> C | <ref1> | <ref2> | <ref3> | <ref4> }"]; D [label="{ <data> D | <ref1> | <ref2> | <ref3> | <ref4> }"]; E [label="{ <data> E | <ref1> | <ref2> | <ref3> | <ref4> }"]; F [label="{ <data> F | <ref1> | <ref2> | <ref3> | <ref4> }"]; {rank=same A} {rank=same B C D} {rank=same E F} A:ref1 -> B A:ref2 -> C A:ref3 -> D C:ref1 -> E C:ref2 -> F } ``` 使用 C++ 表示節點結構的話: ```cpp! template<typename T> struct TreeNode { T data; TreeNode *child1; TreeNode *child2; TreeNode *child3; TreeNode *child4; }; ``` 你會注意到,當 tree 的 degree 最大為 $k$ 時,我們就要讓每個節點都準備 $k$ 條 links 的空間 如果有 $n$ 個節點,那就需要 $k\cdot n$ 條 links 但是根據 tree 本身的數學性質我們知道,非空 links 數只會有 $n-1$ 條 因此,這個方法的空間浪費率為 $\frac{nk-(n-1)}{nk}$,大概是 $\frac{k-1}{k}$ 如果 $k=1$ 就退化成普通的 linked list 了我們就不討論,所以對 tree 來說 $k=2$ 時浪費率最低,但也達到約 $50\%$ 不過這個表示方法很直觀,而且表達 tree 上較有彈性 所以在表達 binary tree (也就是 $k = 2$) 時,我們更習慣用這種方法 ## Binary Tree 我們可以簡單記得 binary tree 就是每個節點的 degree $\le 2$ 的 tree 不過要提醒,在資料結構這邊,binary tree 與 tree 還有其他不同的地方,**其實不能說 binary tree 是 tree** 1. binary tree 可以為空 (node 0 個) 2. 有 root 及左右子樹 (這邊強調左右 表示其不同於樹 有左右方向之分) 3. 左右子樹亦為 binary tree <table> <tr> <th>Tree</th> <th>Binary Tree</th> </tr> <tr> <td>Cannot be empty</td> <td>Can be empty</td> </tr> <tr> <td>Node degree ≥ 0</td> <td>0 ≤ Node degree ≤ 2</td> </tr> <tr> <td>Subtrees have no specific order</td> <td>Subtrees are ordered as left and right</td> </tr> </table> ### 性質 假設定 root 所在的 level 為 $1$ 1. level $i$ 有 $2^{i-1}$ 個 nodes 2. 高度為 $h$ 的 binary tree 最多有 $2^h-1$ 個 nodes 最少 $h$ 個 nodes 3. 若 $n_d$ 表示 degree 為 $d$ 的 nodes 總數, $n_0 = n_2 + 1$ ### 種類 - Skewed Binary Tree - $n$ 節點高度為 $n$ (root level $=1$) - Full Binary Tree - 節點數最大化的 binary tree - Complete Binary Tree - 節點增長順序由上而下,同 level 由左而右 - 如果高度為 $h$ 則節點樹介於 $2^{h-1}-1$ 到 $2^h-1$ - 如果 root 的編號為 $1$ 則 $\mathrm{node}_i$ 的若有左右子點,其編號為 $2i, 2i+1$;若有父點,其編號為 $\left[ \frac{i}{2} \right]$ - Strict Binary Tree - 不存在 degree 為 $1$ 的節點 > [!Caution] > 要注意的是離散在這邊的定義與資結有點不同 ### Binary Tree Traversal - preorder - inorder - postorder - level order 令 BT 如下: ```graphviz graph bt{ rankdir=TD; graph [fontname="DFKai-SB"]; node [fontname="DFKai-SB"]; edge [fontname="DFKai-SB"]; { rank=same D} { rank=same L R } L [shape=triangle] R [shape=triangle] D--L D--R } ``` $L$ 跟 $R$ 代表左右子樹,$D$ 代表該樹的 root 則遍歷順序: <table> <tr> <th>Preorder</th> <th>Inorder</th> <th>Postorder</th> </tr> <tr> <td>DLR</td> <td>LDR</td> <td>LRD</td> </tr> </table> 其實很好記: 1. 先走左子樹再走右子樹 2. $D$ 在前就是 preorder ,在中間就是 inorder ,在後面就是 postorder 而 level order 就是 level 從上往下由左向右遍歷 *** 例如給你下面的 BT ```graphviz graph linked_list{ rankdir=TD; graph [fontname="DFKai-SB"]; node [fontname="DFKai-SB"]; edge [fontname="DFKai-SB"]; {rank=same A} {rank=same B C} {rank=same D E F G} A--B A--C B--D B--E C--F C--G } ``` 則遍歷結果: <table> <tr> <th>Preorder</th> <th>Inorder</th> <th>Postorder</th> <th>Level Order</th> </tr> <tr> <td>ABDECFG</td> <td>DBEAFCG</td> <td>DEBFGCA</td> <td>ABCDEFG</td> </tr> </table> *** 如果今天反過來給你遍歷順序,那麼怎樣的組合能決定唯一的 BT 呢? <table> <tr> <th style="text-align:center">Inputs</th> <th style="text-align:center">Can Determine the Unique BT?</th> </tr> <tr align="center"> <td>inorder + {preorder, postorder, or level order}</td> <td>YES</td> </tr> <tr align="center"> <td>preorder + postorder</td> <td>NO</td> </tr> <tr align="center"> <td>level order + {preorder or postorder}</td> <td>NO</td> </tr> <tr align="center"> <td>full BT + {preorder, inorder, postorder, level order}</td> <td>YES</td> </tr> <tr align="center"> <td>complete BT + {preorder, inorder, postorder, level order}</td> <td>YES</td> </tr> <tr align="center"> <td>BST + {preorder, postorder, level order}</td> <td>YES</td> </tr> </table> *** 使用 linked list 的方式來表示 tree node 則 BT 的遍歷怎麼寫呢? 下面是節點的結構 ```cpp! template <typename T> struct TreeNode { T data; TreeNode *left; TreeNode *right; TreeNode(const T &val) : data(val), left(nullptr), right(nullptr) {} }; ``` 為了表達方便,我們使用 enum class 來表達要選擇哪一種遍歷方式 ```cpp! enum class Traversal { PREORDER, INORDER, POSTORDER, LEVELORDER, }; ``` 則以 preorder 為例,我們可以很輕易寫出其遞迴演算法: ```cpp! template <typename Functor> void preorder(TreeNode<T> *node, Functor func) { if (!node) return; func(node); // D preorder(node->left, func); // L preorder(node->right, func); // R } ``` > [!Tip] > `nullptr` 視為 `false` ,所以 `!root` 為 `true` 的情況即 `root == nullptr` (空樹) 記得我們前面講的遍歷記憶法嗎?沒錯,只要記得 DLR 你就能輕鬆寫出 code 了! 同理,inorder 跟 postorder : ```cpp! template <typename Functor> void inorder(TreeNode<T> *node, Functor func) { if (!node) return; inorder(node->left, func); // L func(node); // D inorder(node->right, func); // R } template <typename Functor> void postorder(TreeNode<T> *node, Functor func) { if (!node) return; postorder(node->left, func); // L postorder(node->right, func); // R func(node); // D } ``` 至於 level order 用 BFS: ```cpp! template <typename Functor> void levelorder(TreeNode<T> *node, Functor func) { if (!node) return; std::queue<TreeNode<T> *> q; q.push(node); while (!q.empty()) { TreeNode<T> *curr = q.front(); q.pop(); func(curr); if (curr->left) q.push(curr->left); if (curr->right) q.push(curr->right); } } ``` 如此,我們便能簡單寫出一個列印 BT 的 method: ```cpp! void printBT(const Traversal &policy) { auto printFunc = [](TreeNode<T> *node) { std::cout << node->data << " "; }; switch (policy) { case Traversal::PREORDER: preorder(root, printFunc); break; case Traversal::INORDER: inorder(root, printFunc); break; case Traversal::POSTORDER: postorder(root, printFunc); break; case Traversal::LEVELORDER: levelorder(root, printFunc); break; default: std::cerr << "Invalid traversal policy!\n"; break; } std::cout << "\n"; } ``` 測試一下 ```graphviz graph linked_list{ rankdir=TD; graph [fontname="DFKai-SB"]; node [fontname="DFKai-SB"]; edge [fontname="DFKai-SB"]; {rank=same A} {rank=same B D} {rank=same NULL C E F} A--B A--D B--NULL B--C D--E D--F NULL [fontsize="0pt"] } ``` ```cpp! int main() { BinaryTree<char> bt; bt.root = new TreeNode('A'); TreeNode<char> *node1 = new TreeNode('B'); TreeNode<char> *node2 = new TreeNode('C'); TreeNode<char> *node3 = new TreeNode('D'); TreeNode<char> *node4 = new TreeNode('E'); TreeNode<char> *node5 = new TreeNode('F'); // Build the BT bt.root->left = node1; bt.root->right = node3; node1->right = node2; node3->left = node4; node3->right = node5; std::cout << "Preorder Traversal:\n"; bt.printBT(Traversal::PREORDER); std::cout << "\nInorder Traversal:\n"; bt.printBT(Traversal::INORDER); std::cout << "\nPostorder Traversal:\n"; bt.printBT(Traversal::POSTORDER); std::cout << "\nLevel-order Traversal:\n"; bt.printBT(Traversal::LEVELORDER); return 0; } ``` ![image](https://hackmd.io/_uploads/BJQPVJODJx.png) *** 一些 BT 有可能會考的應用題 #### 判斷兩樹相等 ```cpp! friend bool operator==(const BinaryTree<T> &lhs, const BinaryTree<T> &rhs) { std::function<bool(TreeNode<T> *, TreeNode<T> *)> btEqual = [&](TreeNode<T> *a, TreeNode<T> *b) { if (!a && !b) return true; if (!a || !b) return false; return (a->data == b->data) && btEqual(a->left, b->left) && btEqual(a->right, b->right); }; return btEqual(lhs.root, rhs.root); } friend bool operator!=(const BinaryTree<T> &lhs, const BinaryTree<T> &rhs) { return !(lhs == rhs); } ``` #### 複製 BT ```cpp! TreeNode<T> *btCopy(TreeNode<T> *node) { if (!node) return nullptr; TreeNode<T> *newNode = new TreeNode<T>(node->data); newNode->left = btCopy(node->left); newNode->right = btCopy(node->right); return newNode; } ``` 現在我們可以進一步補充我們的 constructor 跟 `=` operator ```cpp! /** * @brief Default constructor (empty tree) */ BinaryTree() noexcept : root{nullptr} {} /** * @brief Copy constructor */ BinaryTree(const BinaryTree<T> &other) noexcept : root(btCopy(other.root)) {} /** * @brief Move constructor */ BinaryTree(BinaryTree<T> &&other) noexcept : root(other.root) { other.root = nullptr; } ``` ```cpp! /** * @brief Copy assigment operator */ BinaryTree<T> &operator=(const BinaryTree<T> &other) { if (this != &other) { btDestroy(); root = btCopy(other.root); } return *this; } /** * @brief Move assignment operator */ BinaryTree<T> &operator=(BinaryTree<T> &&other) noexcept { if (this != &other) { root = other.root; other.root = nullptr; } return *this; } ``` #### Expression BT 可以想像是 compiler 建立 parsing tree or syntax tree 的作法 原則: 1. leaf 表示 operand 2. non-leaf 表示 operator 3. 優先權大的鄰近 operator level 較大 (較下面) 例如給 `a + b * c - d / e` 把這想成是 BT inorder 的結果 則 postorder 為 `abc * + de / -` 只要有這兩個遍歷結果就能建構回樹了 ```graphviz graph linked_list{ rankdir=TD; graph [fontname="DFKai-SB"]; node [fontname="DFKai-SB"]; edge [fontname="DFKai-SB"]; {rank=same A} {rank=same B C} {rank=same D E F G} {rank=same H I} A--B A--C B--D B--E C--F C--G E--H E--I A [label="-"] B [label="+"] C [label="/"] D [label="a"] E [label="*"] F [label="d"] G [label="e"] H [label="b"] I [label="c"] } ``` 我覺得這邊只要會手寫作答的部份就好了 #### 其他 這邊附上截止至本文撰寫時,本人印象中有解過,LeetCode 上跟 BT 有關的題目 - [2415. Reverse Odd Levels of Binary Tree](https://hackmd.io/@RyoukiWei/leetcode2415) - [2458. Height of Binary Tree After Subtree Removal Queries](https://hackmd.io/@RyoukiWei/leetcode2458) - [951. Flip Equivalent Binary Trees](https://hackmd.io/@RyoukiWei/leetcode951) - [2641. Cousins in Binary Tree II](https://hackmd.io/@RyoukiWei/leetcode2641) - [2583. Kth Largest Sum in a Binary Tree](https://hackmd.io/@RyoukiWei/leetcode2583) - [979. Distribute Coins in Binary Tree](https://hackmd.io/@RyoukiWei/leetcode979) - [2331. Evaluate Boolean Binary Tree](https://hackmd.io/@RyoukiWei/leetcode2331) - [226. Invert Binary Tree](https://hackmd.io/@RyoukiWei/leecode226) - [257. Binary Tree Paths](https://hackmd.io/@RyoukiWei/leetcode257) - [823. Binary Trees With Factors](https://hackmd.io/@RyoukiWei/leetcode823) - [543. Diameter of Binary Tree](https://hackmd.io/@RyoukiWei/leetcode543) ## Binary Search Tree 是 BT ,故可以為空 若不為空: - root 之左子樹上任意節點之值小於 root 之值 - root 之右子樹上任意節點之值大於 root 之值 - root 之左右子樹亦為 BST > [!Tip] > BST 的 inorder traversal 必定是由小到大排列輸出 ```cpp! namespace riklib { template <typename T> struct Iterator { using Node = TreeNode<T>; Node *curr; std::vector<Node *> ancestors; Iterator(Node *root = nullptr) { curr = root; // find smallest element in BST while (curr && curr->left) { ancestors.push_back(curr); curr = curr->left; } } T &operator*() const { return curr->data; } friend bool operator==(const Iterator &lhs, const Iterator &rhs) { return lhs.curr == rhs.curr; } friend bool operator!=(const Iterator &lhs, const Iterator &rhs) { return !(lhs == rhs); }; }; template <typename T> class BinarySearchTree : public BinaryTree<T> { public: using Node = TreeNode<T>; using iterator = Iterator<T>; BinarySearchTree() {} iterator begin() noexcept { return iterator(this->root); } iterator begin() const noexcept { return iterator(this->root); } iterator cbegin() const noexcept { return iterator(this->root); } iterator end() noexcept { return iterator(nullptr); } iterator end() const noexcept { return iterator(nullptr); } iterator cend() const noexcept { return iterator(nullptr); } }; } // namespace riklib ``` ### Inorder predecessor and successor 我們知道 BST 的 inorder 就是 sorted order 故 inorder successor 即 sorted order 中的下一位; inorder predecessor 同理為上一位 對應我們 `Iterator` 實作便是 `++` 跟 `--` 運算 這邊我大致提供一個簡單的實作以供參考 我們給 `TreeNode` 增加 `parent` 指標 `Iterator` 更新後如下: ```cpp! template <typename T> struct Iterator { using Node = TreeNode<T>; Node *curr; Iterator(Node *root = nullptr) { curr = root; // find smallest element in BST while (curr && curr->left) curr = curr->left; } T &operator*() const { return curr->data; } friend bool operator==(const Iterator &lhs, const Iterator &rhs) { return lhs.curr == rhs.curr; } friend bool operator!=(const Iterator &lhs, const Iterator &rhs) { return !(lhs == rhs); }; Iterator &operator++() { if (!curr) throw std::out_of_range("Iterator out of range"); if (curr->right) { curr = curr->right; while (curr->left) curr = curr->left; } else { Node *parent = curr->parent; while (parent && parent->right == curr) { curr = parent; parent = parent->parent; } curr = parent; } return *this; } Iterator operator++(int) { Iterator temp = *this; ++(*this); return temp; } Iterator &operator--() { if (!curr) throw std::out_of_range("Iterator out of range"); if (curr->left) { curr = curr->left; while (curr->right) curr = curr->right; } else { Node *parent = curr->parent; while (parent && parent->left == curr) { curr = parent; parent = parent->parent; } curr = parent; } return *this; } Iterator operator--(int) { Iterator temp = *this; --(*this); return temp; } Iterator operator+(int n) const { Iterator it = *this; for (int i = 0; i < n; ++i) { ++it; } return it; } Iterator operator-(int n) const { Iterator it = *this; for (int i = 0; i < n; ++i) { --it; } return it; } Iterator &operator+=(int n) { for (int i = 0; i < n; ++i) ++(*this); return *this; } Iterator &operator-=(int n) { for (int i = 0; i < n; ++i) --(*this); return *this; } }; ``` ### Insert 我們就依照 BST 的規則找正確的位置放置新節點即可 多次 `insert()` 便可建立 BST BST 建立之結構受 input 順序影響,如果給 sorted input 則會得 skewed BST 範例: `input: 5, 8, 9, 3, 1, 4, 6, 2, 7` ![image](https://hackmd.io/_uploads/rkMwyYuwkg.png) ```cpp! /** * @brief Insert a value into the BST. * * @param value The value to insert. * @return A pair containing an iterator to the inserted node (or the node * with the same value) and a boolean indicating success. */ std::pair<iterator, bool> insert(const T &val) { Node **curr = &this->root; Node *parent = nullptr; while (*curr) { parent = *curr; if (val == (*curr)->data) // Value already exists in the BST return {iterator(*curr), false}; if (val < (*curr)->data) curr = &(*curr)->left; else curr = &(*curr)->right; } *curr = new Node(val); (*curr)->parent = parent; return {iterator(*curr), true}; } ``` ### Delete 1. 找到目標 `x` 2. 分三種情況: 1. `x` is leaf: 直接砍掉 2. `deg(x) == 1` 把 `x` 的 parent 指向 `x` 的 child 再砍掉 `x` 3. `deg(x) == 2` 找左子樹最大值 or 右子樹最小值 令為 `y` 以 `y` 取代 `x` 再 `delete(y)` (原本位置的 `y`) 例如原本的 BST ![image](https://hackmd.io/_uploads/rkMwyYuwkg.png) 砍 5 ,假設採用左子樹最大值則過程: ![image](https://hackmd.io/_uploads/rJXqG5uDke.png) ![image](https://hackmd.io/_uploads/HyYoM9dPkx.png) ```cpp! /** * @brief Erases the node at the given position in the BST. * * @param pos Iterator pointing to the node to erase. Must be a valid iterator * obtained from this tree or its end iterator. * @return Iterator pointing to the next in-order node after the erased node. * If the erased node was the last in-order node, returns end(). */ iterator erase(iterator pos) { if (!pos.curr) return end(); // Return end iterator if the position is invalid Node **parent_link = &this->root; Node *target = this->root; while (target && target != pos.curr) { if (pos.curr->data < target->data) parent_link = &target->left, target = target->left; else parent_link = &target->right, target = target->right; } if (!target) return end(); // Case 1: Node is a leaf node if (!target->left && !target->right) { delete target; *parent_link = nullptr; } // Case 2: Node has only one child else if (!target->left || !target->right) { Node *child = target->left ? target->left : target->right; delete target; *parent_link = child; if (child) child->parent = *parent_link; } // Case 3: Node has two child else { // Find the inorder successor (smallest node in the right subtree) Node **successor_link = &target->right; Node *successor = target->right; while (successor->left) successor_link = &successor->left, successor = successor->left; // Replace target's value with successor's value target->data = successor->data; // Case 1 or 2 for the successor (successor->left must be nullptr now) *successor_link = successor->right; if (successor->right) successor->right->parent = successor->parent; delete successor; } if (!this->root) return end(); Node *next = nullptr; if (target->right) { next = target->right; while (next->left) next = next->left; } else { Node *parent = target->parent; while (parent && parent->right == target) { target = parent; parent = parent->parent; } next = parent; } if (next == target) return begin(); return iterator(next); } ``` 因為普通 BST 的 `insert()` 相對簡單,所以對 iterator 的影響很好處理 但是 `erase()` 就不一樣了,我們要注意該操作對 iterator 有效性的破壞 下面是我大致 debug 用的 code: ```cpp! #include "binary_search_tree.hpp" using namespace riklib; int main() { BinarySearchTree<int> bst; bst.insert(4); bst.insert(1); bst.insert(0); bst.insert(3); bst.insert(2); bst.printBT(Traversal::PREORDER); for (auto it = bst.begin(); it != bst.end();) { it = bst.erase(it); } std::cout << (bst.root == nullptr) << "\n"; bst.printBT(Traversal::PREORDER); return 0; } ``` 因為每次 `erase()` 會破壞 iterator 之有效性,所以我們要將 `it` 更新成 `erase()` 回傳的下個位置 ![image](https://hackmd.io/_uploads/BJCUeaODkl.png) 有成功砍完整棵 BST ,最後 `bst` 變回了空樹 ### Search ```cpp! /** * @brief Finds a node containing the given value in the BST. * * @param val The value to search for in the tree. * @return Iterator pointing to the node containing the value if found, or the end * iterator if the value is not found. */ iterator find(const T &val) { Node *curr = this->root; while (curr) { if (val == curr->data) return iterator(curr); curr = val < curr->data ? curr->left : curr->right; } return end(); } ``` 看到這邊,相信各位算是基礎認識 BST 了 那各位會發現,best case 跟 average case 下 `find()` 的時間複雜度是 $O(\lg n)$ 沒有什麼問題 但如果 BST 是 skewed tree 的話,就會有 worst case $O(n)$ `insert()` 跟 `erase()` 也跟 `find()` 一樣 這問題就是 BST 的高度並未受到控制,而都叫 binary **search** tree 了,`find()` 的時間複雜度我們肯定是希望能好點,不要退化到線性時間 在未來,我們將會討論高度平衡的 BST 來解決這問題 ### Find k-th smallest in BST 最直覺的方法當然是按照 inorder 等第 `k` 個出現 但我們有其他作法 關鍵是我們在多紀錄每個節點其左子樹的節點個數 如此一來我們便可以輕鬆找出第 `k` 小了 大致 pseudocode 長這樣: ```pseudocode! FindKthSmallest(node, k) // node: the root of the current subtree // k: the rank of the smallest element to find let r ← node.left.size + 1 // Compute the rank of node within its subtree if k = r then return node // node is the k-th smallest element else if k < r then return FindKthSmallest(node.left, k) // Recur into the left subtree else return FindKthSmallest(node.right, k - r) // Recur into the right subtree with adjusted rank ``` `k == r` 的 case 不必多說 `k < r ` 時,表示要找的目標在當前根節點的左子樹裡;`k > r` 時,表示左子樹裡沒有想要的,前 `r` 個後面都不用看了,前 `k` 個少了 `r` 個,所以所求是剩下元素中前 `k - r` 小的那個 這個可以記一下大概演算法怎麼寫 不難,但經典 ## Heap - 為 complete BT - 以 Max-Heap 來說父點必大於其左右子點;Min-Heap 則小於 - Root Max-Heap 為最大值 而 Min-Heap 為最小值 前面講過 complete BT 會發現這種 BT 其實節點長很滿 這種時候其實反而使用 array 保存更適合 Heap 可以用來製作 priority queue 也能用來 heap sort | Operations | Time Complexity | | :-------: | :--------: | | **Insert** | $O(\lg n)$ | | **Delete-Max** (**Delete-Min**)| $O(\lg n)$ | | **Heapifty** | $O(\lg n)$ | | **Find-Max** (**Find-Min**)| $O(1)$ | | **Build** | $O(n)$ | | **Decrease key** | $O(\lg n)$ | | **Merge** | $O(n)$ | *** 以下提供基礎的 binary heap 實作 ```cpp! namespace riklib { // Defualt: Min-Heap template <typename T, typename Compare = std::less<T>> class BinaryHeap { private: std::vector<T> heap; Compare compare; public: using iterator = typename std::vector<T>::iterator; using const_iterator = typename std::vector<T>::const_iterator; BinaryHeap() noexcept = default; explicit BinaryHeap(const Compare &comp) : compare(comp) {} BinaryHeap(std::initializer_list<T> init, const Compare &comp = Compare()) : heap(init), compare(comp) { buildHeap(); } BinaryHeap(const BinaryHeap &other) = default; BinaryHeap(BinaryHeap &&other) noexcept = default; BinaryHeap &operator=(const BinaryHeap &other) = default; BinaryHeap &operator=(BinaryHeap &&other) noexcept = default; ~BinaryHeap() = default; iterator begin() noexcept { return heap.begin(); } const_iterator begin() const noexcept { return heap.begin(); } const_iterator cbegin() const noexcept { return heap.cbegin(); } iterator end() noexcept { return heap.end(); } const_iterator end() const noexcept { return heap.end(); } const_iterator cend() const noexcept { return heap.cend(); } /** * @brief Inserts a value into the heap while maintaining the heap property. * @param val The value to insert. */ void push(const T &val) { heap.push_back(val); size_t idx = heap.size() - 1; while (idx > 0) { size_t parent = (idx - 1) / 2; if (compare(heap[idx], heap[parent])) { std::swap(heap[idx], heap[parent]); idx = parent; } else break; } } /** * @brief Removes the top element from the heap. * @throw std::runtime_error if the heap is empty. */ void pop() { if (heap.empty()) throw std::runtime_error("Cannot pop from an empty heap."); std::swap(heap.front(), heap.back()); heap.pop_back(); if (!heap.empty()) heapify(0); } /** * @brief Returns the top element of the heap. * @throws std::runtime_error if the heap is empty. * @return const T& The top element of the heap. */ const T &top() const { if (heap.empty()) throw std::runtime_error("Heap is empty."); return heap.front(); } /** * @brief Checks if the heap is empty. * @return true if the heap is empty, false otherwise. */ bool empty() const noexcept { return heap.empty(); } /** * @brief Returns the number of elements in the heap. * @return size_t The size of the heap. */ size_t size() const noexcept { return heap.size(); } private: void heapify(size_t idx) { const size_t n = heap.size(); size_t target = idx; size_t left = 2 * idx + 1; size_t right = 2 * idx + 2; if (left < n && compare(heap[left], heap[target])) target = left; if (right < n && compare(heap[right], heap[target])) target = right; if (target != idx) { std::swap(heap[idx], heap[target]); heapify(target); } } void buildHeap() { const size_t n = heap.size(); for (size_t i = n / 2; i > 0; --i) { heapify(i - 1); } } }; } ``` ### build 我們有兩種方式來初始建構 heap: 1. top-down 2. bottom-up top-down 很簡單,就是依照輸入順序一個個 `push()`(insert) 進去 可是這樣的時間複雜度其實是 $O(n\lg n)$ $$ \sum\limits_{i=1}^{n}\lg i = \lg (n!) = O(n\lg n) $$ 另一種方式即 bottom-up 我們是先讓所有元素都放進 array (可以想成先填進 complete BT 中) 接著使用 `heapify` 的方式調整陣列使其符合 heap 的性質 這樣講有點抽象 具體來說就是從最後一個 parent 開始往上使每棵子樹皆符合 heap 性質直到 root 這樣的時間複雜度就是 $O(n)$ 了 ### merge ```cpp! /** * @brief Merges another binary heap into this heap. * The merge operation maintains the heap property. * @param other The other binary heap to merge with this heap. */ void merge(const BinaryHeap &other) noexcept { heap.insert(heap.end(), other.heap.begin(), other.heap.end()); buildHeap(); } ``` ## 【資工考研】DS: Binary Tree 全系列 - [【資工考研】DS: Binary Tree (1)](https://hackmd.io/@RyoukiWei/binary_tree_1) - [【資工考研】DS: Binary Tree (2)](https://hackmd.io/@RyoukiWei/binary_tree_2) - [【資工考研】DS: Binary Tree (3)](https://hackmd.io/@RyoukiWei/binary_tree_3) - [【資工考研】DS: Binary Tree (4)](https://hackmd.io/@RyoukiWei/binary_tree_4) - [【資工考研】DS: Binary Tree (5)](https://hackmd.io/@RyoukiWei/binary_tree_5)