# 【資工考研】DS: Binary Tree (2) Threaded Binary Tree + Double-ended Priority Queue ## Threaded Binary Tree 我們[前面](https://hackmd.io/@RyoukiWei/binary_tree_1#Binary-Tree)談過使用 linked list 表示 binary tree 會造成的空間浪費 那麼我們能否更充分地利用它們呢? Threaded 有以下規則: 1. `if x->left == nullptr` 指向其 predecessor 2. `if x->right == nullptr` 指向其 successor > A binary tree is threaded by making all right child pointers that would normally be null point to the in-order successor of the node (if it exists), and all left child pointers that would normally be null point to the in-order predecessor of the node. 如下圖: <center><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/7/7a/Threaded_tree.svg/220px-Threaded_tree.svg.png" alt="Threaded Binary Tree from Wiki"></center> [source](https://en.wikipedia.org/wiki/Threaded_binary_tree) *** 除了本來就會有的 root 外,我們還需要再一個 dummy node 其 `right` 指向自己,`left` 指向 root 並且我們需要標記其左右兩指標的作用是否為引線 (`true` 表示是) 以下為空樹的 dummy node 我們在節點左右兩邊新增布林值用來區分該條 link 是否為引線 因為 dummy node 的 `right` 指向自己,所以不是引線 (`false`) ```graphviz digraph structure { rankdir=LR; graph [fontname="DFKai-SB"]; node [fontname="DFKai-SB" shape=record, width=1.2, height=0.6]; edge [fontname="DFKai-SB"]; A [label="{ <lt> false | <left> left | <data> data | <right> right | <rt> true }"]; A:left:s -> A:lt [style=dashed]; A:right:s -> A:rt:r; } ``` Threaded BT 的節點結構如下 ```cpp! template <typename T> struct ThreadedTreeNode { T data; ThreadedTreeNode *left; ThreadedTreeNode *right; bool lthread; bool rthread; ThreadedTreeNode(const T &val) : data(val), left(nullptr), right(nullptr), lthread(true), rthread(true) {} }; ``` 我們需要在 constructor 設置好 dummy: ```cpp! using Node = ThreadedTreeNode<T>; ThreadedBinaryTree() : root(nullptr), dummy(new Node(T())) { dummy->left = dummy; dummy->right = dummy; dummy->rthread = false; } ``` 在 Threaded BT 的篇章我們至少要知道以下三種操作: 1. `inSuc()` and `inPre()` ,也就是中序後繼者與中序先行者 2. 簡化過後的中序遍歷 3. 指定位置插入 演算法都不難 ### `inSuc(x)` and `inPre(x)` 以找後繼者為例: 1. 如果 `x->rthread == true` 表示 `x` 沒有實際上的右子點,根據定義,其右引線會指向後繼者,故 `x->right` 即為所求 2. 反之,就像 BT 一樣,找右子樹最左的點即為所求 ```cpp! /** * @brief Find the inorder successor of the given node in threaded BT. * @param node Pointer to the current node. * @return Pointer to the inorder successor of the node. */ Node *inSuc(Node *node) { if (node->rthread) return node->right; node = node->right; while (!node->lthread) node = node->left; return node; } ``` 除了檢查左邊是否為引線,根據定義, `x` 的後繼者的先行者就是 `x`,這樣看也是可以的 > [!Note] > 試問:那 `inSuc(dummy)` 會得到什麼? > > 很簡單,`dummy->right` 指向它自己,`dummy->rthread == false` > 故我們便會從 `dummy->right` 也就是它自己開始,一直往左走,先到 root 最後走到的即整棵樹最左的那個 leaf (中序順序最開頭) ### Inorder Traversal 現在我們找後繼者可簡單了,那 inorder traversal 只要從 dummy 開始不斷找後繼者,直到重回 dummy 即可 ```cpp! /** * @brief Preforn an inorder traversal of the threaded BT. * @tparam Functor A callable type to apply to each node. * @param func A function or callable object to process node. */ template <typename Functor> void inorderTraversal(Functor func) { Node *curr = inSuc(dummy); while (curr != dummy) { func(curr); curr = inSuc(curr); } } ``` ### Insert `y` as a child of `x` 以插入右子點為例: 1. 如果 `x` 原先沒有右子點,把 `x->right` 指向 `y` ,`y` 的先行者即 `x` ,後繼者則為原先 `x` 的後繼者 2. 如果 `x` 本來是有右子點的,把 `x->right` 指向 `y` ,`y->right` 指向原本的 `x->right`,`y` 的先行者即 `x`,原本的 `x->right` 的先行者變成了 `y` 這邊強調先行者、後繼者代表在處理引線 ```cpp! /** * @brief Insert the target node as a child if tge given node. * @param node Pointer to the parent node where the target will be inserted. * @param target Pointer to the node to be inserted. * @param asRightChild Boolean flag indicating insertion as the right child (default: true). * @return True if the insertion was seccessful, false otherwise. */ bool insertAt(Node *node, Node *target, bool asRightChild = true) { if (!root) setRoot(node); if (!node || !target) return false; if (asRightChild) { target->rthread = node->rthread; target->right = node->right; target->lthread = true; target->left = node; node->rthread = false; node->right = target; if (!target->rthread) inSuc(target)->left = target; } else { target->lthread = node->lthread; target->left = node->left; target->rthread = true; target->right = node; node->lthread = false; node->left = target; if (!target->lthread) inPre(target)->right = target; } return true; } ``` ## Double-ended Priority Queue 我們說過[Heap](https://hackmd.io/@RyoukiWei/binary_tree_1#Heap)能夠用來作為 priority queue,不過我們只能從挑最小值跟最大值兩個操作中挑一種,也就是要每次挑最小值就用 Min-Heap 但不適合對此挑最大值;想挑最大值就用 Max-Heap 但同理就不適合挑最小值 Double-ended priority queue 則設計成類似 priority queue 的資料結構,但能同時支援有效地刪除最小值跟最大值 以下幾種資料結構適合用來製作 double-ended priority queue - Min-Max Heap - Deap - SMMH | Operations | Time Complexity | | :-------: | :--------: | | **Insert** | $O(\lg n)$ | | **Delete-Max** (**Delete-Min**)| $O(\lg n)$ | | **Find-Max** (**Find-Min**)| $O(1)$ | ### Min-Max Heap Min-Max heap 也是 complete binary tree 不過跟 binary heap 的差別是它每層是 min max 交錯 什麼意思呢? Min-Max heap 的 root 所在是 min level,root 是整棵樹中值最小者 對於 min-level 的節點,其必定是以該節點作為 root 的子樹中值最小者;對於 max-level 的節點,其必定為以該節點作為 root 的子樹中值最大者 > [!Tip] > Max-Min heap 就反過來,root 是最大值 因為 Min-Max heap 也是 complete binary tree ,所以同樣適合用 array 來表示 *** ```cpp! template <typename T> class MinMaxHeap { public: MinMaxHeap() : m_h() {} /** * @brief Inserts a new element into the Min-Max heap. * @param val The value to be inserted */ void push(const T &val) { m_h.push_back(val); bubbleUp(m_h.size() - 1); } /** * @brief Removes the smallest element (root) from the Min-Max heap. * @throws std::underflow_error if the heap is empty. */ void popMin() { if (m_h.empty()) throw std::underflow_error("Min-Max is empty."); std::swap(m_h.front(), m_h.back()); m_h.pop_back(); if (!m_h.empty()) trickleDown(0); } /** * @brief Removes the largest element from the Min-Max heap. * @throws std::underflow_error if the heap is empty. */ void popMax() { if (m_h.empty()) throw std::underflow_error("Min-Max is empty."); size_t maxIdx; if (m_h.size() < 3) maxIdx = m_h.size() - 1; else maxIdx = (m_h[1] > m_h[2]) ? 1 : 2; std::swap(m_h[maxIdx], m_h.back()); m_h.pop_back(); if (maxIdx < m_h.size()) trickleDown(maxIdx); } /** * @brief Retrieces the smallest element (root) from the Min-Max heap. * @return A reference to the smallest element. * @throws std::underflow_error if the heap is empty. */ T &min() const { if (m_h.empty()) throw std::underflow_error("Min-Max is empty."); return m_h.front(); } /** * @brief Retrieces the largest element from the Min-Max heap. * @return A reference to the largest element. * @throws std::underflow_error if the heap is empty. */ T &max() const { if (m_h.empty()) throw std::underflow_error("Min-Max is empty."); if (m_h.size() == 1) return m_h.front(); if (m_h.size() == 2) return m_h[1]; return std::max(m_h[1], m_h[2]); } /** * @brief Returns the number of elements in the heap. * @return The size of the heap. */ size_t size() const noexcept { return m_h.size(); } /** * @brief Removes all elements from the heap. */ void clear() { m_h.clear(); } private: std::vector<T> m_h; size_t getParentIdx(size_t idx) const { assert(idx > 0); return (idx - 1) / 2; } size_t getLeftChildIdx(size_t idx) const { return 2 * i + 1; } size_t getRightChildIdx(size_t idx) const { return 2 * i + 2; } bool isMinLevel(size_t idx) const { return static_cast<int>(log2(i + 1)) & 1; } /** * @brief Helper function for `push()`. */ void bubbleUp(size_t idx) { if (idx == 0) return; size_t parent = getParentIdx(idx); if (isMinLevel(idx)) { if (m_h[idx] > m_h[parent]) { std::swap(m_h[idx], m_h[parent]); bubbleUpMax(parent); } else bubbleUpMin(idx); } else { if (m_h[idx] < m_h[parent]) { std::swap(m_h[idx], m_h[parent]); bubbleUpMin(parent); } else bubbleUpMax(idx); } } void bubbleUpMin(size_t idx) { size_t grandparent = getParentIdx(getParentIdx(idx)); if (idx > 2 && m_h[idx] < m_h[grandparent]) { std::swap(m_h[idx], m_h[grandparent]); bubbleUpMin(grandparent); } } void bubbleUpMax(size_t idx) { size_t grandparent = getParentIdx(getParentIdx(idx)); if (idx > 2 && m_h[idx] > m_h[grandparent]) { std::swap(m_h[idx], m_h[grandparent]); bubbleUpMax(grandparent); } } void trickleDown(size_t idx) { if (isMinLevel(idx)) trickleDownMin(idx); else trickleDownMax(idx); } void trickleDownMin(size_t idx) { size_t j = getChildOrGrandchild(idx, true); if (j >= m_h.size()) return; if (m_h[j] < m_h[idx]) { std::swap(m_h[j], m_h[idx]); if (j >= getLeftChildIdx(getLeftChildIdx(idx))) { size_t parent = getParentIdx(j); if (parent < m_h.size() && m_h[j] > m_h[parent]) std::swap(m_h[j], m_h[parent]); trickleDownMin(j); } } } void trickleDownMax(size_t idx) { size_t j = getChildOrGrandchild(idx, false); if (j >= m_h.size()) return; if (m_h[j] > m_h[idx]) { std::swap(m_h[j], m_h[idx]); if (j >= getLeftChildIdx(getLeftChildIdx(idx))) { size_t parent = getParentIdx(j); if (parent < m_h.size() && m_h[j] < m_h[parent]) std::swap(m_h[j], m_h[parent]); trickleDownMax(j); } } } size_t getChildOrGrandchild(size_t idx, bool findMin) const { const size_t n = m_h.size(); size_t res = idx; for (size_t child = getLeftChildIdx(idx); child <= getRightChildIdx(idx) && child < n; ++child) { if ((findMin && m_h[child] < m_h[res]) || (!findMin && m_h[child] > m_h[res])) res = child; for (size_t grandchild = getLeftChildIdx(child); grandchild <= getRightChildIdx(child) && grandchild < n; ++grandchild) { if ((findMin && m_h[grandchild] < m_h[res]) || (!findMin && m_h[grandchild] > m_h[res])) res = grandchild; } } return res; } }; ``` ### Deap - root 不存資料 - root 的左子樹為 Min-Heap - root 的右子樹為 Max-Heap - 如果 $i$ 在 root 的左子樹,$j = 2^{\left\lfloor \lg i \right\rfloor - 1}$ 為其在 root 右子樹對應的點 (如果 $j$ 不存在,則 $j = \frac{j}{2} = 2^{\left\lfloor \lg i \right\rfloor - 2}$) ,$i$ 節點的值不可超過 $j$ 的值 插入的部份,會 binary heap 的插入就差不多了,剩下就是讓 $i$ 跟其對應點 $j$ 符合規則 刪除的部份,以刪最小為例,一樣就是先把 Min-Heap 那的最小一路跟其最小子點 swap 直到 leaf ,然後我們把整棵 Min-Max heap 最後一個點拔下來做插入 最容易忘記的大概就是要比較 $i$ 跟 $j$ 它們的值,要記得檢查 ### SMMH 也是 complete binary tree,root 不存資料 要符合以下規則: 1. left sibling $\le$ right sibling data 2. 若某節點具祖父節點,則其祖父節點的左子點必定 $\le$ 該節點 3. 若某節點具祖父節點,則其祖父節點的右子點必定 $\ge$ 該節點 總結一下上面的規則,可以得出: **任一子樹的最小值即其 root 的左子點、最大值即其 root 的右子點** 這是一個很方便手寫時檢查有沒有做對的性質 插入的方法,先放在整棵樹最後面 (新增在陣列最後面),重複檢查: - 1.是否滿足?如果不滿足,左右兄弟 swap - 2.跟 3.是否皆滿足?如果不滿足,往上 (or 斜上)調整 刪除的方法應該也猜出來了,先把目標跟陣列最後的元素 swap 並 pop back (這樣目標就山掉了),接著一樣檢查並調整直到規則都滿足了 (以 `popMin()` 來說, 3.不必檢查,因為該性質不會被影響到) 我只講怎麼操作的話,代表我覺得了解手寫怎麼寫就好,沒想過怎麼寫 code 不成大礙 ## 【資工考研】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)