From 53fe17c3b0f31731e4c0a8f98e1df030b86341e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Do=C4=9Fa=20Oru=C3=A7?= <25724155+aeris170@users.noreply.github.com> Date: Mon, 6 Jan 2025 06:38:27 +0300 Subject: [PATCH] Implement array based N-ary tree data structure --- Utility/CMakeLists.txt | 1 + Utility/Tree.hpp | 273 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 274 insertions(+) create mode 100644 Utility/Tree.hpp diff --git a/Utility/CMakeLists.txt b/Utility/CMakeLists.txt index efc26697..6ae651e6 100644 --- a/Utility/CMakeLists.txt +++ b/Utility/CMakeLists.txt @@ -39,6 +39,7 @@ set(GROUP_LIST "StringTransform.cpp" "StringTransform.hpp" "TemplateUtilities.hpp" + "Tree.hpp" "Trim.cpp" "Trim.hpp" "UndoRedoStack.cpp" diff --git a/Utility/Tree.hpp b/Utility/Tree.hpp new file mode 100644 index 00000000..8383068e --- /dev/null +++ b/Utility/Tree.hpp @@ -0,0 +1,273 @@ +#pragma once + +#include +#include +#include +#include +#include + +template +struct Tree { + + static constexpr size_t Root = 0; + + using NodeIndex = size_t; + + struct Node { + NodeData Data; + std::vector Children; + }; + + using DataStructure = std::vector; + + struct ChildrenList { + struct NodeIterator { + using iterator_category = std::forward_iterator_tag; + using difference_type = std::ptrdiff_t; + using value_type = NodeData; + using pointer = value_type*; + using reference = value_type&; + + NodeIterator(Tree& tree, const std::vector& indices, size_t pos) : + tree(tree), indices(indices), pos(pos) {} + + reference operator*() const { return tree.data[indices[pos]].Data; } + pointer operator->() const { return &tree.data[indices[pos]].Data; } + NodeIterator& operator++() { ++pos; return *this; } + NodeIterator operator++(int) { NodeIterator tmp = *this; ++(*this); return tmp; } + friend bool operator==(const NodeIterator& a, const NodeIterator& b) { return a.pos == b.pos; } + + private: + Tree& tree; + const std::vector& indices; + size_t pos; + }; + + ChildrenList(Tree& tree, const std::vector& indices) noexcept : + tree(tree), indices(indices) {}; + + NodeData& operator[](std::size_t idx) { return tree.data[indices[idx]].Data; } + const NodeData& operator[](std::size_t idx) const { return tree.data[indices[idx]].Data; } + + NodeIterator begin() { return NodeIterator(tree, indices, 0); } + NodeIterator end() { return NodeIterator(tree, indices, indices.size()); } + + size_t size() const { return indices.size(); } + + private: + Tree& tree; + const std::vector& indices; + }; + + Tree() noexcept requires(std::is_nothrow_default_constructible_v); + explicit Tree(const NodeData& rootData) noexcept; + + /// + /// Precondition: A node at parentIndex is present. + /// Postcondition: A node under specified parent is present with data created using args. + /// + template + requires std::constructible_from + NodeIndex EmplaceNode(const NodeIndex parentIndex, Args&&... args) noexcept; + + /// + /// Precondition: A node at parentIndex is present. + /// Postcondition: A node under specified parent is present with data nodeData (copy). + /// + NodeIndex InsertNode(const NodeIndex parentIndex, const NodeData& nodeData) noexcept; + + /// + /// Precondition: A node at parentIndex is present. + /// Postcondition: A node under specified parent is present with data nodeData (move). + /// + NodeIndex InsertNode(const NodeIndex parentIndex, NodeData&& nodeData) noexcept; + + /// + /// Precondition: None. + /// Postcondition: None. + /// + bool HasNodeAt(const NodeIndex index) const noexcept; + + /// + /// Precondition: A node at index is present. + /// Postcondition: None. + /// + NodeData& NodeAt(const NodeIndex index) noexcept; + + /// + /// Precondition: A node at index is present. + /// Postcondition: None. + /// + const NodeData& NodeAt(const NodeIndex index) const noexcept; + + /// + /// Precondition: A node at index is present, index is not Root. + /// Postcondition: None. + /// + NodeIndex FindParentOfNodeAt(const NodeIndex index) const noexcept; + + /// + /// Precondition: A node at index is present. + /// Postcondition: None. + /// + ChildrenList ChildrenOfNodeAt(const NodeIndex index) noexcept; + + /// + /// Precondition: A node at index is present. + /// Postcondition: None. + /// + ChildrenList ChildrenOfNodeAt(const NodeIndex index) const noexcept; + + /// + /// Precondition: A node at index is present. + /// Postcondition: Node at index and it's children are deleted. If index == Root, only Root node remains. + /// + void DeleteNode(const NodeIndex index) noexcept; + +private: + DataStructure data; +}; + +template +inline Tree::Tree() noexcept requires(std::is_nothrow_default_constructible_v) { + data.reserve(InitialNodeCapacity); + data.emplace_back(NodeData{}, decltype(Node::Children){}).Children.reserve(InitialChildCapacity); +} +template +inline Tree::Tree(const NodeData& rootData) noexcept { + data.reserve(InitialNodeCapacity); + data.emplace_back(rootData, decltype(Node::Children){}).Children.reserve(InitialChildCapacity); +} + +template +template + requires std::constructible_from +inline auto Tree::EmplaceNode(const NodeIndex parentIndex, Args&&... args) noexcept -> NodeIndex { + assert(parentIndex < data.size()); + data.emplace_back(std::forward(args)..., decltype(Node::Children){}).Children.reserve(InitialChildCapacity); + data[parentIndex].Children.emplace_back(data.size() - 1); + return data.size() - 1; +} + +template +inline auto Tree::InsertNode(const NodeIndex parentIndex, const NodeData& nodeData) noexcept -> NodeIndex { + assert(parentIndex < data.size()); + data.emplace_back(nodeData, decltype(Node::Children){}).Children.reserve(InitialChildCapacity); + data[parentIndex].Children.emplace_back(data.size() - 1); + return data.size() - 1; +} +template +inline auto Tree::InsertNode(const NodeIndex parentIndex, NodeData&& nodeData) noexcept -> NodeIndex { + assert(parentIndex < data.size()); + data.emplace_back(std::move(nodeData), decltype(Node::Children){}).Children.reserve(InitialChildCapacity); + data[parentIndex].Children.emplace_back(data.size() - 1); + return data.size() - 1; +} + +template +inline auto Tree::HasNodeAt(const NodeIndex index) const noexcept -> bool { + return index < data.size(); +} +template +inline auto Tree::NodeAt(const NodeIndex index) noexcept -> NodeData& { + assert(index < data.size()); + return data[index].Data; +} +template +inline auto Tree::NodeAt(const NodeIndex index) const noexcept -> const NodeData& { + assert(index < data.size()); + return data[index].Data; +} +template +inline auto Tree::FindParentOfNodeAt(const NodeIndex index) const noexcept -> NodeIndex { + assert(index > Root); // Parent of root? Are you crazy? + assert(index < data.size()); + + for (int i = 0; i < data.size(); i++) { + const Node& node = data[i]; + if (std::ranges::find(node.Children, index) != node.Children.end()) { + return i; + } + } + std::unreachable(); +} +template +inline auto Tree::ChildrenOfNodeAt(const NodeIndex index) noexcept -> ChildrenList { + assert(index < data.size()); + return ChildrenList(*this, data[index].Children); +} +template +inline auto Tree::ChildrenOfNodeAt(const NodeIndex index) const noexcept -> ChildrenList { + assert(index < data.size()); + return ChildrenList(*this, data[index].Children); +} + +template +inline void Tree::DeleteNode(const NodeIndex index) noexcept { + assert(index < data.size()); + + if (index == Root) { + auto root = std::move(data[0]); + data.clear(); + data.emplace_back(std::move(root)); + return; + } + + // Deletion is complicated. Because the underlying data structure + // is a vector, any deletion will slide the indices of elements + // after it. Here's the problems illustration: + // + // ROOT -> [0], VN -> [N] + // + // (ROOT)[0] + // / \ + // / \ + // (V1)[1] (V2)[2] + // / \ \ + // / \ \ + // / \ \ + // (V3)[3] (V4)[4] (V5)[5] + // + // Here, after deleting V1, V2 will receive index 1, V5 will receive + // index 2. A deletion operation on a sibling will cause V2 to update + // it's ChildrenList, as V5 is no longer at 5, it's at 2! + // + // (ROOT)[0] + // | + // | + // (V2)[1] + // | + // | + // (V5)[2] + // + // Therefore, here's the algorithm: + // 1. Recurse. We now have some form of invariancy. + // 2. Erase yourself from tree. + // 3. Fix indices. Traverse entire tree, for each node, traverse children + // 3.1. if childIndex == N, erase (delete yourself from parent) + // 3.2. if childIndex > N, decrement (fix yourself, someone got deleted) + // + + // Step 1 + Node& node = data[index]; + while (!node.Children.empty()) { + const NodeIndex childIndex = node.Children[0]; + DeleteNode(childIndex); + } + + // Step 2 + data.erase(data.begin() + index); + + // Step 3 + for (Node& node : data) { + // Step 3.1 + std::erase(node.Children, index); + + // Step 3.2 + std::ranges::for_each(node.Children, [index](NodeIndex& childIndex) { + if (childIndex > index) { + childIndex--; + } + }); + } +} \ No newline at end of file