From 132210dfab47a020a0e7b7377a33f93ac422fb33 Mon Sep 17 00:00:00 2001 From: GenYuLi Date: Wed, 10 Jun 2026 19:27:42 +0800 Subject: [PATCH] feat: add data-structure playground (src/ds) with doctest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 對應 rust playground 的 src/ds:header-only 資料結構練習場, 每個結構一個資料夾,旁邊放 doctest 測試,ctest 一鍵全跑 (≈ cargo test)。 - ds.hpp umbrella header(≈ ds/mod.rs)+ ds_test_main.cpp - tree/ 以 BinarySearchTree 作為已實作 + 已測試的範例 - graph/ trie/ list/ 為骨架:class 外形 + 預期 API 的 TODO, 測試以 doctest::skip() 跳過,留給之後填實作 - CMake: FetchContent 接 doctest(SOURCE_SUBDIR 跳過其過舊 CMakeLists, 自建 interface target),GLOB_RECURSE 收 *_test.cpp 進 ds_tests,掛上 ctest - src/ds/README.md 說明用法 Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 34 ++++++++++++++++++ src/ds/README.md | 60 +++++++++++++++++++++++++++++++ src/ds/ds.hpp | 10 ++++++ src/ds/ds_test_main.cpp | 4 +++ src/ds/graph/graph.hpp | 24 +++++++++++++ src/ds/graph/graph_test.cpp | 8 +++++ src/ds/list/list.hpp | 26 ++++++++++++++ src/ds/list/list_test.cpp | 8 +++++ src/ds/tree/tree.hpp | 72 +++++++++++++++++++++++++++++++++++++ src/ds/tree/tree_test.cpp | 36 +++++++++++++++++++ src/ds/trie/trie.hpp | 23 ++++++++++++ src/ds/trie/trie_test.cpp | 8 +++++ 12 files changed, 313 insertions(+) create mode 100644 src/ds/README.md create mode 100644 src/ds/ds.hpp create mode 100644 src/ds/ds_test_main.cpp create mode 100644 src/ds/graph/graph.hpp create mode 100644 src/ds/graph/graph_test.cpp create mode 100644 src/ds/list/list.hpp create mode 100644 src/ds/list/list_test.cpp create mode 100644 src/ds/tree/tree.hpp create mode 100644 src/ds/tree/tree_test.cpp create mode 100644 src/ds/trie/trie.hpp create mode 100644 src/ds/trie/trie_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5870725..fe1e343 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,18 @@ set(JSON_BuildTests CACHE BOOL "" FORCE) FetchContent_MakeAvailable(json) +# doctest 只需要一個 header;用 SOURCE_SUBDIR 指向不存在的目錄,讓 FetchContent +# 只下載原始碼、不去跑它過舊的 CMakeLists (會被新版 CMake 拒絕)。我們自己建 target。 +FetchContent_Declare( + doctest + GIT_REPOSITORY https://github.com/doctest/doctest.git + GIT_TAG v2.4.11 + SOURCE_SUBDIR do-not-build) +FetchContent_MakeAvailable(doctest) + +add_library(doctest_header INTERFACE) +target_include_directories(doctest_header INTERFACE ${doctest_SOURCE_DIR}) + find_package(Boost 1.74.0 REQUIRED CONFIG COMPONENTS system) # --- 建立函式庫和 header files link --- @@ -98,6 +110,28 @@ target_link_libraries( # Ensure C++20 for coroutines target_compile_features(matching_engine_lib INTERFACE cxx_std_20) +# --- Data Structure Playground (Header-Only) --- +# ds/ 是 header-only 的資料結構練習場,對應 rust 的 src/ds/。 +# 每個結構一個資料夾 (tree/, graph/, ...),src/ds/ds.hpp 是 umbrella header。 +add_library(ds_lib INTERFACE) + +target_include_directories(ds_lib INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/src/ds) + +target_compile_features(ds_lib INTERFACE cxx_std_${CMAKE_CXX_STANDARD}) + +# 把每個結構旁邊的 *_test.cpp 全部 GLOB 進一個 ds_tests 執行檔, +# 用 doctest 提供的 main,掛上 ctest → `ctest` 就能一鍵全跑 (= cargo test)。 +enable_testing() + +file(GLOB_RECURSE DS_TEST_FILES CONFIGURE_DEPENDS src/ds/*_test.cpp) + +if(DS_TEST_FILES) + add_executable(ds_tests src/ds/ds_test_main.cpp ${DS_TEST_FILES}) + target_link_libraries(ds_tests PRIVATE ds_lib doctest_header fmt::fmt) + add_test(NAME ds_tests COMMAND ds_tests) +endif() + # --- 建立執行檔與連結函式庫 --- file(GLOB BIN_FILES src/bin/*.cpp) diff --git a/src/ds/README.md b/src/ds/README.md new file mode 100644 index 0000000..044c317 --- /dev/null +++ b/src/ds/README.md @@ -0,0 +1,60 @@ +# ds — 資料結構練習場 + +對應 Rust playground 的 `src/ds/`。每個資料結構一個資料夾,header-only 實作 + +旁邊的 doctest 測試,`ctest` 一鍵全跑(≈ `cargo test`)。 + +## 結構 + +``` +src/ds/ +├── ds.hpp # umbrella header(≈ rust 的 ds/mod.rs),#include 全部結構 +├── ds_test_main.cpp # 唯一定義 doctest main 的 TU,別動 +├── tree/ # ✅ BinarySearchTree —— 範例,已實作 + 測試 +│ ├── tree.hpp +│ └── tree_test.cpp +├── graph/ # 🚧 骨架,待實作 +├── trie/ # 🚧 骨架,待實作 +└── list/ # 🚧 骨架,待實作 +``` + +## 跑測試 + +一定要在 nix 環境裡(`direnv` 進目錄會自動載入,或前綴 `direnv exec .`): + +```bash +cmake -S . -B build # 第一次 / CMakeLists 改過才需要 +cmake --build build --target ds_tests +ctest --test-dir build --output-on-failure +``` + +只跑某個結構: + +```bash +./build/ds_tests --test-case="*BST*" # doctest 的過濾語法 +``` + +## 新增一個資料結構 + +1. 開資料夾,寫 `xxx/xxx.hpp`(header-only 實作)。 +2. 旁邊寫 `xxx/xxx_test.cpp`,用 doctest 的 `TEST_CASE` / `CHECK` / `REQUIRE`: + + ```cpp + #include + #include "xxx/xxx.hpp" + + TEST_CASE("xxx: 做了什麼") { + ds::Xxx x; + CHECK(x.empty()); + } + ``` + +3. (可選)在 `ds.hpp` 加一行 `#include "xxx/xxx.hpp"`。 +4. 直接 build + ctest。`CMakeLists.txt` 用 `GLOB_RECURSE ... CONFIGURE_DEPENDS` + 自動收 `*_test.cpp`,不用改 CMake。 + +## 骨架說明 + +`graph/` `trie/` `list/` 目前是空殼:header 只有 class 外形 + 預期 API 的 TODO, +測試是 `TEST_CASE(... * doctest::skip())` 先被跳過(所以 ctest 仍是綠的)。 +動手時把實作補進 header、把測試的 `* doctest::skip()` 拿掉再填內容即可。 +``` diff --git a/src/ds/ds.hpp b/src/ds/ds.hpp new file mode 100644 index 0000000..fd9cf2c --- /dev/null +++ b/src/ds/ds.hpp @@ -0,0 +1,10 @@ +#pragma once + +// Umbrella header for the data-structure playground (對應 rust 的 ds/mod.rs)。 +// 每新增一個資料結構,就在這裡 #include 它的 header,外部只要 #include "ds.hpp" +// 就能拿到全部。測試檔則各自 include 自己那一個即可。 + +#include "graph/graph.hpp" +#include "list/list.hpp" +#include "tree/tree.hpp" +#include "trie/trie.hpp" diff --git a/src/ds/ds_test_main.cpp b/src/ds/ds_test_main.cpp new file mode 100644 index 0000000..51444f2 --- /dev/null +++ b/src/ds/ds_test_main.cpp @@ -0,0 +1,4 @@ +// 唯一一個定義 doctest main 的 translation unit。其餘 *_test.cpp 只 include +// 寫 TEST_CASE,連結到一起就組成單一的 ds_tests 執行檔。 +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include diff --git a/src/ds/graph/graph.hpp b/src/ds/graph/graph.hpp new file mode 100644 index 0000000..979cfcb --- /dev/null +++ b/src/ds/graph/graph.hpp @@ -0,0 +1,24 @@ +#pragma once + +// 無向圖 (adjacency list)。骨架而已,實作留給你。 +// +// 建議的 API(自己增刪): +// void add_edge(const T& a, const T& b); +// bool has_edge(const T& a, const T& b) const; +// std::vector neighbors(const T& v) const; +// std::size_t num_vertices() const; +// +// 建議的儲存結構:std::unordered_map> adj_; + +namespace ds { + +template +class Graph { +public: + // TODO: 你的 public API + +private: + // TODO: 你的儲存結構 +}; + +} // namespace ds diff --git a/src/ds/graph/graph_test.cpp b/src/ds/graph/graph_test.cpp new file mode 100644 index 0000000..3420461 --- /dev/null +++ b/src/ds/graph/graph_test.cpp @@ -0,0 +1,8 @@ +#include + +#include "graph/graph.hpp" + +// 拿掉 doctest::skip() 之後就會被 ctest 跑到。先把實作寫好,再回來填測試。 +TEST_CASE("graph: add_edge / has_edge" * doctest::skip()) { + // TODO: 你的測試 +} diff --git a/src/ds/list/list.hpp b/src/ds/list/list.hpp new file mode 100644 index 0000000..4400d35 --- /dev/null +++ b/src/ds/list/list.hpp @@ -0,0 +1,26 @@ +#pragma once + +// 單向鏈結串列 (singly linked list)。骨架而已,實作留給你。 +// +// 建議的 API(自己增刪): +// void push_front(const T& value); +// void push_back(const T& value); +// bool empty() const; +// std::size_t size() const; +// std::vector to_vector() const; // 方便寫測試比對 +// +// 建議的節點:struct Node { T value; std::unique_ptr next; }; +// head_ 持有第一個節點,注意 unique_ptr 的所有權轉移。 + +namespace ds { + +template +class List { +public: + // TODO: 你的 public API + +private: + // TODO: 你的 Node 結構與 head_ / size_ +}; + +} // namespace ds diff --git a/src/ds/list/list_test.cpp b/src/ds/list/list_test.cpp new file mode 100644 index 0000000..2579569 --- /dev/null +++ b/src/ds/list/list_test.cpp @@ -0,0 +1,8 @@ +#include + +#include "list/list.hpp" + +// 拿掉 doctest::skip() 之後就會被 ctest 跑到。先把實作寫好,再回來填測試。 +TEST_CASE("list: push_front / push_back / to_vector" * doctest::skip()) { + // TODO: 你的測試 +} diff --git a/src/ds/tree/tree.hpp b/src/ds/tree/tree.hpp new file mode 100644 index 0000000..5a2bc09 --- /dev/null +++ b/src/ds/tree/tree.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include + +namespace ds { + +// 一個最小的二元搜尋樹 (BST),header-only,當作 ds/ playground 的範例結構。 +// 重點不在效能,而在「實作 + 旁邊 tree_test.cpp 測試 + ctest 一鍵跑」的流程。 +template +class BinarySearchTree { +public: + void insert(const T& value) { root_ = insert(std::move(root_), value); } + + bool contains(const T& value) const { + const Node* cur = root_.get(); + while (cur) { + if (value < cur->value) { + cur = cur->left.get(); + } else if (cur->value < value) { + cur = cur->right.get(); + } else { + return true; + } + } + return false; + } + + // 中序走訪 → 由小到大的排序序列。 + std::vector inorder() const { + std::vector out; + inorder(root_.get(), out); + return out; + } + + std::size_t size() const { return size_; } + bool empty() const { return size_ == 0; } + +private: + struct Node { + explicit Node(T v) : value(std::move(v)) {} + T value; + std::unique_ptr left; + std::unique_ptr right; + }; + + std::unique_ptr insert(std::unique_ptr node, const T& value) { + if (!node) { + ++size_; + return std::make_unique(value); + } + if (value < node->value) { + node->left = insert(std::move(node->left), value); + } else if (node->value < value) { + node->right = insert(std::move(node->right), value); + } + // 相等 → 視為已存在,不重複插入。 + return node; + } + + void inorder(const Node* node, std::vector& out) const { + if (!node) return; + inorder(node->left.get(), out); + out.push_back(node->value); + inorder(node->right.get(), out); + } + + std::unique_ptr root_; + std::size_t size_ = 0; +}; + +} // namespace ds diff --git a/src/ds/tree/tree_test.cpp b/src/ds/tree/tree_test.cpp new file mode 100644 index 0000000..dd70fa9 --- /dev/null +++ b/src/ds/tree/tree_test.cpp @@ -0,0 +1,36 @@ +#include + +#include + +#include "tree/tree.hpp" + +TEST_CASE("BST: insert 後 contains 找得到") { + ds::BinarySearchTree bst; + CHECK(bst.empty()); + + bst.insert(5); + bst.insert(3); + bst.insert(8); + + CHECK(bst.size() == 3); + CHECK(bst.contains(5)); + CHECK(bst.contains(3)); + CHECK(bst.contains(8)); + CHECK_FALSE(bst.contains(42)); +} + +TEST_CASE("BST: 重複插入不會增加 size") { + ds::BinarySearchTree bst; + bst.insert(1); + bst.insert(1); + bst.insert(1); + CHECK(bst.size() == 1); +} + +TEST_CASE("BST: 中序走訪是排序好的") { + ds::BinarySearchTree bst; + for (int v : {5, 3, 8, 1, 4, 7, 9}) bst.insert(v); + + std::vector expected{1, 3, 4, 5, 7, 8, 9}; + CHECK(bst.inorder() == expected); +} diff --git a/src/ds/trie/trie.hpp b/src/ds/trie/trie.hpp new file mode 100644 index 0000000..9c4617e --- /dev/null +++ b/src/ds/trie/trie.hpp @@ -0,0 +1,23 @@ +#pragma once + +// 前綴樹 (Trie),存字串。骨架而已,實作留給你。 +// +// 建議的 API(自己增刪): +// void insert(const std::string& word); +// bool contains(const std::string& word) const; // 完整單字 +// bool starts_with(const std::string& prefix) const; // 任意前綴 +// +// 建議的節點:每個 Node 有 children (例如 std::array, 26> +// 或 std::unordered_map>) 與 is_end 旗標。 + +namespace ds { + +class Trie { +public: + // TODO: 你的 public API + +private: + // TODO: 你的 Node 結構與 root_ +}; + +} // namespace ds diff --git a/src/ds/trie/trie_test.cpp b/src/ds/trie/trie_test.cpp new file mode 100644 index 0000000..a298a65 --- /dev/null +++ b/src/ds/trie/trie_test.cpp @@ -0,0 +1,8 @@ +#include + +#include "trie/trie.hpp" + +// 拿掉 doctest::skip() 之後就會被 ctest 跑到。先把實作寫好,再回來填測試。 +TEST_CASE("trie: insert / contains / starts_with" * doctest::skip()) { + // TODO: 你的測試 +}