/** * @file * @brief A demo 2-3-4 tree implementation * @details * 2–3–4 tree is a self-balancing data structure that is an isometry of * red–black trees. Though we seldom use them in practice, we study them * to understand the theory behind Red-Black tree. Please read following * links for more infomation. * [2–3–4 tree](https://en.wikipedia.org/wiki/2%E2%80%933%E2%80%934_tree) * [2-3-4 Trees: A Visual Introduction](https://www.educative.io/page/5689413791121408/80001) * We Only implement some basic and complicated operations in this demo. * Other operations should be easy to be added. * @author [liuhuan](https://github.com/fedom) */ #include /// for std::array #include /// for assert #include /// for std::ofstream #include /// for std::cout #include /// for std::unique_ptr #include /// for std::queue #include /// for std::to_string /** * @namespace data_structures * @brief Algorithms with data structures */ namespace data_structures { /** * @namespace tree_234 * @brief Functions for [2–3–4 tree](https://en.wikipedia.org/wiki/2%E2%80%933%E2%80%934_tree) */ namespace tree_234 { /** @brief 2-3-4 tree node class */ class Node { public: /** * @brief Node constructor * @param item the first value we insert to the node */ explicit Node(int64_t item) : count(1), items({{item, 0, 0}}), children({{nullptr, nullptr, nullptr, nullptr}}) {} /** * @brief Get the item count that current saved in the node * @return item count */ int8_t GetCount() { return count; } /** * @brief Set the item count of the node * * This is only used when we spliting and merging node where we need to do * some raw operation manually. In common inserting and removing operation * the count is maintained automatically. * * @param c the count to set */ void SetCount(int8_t c) { count = c; } /** * @brief Check if node is a leaf * @return true if node is leaf, false otherwise */ bool IsLeaf() { return children[0] == nullptr; } /** * @brief Check if node is a full (4-node) * @return true if node is full (4-node), false otherwise */ bool IsFull() { return count == 3; } /** * @brief Check if node is a 2-node * @return true if node is 2-node, otherwise false */ bool Is2Node() { return count == 1; } /** @brief Check if node is a 3-node or 4-node, this is useful when we * delete item from 2-3-4 tree * @return true if node is 3-node or 4-node, false otherwise */ bool Is34Node() { return count == 2 || count == 3; } /** * @brief Check if item is in the node * @param item item to check * @return true if item in the node, otherwise false */ bool Contains(int64_t item) { for (int8_t i = 0; i < count; i++) { if (item == items[i]) { return true; } } return false; } /** * @brief Get the index of the item in the node, 0-based * @param item item to check * @return 0-based index of the item in the node, if not in the node, -1 is * returned */ int8_t GetItemIndex(int64_t item) { for (int8_t i = 0; i < count; i++) { if (items[i] == item) { return i; } } return -1; } /** * @brief Get max item (rightmost) in the current node * @return max item */ int64_t GetMaxItem() { return items[count - 1]; } /** * @brief get min item (leftmost) in the current node * @return min item */ int64_t GetMinItem() { return items[0]; } /** * @brief Get item of the \index index * @param index the item index to get * @return the item */ int64_t GetItem(int8_t index) { return items[index]; } /** * @brief Set item value at position of index * @param index the index of the item to set * @param new_item item value */ void SetItem(int8_t index, int64_t new_item) { assert(index >= 0 && index <= 2); items[index] = new_item; } /** * @brief Insert item to the proper position of the node and return the * position index. * * This is a helper function we use during insertion. Please mind that when * insert a item, we aslo need to take care of two child pointers. One is * the original child pointer at the insertion position. It can be placed as * new item's either left child or right child. And the other is the new * child that should be added. For our dedicated situation here, we choose * to use the original child as the new item's left child, and add a null * pointer to its right child. So after use the function, please update * these two children pointer manually. * * @param item value to be inserted to the node * @return index where item is inserted, caller can use this * index to update its left and right child */ int InsertItem(int item) { assert(!IsFull()); if (Contains(item)) { return -1; } int8_t i = 0; for (i = 0; i < count; i++) { if (items[i] > item) { break; } } InsertItemByIndex(i, item, nullptr, true); return i; } /** * @brief Insert a value to the index position * @param index index where to insert item * @param item value to insert * @param with_child new added child pointer * @param to_left true indicate adding with_child to new item's left child, * otherwise to right child */ void InsertItemByIndex(int8_t index, int64_t item, Node *with_child, bool to_left = true) { assert(count < 3 && index >= 0 && index < 3); for (int8_t i = count - 1; i >= index; i--) { items[i + 1] = items[i]; } items[index] = item; int8_t start_index = to_left ? index : index + 1; for (int8_t i = count; i >= start_index; i--) { children[i + 1] = children[i]; } children[start_index] = with_child; count++; } /** * @brief Insert a value to the index position * @param index index of the item to remove * @param keep_left which child of the item to keep, true keep the left * child, false keep the right child * @return the removed child pointer */ Node *RemoveItemByIndex(int8_t index, bool keep_left) { assert(index >= 0 && index < count); Node *removed_child = keep_left ? children[index + 1] : children[index]; for (int8_t i = index; i < count - 1; i++) { items[i] = items[i + 1]; } for (int8_t i = keep_left ? index + 1 : index; i < count; i++) { children[i] = children[i + 1]; } count--; return removed_child; } /** * @brief Get the child's index of the children array * @param child child pointer of which to get the index * @return the index of child */ int8_t GetChildIndex(Node *child) { for (int8_t i = 0; i < count + 1; i++) { if (children[i] == child) { return i; } } return -1; } /** * @brief Get the child pointer at position of index * @param index index of child to get * @return the child pointer */ Node *GetChild(int8_t index) { return children[index]; } /** * @brief Set child pointer to the position of index * @param index children index * @param child pointer to set */ void SetChild(int8_t index, Node *child) { children[index] = child; } /** * @brief Get rightmose child of the current node * @return the rightmost child */ Node *GetRightmostChild() { return children[count]; } /** * @brief Get leftmose child of the current node * @return the leftmost child */ Node *GetLeftmostChild() { return children[0]; } /** * @brief Get left child of item at item_index * @param item_index index of the item whose left child to be get * @return left child of items[index]'s */ Node *GetItemLeftChild(int8_t item_index) { if (item_index < 0 || item_index > count - 1) { return nullptr; } return children[item_index]; } /** * @brief Get right child of item at item_index * @param item_index index of the item whose right child to be get * @return right child of items[index]'s */ Node *GetItemRightChild(int8_t item_index) { if (item_index < 0 || item_index > count - 1) { return nullptr; } return children[item_index + 1]; } /** * @brief Get next node which is possibly contains item * @param item item to search * @return the next node that possibly contains item */ Node *GetNextPossibleChild(int64_t item) { int i = 0; for (i = 0; i < count; i++) { if (items[i] > item) { break; } } return children[i]; } private: std::array items; ///< store items std::array children; ///< store the children pointers int8_t count = 0; ///< track the current item count }; /** @brief 2-3-4 tree class */ class Tree234 { public: Tree234() = default; Tree234(const Tree234 &) = delete; Tree234(const Tree234 &&) = delete; Tree234 &operator=(const Tree234 &) = delete; Tree234 &operator=(const Tree234 &&) = delete; ~Tree234(); /** * @brief Insert item to tree * @param item item to insert */ void Insert(int64_t item); /** * @brief Remove item from tree * @param item item to remove * @return true if item found and removed, false otherwise */ bool Remove(int64_t item); /** @brief In-order traverse */ void Traverse(); /** * @brief Print tree into a dot file * @param file_name output file name, if nullptr then use "out.dot" as * default */ void Print(const char *file_name = nullptr); private: /** * @brief A insert implementation of pre-split * @param item item to insert */ void InsertPreSplit(int64_t item); /** * @brief A insert implementation of post-merge * @param item item to insert */ void InsertPostMerge(int64_t item); /** * @brief A helper function used by post-merge insert * @param tree tree where to insert item * @param item item to insert * @return the node that split as the parent when overflow happen */ Node *Insert(Node *tree, int64_t item); /** * @brief A helper function used during post-merge insert * * When the inserting leads to overflow, it will split the node to 1 parent * and 2 children. The parent will be merged to its origin parent after * that. This is the function to complete this task. So the param node is * always a 2-node. * * @param dst_node the target node we will merge node to, can be type of * 2-node, 3-node or 4-node * @param node the source node we will merge from, type must be 2-node * @return overflow node of this level */ Node *MergeNode(Node *dst_node, Node *node); /** * @brief Merge node to a not-full target node * * Since the target node is not-full, no overflow will happen. So we have * nothing to return. * * @param dst_node the target not-full node, that is the type is either * 2-node or 3-node, but not 4-node * @param node the source node we will merge from, type must be 2-node */ void MergeNodeNotFull(Node *dst_node, Node *node); /** * @brief Split a 4-node to 1 parent and 2 children, and return the parent * node * @param node the node to split, it must be a 4-node * @return split parent node */ Node *SplitNode(Node *node); /** * @brief Get the max item of the tree * @param tree the tree we will get item from * @return max item of the tree */ int64_t GetTreeMaxItem(Node *tree); /** * @brief Get the min item of the tree * @param tree the tree we will get item from * @return min item of the tree */ int64_t GetTreeMinItem(Node *tree); /** * @brief A handy function to try if we can do a left rotate to the target * node * * Given two node, the parent and the target child, the left rotate * operation is uniquely identified. The source node must be the right * sibling of the target child. The operation can be successfully done if * the to_child has a right sibling and its right sibling is not 2-node. * * @param parent the parent node in this left rotate operation * @param to_child the target child of this left rotate operation. In our * case, this node is always 2-node * @return true if we successfully do the rotate. false if the * requirements are not fulfilled. */ bool TryLeftRotate(Node *parent, Node *to_child); /** * @brief A handy function to try if we can do a right rotate to the target * node * * Given two node, the parent and the target child, the right rotate * operation is uniquely identified. The source node must be the left * sibling of the target child. The operation can be successfully done if * the to_child has a left sibling and its left sibling is not 2-node. * * @param parent the parent node in this right rotate operation * @param to_child the target child of this right rotate operation. In our * case, it is always 2-node * @return true if we successfully do the rotate. false if the * requirements are not fulfilled. */ bool TryRightRotate(Node *parent, Node *to_child); /** * @brief Do the actual right rotate operation * * Given parent node, and the pivot item index, the right rotate operation * is uniquely identified. The function assume the requirements are * fulfilled and won't do any extra check. This function is call by * TryRightRotate(), and the condition checking should be done before call * it. * * @param parent the parent node in this right rotate operation * @param index the pivot item index of this right rotate operation. */ void RightRotate(Node *parent, int8_t index); /** * @brief Do the actual left rotate operation * * Given parent node, and the pivot item index, the left rotate operation is * uniquely identified. The function assume the requirements are fulfilled * and won't do any extra check. This function is call by TryLeftRotate(), * and the condition checking should be done before call it. * * @param parent the parent node in this right rotate operation * @param index the pivot item index of this right rotate operation. */ void LeftRotate(Node *parent, int8_t index); /** * @brief Main function implement the pre-merge remove operation * @param node the tree to remove item from * @param item item to remove * @return true if remove success, false otherwise * */ bool RemovePreMerge(Node *node, int64_t item); /** * @brief Merge the item at index of the parent node, and its left and right * child * * the left and right child node must be 2-node. The 3 items will be merged * into a 4-node. In our case the parent can be a 2-node iff it is the root. * Otherwise, it must be 3-node or 4-node. * * @param parent the parent node in the merging operation * @param index the item index of the parent node that involved in the * merging * @return the merged 4-node */ Node *Merge(Node *parent, int8_t index); /** * @brief Recursive release the tree * @param tree root node of the tree to delete */ void DeleteNode(Node *tree); /** * @brief In-order traverse the tree, print items * @param tree tree to traverse */ void Traverse(Node *tree); /** * @brief Print the tree to a dot file. You can convert it to picture with * graphviz * @param ofs output file stream to print to * @param node current node to print * @param parent_index current node's parent node index, this is used to * draw the link from parent to current node * @param index current node's index of level order which is used to name * the node in dot file * @param parent_child_index the index that current node in parent's * children array, range in [0,4), help to locate the start position of the * link between nodes */ void PrintNode(std::ofstream &ofs, Node *node, int64_t parent_index, int64_t index, int8_t parent_child_index); Node *root_{nullptr}; ///< root node of the tree }; Tree234::~Tree234() { DeleteNode(root_); } /** * @brief Recursive release the tree * @param tree root node of the tree to delete */ void Tree234::DeleteNode(Node *tree) { if (!tree) { return; } for (int8_t i = 0; i <= tree->GetCount(); i++) { DeleteNode(tree->GetChild(i)); } delete tree; } /** * @brief In-order traverse the tree, print items * @param tree tree to traverse */ void Tree234::Traverse() { Traverse(root_); std::cout << std::endl; } void Tree234::Traverse(Node *node) { if (!node) { return; } int8_t i = 0; for (i = 0; i < node->GetCount(); i++) { Traverse(node->GetChild(i)); std::cout << node->GetItem(i) << ", "; } Traverse(node->GetChild(i)); } /** * @brief A insert implementation of pre-split * @param item item to insert */ void Tree234::InsertPreSplit(int64_t item) { if (!root_) { root_ = new Node(item); return; } Node *parent = nullptr; Node *node = root_; while (true) { if (!node) { std::unique_ptr tmp(new Node(item)); MergeNodeNotFull(parent, tmp.get()); return; } if (node->Contains(item)) { return; } if (node->IsFull()) { node = SplitNode(node); Node *cur_node = nullptr; if (item < node->GetItem(0)) { cur_node = node->GetChild(0); } else { cur_node = node->GetChild(1); } if (!parent) { // for the root node parent is nullptr, we simply assign the // split parent to root_ root_ = node; } else { // merge the split parent to its origin parent MergeNodeNotFull(parent, node); } node = cur_node; } parent = node; node = parent->GetNextPossibleChild(item); } } /** * @brief A insert implementation of post-merge * @param item item to insert */ void Tree234::InsertPostMerge(int64_t item) { if (!root_) { root_ = new Node(item); return; } Node *split_node = Insert(root_, item); // if root has split, then update root_ if (split_node) { root_ = split_node; } } /** * @brief Insert item to tree * @param item item to insert */ void Tree234::Insert(int64_t item) { InsertPreSplit(item); } /** * @brief A helper function used by post-merge insert * @param tree tree where to insert item * @param item item to insert * @return the node that split as the parent when overflow happen */ Node *Tree234::Insert(Node *tree, int64_t item) { assert(tree != nullptr); std::unique_ptr split_node; if (tree->Contains(item)) { // return nullptr indicate current node not overflow return nullptr; } Node *next_node = tree->GetNextPossibleChild(item); if (next_node) { split_node.reset(Insert(next_node, item)); } else { split_node.reset(new Node(item)); } if (split_node) { return MergeNode(tree, split_node.get()); } return nullptr; } /** * @brief A helper function used during post-merge insert * * When the inserting leads to overflow, it will split the node to 1 parent * and 2 children. The parent will be merged to its origin parent after * that. This is the function to complete this task. So the param node is * always a 2-node. * * @param dst_node the target node we will merge node to, can be type of * 2-node, 3-node or 4-node * @param node the source node we will merge from, type must be 2-node * @return overflow node of this level */ Node *Tree234::MergeNode(Node *dst_node, Node *node) { assert(dst_node != nullptr && node != nullptr); if (!dst_node->IsFull()) { MergeNodeNotFull(dst_node, node); return nullptr; } dst_node = SplitNode(dst_node); if (node->GetItem(0) < dst_node->GetItem(0)) { MergeNodeNotFull(dst_node->GetChild(0), node); } else { MergeNodeNotFull(dst_node->GetChild(1), node); } return dst_node; } /** * @brief Merge node to a not-full target node * * Since the target node is not-full, no overflow will happen. So we have * nothing to return. * * @param dst_node the target not-full node, that is the type is either * 2-node or 3-node, but not 4-node * @param node the source node we will merge from, type must be 2-node */ void Tree234::MergeNodeNotFull(Node *dst_node, Node *node) { assert(dst_node && node && !dst_node->IsFull() && node->Is2Node()); int8_t i = dst_node->InsertItem(node->GetItem(0)); dst_node->SetChild(i, node->GetChild(0)); dst_node->SetChild(i + 1, node->GetChild(1)); } /** * @brief Split a 4-node to 1 parent and 2 children, and return the parent * node * @param node the node to split, it must be a 4-node * @return split parent node */ Node *Tree234::SplitNode(Node *node) { assert(node->GetCount() == 3); Node *left = node; Node *right = new Node(node->GetItem(2)); right->SetChild(0, node->GetChild(2)); right->SetChild(1, node->GetChild(3)); Node *parent = new Node(node->GetItem(1)); parent->SetChild(0, left); parent->SetChild(1, right); left->SetCount(1); return parent; } /** * @brief A handy function to try if we can do a left rotate to the target * node * * Given two node, the parent and the target child, the left rotate * operation is uniquely identified. The source node must be the right * sibling of the target child. The operation can be successfully done if * the to_child has a right sibling and its right sibling is not 2-node. * * @param parent the parent node in this left rotate operation * @param to_child the target child of this left rotate operation. In our * case, this node is always 2-node * @return true if we successfully do the rotate. false if the * requirements are not fulfilled. */ bool Tree234::TryLeftRotate(Node *parent, Node *to_child) { int to_child_index = parent->GetChildIndex(to_child); // child is right most, can not do left rotate to it if (to_child_index >= parent->GetCount()) { return false; } Node *right_sibling = parent->GetChild(to_child_index + 1); // right sibling is 2-node. can not do left rotate. if (right_sibling->Is2Node()) { return false; } LeftRotate(parent, to_child_index); return true; } /** * @brief A handy function to try if we can do a right rotate to the target * node * * Given two node, the parent and the target child, the right rotate * operation is uniquely identified. The source node must be the left * sibling of the target child. The operation can be successfully done if * the to_child has a left sibling and its left sibling is not 2-node. * * @param parent the parent node in this right rotate operation * @param to_child the target child of this right rotate operation. In our * case, it is always 2-node * @return true if we successfully do the rotate. false if the * requirements are not fulfilled. */ bool Tree234::TryRightRotate(Node *parent, Node *to_child) { int8_t to_child_index = parent->GetChildIndex(to_child); // child is left most, can not do right rotate to it if (to_child_index <= 0) { return false; } Node *left_sibling = parent->GetChild(to_child_index - 1); // right sibling is 2-node. can not do left rotate. if (left_sibling->Is2Node()) { return false; } RightRotate(parent, to_child_index - 1); return true; } /** * @brief Do the actual right rotate operation * * Given parent node, and the pivot item index, the right rotate operation * is uniquely identified. The function assume the requirements are * fulfilled and won't do any extra check. This function is call by * TryRightRotate(), and the condition checking should be done before call * it. * * @param parent the parent node in this right rotate operation * @param index the pivot item index of this right rotate operation. */ void Tree234::RightRotate(Node *parent, int8_t index) { Node *left = parent->GetItemLeftChild(index); Node *right = parent->GetItemRightChild(index); assert(left && left->Is34Node()); assert(right && right->Is2Node()); right->InsertItemByIndex(0, parent->GetItem(index), left->GetRightmostChild(), true); parent->SetItem(index, left->GetMaxItem()); left->RemoveItemByIndex(left->GetCount() - 1, true); } /** * @brief Do the actual left rotate operation * * Given parent node, and the pivot item index, the left rotate operation is * uniquely identified. The function assume the requirements are fulfilled * and won't do any extra check. This function is call by TryLeftRotate(), * and the condition checking should be done before call it. * * @param parent the parent node in this right rotate operation * @param index the pivot item index of this right rotate operation. */ void Tree234::LeftRotate(Node *parent, int8_t index) { Node *left = parent->GetItemLeftChild(index); Node *right = parent->GetItemRightChild(index); assert(right && right->Is34Node()); assert(left && left->Is2Node()); left->InsertItemByIndex(left->GetCount(), parent->GetItem(index), right->GetLeftmostChild(), false); parent->SetItem(index, right->GetMinItem()); right->RemoveItemByIndex(0, false); } /** * @brief Merge the item at index of the parent node, and its left and right * child * * the left and right child node must be 2-node. The 3 items will be merged * into a 4-node. In our case the parent can be a 2-node iff it is the root. * Otherwise, it must be 3-node or 4-node. * * @param parent the parent node in the merging operation * @param index the item index of the parent node that involved in the * merging * @return the merged 4-node */ Node *Tree234::Merge(Node *parent, int8_t index) { assert(parent); // bool is_parent_2node = parent->Is2Node(); Node *left_child = parent->GetItemLeftChild(index); Node *right_child = parent->GetItemRightChild(index); assert(left_child->Is2Node() && right_child->Is2Node()); int64_t item = parent->GetItem(index); // 1. merge parent's item and right child to left child left_child->SetItem(1, item); left_child->SetItem(2, right_child->GetItem(0)); left_child->SetChild(2, right_child->GetChild(0)); left_child->SetChild(3, right_child->GetChild(1)); left_child->SetCount(3); // 2. remove the parent's item parent->RemoveItemByIndex(index, true); // 3. delete the unused right child delete right_child; return left_child; } /** * @brief Remove item from tree * @param item item to remove * @return true if item found and removed, false otherwise */ bool Tree234::Remove(int64_t item) { return RemovePreMerge(root_, item); } /** * @brief Main function implement the pre-merge remove operation * @param node the tree to remove item from * @param item item to remove * @return true if remove success, false otherwise */ bool Tree234::RemovePreMerge(Node *node, int64_t item) { while (node) { if (node->IsLeaf()) { if (node->Contains(item)) { if (node->Is2Node()) { // node must be root delete node; root_ = nullptr; } else { node->RemoveItemByIndex(node->GetItemIndex(item), true); } return true; } return false; } // node is internal if (node->Contains(item)) { int8_t index = node->GetItemIndex(item); // Here is important!!! What we do next depend on its children's // state. Why? Node *left_child = node->GetItemLeftChild(index); Node *right_child = node->GetItemRightChild(index); assert(left_child && right_child); if (left_child->Is2Node() && right_child->Is2Node()) { // both left and right child are 2-node,we should not modify // current node in this situation. Because we are going to do // merge with its children which will move target item to next // layer. so if we replace the item with successor or // predecessor now, when we do the recursive remove with // successor or predecessor, we will result in removing the just // replaced one in the merged node. That's not what we want. // we need to convert the child 2-node to 3-node or 4-node // first. First we try to see if any of them can convert to // 3-node by rotate. By using rotate we keep the empty house for // the future insertion which will be more efficient than merge. // // | ? | node | ? | // / | | \ // / | | \ // / | | \ // / | | \ // / | | \ // / | | \ // ? left_child right_child ? // // node must be the root if (node->Is2Node()) { // this means we can't avoid merging the target item into // next layer, and this will cause us do different process // compared with other cases Node *new_root = Merge(node, index); delete root_; root_ = new_root; node = root_; // now node point to the continue; } // here means we can avoid merging the target item into next // layer. So we convert one of its left or right child to 3-node // and then do the successor or predecessor swap and recursive // remove the next layer will successor or predecessor. do { if (index > 0) { // left_child has left-sibling, we check if we can do a // rotate Node *left_sibling = node->GetItemLeftChild(index - 1); if (left_sibling->Is34Node()) { RightRotate(node, index - 1); break; } } if (index < node->GetCount() - 1) { // right_child has right-sibling, we check if we can do // a rotate Node *right_sibling = node->GetItemRightChild(index + 1); if (right_sibling->Is34Node()) { LeftRotate(node, index + 1); break; } } // we do a merge. We avoid merging the target item, which // may trigger another merge in the recursion process. if (index > 0) { Merge(node, index - 1); break; } Merge(node, index + 1); } while (false); } // refresh the left_child and right_child since they may be invalid // because of merge left_child = node->GetItemLeftChild(index); right_child = node->GetItemRightChild(index); if (left_child->Is34Node()) { int64_t predecessor_item = GetTreeMaxItem(left_child); node->SetItem(node->GetItemIndex(item), predecessor_item); node = left_child; item = predecessor_item; continue; } if (right_child->Is34Node()) { int64_t successor_item = GetTreeMinItem(right_child); node->SetItem(node->GetItemIndex(item), successor_item); node = right_child; item = successor_item; continue; } } Node *next_node = node->GetNextPossibleChild(item); if (next_node->Is34Node()) { node = next_node; continue; } if (TryRightRotate(node, next_node)) { node = next_node; continue; } if (TryLeftRotate(node, next_node)) { node = next_node; continue; } // get here means both left sibling and right sibling of next_node is // 2-node, so we do merge int8_t child_index = node->GetChildIndex(next_node); if (child_index > 0) { node = Merge(node, child_index - 1); } else { node = Merge(node, child_index); } } // while return false; } /** * @brief Get the max item of the tree * @param tree the tree we will get item from * @return max item of the tree */ int64_t Tree234::GetTreeMaxItem(Node *tree) { assert(tree); int64_t max = 0; while (tree) { max = tree->GetMaxItem(); tree = tree->GetRightmostChild(); } return max; } /** * @brief Get the min item of the tree * @param tree the tree we will get item from * @return min item of the tree */ int64_t Tree234::GetTreeMinItem(Node *tree) { assert(tree); int64_t min = 0; while (tree) { min = tree->GetMinItem(); tree = tree->GetLeftmostChild(); } return min; } /** * @brief Print tree into a dot file * @param file_name output file name, if nullptr then use "out.dot" as default */ void Tree234::Print(const char *file_name) { if (!file_name) { file_name = "out.dot"; } std::ofstream ofs; ofs.open(file_name); if (!ofs) { std::cout << "create tree dot file failed, " << file_name << std::endl; return; } ofs << "digraph G {\n"; ofs << "node [shape=record]\n"; int64_t index = 0; /** @brief This is a helper structure to do a level order traversal to print * the tree. */ struct NodeInfo { Node *node; ///< tree node int64_t index; ///< node index of level order that used when draw the ///< link between child and parent }; std::queue q; if (root_) { // print root node PrintNode(ofs, root_, -1, index, 0); NodeInfo ni{}; ni.node = root_; ni.index = index; q.push(ni); while (!q.empty()) { NodeInfo node_info = q.front(); q.pop(); assert(node_info.node->GetCount() > 0); if (!node_info.node->IsLeaf()) { if (node_info.node->GetCount() > 0) { PrintNode(ofs, node_info.node->GetChild(0), node_info.index, ++index, 0); ni.node = node_info.node->GetChild(0); ni.index = index; q.push(ni); PrintNode(ofs, node_info.node->GetChild(1), node_info.index, ++index, 1); ni.node = node_info.node->GetChild(1); ni.index = index; q.push(ni); } if (node_info.node->GetCount() > 1) { PrintNode(ofs, node_info.node->GetChild(2), node_info.index, ++index, 2); ni.node = node_info.node->GetChild(2); ni.index = index; q.push(ni); } if (node_info.node->GetCount() > 2) { PrintNode(ofs, node_info.node->GetChild(3), node_info.index, ++index, 3); ni.node = node_info.node->GetChild(3); ni.index = index; q.push(ni); } } } } ofs << "}\n"; ofs.close(); } /** * @brief Print the tree to a dot file. You can convert it to picture with * graphviz * @param ofs output file stream to print to * @param node current node to print * @param parent_index current node's parent node index, this is used to draw * the link from parent to current node * @param index current node's index of level order which is used to name the * node in dot file * @param parent_child_index the index that current node in parent's children * array, range in [0,4), help to locate the start position of the link between * nodes */ void Tree234::PrintNode(std::ofstream &ofs, Node *node, int64_t parent_index, int64_t index, int8_t parent_child_index) { assert(node); switch (node->GetCount()) { case 1: ofs << "node_" << index << " [label=\" " << node->GetItem(0) << "\"]\n"; break; case 2: ofs << "node_" << index << " [label=\" " << node->GetItem(0) << " | " << node->GetItem(1) << "\"]\n"; break; case 3: ofs << "node_" << index << " [label=\" " << node->GetItem(0) << " | " << node->GetItem(1) << "| " << node->GetItem(2) << "\"]\n"; break; default: break; } // draw the edge if (parent_index >= 0) { ofs << "node_" << parent_index << ":f" << (parent_child_index == 0 ? 0 : parent_child_index - 1) << ":" << (parent_child_index == 0 ? "sw" : "se") << " -> node_" << index << "\n"; } } } // namespace tree_234 } // namespace data_structures /** @brief simple test to insert a given array and delete some item, and print * the tree*/ static void test1() { std::array arr = {3, 1, 5, 4, 2, 9, 10, 8, 7, 6, 16, 13, 14}; data_structures::tree_234::Tree234 tree; for (auto i : arr) { tree.Insert(i); } // tree.Remove(10); tree.Remove(5); tree.Print(); } /** * @brief simple test to insert continuous number of range [0, n), and print * the tree * @param n upper bound of the range number to insert */ static void test2(int64_t n) { data_structures::tree_234::Tree234 tree; for (int64_t i = 0; i < n; i++) { tree.Insert(i); } tree.Traverse(); tree.Print((std::to_string(n) + ".dot").c_str()); } /** * @brief Main function * @param argc commandline argument count (ignored) * @param argv commandline array of arguments (ignored) * @returns 0 on exit */ int main(int argc, char *argv[]) { if (argc < 2) { test1(); // execute 1st test } else { test2(std::stoi(argv[1])); // execute 2nd test } return 0; }