From 3d05d8f34057670d6bb25d1c17d2615416965326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teodor=20Sp=C3=A6ren?= Date: Mon, 10 Jun 2024 10:29:07 +0200 Subject: [PATCH] ds: Begin on adding AVLTree --- README.md | 22 ++ include/hage/ds/avl_tree.hpp | 460 +++++++++++++++++++++++++++++++++++ src/CMakeLists.txt | 15 +- tests/CMakeLists.txt | 5 +- tests/avl_tree_tests.cpp | 32 +++ tests/fast_pimpl_test.hpp | 1 + 6 files changed, 532 insertions(+), 3 deletions(-) create mode 100644 include/hage/ds/avl_tree.hpp create mode 100644 tests/avl_tree_tests.cpp diff --git a/README.md b/README.md index b198b7a..23da91c 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,28 @@ Planned datastructures: - Index based linked list, with a skip list - Interval map, built on top of +#### AVLTree + +An AVLTree implementation, currently in the process of being completed. Features are: + +- Full iterator support +- Node pooling +- Supports most of the same functionality as `std::map` + + +#### TODO + +##### AVLTREE +- Add `generation` tag to all nodes in debug mode, and check this tag when accessing through +an iterator, to detect dangling pointers! +- Add examples and tests for this + + +##### General +- Also implement a redblack tree +- Add some benchmarks for all trees +- Add a B-Tree? + ### Utility library This is not implemented yet, but here are some components I want: diff --git a/include/hage/ds/avl_tree.hpp b/include/hage/ds/avl_tree.hpp new file mode 100644 index 0000000..8c91577 --- /dev/null +++ b/include/hage/ds/avl_tree.hpp @@ -0,0 +1,460 @@ +#pragma once + +#include +#include + +namespace hage::ds { + +// TODO(rHermes): Specify that the key must be moveable and default constructable. Same with keys. +// This is a flaw, but it made the design so much easier. +template +class AVLTree +{ +private: + class Iterator; + class ConstIterator; + class Node; + + using node_id_type = std::int32_t; + +public: + using value_type = Value; + using size_type = std::size_t; + using iterator = Iterator; + using const_iterator = ConstIterator; + + AVLTree() + { + // This nodes parent is always the last node. + m_end = get_node(); + } + + template + constexpr std::pair try_emplace(K&& key, Args&&... args) + { + if (m_root == -1) { + m_root = get_node(std::forward(key), Value{ std::forward(args)... }); + m_begin = m_root; + m_nodes[m_end].m_parent = m_root; + + m_size++; + return { make_iterator(m_root), true }; + } + + const auto [id, inserted] = internal_try_emplace(m_root, std::forward(key), std::forward(args)...); + return { make_iterator(id), inserted }; + } + + [[nodiscard]] constexpr iterator find(const Key& key) + { + auto id = internal_find(m_root, key); + if (id == -1) { + return end(); + } else { + return make_iterator(id); + } + } + + [[nodiscard]] constexpr iterator end() noexcept { return make_iterator(m_end); } + [[nodiscard]] constexpr iterator begin() noexcept { return make_iterator(m_begin); } + + [[nodiscard]] constexpr bool contains(const Key& key) const { return internal_find(m_root, key) != -1; } + + [[nodiscard]] constexpr size_type size() const { return m_size; } + [[nodiscard]] constexpr bool empty() const { return size() == 0; } + +private: + class Node + { + private: + friend class AVLTree; + + Key m_key{}; + Value m_value{}; + + char m_balance{ 0 }; + node_id_type m_parent{ -1 }; + node_id_type m_left{ -1 }; + node_id_type m_right{ -1 }; + + public: + constexpr Node(){}; + + template + constexpr Node(K&& k, V&& val) : m_key{ std::forward(k) } + , m_value{ std::forward(val) } + { + } + + const Key& key() const { return m_key; } + Value& value() { return m_value; } + }; + + std::size_t m_size{ 0 }; + std::vector m_nodes; + node_id_type m_freeList{ -1 }; + + node_id_type m_root{ -1 }; + node_id_type m_begin{ -1 }; + node_id_type m_end{ -1 }; + + class Iterator + { + public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = Node; + using difference_type = std::int32_t; + using pointer = Node*; + using reference = Node&; + + Iterator(AVLTree* tree, node_id_type id) : m_tree{ tree }, m_id{ id } {} + + constexpr reference operator*() const { return m_tree->m_nodes[m_id]; } + constexpr pointer operator->() { return &m_tree->m_nodes[m_id]; } + + friend constexpr bool operator==(const Iterator& lhs, const Iterator& rhs) + { + return lhs.m_tree == rhs.m_tree && lhs.m_id == rhs.m_id; + } + + friend constexpr bool operator!=(const Iterator& lhs, const Iterator& rhs) { return !(lhs == rhs); } + + private: + friend class AVLTree; + + AVLTree* m_tree{ nullptr }; + node_id_type m_id{ -1 }; + + // TODO(rHermes): + // create an "end" iterator, that doesn't take up a node. + }; + + [[nodiscard]] constexpr Iterator make_iterator(node_id_type id) { return Iterator{ this, id }; } + + template + [[nodiscard]] constexpr node_id_type get_node(K&& key, V&& value) + { + if (m_freeList != -1) { + auto ret = m_freeList; + m_freeList = m_nodes[ret].m_parent; + + auto& node = m_nodes[ret]; + node = { std::forward(key), std::forward(value) }; + + return ret; + } + + const auto ret = static_cast(m_nodes.size()); + m_nodes.emplace_back(std::forward(key), std::forward(value)); + return ret; + } + + [[nodiscard]] constexpr node_id_type get_node() + { + if (m_freeList != -1) { + auto ret = m_freeList; + m_freeList = m_nodes[ret].m_parent; + + auto& node = m_nodes[ret]; + node = {}; + return ret; + } + + const auto ret = static_cast(m_nodes.size()); + m_nodes.emplace_back(); + return ret; + } + + constexpr void free_node(node_id_type node) + { + m_nodes[node] = {}; + m_nodes[node].m_parent = m_freeList; + m_freeList = node; + } + + [[nodiscard]] constexpr node_id_type internal_find(node_id_type root, const Key& key) const + { + while (root != -1) { + const auto& node = m_nodes[root]; + if (node.m_key == key) { + break; + } else if (key < node.m_key) { + root = node.m_left; + } else { + root = node.m_right; + } + } + + return root; + } + + template + constexpr std::pair internal_try_emplace(node_id_type root, K&& key, Args&&... args) + { + auto cur = root; + auto* node = &m_nodes[cur]; + + node_id_type child = -1; + while (true) { + node = &m_nodes[cur]; + if (node->m_key == key) { + return { cur, false }; + } else if (key < node->m_key) { + if (node->m_left != -1) { + cur = node->m_left; + continue; + } + + child = get_node(std::forward(key), std::forward(args)...); + m_nodes[child].m_parent = cur; + + // Update begin + if (cur == m_begin) + m_begin = child; + + node = &m_nodes[cur]; + node->m_left = child; + break; + + } else { + if (node->m_right != -1) { + cur = node->m_right; + continue; + } + + child = get_node(std::forward(key), std::forward(args)...); + m_nodes[child].m_parent = cur; + + // ok, the only way this updates end is if the parent of m_end is the parent node. + if (m_nodes[m_end].m_parent == cur) + m_nodes[m_end].m_parent = child; + + node = &m_nodes[cur]; + node->m_right = child; + break; + } + } + + // Ok, child is now the newly inserted item and cur is the parent. + const auto newChild = child; + + for (; cur != -1; cur = m_nodes[child].m_parent) { + + node_id_type oldRoot = -1; + node_id_type newRoot = -1; + + auto* parent = &m_nodes[cur]; + auto* ch = &m_nodes[child]; + + if (child == parent->m_right) { + if (parent->m_balance == 1) { + // We are right heavy, we will need to rebalance. + oldRoot = parent->m_parent; + if (ch->m_balance == -1) { + // Right left case + newRoot = rotate_right_left(cur, child); + } else { + // right right case + newRoot = rotate_left(cur, child); + } + } else { + ++parent->m_balance; + if (parent->m_balance == 0) + break; + + child = cur; + continue; + } + } else { + // It's a left child. + if (parent->m_balance == -1) { + // Rebalancing will be required. + + oldRoot = parent->m_parent; + if (ch->m_left == 1) { + // left right + newRoot = rotate_left_right(cur, child); + } else { + // left left case + newRoot = rotate_right(cur, child); + } + + // After rotation adapt parent link + } else { + --parent->m_balance; + if (parent->m_balance == 0) { + break; + } + + child = cur; // The height of z increases + continue; + } + } + + // ok, now we have rotated a parent link. + // N is the new root of the rotated subtree. + // The height did not change. Height(N) == old height(X) + m_nodes[newRoot].m_parent = oldRoot; + if (oldRoot != -1) { + if (cur == m_nodes[oldRoot].m_left) { + m_nodes[oldRoot].m_left = newRoot; + } else { + m_nodes[oldRoot].m_right = newRoot; + } + } else { + m_root = newRoot; + } + + break; + } + + m_size++; + return { newChild, true }; + } + + [[nodiscard]] constexpr node_id_type rotate_left(node_id_type parentId, node_id_type childId) + { + Node* parent = &m_nodes[parentId]; + Node* child = &m_nodes[childId]; + + auto t23 = child->m_left; + parent->m_right = t23; + if (t23 != -1) { + m_nodes[t23].m_parent = parentId; + } + + child->m_left = parentId; + parent->m_parent = childId; + + // This only happens on deletion + if (child->m_balance == 0) { + parent->m_balance = 1; + child->m_balance = -1; + } else { + parent->m_balance = 0; + child->m_balance = 0; + } + + return childId; + } + + [[nodiscard]] constexpr node_id_type rotate_right(node_id_type parentId, node_id_type childId) + { + Node* parent = &m_nodes[parentId]; + Node* child = &m_nodes[childId]; + + auto t23 = child->m_right; + parent->m_left = t23; + if (t23 != -1) { + m_nodes[t23].m_parent = parentId; + } + + child->m_right = parentId; + parent->m_parent = childId; + + // This only happens on deletion + if (child->m_balance == 0) { + parent->m_balance = -1; + child->m_balance = 1; + } else { + parent->m_balance = 0; + child->m_balance = 0; + } + + return childId; + } + + [[nodiscard]] constexpr node_id_type rotate_right_left(node_id_type parentId, node_id_type childId) + { + Node* parent = &m_nodes[parentId]; + Node* child = &m_nodes[childId]; + + auto Y = child->m_left; + Node* yc = &m_nodes[Y]; + + // swap over the underlying node + auto t3 = yc->m_right; + child->m_left = t3; + if (t3 != -1) { + m_nodes[t3].m_parent = childId; + } + + yc->m_right = childId; + child->m_parent = Y; + + auto t2 = yc->m_left; + parent->m_right = t2; + if (t2 != -1) { + m_nodes[t2].m_parent = parentId; + } + + yc->m_left = parentId; + parent->m_parent = Y; + + // This only happens on deletion + // If the inner left child was balanced + if (yc->m_balance == 0) { + parent->m_balance = 0; + child->m_balance = 0; + } else if (yc->m_balance == 1) { + // The inner child was right heavy. + parent->m_balance = -1; // T1 is now higher + child->m_balance = 0; + } else { + // T2 was higher + parent->m_balance = 0; + child->m_balance = +1; + } + + yc->m_balance = 0; + return Y; + } + + [[nodiscard]] constexpr node_id_type rotate_left_right(node_id_type parentId, node_id_type childId) + { + Node* parent = &m_nodes[parentId]; + Node* child = &m_nodes[childId]; + + auto Y = child->m_right; + Node* yc = &m_nodes[Y]; + + // swap over the underlying node + auto t3 = yc->m_left; + child->m_right = t3; + if (t3 != -1) { + m_nodes[t3].m_parent = childId; + } + + yc->m_left = childId; + child->m_parent = Y; + + auto t2 = yc->m_right; + parent->m_left = t2; + if (t2 != -1) { + m_nodes[t2].m_parent = parentId; + } + + yc->m_right = parentId; + parent->m_parent = Y; + + // This only happens on deletion + // If the inner left child was balanced + if (yc->m_balance == 0) { + parent->m_balance = 0; + child->m_balance = 0; + } else if (yc->m_balance == -1) { + // The inner child was right heavy. + parent->m_balance = 1; // T1 is now higher + child->m_balance = 0; + } else { + // T2 was higher + parent->m_balance = 0; + child->m_balance = -1; + } + + yc->m_balance = 0; + return Y; + } +}; + +} // namespace hage::ds \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 228e056..0541c07 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,6 +19,10 @@ set(CORE_HEADER_LIST "${hage_SOURCE_DIR}/include/hage/core/forward_declared_storage.hpp" ) +set(DATA_STRUCTURES_HEADER_LIST + "${hage_SOURCE_DIR}/include/hage/ds/avl_tree.hpp" +) + set(ATOMIC_HEADER_LIST "${hage_SOURCE_DIR}/include/hage/atomic/atomic.hpp" @@ -67,6 +71,15 @@ target_sources(hage_atomic INTERFACE target_include_directories(hage_atomic INTERFACE ../include) +add_library(hage_data_structures INTERFACE) + +target_sources(hage_data_structures INTERFACE + ${DATA_STRUCTURES_HEADER_LIST} +) + +target_include_directories(hage_data_structures INTERFACE ../include) +target_link_libraries(hage_data_structures INTERFACE hage_core) + add_library(hage_logging ${LOGGING_HEADER_LIST} logging/console_sink.cpp logging/file_sink.cpp @@ -76,7 +89,7 @@ add_library(hage_logging ${LOGGING_HEADER_LIST} target_include_directories(hage_logging PUBLIC ../include) target_link_libraries(hage_logging PUBLIC fmt::fmt hage_atomic hage_core) -foreach (target_var IN ITEMS hage_atomic hage_logging hage_core) +foreach (target_var IN ITEMS hage_atomic hage_logging hage_core hage_data_structures) get_target_property(target_type ${target_var} TYPE) if (target_type STREQUAL "INTERFACE_LIBRARY") diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2b03d53..507dd7e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,7 +21,8 @@ add_executable(hage_test doctest.cpp core_traits_tests.cpp fast_pimpl_test.cpp fast_pimpl_test.hpp - forward_declared_storage_tests.cpp) + forward_declared_storage_tests.cpp + avl_tree_tests.cpp) find_package(Threads REQUIRED) @@ -56,7 +57,7 @@ endif () # target_compile_definitions(hage_test PRIVATE DOCTEST_CONFIG_SUPER_FAST_ASSERTS) -target_link_libraries(hage_test PRIVATE hage_logging hage_atomic doctest::doctest +target_link_libraries(hage_test PRIVATE hage_logging hage_atomic doctest::doctest hage_data_structures Threads::Threads) # add_test(NAME hage_test_test COMMAND hage_test) diff --git a/tests/avl_tree_tests.cpp b/tests/avl_tree_tests.cpp new file mode 100644 index 0000000..6f4c92e --- /dev/null +++ b/tests/avl_tree_tests.cpp @@ -0,0 +1,32 @@ +#include + +#include + +using namespace hage; + +TEST_SUITE_BEGIN("data_structures"); + +TEST_CASE("Simple avl tests") +{ + ds::AVLTree tree; + + auto res1 = tree.try_emplace(10, 23); + REQUIRE_UNARY(res1.second); + + auto res2 = tree.try_emplace(100, 10); + REQUIRE_UNARY(res2.second); + + SUBCASE("try_emplace should return iterator and false for existing key") + { + auto res3 = tree.try_emplace(10, 0xf001); + REQUIRE_UNARY_FALSE(res3.second); + REQUIRE_EQ(res1.first, res3.first); + } + + REQUIRE_EQ(tree.size(), 2); + + REQUIRE_UNARY(tree.contains(100)); + REQUIRE_UNARY_FALSE(tree.contains(200)); +} + +TEST_SUITE_END(); diff --git a/tests/fast_pimpl_test.hpp b/tests/fast_pimpl_test.hpp index d32aba9..17d2e5b 100644 --- a/tests/fast_pimpl_test.hpp +++ b/tests/fast_pimpl_test.hpp @@ -25,6 +25,7 @@ class FastPimplTest #else hage::ForwardDeclaredStorage m_impl; #endif + public: FastPimplTest(); ~FastPimplTest();