diff --git a/.github/workflows/all-tests.yml b/.github/workflows/all-tests.yml index 4d1ea218..895a8be2 100644 --- a/.github/workflows/all-tests.yml +++ b/.github/workflows/all-tests.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Checkout repository (full history) - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 # ← fetch all branches and commits @@ -19,7 +19,7 @@ jobs: git fetch origin main:main || true - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: '3.x' diff --git a/README.md b/README.md index d8ea47b7..566f240e 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ My code is written in two programming languages I really enjoy, C++ and Python. The following requirements are necessary to build and run the code in this repository: * For C++ projects: - - A C++ compiler supporting C++14 + - A C++ compiler supporting C++17 - CMake 3.15 or later * For Python projects: - Python 3.10 or later diff --git a/run_tests.sh b/run_tests.sh index 29e9d6a9..f651f7d0 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,225 +1,143 @@ #!/bin/bash -set -e - -# Determine base branch by looking at the upstream. If no upstream is set, fall back to "master". -detect_base_branch() { - # If there’s an upstream (e.g. origin/master or origin/main), use that. - if base="$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null)"; then - echo "$base" - else - echo "master" - fi +# Runs the Python (unittest) and C++ (CMake + CTest) test suites. +# +# By default only projects touched since the base branch are tested; when +# nothing differs from the base branch (e.g. a push to master) every project +# is tested. Exits non-zero if any project fails to build or any test fails. +set -u -o pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT" + +BUILD_ROOT="$(mktemp -d)" +trap 'rm -rf "$BUILD_ROOT"' EXIT + +FAILED_PROJECTS=() + +usage() { + cat <<'EOF' +Usage: run_tests.sh [OPTION] +Run tests for projects modified since the base branch. + +Options: + -h, --help Show this help message and exit + -p, --python Run Python tests only + -c, --cpp Run C++ tests only + -a, --all Test every project, not just modified ones +EOF } -# Find all directories (under start_dir) that contain a CMakeLists.txt -find_cmake_subdirs() { - local start_dir="$1" - find "$start_dir" -type f -name 'CMakeLists.txt' -printf '%h\n' | sort -u -} - -# Find only those Python-package roots that actually have a tests/ subdirectory. -# In other words, look for “*/tests” and return the parent directory. -find_python_subdirs() { - local start_dir="$1" - find "$start_dir" -type d -name 'tests' -printf '%h\n' | sort -u +# Base ref to diff against: the upstream if one is set, otherwise master. +detect_base_branch() { + git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || echo "master" } -# Get the list of files changed since the last commit on base branch +# Files changed relative to the base branch, including uncommitted work. get_modified_files() { - local base_branch - base_branch="$(detect_base_branch)" - git diff --name-only "$base_branch"...HEAD + local base + base="$(detect_base_branch)" + { + git diff --name-only "$base"...HEAD 2>/dev/null + git diff --name-only HEAD 2>/dev/null + } | sort -u } -test_cpp_projects() { - local current_dir - current_dir=$(pwd) - - # All C++ project directories (where CMakeLists.txt lives) - local all_subdirs - all_subdirs=$(find_cmake_subdirs .) - - # Files modified in this PR relative to base - local modified_files - modified_files=$(get_modified_files) - - # Filter to only those C++ subdirs in which at least one file was modified - local modified_subdirs="" - for subdir in $all_subdirs; do - # strip leading "./" for comparison against git output - local sub="${subdir#./}" - if echo "$modified_files" | grep -q "^$sub/"; then - modified_subdirs="$modified_subdirs $subdir" +# Print the subset of the given directories that contain a modified file. +filter_modified() { + local modified="$1" + shift + local dir + for dir in "$@"; do + if grep -q "^${dir#./}/" <<<"$modified"; then + echo "$dir" fi done +} - if [ -z "$modified_subdirs" ]; then - echo "No modified C++ projects to test." +select_projects() { + local all="$1" modified selected + if [ "$RUN_ALL" = 1 ]; then + echo "$all" return fi - - local total_passed_tests=0 - local total_failed_tests=0 - local cpp_test_log - - cleanup() { - cd "$current_dir" - rm -rf build - } - - echo "Running tests for modified C++ projects:" - for subdir in $modified_subdirs; do - echo -e "\nRunning tests for C++ project at: $subdir" - cd "$subdir" - - mkdir -p build && cd build - trap cleanup EXIT - - # Hide cmake/make output - cmake .. 1>/dev/null 2>&1 && make 1>/dev/null 2>&1 - - cpp_test_log="$current_dir/cpp_test_$(echo "$subdir" | tr '/' '_').log" - : > "$cpp_test_log" - ctest --verbose 2>&1 | tee -a "$cpp_test_log" - - trap - EXIT - cleanup - cd "$current_dir" - - # Safely extract “ tests from” (if any) or default to 0 - local cpp_total_tests - cpp_total_tests=$(grep -oP '\d+(?= tests from)' "$cpp_test_log" | tail -1 || echo 0) - cpp_total_tests="${cpp_total_tests:-0}" - - # Safely extract passed count (if any) or default to 0 - local cpp_passed_tests - cpp_passed_tests=$(grep -oP '(?<=\[ *PASSED *\] )\d+' "$cpp_test_log" | tail -1 || echo 0) - cpp_passed_tests="${cpp_passed_tests:-0}" - - local cpp_failed_tests=$(( cpp_total_tests - cpp_passed_tests )) - - total_passed_tests=$(( total_passed_tests + cpp_passed_tests )) - total_failed_tests=$(( total_failed_tests + cpp_failed_tests )) - - echo "C++ Tests summary for $subdir:" - echo -e " Passed: \e[32m$cpp_passed_tests\e[0m, Failed: \e[31m$cpp_failed_tests\e[0m" - done - - echo -e "\nTotal C++ Tests summary for modified projects:" - echo -e " Total Passed: \e[32m$total_passed_tests\e[0m, Total Failed: \e[31m$total_failed_tests\e[0m" + modified="$(get_modified_files)" + if [ -z "$modified" ]; then + echo "$all" + return + fi + # shellcheck disable=SC2086 + filter_modified "$modified" $all } test_python_projects() { - local current_dir - current_dir=$(pwd) - - # Only pick up directories that actually have a “tests/” folder - local all_subdirs - all_subdirs=$(find_python_subdirs .) - - # Which files changed in this PR (relative to base) - local modified_files - modified_files=$(get_modified_files) - - # Filter to only those Python-root dirs where at least one file was modified - local modified_subdirs="" - for subdir in $all_subdirs; do - local sub="${subdir#./}" - if echo "$modified_files" | grep -q "^$sub/"; then - modified_subdirs="$modified_subdirs $subdir" - fi - done - - if [ -z "$modified_subdirs" ]; then - echo "No modified Python projects to test." + local all projects dir + # Python projects keep their tests in either tests/ or test/ (C++ projects + # also use test/, hence the /python/ filter). + all="$(find . -type d -path '*/python/*' \( -name tests -o -name test \) \ + -not -path '*/build/*' -printf '%h\n' | sort -u)" + projects="$(select_projects "$all")" + if [ -z "$projects" ]; then + echo "No Python projects to test." return fi - local total_passed_tests=0 - local total_failed_tests=0 - local python_test_log - - echo "Running tests for modified Python projects:" - for subdir in $modified_subdirs; do - echo -e "\nRunning tests for Python project at: $subdir" - cd "$subdir" - - python_test_log="$current_dir/python_test_$(echo "$subdir" | tr '/' '_').log" - : > "$python_test_log" - - # Run unittest discovery; any output goes into the log - python3 -m unittest discover -v 2>&1 | tee -a "$python_test_log" - cd "$current_dir" - - # Safely grab “Ran X tests” (default to 0 if none found) - local python_total_tests - python_total_tests=$(grep -oP '(?<=Ran )\d+' "$python_test_log" | head -1 || echo 0) - python_total_tests="${python_total_tests:-0}" - - # Count how many “... ok” lines (default to 0) - local python_passed_tests - python_passed_tests=$(grep -c '\.\.\. ok' "$python_test_log" || echo 0) - python_passed_tests="${python_passed_tests:-0}" - - # Compute failures - local python_failed_tests=$(( python_total_tests - python_passed_tests )) - - total_passed_tests=$(( total_passed_tests + python_passed_tests )) - total_failed_tests=$(( total_failed_tests + python_failed_tests )) - - echo "Python Tests summary for $subdir:" - echo -e " Passed: \e[32m$python_passed_tests\e[0m, Failed: \e[31m$python_failed_tests\e[0m" + echo "Running tests for Python projects:" + for dir in $projects; do + echo -e "\n== Python project: $dir" + if ! (cd "$dir" && python3 -m unittest discover -v); then + FAILED_PROJECTS+=("python:$dir") + fi done - - echo -e "\nTotal Python Tests summary for modified projects:" - echo -e " Total Passed: \e[32m$total_passed_tests\e[0m, Total Failed: \e[31m$total_failed_tests\e[0m" } -main() { - if [ "$#" -eq 0 ]; then - echo "Running tests only for projects modified since last commit on base branch." - echo "---- Python Projects ----" - test_python_projects - echo "---- C++ Projects ----" - test_cpp_projects - exit 0 - fi - - if [ "$#" -eq 1 ]; then - case "$1" in - -h|--help) - echo "Usage: run_tests.sh [OPTION]" - echo "Run tests for projects modified since base branch." - echo "" - echo "Options:" - echo " -h, --help Show this help message and exit" - echo " -p, --python Run tests for modified Python projects only" - echo " -c, --cpp Run tests for modified C++ projects only" - exit 0 - ;; - -p|--python) - echo "Running tests for modified Python projects:" - test_python_projects - exit 0 - ;; - -c|--cpp) - echo "Running tests for modified C++ projects:" - test_cpp_projects - exit 0 - ;; - *) - echo "Unknown option: $1" - echo "Usage: run_tests.sh [OPTION]" - exit 1 - ;; - esac +test_cpp_projects() { + local all projects dir build + # A project root is a CMakeLists.txt whose parent directory has none + # (src/ and test/ are added via add_subdirectory). + all="$(find . -name CMakeLists.txt -not -path '*/build/*' -printf '%h\n' | sort -u | + while read -r d; do [ -f "$d/../CMakeLists.txt" ] || echo "$d"; done)" + projects="$(select_projects "$all")" + if [ -z "$projects" ]; then + echo "No C++ projects to test." + return fi - if [ "$#" -gt 1 ]; then - echo "Too many arguments" - echo "Usage: run_tests.sh [OPTION]" - exit 1 - fi + echo "Running tests for C++ projects:" + for dir in $projects; do + echo -e "\n== C++ project: $dir" + build="$BUILD_ROOT/$(echo "${dir#./}" | tr '/' '_')" + if cmake -S "$dir" -B "$build" >"$build.log" 2>&1 && + cmake --build "$build" -j"$(nproc)" >>"$build.log" 2>&1; then + (cd "$build" && ctest --output-on-failure) || + FAILED_PROJECTS+=("cpp:$dir") + else + cat "$build.log" + echo "Build failed" + FAILED_PROJECTS+=("cpp:$dir") + fi + done } -main "$@" +RUN_ALL=0 +RUN_PYTHON=1 +RUN_CPP=1 +for arg in "$@"; do + case "$arg" in + -h | --help) usage; exit 0 ;; + -p | --python) RUN_CPP=0 ;; + -c | --cpp) RUN_PYTHON=0 ;; + -a | --all) RUN_ALL=1 ;; + *) echo "Unknown option: $arg" >&2; usage >&2; exit 1 ;; + esac +done + +[ "$RUN_PYTHON" = 1 ] && test_python_projects +[ "$RUN_CPP" = 1 ] && test_cpp_projects + +echo +if [ "${#FAILED_PROJECTS[@]}" -gt 0 ]; then + echo "FAILED projects:" + printf ' %s\n' "${FAILED_PROJECTS[@]}" + exit 1 +fi +echo "All tests passed." diff --git a/src/backtracking/cpp/all_combinations/CMakeLists.txt b/src/backtracking/cpp/all_combinations/CMakeLists.txt index dc3c9956..d02e12b9 100644 --- a/src/backtracking/cpp/all_combinations/CMakeLists.txt +++ b/src/backtracking/cpp/all_combinations/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(project-all_combinations CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/backtracking/cpp/all_permutations/CMakeLists.txt b/src/backtracking/cpp/all_permutations/CMakeLists.txt index 8510b0c8..99960ff9 100644 --- a/src/backtracking/cpp/all_permutations/CMakeLists.txt +++ b/src/backtracking/cpp/all_permutations/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(project-all_permutations CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/backtracking/cpp/all_permutations/src/all_permutations.cpp b/src/backtracking/cpp/all_permutations/src/all_permutations.cpp index ebed973e..f29b17bb 100644 --- a/src/backtracking/cpp/all_permutations/src/all_permutations.cpp +++ b/src/backtracking/cpp/all_permutations/src/all_permutations.cpp @@ -1,29 +1,32 @@ #include "all_permutations.h" +#include #include std::vector> all_permutations(std::vector input_list) { std::vector> result; // Nested function to generate permutations - std::function, std::vector)> _all_permutations; - _all_permutations = [&](std::vector input_list, - std::vector output_list) { + // Track used positions (not values) so duplicate elements are handled. + std::vector used(input_list.size(), false); + std::vector output_list; + std::function _all_permutations; + _all_permutations = [&]() { if (output_list.size() == input_list.size()) { result.push_back(output_list); return; } - for (const auto &element : input_list) { - if (std::find(output_list.begin(), output_list.end(), element) == - output_list.end()) { - output_list.push_back(element); - _all_permutations(input_list, output_list); + for (std::size_t i = 0; i < input_list.size(); ++i) { + if (!used[i]) { + used[i] = true; + output_list.push_back(input_list[i]); + _all_permutations(); output_list.pop_back(); + used[i] = false; } } }; - // Initialize result and call nested function - _all_permutations(input_list, {}); + _all_permutations(); return result; } diff --git a/src/backtracking/cpp/all_permutations/test/test_all_permutations.cpp b/src/backtracking/cpp/all_permutations/test/test_all_permutations.cpp index 35a6752d..1bb123d2 100644 --- a/src/backtracking/cpp/all_permutations/test/test_all_permutations.cpp +++ b/src/backtracking/cpp/all_permutations/test/test_all_permutations.cpp @@ -1,6 +1,19 @@ #include "all_permutations.h" #include "gtest/gtest.h" +TEST(TestAllPermutations, EmptyInput) { + std::vector> expected = {{}}; + EXPECT_EQ(expected, all_permutations({})); +} + +TEST(TestAllPermutations, DuplicateElements) { + std::vector> actual = all_permutations({1, 1, 2}); + std::sort(actual.begin(), actual.end()); + std::vector> expected = {{1, 1, 2}, {1, 1, 2}, {1, 2, 1}, + {1, 2, 1}, {2, 1, 1}, {2, 1, 1}}; + EXPECT_EQ(expected, actual); +} + TEST(TestAllPermutations, TwoElements) { std::vector input_list = {1, 2}; std::vector> excepted = {{1, 2}, {2, 1}}; diff --git a/src/backtracking/cpp/generating_words/CMakeLists.txt b/src/backtracking/cpp/generating_words/CMakeLists.txt index 508eafe0..09a1e524 100644 --- a/src/backtracking/cpp/generating_words/CMakeLists.txt +++ b/src/backtracking/cpp/generating_words/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(project-generating_words CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/backtracking/cpp/hamiltonian_paths/CMakeLists.txt b/src/backtracking/cpp/hamiltonian_paths/CMakeLists.txt index 342838de..4f501fe1 100644 --- a/src/backtracking/cpp/hamiltonian_paths/CMakeLists.txt +++ b/src/backtracking/cpp/hamiltonian_paths/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(project-hamiltonian_paths CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/backtracking/cpp/k_colorable_configurations/CMakeLists.txt b/src/backtracking/cpp/k_colorable_configurations/CMakeLists.txt index cd25802d..2297172d 100644 --- a/src/backtracking/cpp/k_colorable_configurations/CMakeLists.txt +++ b/src/backtracking/cpp/k_colorable_configurations/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(project-k_colorable_configurations CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/backtracking/cpp/k_colorable_configurations/src/k_colorable_configurations.cpp b/src/backtracking/cpp/k_colorable_configurations/src/k_colorable_configurations.cpp index 585015dd..f5a24ba1 100644 --- a/src/backtracking/cpp/k_colorable_configurations/src/k_colorable_configurations.cpp +++ b/src/backtracking/cpp/k_colorable_configurations/src/k_colorable_configurations.cpp @@ -25,7 +25,7 @@ std::vector> find_k_colorable_configurations(Graph &G, int k) { std::function &)> is_colorable; is_colorable = [&](Graph &G, int v, int color, std::vector &colors) { for (int u : G.adjacency_dict[v]) { - if (colors[u] == color) { + if (u == v || colors[u] == color) { return false; } } diff --git a/src/backtracking/cpp/k_colorable_configurations/test/test_k_colorable_configurations.cpp b/src/backtracking/cpp/k_colorable_configurations/test/test_k_colorable_configurations.cpp index 98797ba0..da248311 100644 --- a/src/backtracking/cpp/k_colorable_configurations/test/test_k_colorable_configurations.cpp +++ b/src/backtracking/cpp/k_colorable_configurations/test/test_k_colorable_configurations.cpp @@ -42,3 +42,10 @@ TEST(TestKColorableConfigurations, TestNegative1) { std::vector> actual = find_k_colorable_configurations(G, k); EXPECT_EQ(expected, actual); } + +TEST(TestKColorableConfigurations, TestSelfLoopIsNotColorable) { + Graph G; + G.add_edge(0, 0); + std::vector> actual = find_k_colorable_configurations(G, 3); + EXPECT_TRUE(actual.empty()); +} diff --git a/src/backtracking/cpp/knight_tour/CMakeLists.txt b/src/backtracking/cpp/knight_tour/CMakeLists.txt index 0864ed7d..787c8123 100644 --- a/src/backtracking/cpp/knight_tour/CMakeLists.txt +++ b/src/backtracking/cpp/knight_tour/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(project-knight_tour CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/backtracking/cpp/knight_tour/src/knight_tour.cpp b/src/backtracking/cpp/knight_tour/src/knight_tour.cpp index 4d8a637c..a9b9c31a 100644 --- a/src/backtracking/cpp/knight_tour/src/knight_tour.cpp +++ b/src/backtracking/cpp/knight_tour/src/knight_tour.cpp @@ -70,6 +70,9 @@ void find_tours(std::vector> &board, int row, int col, std::vector>> knight_tour(int n) { // initialize the board and the tours list + if (n <= 0) { + return {}; + } std::vector> board(n, std::vector(n, -1)); board[0][0] = 0; std::vector> tour = {{0, 0}}; diff --git a/src/backtracking/cpp/knight_tour/test/test_knight_tour.cpp b/src/backtracking/cpp/knight_tour/test/test_knight_tour.cpp index 19100d85..7041cb44 100644 --- a/src/backtracking/cpp/knight_tour/test/test_knight_tour.cpp +++ b/src/backtracking/cpp/knight_tour/test/test_knight_tour.cpp @@ -94,4 +94,9 @@ TEST(TestKnightTour, TestN5) { EXPECT_EQ(actual.size(), excepted_length); EXPECT_TRUE(std::find(actual.begin(), actual.end(), expected_element) != actual.end()); -} \ No newline at end of file +} + +TEST(TestKnightTour, TestNonPositiveN) { + EXPECT_TRUE(knight_tour(0).empty()); + EXPECT_TRUE(knight_tour(-1).empty()); +} diff --git a/src/backtracking/cpp/minimax/CMakeLists.txt b/src/backtracking/cpp/minimax/CMakeLists.txt index cb1c2d9c..e6a0abd3 100644 --- a/src/backtracking/cpp/minimax/CMakeLists.txt +++ b/src/backtracking/cpp/minimax/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(project-minimax CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/backtracking/cpp/minimax/src/minimax.cpp b/src/backtracking/cpp/minimax/src/minimax.cpp index b5eb86e8..dc1d96d4 100644 --- a/src/backtracking/cpp/minimax/src/minimax.cpp +++ b/src/backtracking/cpp/minimax/src/minimax.cpp @@ -80,7 +80,7 @@ int minimax(std::vector> &board, int player) { } // initialize the best score - int best_score = (player == 1) ? INT_MIN : INT_MAX; + int best_score = (player == 1) ? INT_MAX : INT_MIN; // try all possible moves for (int row = 0; row < board.size(); ++row) { @@ -95,9 +95,9 @@ int minimax(std::vector> &board, int player) { // update the best score if (player == 1) { - best_score = std::max(best_score, score); - } else { best_score = std::min(best_score, score); + } else { + best_score = std::max(best_score, score); } } } diff --git a/src/backtracking/cpp/minimax/test/test_minimax.cpp b/src/backtracking/cpp/minimax/test/test_minimax.cpp index a147e3e3..7e059746 100644 --- a/src/backtracking/cpp/minimax/test/test_minimax.cpp +++ b/src/backtracking/cpp/minimax/test/test_minimax.cpp @@ -42,3 +42,15 @@ TEST(TestMinimax, TestPlayerAWins) { EXPECT_EQ(expected, actual); } + +TEST(TestMinimax, TestBlocksOpponentWin) { + std::vector> board = {{0, 0, 0}, {0, -1, 0}, {1, 1, 0}}; + std::pair expected = {2, 2}; + EXPECT_EQ(expected, find_best_move(board, -1)); +} + +TEST(TestMinimax, TestScoresEachSideByItsOwnGoal) { + std::vector> board = {{1, 1, 0}, {-1, -1, 0}, {0, 0, 0}}; + EXPECT_EQ(-1, minimax(board, 1)); + EXPECT_EQ(1, minimax(board, -1)); +} diff --git a/src/backtracking/cpp/string_pattern/CMakeLists.txt b/src/backtracking/cpp/string_pattern/CMakeLists.txt index 3256cc7e..6597c093 100644 --- a/src/backtracking/cpp/string_pattern/CMakeLists.txt +++ b/src/backtracking/cpp/string_pattern/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(project-string_pattern CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/backtracking/cpp/topological_sort/CMakeLists.txt b/src/backtracking/cpp/topological_sort/CMakeLists.txt index 53c3ac71..dfc17030 100644 --- a/src/backtracking/cpp/topological_sort/CMakeLists.txt +++ b/src/backtracking/cpp/topological_sort/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(project-topological_sort CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/backtracking/cpp/topological_sort/README.md b/src/backtracking/cpp/topological_sort/README.md index a246c677..16dca6a5 100644 --- a/src/backtracking/cpp/topological_sort/README.md +++ b/src/backtracking/cpp/topological_sort/README.md @@ -1,6 +1,6 @@ ## Topological Sort -Given a Directed Acyclic Graph (DAG), find it in topological order using topological sort algorithm. If the graph has more than one topological ordering, output any of them. Assume valid Directed Acyclic Graph (DAG). +Given a Directed Acyclic Graph (DAG), find it in topological order using topological sort algorithm. If the graph has more than one topological ordering, output any of them. The implementation raises an error if the graph contains a cycle, since no ordering exists in that case. A Topological sort or Topological ordering of a directed graph is a linear ordering of its vertices such that for every directed edge uv from vertex u to vertex v, u comes before v in the ordering. A topological ordering is possible if and only if the graph has no directed cycles, i.e. if the graph is DAG. @@ -8,16 +8,17 @@ A Topological sort or Topological ordering of a directed graph is a linear order ``` 3 --> 0 --> 6 -| | -v v -4 --> 1 --> 2 +| ^ +v | +4 --> 1 --> + | v - 4 --> 6 + 2 ``` -The topological ordering of the above graph is $3, 4, 1, 2, 0, 6$. +Edges: `3→0`, `3→4`, `0→6`, `4→1`, `1→2`, `1→6`. +One valid topological ordering of the above graph is $3, 0, 4, 1, 2, 6$ (vertices with no edges, such as an unused label, can appear anywhere). Orderings are generally not unique. ## Approach diff --git a/src/backtracking/cpp/topological_sort/src/topological_sort.cpp b/src/backtracking/cpp/topological_sort/src/topological_sort.cpp index 1564cf32..1e5afffe 100644 --- a/src/backtracking/cpp/topological_sort/src/topological_sort.cpp +++ b/src/backtracking/cpp/topological_sort/src/topological_sort.cpp @@ -1,35 +1,58 @@ #include "topological_sort.h" - -Graph::Graph(int V) : num_vertices(V) { - adjacency_list.resize(V); +#include +#include +#include + +Graph::Graph(int n) : num_vertices(n) { + if (n < 0) { + throw std::invalid_argument("number of vertices must be non-negative"); + } + adjacency_list.resize(n); } void Graph::add_edge(int u, int v) { - adjacency_list[u].push_back(v); -} - -void Graph::topological_sort_util(int v, std::vector& visited, std::stack& Stack) { - visited[v] = true; - for (int i : adjacency_list[v]) - if (!visited[i]) - topological_sort_util(i, visited, Stack); - Stack.push(v); + if (u < 0 || u >= num_vertices || v < 0 || v >= num_vertices) { + throw std::out_of_range("vertex out of range"); + } + adjacency_list[u].push_back(v); } -std::vector Graph::topological_sort() { - std::stack Stack; - std::vector visited(num_vertices, false); - - for (int i = 0; i < num_vertices; i++) - if (!visited[i]) - topological_sort_util(i, visited, Stack); - - std::vector result; - while (!Stack.empty()) { - result.push_back(Stack.top()); - Stack.pop(); +std::vector Graph::topological_sort() const { + enum class State { Unvisited, InProgress, Done }; + std::vector state(num_vertices, State::Unvisited); + std::vector order; + order.reserve(num_vertices); + + // Iterative DFS: (vertex, index of the next neighbour to explore). + std::vector> stack; + for (int start = 0; start < num_vertices; ++start) { + if (state[start] != State::Unvisited) { + continue; } - return result; -} + state[start] = State::InProgress; + stack.emplace_back(start, 0); + + while (!stack.empty()) { + const int v = stack.back().first; + std::size_t &next = stack.back().second; + if (next < adjacency_list[v].size()) { + const int u = adjacency_list[v][next++]; + if (state[u] == State::InProgress) { + throw std::invalid_argument("graph contains a cycle"); + } + if (state[u] == State::Unvisited) { + state[u] = State::InProgress; + stack.emplace_back(u, 0); + } + } else { + state[v] = State::Done; + order.push_back(v); + stack.pop_back(); + } + } + } + // Reverse post-order is a topological order. + return {order.rbegin(), order.rend()}; +} diff --git a/src/backtracking/cpp/topological_sort/src/topological_sort.h b/src/backtracking/cpp/topological_sort/src/topological_sort.h index 543e0f9a..95ef5520 100644 --- a/src/backtracking/cpp/topological_sort/src/topological_sort.h +++ b/src/backtracking/cpp/topological_sort/src/topological_sort.h @@ -1,4 +1,5 @@ -#include +#pragma once + #include /** @@ -7,28 +8,28 @@ class Graph { public: /** - * @brief Constructs a graph with n vertices. + * @brief Constructs a graph with n vertices labelled 0..n-1. * @param n Number of vertices. + * @throws std::invalid_argument if n is negative. */ - Graph(int n); + explicit Graph(int n); /** * @brief Adds a directed edge from u to v. * @param u Source vertex. * @param v Destination vertex. + * @throws std::out_of_range if u or v is not a valid vertex. */ void add_edge(int u, int v); /** * @brief Performs topological sort on the graph. - * @return A vector of vertices in topological order. + * @return Every vertex, ordered so that u precedes v for each edge (u, v). + * @throws std::invalid_argument if the graph contains a cycle. */ - std::vector topological_sort(); + std::vector topological_sort() const; private: int num_vertices; std::vector> adjacency_list; - void topological_sort_util(int v, std::vector &visited, - std::stack &Stack); }; - diff --git a/src/backtracking/cpp/topological_sort/test/test_topological_sort.cpp b/src/backtracking/cpp/topological_sort/test/test_topological_sort.cpp index 7e3860b6..0e6f6597 100644 --- a/src/backtracking/cpp/topological_sort/test/test_topological_sort.cpp +++ b/src/backtracking/cpp/topological_sort/test/test_topological_sort.cpp @@ -1,37 +1,82 @@ #include "topological_sort.h" #include "gtest/gtest.h" +#include +#include +#include + +namespace { + +// True if `order` is a permutation of 0..n-1 that respects every edge. +bool IsValidOrder(const std::vector &order, int n, + const std::vector> &edges) { + if (static_cast(order.size()) != n) { + return false; + } + std::vector position(n, -1); + for (int i = 0; i < n; ++i) { + if (order[i] < 0 || order[i] >= n || position[order[i]] != -1) { + return false; + } + position[order[i]] = i; + } + return std::all_of(edges.begin(), edges.end(), [&](const auto &e) { + return position[e.first] < position[e.second]; + }); +} + +} // namespace + TEST(TestTopologicalSort, TestPositive1) { Graph g(8); - g.add_edge(0, 6); - g.add_edge(1, 2); - g.add_edge(1, 4); - g.add_edge(1, 6); - g.add_edge(3, 0); - g.add_edge(3, 4); - g.add_edge(5, 1); - g.add_edge(7, 0); - g.add_edge(7, 1); + const std::vector> edges = { + {0, 6}, {1, 2}, {1, 4}, {1, 6}, {3, 0}, {3, 4}, {5, 1}, {7, 0}, {7, 1}}; + for (const auto &e : edges) { + g.add_edge(e.first, e.second); + } std::vector expected = {7, 5, 3, 1, 4, 2, 0, 6}; - std::vector actual = g.topological_sort(); - - EXPECT_EQ(expected, actual); + EXPECT_EQ(expected, g.topological_sort()); } TEST(TestTopologicalSort, TestPositive2) { Graph g(7); - g.add_edge(3, 0); - g.add_edge(3, 4); - g.add_edge(0, 6); - g.add_edge(4, 1); - g.add_edge(1, 2); - g.add_edge(1, 4); - g.add_edge(1, 6); + const std::vector> edges = {{3, 0}, {3, 4}, {0, 6}, + {4, 1}, {1, 2}, {1, 6}}; + for (const auto &e : edges) { + g.add_edge(e.first, e.second); + } - std::vector expected = {3, 4, 1, 2, 0, 6}; - std::vector actual = g.topological_sort(); + // Vertex 5 is isolated and must still appear in the result. + EXPECT_TRUE(IsValidOrder(g.topological_sort(), 7, edges)); +} - EXPECT_EQ(expected, actual); +TEST(TestTopologicalSort, EmptyGraph) { + Graph g(0); + EXPECT_TRUE(g.topological_sort().empty()); } +TEST(TestTopologicalSort, NoEdges) { + Graph g(3); + EXPECT_TRUE(IsValidOrder(g.topological_sort(), 3, {})); +} + +TEST(TestTopologicalSort, CycleThrows) { + Graph g(3); + g.add_edge(0, 1); + g.add_edge(1, 2); + g.add_edge(2, 0); + EXPECT_THROW(g.topological_sort(), std::invalid_argument); +} + +TEST(TestTopologicalSort, SelfLoopThrows) { + Graph g(2); + g.add_edge(1, 1); + EXPECT_THROW(g.topological_sort(), std::invalid_argument); +} + +TEST(TestTopologicalSort, InvalidVertexThrows) { + Graph g(2); + EXPECT_THROW(g.add_edge(0, 2), std::out_of_range); + EXPECT_THROW(g.add_edge(-1, 0), std::out_of_range); +} diff --git a/src/backtracking/python/k_colorable_configurations/src/k_colorable_configurations.py b/src/backtracking/python/k_colorable_configurations/src/k_colorable_configurations.py index dbc0f7a1..6e25cb30 100644 --- a/src/backtracking/python/k_colorable_configurations/src/k_colorable_configurations.py +++ b/src/backtracking/python/k_colorable_configurations/src/k_colorable_configurations.py @@ -45,9 +45,7 @@ def find_k_colorable_configurations(G: Graph, k: int) -> List[List[int]]: of color assignments for each vertex. """ - def dfs( - G: Graph, v: int, colors: List[int], configuration: List[int] - ) -> None: + def dfs(G: Graph, v: int, colors: List[int], configuration: List[int]) -> None: if v == G.num_vertices: configurations.append(configuration.copy()) return @@ -63,7 +61,7 @@ def dfs( def is_colorable(G: Graph, v: int, color: int, colors: List[int]) -> bool: """Check if assigning color to vertex v is valid.""" for u in G.adjacency_dict[v]: - if colors[u] == color: + if u == v or colors[u] == color: return False return True diff --git a/src/backtracking/python/k_colorable_configurations/tests/test_k_colorable_configurations.py b/src/backtracking/python/k_colorable_configurations/tests/test_k_colorable_configurations.py index 6f080090..4a9ee157 100644 --- a/src/backtracking/python/k_colorable_configurations/tests/test_k_colorable_configurations.py +++ b/src/backtracking/python/k_colorable_configurations/tests/test_k_colorable_configurations.py @@ -53,6 +53,11 @@ def test_negative_1(self): actual = find_k_colorable_configurations(G, k) self.assertEqual(expected, actual) + def test_self_loop_is_not_colorable(self): + G = Graph() + G.add_edge(0, 0) + self.assertEqual(find_k_colorable_configurations(G, 3), []) + if __name__ == "__main__": unittest.main() diff --git a/src/backtracking/python/knight_tour/src/knight_tour.py b/src/backtracking/python/knight_tour/src/knight_tour.py index 315add8e..14a49b1f 100644 --- a/src/backtracking/python/knight_tour/src/knight_tour.py +++ b/src/backtracking/python/knight_tour/src/knight_tour.py @@ -84,6 +84,9 @@ def knight_tour(n: int) -> List[List[List[int]]]: List of all valid tours, where each tour is an n x n grid showing the order of moves. """ + if n <= 0: + return [] + # initialize the board and the tours list board: List[List[Optional[int]]] = [[None] * n for _ in range(n)] board[0][0] = 0 diff --git a/src/backtracking/python/knight_tour/tests/test_knight_tour.py b/src/backtracking/python/knight_tour/tests/test_knight_tour.py index 3fb347cd..4e22554e 100644 --- a/src/backtracking/python/knight_tour/tests/test_knight_tour.py +++ b/src/backtracking/python/knight_tour/tests/test_knight_tour.py @@ -46,6 +46,10 @@ def test_5(self): self.assertTrue(expected_element in actual) self.assertEqual(len(actual), excepted_length) + def test_non_positive_n(self): + self.assertEqual(knight_tour(0), []) + self.assertEqual(knight_tour(-1), []) + if __name__ == "__main__": unittest.main() diff --git a/src/backtracking/python/minimax/src/minimax.py b/src/backtracking/python/minimax/src/minimax.py index 5e6e69b2..21aec8f4 100644 --- a/src/backtracking/python/minimax/src/minimax.py +++ b/src/backtracking/python/minimax/src/minimax.py @@ -87,7 +87,7 @@ def minimax(board: List[List[int]], player: int) -> int: return final_score(board) # initialize the best score - best_score = float("-inf") if player == 1 else float("inf") + best_score = float("inf") if player == 1 else float("-inf") # try all possible moves for row in range(3): @@ -102,17 +102,15 @@ def minimax(board: List[List[int]], player: int) -> int: # update the best score if player == 1: - best_score = max(best_score, score) - else: best_score = min(best_score, score) + else: + best_score = max(best_score, score) # return the best score return int(best_score) -def find_best_move( - board: List[List[int]], player: int -) -> Optional[Tuple[int, int]]: +def find_best_move(board: List[List[int]], player: int) -> Optional[Tuple[int, int]]: """ Find the best move for the current player using minimax. diff --git a/src/backtracking/python/minimax/tests/test_minimax.py b/src/backtracking/python/minimax/tests/test_minimax.py index 82827ea0..416adff9 100644 --- a/src/backtracking/python/minimax/tests/test_minimax.py +++ b/src/backtracking/python/minimax/tests/test_minimax.py @@ -39,3 +39,14 @@ def test_player_a_wins(self): actual = find_best_move(board, player) self.assertEqual(expected, actual) + + def test_blocks_opponent_win(self): + board = [[0, 0, 0], [0, -1, 0], [1, 1, 0]] + self.assertEqual((2, 2), find_best_move(board, -1)) + + def test_minimax_scores_each_side_by_its_own_goal(self): + # player 1 to move and can win immediately -> score -1 + board = [[1, 1, 0], [-1, -1, 0], [0, 0, 0]] + self.assertEqual(-1, minimax(board, 1)) + # player -1 to move and can win immediately -> score 1 + self.assertEqual(1, minimax(board, -1)) diff --git a/src/backtracking/python/topological_sort/README.md b/src/backtracking/python/topological_sort/README.md index a246c677..16dca6a5 100644 --- a/src/backtracking/python/topological_sort/README.md +++ b/src/backtracking/python/topological_sort/README.md @@ -1,6 +1,6 @@ ## Topological Sort -Given a Directed Acyclic Graph (DAG), find it in topological order using topological sort algorithm. If the graph has more than one topological ordering, output any of them. Assume valid Directed Acyclic Graph (DAG). +Given a Directed Acyclic Graph (DAG), find it in topological order using topological sort algorithm. If the graph has more than one topological ordering, output any of them. The implementation raises an error if the graph contains a cycle, since no ordering exists in that case. A Topological sort or Topological ordering of a directed graph is a linear ordering of its vertices such that for every directed edge uv from vertex u to vertex v, u comes before v in the ordering. A topological ordering is possible if and only if the graph has no directed cycles, i.e. if the graph is DAG. @@ -8,16 +8,17 @@ A Topological sort or Topological ordering of a directed graph is a linear order ``` 3 --> 0 --> 6 -| | -v v -4 --> 1 --> 2 +| ^ +v | +4 --> 1 --> + | v - 4 --> 6 + 2 ``` -The topological ordering of the above graph is $3, 4, 1, 2, 0, 6$. +Edges: `3→0`, `3→4`, `0→6`, `4→1`, `1→2`, `1→6`. +One valid topological ordering of the above graph is $3, 0, 4, 1, 2, 6$ (vertices with no edges, such as an unused label, can appear anywhere). Orderings are generally not unique. ## Approach diff --git a/src/backtracking/python/topological_sort/src/topological_sort.py b/src/backtracking/python/topological_sort/src/topological_sort.py index f0763804..0f5b0c91 100644 --- a/src/backtracking/python/topological_sort/src/topological_sort.py +++ b/src/backtracking/python/topological_sort/src/topological_sort.py @@ -1,5 +1,5 @@ from collections import defaultdict -from typing import List, Dict +from typing import Dict, List class Graph: @@ -9,6 +9,9 @@ def __init__(self, num_vertices: int = 0) -> None: """ Initialize a graph with the specified number of vertices. + Vertices are labelled 0..num_vertices-1; adding an edge that + mentions a larger label grows the graph accordingly. + Args: num_vertices: Initial number of vertices (default 0). """ @@ -22,7 +25,12 @@ def add_edge(self, u: int, v: int) -> None: Args: u: Source vertex. v: Destination vertex. + + Raises: + ValueError: If u or v is negative. """ + if u < 0 or v < 0: + raise ValueError("vertices must be non-negative") self.adjacency_dict[u].append(v) self.num_vertices = max(self.num_vertices, u + 1, v + 1) @@ -31,56 +39,43 @@ def topological_sort(G: Graph) -> List[int]: """ Perform topological sort on a directed acyclic graph (DAG). + Every vertex 0..G.num_vertices-1 appears in the result, including + isolated ones. + Args: G: The directed graph to sort. Returns: - A list of vertices in topological order. - """ + A list of vertices in which u precedes v for every edge (u, v). - def dfs( - G: Graph, - v: int, - discovered: List[bool], - departure: List[int], - time: int, - ) -> int: - # mark the current node as discovered - discovered[v] = True - - # set the arrival time of vertex `v` - time = time + 1 - - # do for every edge (v, u) - for u in G.adjacency_dict[v]: - # if `u` is not yet discovered - if not discovered[u]: - time = dfs(G, u, discovered, departure, time) - - # ready to backtrack - # set departure time of vertex `v` - departure[time] = v - time = time + 1 - - return time - - # departure[] stores the vertex number using departure time as an index - departure: List[int] = [-1] * 2 * G.num_vertices - - # to keep track of whether a vertex is discovered or not - discovered: List[bool] = [False] * G.num_vertices - time = 0 - - # perform dfs on all undiscovered vertices - for v in G.adjacency_dict.copy(): - if not discovered[v]: - time = dfs(G, v, discovered, departure, time) - - # Iterate over the vertices in order of their decreasing - # departure time in dfs, i.e., in topological order - topological_order: List[int] = [] - for i in reversed(range(2 * G.num_vertices)): - if departure[i] != -1: - topological_order.append(departure[i]) - - return topological_order + Raises: + ValueError: If the graph contains a cycle. + """ + unvisited, in_progress, done = 0, 1, 2 + state = [unvisited] * G.num_vertices + post_order: List[int] = [] + + for start in range(G.num_vertices): + if state[start] != unvisited: + continue + + # Iterative DFS holding (vertex, iterator over its neighbours). + state[start] = in_progress + stack = [(start, iter(G.adjacency_dict.get(start, ())))] + while stack: + v, neighbours = stack[-1] + for u in neighbours: + if state[u] == in_progress: + raise ValueError("graph contains a cycle") + if state[u] == unvisited: + state[u] = in_progress + stack.append((u, iter(G.adjacency_dict.get(u, ())))) + break + else: + state[v] = done + post_order.append(v) + stack.pop() + + # Reverse post-order is a topological order. + post_order.reverse() + return post_order diff --git a/src/backtracking/python/topological_sort/tests/test_topological_sort.py b/src/backtracking/python/topological_sort/tests/test_topological_sort.py index 65c85ff7..445dbedb 100644 --- a/src/backtracking/python/topological_sort/tests/test_topological_sort.py +++ b/src/backtracking/python/topological_sort/tests/test_topological_sort.py @@ -1,39 +1,58 @@ import unittest +from typing import List, Tuple + from src.topological_sort import Graph, topological_sort +def build(edges: List[Tuple[int, int]], num_vertices: int = 0) -> Graph: + g = Graph(num_vertices) + for u, v in edges: + g.add_edge(u, v) + return g + + class TestTopologicalSort(unittest.TestCase): + def assert_valid_order(self, order, num_vertices, edges): + self.assertCountEqual(order, range(num_vertices)) + position = {v: i for i, v in enumerate(order)} + for u, v in edges: + self.assertLess(position[u], position[v]) + def test_positive_1(self): - g = Graph() - g.add_edge(0, 6) - g.add_edge(1, 2) - g.add_edge(1, 4) - g.add_edge(1, 6) - g.add_edge(3, 0) - g.add_edge(3, 4) - g.add_edge(5, 1) - g.add_edge(7, 0) - g.add_edge(7, 1) - - expected = [7, 5, 3, 1, 4, 2, 0, 6] - actual = topological_sort(g) - - self.assertListEqual(expected, actual) - - def test_positive_2(self): - g = Graph() - g.add_edge(3, 0) - g.add_edge(3, 4) - g.add_edge(0, 6) - g.add_edge(4, 1) - g.add_edge(1, 2) - g.add_edge(1, 4) - g.add_edge(1, 6) - - expected = [3, 4, 1, 2, 0, 6] - actual = topological_sort(g) - - self.assertListEqual(expected, actual) + edges = [(0, 6), (1, 2), (1, 4), (1, 6), (3, 0), (3, 4), (5, 1), (7, 0), (7, 1)] + actual = topological_sort(build(edges)) + + self.assertListEqual([7, 5, 3, 1, 4, 2, 0, 6], actual) + + def test_positive_2_includes_isolated_vertex(self): + edges = [(3, 0), (3, 4), (0, 6), (4, 1), (1, 2), (1, 6)] + actual = topological_sort(build(edges)) + + # vertex 5 has no edges but must still be returned + self.assert_valid_order(actual, 7, edges) + + def test_empty_graph(self): + self.assertListEqual([], topological_sort(Graph())) + + def test_vertices_without_edges(self): + self.assert_valid_order(topological_sort(Graph(3)), 3, []) + + def test_cycle_raises(self): + with self.assertRaises(ValueError): + topological_sort(build([(0, 1), (1, 2), (2, 0)])) + + def test_self_loop_raises(self): + with self.assertRaises(ValueError): + topological_sort(build([(1, 1)])) + + def test_negative_vertex_raises(self): + with self.assertRaises(ValueError): + Graph().add_edge(-1, 0) + + def test_deep_chain_does_not_hit_recursion_limit(self): + n = 5000 + edges = [(i, i + 1) for i in range(n - 1)] + self.assertListEqual(list(range(n)), topological_sort(build(edges))) if __name__ == "__main__": diff --git a/src/brain_teasers/cpp/add_string_numbers/CMakeLists.txt b/src/brain_teasers/cpp/add_string_numbers/CMakeLists.txt index 50d2a1e9..43165a5e 100644 --- a/src/brain_teasers/cpp/add_string_numbers/CMakeLists.txt +++ b/src/brain_teasers/cpp/add_string_numbers/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(add-string-numbers CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/array_product/CMakeLists.txt b/src/brain_teasers/cpp/array_product/CMakeLists.txt index b9a2ebfc..fcdada18 100644 --- a/src/brain_teasers/cpp/array_product/CMakeLists.txt +++ b/src/brain_teasers/cpp/array_product/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(array-product CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/array_product/src/array_product.cpp b/src/brain_teasers/cpp/array_product/src/array_product.cpp index 4df51c0c..8297b8a8 100644 --- a/src/brain_teasers/cpp/array_product/src/array_product.cpp +++ b/src/brain_teasers/cpp/array_product/src/array_product.cpp @@ -1,24 +1,32 @@ #include "array_product.h" -std::vector productExceptSelf(const std::vector& nums) { - int n = static_cast(nums.size()); - if (n == 0) return {}; - - std::vector result(n, 1); - - // Calculate prefix products (product of all elements before index i) - int prefix = 1; - for (int i = 0; i < n; ++i) { - result[i] = prefix; - prefix *= nums[i]; - } - - // Calculate suffix products and multiply with prefix - int suffix = 1; - for (int i = n - 1; i >= 0; --i) { - result[i] *= suffix; - suffix *= nums[i]; - } - - return result; +std::vector productExceptSelf(const std::vector &nums) { + int n = static_cast(nums.size()); + if (n == 0) + return {}; + + // Unsigned arithmetic wraps instead of invoking signed-overflow UB, so + // intermediate products that overflow (e.g. when a later zero cancels + // them) still yield the correct result. + std::vector acc(n, 1u); + + // Calculate prefix products (product of all elements before index i) + unsigned int prefix = 1u; + for (int i = 0; i < n; ++i) { + acc[i] = prefix; + prefix *= static_cast(nums[i]); + } + + // Calculate suffix products and multiply with prefix + unsigned int suffix = 1u; + for (int i = n - 1; i >= 0; --i) { + acc[i] *= suffix; + suffix *= static_cast(nums[i]); + } + + std::vector result(n); + for (int i = 0; i < n; ++i) { + result[i] = static_cast(acc[i]); + } + return result; } diff --git a/src/brain_teasers/cpp/array_product/test/test_array_product.cpp b/src/brain_teasers/cpp/array_product/test/test_array_product.cpp index 18cbf51e..c89a3ebe 100644 --- a/src/brain_teasers/cpp/array_product/test/test_array_product.cpp +++ b/src/brain_teasers/cpp/array_product/test/test_array_product.cpp @@ -2,43 +2,52 @@ #include "gtest/gtest.h" TEST(ArrayProductTest, BasicExample) { - std::vector nums = {1, 2, 3, 4}; - std::vector expected = {24, 12, 8, 6}; - EXPECT_EQ(productExceptSelf(nums), expected); + std::vector nums = {1, 2, 3, 4}; + std::vector expected = {24, 12, 8, 6}; + EXPECT_EQ(productExceptSelf(nums), expected); } TEST(ArrayProductTest, ContainsZero) { - std::vector nums = {1, 2, 0, 4}; - std::vector expected = {0, 0, 8, 0}; - EXPECT_EQ(productExceptSelf(nums), expected); + std::vector nums = {1, 2, 0, 4}; + std::vector expected = {0, 0, 8, 0}; + EXPECT_EQ(productExceptSelf(nums), expected); } TEST(ArrayProductTest, TwoZeros) { - std::vector nums = {0, 2, 0, 4}; - std::vector expected = {0, 0, 0, 0}; - EXPECT_EQ(productExceptSelf(nums), expected); + std::vector nums = {0, 2, 0, 4}; + std::vector expected = {0, 0, 0, 0}; + EXPECT_EQ(productExceptSelf(nums), expected); } TEST(ArrayProductTest, SingleElement) { - std::vector nums = {5}; - std::vector expected = {1}; - EXPECT_EQ(productExceptSelf(nums), expected); + std::vector nums = {5}; + std::vector expected = {1}; + EXPECT_EQ(productExceptSelf(nums), expected); } TEST(ArrayProductTest, TwoElements) { - std::vector nums = {3, 4}; - std::vector expected = {4, 3}; - EXPECT_EQ(productExceptSelf(nums), expected); + std::vector nums = {3, 4}; + std::vector expected = {4, 3}; + EXPECT_EQ(productExceptSelf(nums), expected); } TEST(ArrayProductTest, NegativeNumbers) { - std::vector nums = {-1, 2, -3, 4}; - std::vector expected = {-24, 12, -8, 6}; - EXPECT_EQ(productExceptSelf(nums), expected); + std::vector nums = {-1, 2, -3, 4}; + std::vector expected = {-24, 12, -8, 6}; + EXPECT_EQ(productExceptSelf(nums), expected); } TEST(ArrayProductTest, EmptyArray) { - std::vector nums = {}; - std::vector expected = {}; - EXPECT_EQ(productExceptSelf(nums), expected); + std::vector nums = {}; + std::vector expected = {}; + EXPECT_EQ(productExceptSelf(nums), expected); +} + +TEST(ArrayProductTest, OverflowingPrefixCancelledByZero) { + // Prefix product 100000^3 overflows int but the true result is 0 + std::vector nums = {100000, 100000, 100000, 0}; + std::vector result = productExceptSelf(nums); + EXPECT_EQ(result[0], 0); + EXPECT_EQ(result[1], 0); + EXPECT_EQ(result[2], 0); } diff --git a/src/brain_teasers/cpp/binary_tree_right_side_view/CMakeLists.txt b/src/brain_teasers/cpp/binary_tree_right_side_view/CMakeLists.txt index 65fa01b6..6f9b4d15 100644 --- a/src/brain_teasers/cpp/binary_tree_right_side_view/CMakeLists.txt +++ b/src/brain_teasers/cpp/binary_tree_right_side_view/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(right-side-view CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/bst_to_list/CMakeLists.txt b/src/brain_teasers/cpp/bst_to_list/CMakeLists.txt index 92fabf45..d80d5387 100644 --- a/src/brain_teasers/cpp/bst_to_list/CMakeLists.txt +++ b/src/brain_teasers/cpp/bst_to_list/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(bst-to-list CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/deletions_to_make_valid_parentheses/CMakeLists.txt b/src/brain_teasers/cpp/deletions_to_make_valid_parentheses/CMakeLists.txt index 59c85f94..de5044d6 100644 --- a/src/brain_teasers/cpp/deletions_to_make_valid_parentheses/CMakeLists.txt +++ b/src/brain_teasers/cpp/deletions_to_make_valid_parentheses/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(valid-parentheses CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/design_browser_history/CMakeLists.txt b/src/brain_teasers/cpp/design_browser_history/CMakeLists.txt index d1f91c9e..d7dcf997 100644 --- a/src/brain_teasers/cpp/design_browser_history/CMakeLists.txt +++ b/src/brain_teasers/cpp/design_browser_history/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(browser-history CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/design_browser_history/src/browser_history.cpp b/src/brain_teasers/cpp/design_browser_history/src/browser_history.cpp index 5253a6e3..a859507e 100644 --- a/src/brain_teasers/cpp/design_browser_history/src/browser_history.cpp +++ b/src/brain_teasers/cpp/design_browser_history/src/browser_history.cpp @@ -1,29 +1,27 @@ #include "browser_history.h" #include -BrowserHistory::BrowserHistory(const std::string& homepage) - : currentIndex_(0) { - history_.push_back(homepage); +BrowserHistory::BrowserHistory(const std::string &homepage) : currentIndex_(0) { + history_.push_back(homepage); } -void BrowserHistory::visit(const std::string& url) { - // Remove all forward history - history_.resize(currentIndex_ + 1); - history_.push_back(url); - currentIndex_++; +void BrowserHistory::visit(const std::string &url) { + // Remove all forward history + history_.resize(currentIndex_ + 1); + history_.push_back(url); + currentIndex_++; } std::string BrowserHistory::back(int steps) { - currentIndex_ = std::max(0, currentIndex_ - steps); - return history_[currentIndex_]; + currentIndex_ -= std::min(steps, currentIndex_); + return history_[currentIndex_]; } std::string BrowserHistory::forward(int steps) { - currentIndex_ = std::min(static_cast(history_.size()) - 1, - currentIndex_ + steps); - return history_[currentIndex_]; + int remaining = static_cast(history_.size()) - 1 - currentIndex_; + // Compare against remaining pages to avoid signed overflow of index + steps + currentIndex_ += std::min(steps, remaining); + return history_[currentIndex_]; } -std::string BrowserHistory::current() const { - return history_[currentIndex_]; -} +std::string BrowserHistory::current() const { return history_[currentIndex_]; } diff --git a/src/brain_teasers/cpp/design_browser_history/test/test_browser_history.cpp b/src/brain_teasers/cpp/design_browser_history/test/test_browser_history.cpp index 166ce87e..3692c2e6 100644 --- a/src/brain_teasers/cpp/design_browser_history/test/test_browser_history.cpp +++ b/src/brain_teasers/cpp/design_browser_history/test/test_browser_history.cpp @@ -1,54 +1,65 @@ #include "browser_history.h" #include "gtest/gtest.h" +#include TEST(BrowserHistoryTest, BasicNavigation) { - BrowserHistory browser("leetcode.com"); - - browser.visit("google.com"); - browser.visit("facebook.com"); - browser.visit("youtube.com"); - - EXPECT_EQ(browser.back(1), "facebook.com"); - EXPECT_EQ(browser.back(1), "google.com"); - EXPECT_EQ(browser.forward(1), "facebook.com"); - - browser.visit("linkedin.com"); // Clears forward history - - EXPECT_EQ(browser.forward(2), "linkedin.com"); // Can't go forward - EXPECT_EQ(browser.back(2), "google.com"); - EXPECT_EQ(browser.back(7), "leetcode.com"); // Can only go back to homepage + BrowserHistory browser("leetcode.com"); + + browser.visit("google.com"); + browser.visit("facebook.com"); + browser.visit("youtube.com"); + + EXPECT_EQ(browser.back(1), "facebook.com"); + EXPECT_EQ(browser.back(1), "google.com"); + EXPECT_EQ(browser.forward(1), "facebook.com"); + + browser.visit("linkedin.com"); // Clears forward history + + EXPECT_EQ(browser.forward(2), "linkedin.com"); // Can't go forward + EXPECT_EQ(browser.back(2), "google.com"); + EXPECT_EQ(browser.back(7), "leetcode.com"); // Can only go back to homepage } TEST(BrowserHistoryTest, CurrentPage) { - BrowserHistory browser("home.com"); - EXPECT_EQ(browser.current(), "home.com"); - - browser.visit("page1.com"); - EXPECT_EQ(browser.current(), "page1.com"); + BrowserHistory browser("home.com"); + EXPECT_EQ(browser.current(), "home.com"); + + browser.visit("page1.com"); + EXPECT_EQ(browser.current(), "page1.com"); } TEST(BrowserHistoryTest, BackBeyondHistory) { - BrowserHistory browser("home.com"); - browser.visit("page1.com"); - - EXPECT_EQ(browser.back(100), "home.com"); + BrowserHistory browser("home.com"); + browser.visit("page1.com"); + + EXPECT_EQ(browser.back(100), "home.com"); } TEST(BrowserHistoryTest, ForwardBeyondHistory) { - BrowserHistory browser("home.com"); - browser.visit("page1.com"); - browser.visit("page2.com"); - browser.back(1); - - EXPECT_EQ(browser.forward(100), "page2.com"); + BrowserHistory browser("home.com"); + browser.visit("page1.com"); + browser.visit("page2.com"); + browser.back(1); + + EXPECT_EQ(browser.forward(100), "page2.com"); } TEST(BrowserHistoryTest, VisitClearsForwardHistory) { - BrowserHistory browser("home.com"); - browser.visit("page1.com"); - browser.visit("page2.com"); - browser.back(1); // Now at page1.com - browser.visit("page3.com"); // Clears page2.com from forward history - - EXPECT_EQ(browser.forward(1), "page3.com"); // Can't go to page2.com + BrowserHistory browser("home.com"); + browser.visit("page1.com"); + browser.visit("page2.com"); + browser.back(1); // Now at page1.com + browser.visit("page3.com"); // Clears page2.com from forward history + + EXPECT_EQ(browser.forward(1), "page3.com"); // Can't go to page2.com +} + +TEST(BrowserHistoryTest, HugeStepsDoNotOverflow) { + BrowserHistory browser("home.com"); + browser.visit("page1.com"); + browser.visit("page2.com"); + + EXPECT_EQ(browser.forward(INT_MAX), "page2.com"); + EXPECT_EQ(browser.back(INT_MAX), "home.com"); + EXPECT_EQ(browser.forward(INT_MAX), "page2.com"); } diff --git a/src/brain_teasers/cpp/is_palindrome_after_char_deletion/CMakeLists.txt b/src/brain_teasers/cpp/is_palindrome_after_char_deletion/CMakeLists.txt index e36e7491..78838a7d 100644 --- a/src/brain_teasers/cpp/is_palindrome_after_char_deletion/CMakeLists.txt +++ b/src/brain_teasers/cpp/is_palindrome_after_char_deletion/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(is-palindrome CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/k_closest_points/CMakeLists.txt b/src/brain_teasers/cpp/k_closest_points/CMakeLists.txt index 7acf6911..8b422b4e 100644 --- a/src/brain_teasers/cpp/k_closest_points/CMakeLists.txt +++ b/src/brain_teasers/cpp/k_closest_points/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(k-closest-points CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/k_closest_points/src/k_closest_points.cpp b/src/brain_teasers/cpp/k_closest_points/src/k_closest_points.cpp index a9479714..183a8a5b 100644 --- a/src/brain_teasers/cpp/k_closest_points/src/k_closest_points.cpp +++ b/src/brain_teasers/cpp/k_closest_points/src/k_closest_points.cpp @@ -3,19 +3,25 @@ #include // Helper function to calculate squared distance from origin -static int squaredDistance(const Point& p) { - return p.x * p.x + p.y * p.y; +static long long squaredDistance(const Point &p) { + return static_cast(p.x) * p.x + static_cast(p.y) * p.y; } -std::vector kClosestPoints(std::vector& points, int k) { - if (k <= 0) return {}; - if (k >= static_cast(points.size())) return points; - - // Use partial_sort for O(n log k) complexity - std::partial_sort(points.begin(), points.begin() + k, points.end(), - [](const Point& a, const Point& b) { - return squaredDistance(a) < squaredDistance(b); - }); - - return std::vector(points.begin(), points.begin() + k); +std::vector kClosestPoints(std::vector &points, int k) { + if (k <= 0) + return {}; + + // Work on a copy so the caller's vector is left untouched + std::vector sorted = points; + if (k > static_cast(sorted.size())) + k = static_cast(sorted.size()); + + // Use partial_sort for O(n log k) complexity + std::partial_sort(sorted.begin(), sorted.begin() + k, sorted.end(), + [](const Point &a, const Point &b) { + return squaredDistance(a) < squaredDistance(b); + }); + + sorted.resize(k); + return sorted; } diff --git a/src/brain_teasers/cpp/k_closest_points/test/test_k_closest_points.cpp b/src/brain_teasers/cpp/k_closest_points/test/test_k_closest_points.cpp index 2c0c3a0c..5334f6c4 100644 --- a/src/brain_teasers/cpp/k_closest_points/test/test_k_closest_points.cpp +++ b/src/brain_teasers/cpp/k_closest_points/test/test_k_closest_points.cpp @@ -2,71 +2,104 @@ #include "gtest/gtest.h" #include -// Helper to check if two vectors of points contain the same points (order independent) -bool containsSamePoints(std::vector& a, std::vector& b) { - if (a.size() != b.size()) return false; - - auto distanceCompare = [](const Point& p1, const Point& p2) { - int d1 = p1.x * p1.x + p1.y * p1.y; - int d2 = p2.x * p2.x + p2.y * p2.y; - if (d1 != d2) return d1 < d2; - if (p1.x != p2.x) return p1.x < p2.x; - return p1.y < p2.y; - }; - - std::sort(a.begin(), a.end(), distanceCompare); - std::sort(b.begin(), b.end(), distanceCompare); - - for (size_t i = 0; i < a.size(); ++i) { - if (a[i].x != b[i].x || a[i].y != b[i].y) return false; - } - return true; +// Helper to check if two vectors of points contain the same points (order +// independent) +bool containsSamePoints(std::vector &a, std::vector &b) { + if (a.size() != b.size()) + return false; + + auto distanceCompare = [](const Point &p1, const Point &p2) { + int d1 = p1.x * p1.x + p1.y * p1.y; + int d2 = p2.x * p2.x + p2.y * p2.y; + if (d1 != d2) + return d1 < d2; + if (p1.x != p2.x) + return p1.x < p2.x; + return p1.y < p2.y; + }; + + std::sort(a.begin(), a.end(), distanceCompare); + std::sort(b.begin(), b.end(), distanceCompare); + + for (size_t i = 0; i < a.size(); ++i) { + if (a[i].x != b[i].x || a[i].y != b[i].y) + return false; + } + return true; } TEST(KClosestPointsTest, BasicExample) { - std::vector points = {{1, 2}, {3, 4}, {-1, -2}, {-3, -4}}; - auto result = kClosestPoints(points, 2); - std::vector expected = {{1, 2}, {-1, -2}}; - EXPECT_TRUE(containsSamePoints(result, expected)); + std::vector points = {{1, 2}, {3, 4}, {-1, -2}, {-3, -4}}; + auto result = kClosestPoints(points, 2); + std::vector expected = {{1, 2}, {-1, -2}}; + EXPECT_TRUE(containsSamePoints(result, expected)); } TEST(KClosestPointsTest, ThreeClosest) { - std::vector points = {{1, 1}, {1, 2}, {3, 4}, {-1, -2}, {-3, -4}}; - auto result = kClosestPoints(points, 3); - std::vector expected = {{1, 1}, {1, 2}, {-1, -2}}; - EXPECT_TRUE(containsSamePoints(result, expected)); + std::vector points = {{1, 1}, {1, 2}, {3, 4}, {-1, -2}, {-3, -4}}; + auto result = kClosestPoints(points, 3); + std::vector expected = {{1, 1}, {1, 2}, {-1, -2}}; + EXPECT_TRUE(containsSamePoints(result, expected)); } TEST(KClosestPointsTest, KEqualsSize) { - std::vector points = {{1, 2}, {3, 4}}; - auto result = kClosestPoints(points, 2); - EXPECT_EQ(result.size(), 2u); + std::vector points = {{1, 2}, {3, 4}}; + auto result = kClosestPoints(points, 2); + EXPECT_EQ(result.size(), 2u); } TEST(KClosestPointsTest, KGreaterThanSize) { - std::vector points = {{1, 2}, {3, 4}}; - auto result = kClosestPoints(points, 10); - EXPECT_EQ(result.size(), 2u); + std::vector points = {{1, 2}, {3, 4}}; + auto result = kClosestPoints(points, 10); + EXPECT_EQ(result.size(), 2u); } TEST(KClosestPointsTest, KIsZero) { - std::vector points = {{1, 2}, {3, 4}}; - auto result = kClosestPoints(points, 0); - EXPECT_TRUE(result.empty()); + std::vector points = {{1, 2}, {3, 4}}; + auto result = kClosestPoints(points, 0); + EXPECT_TRUE(result.empty()); } TEST(KClosestPointsTest, SinglePoint) { - std::vector points = {{1, 1}}; - auto result = kClosestPoints(points, 1); - EXPECT_EQ(result.size(), 1u); - EXPECT_EQ(result[0].x, 1); - EXPECT_EQ(result[0].y, 1); + std::vector points = {{1, 1}}; + auto result = kClosestPoints(points, 1); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0].x, 1); + EXPECT_EQ(result[0].y, 1); } TEST(KClosestPointsTest, OriginPoint) { - std::vector points = {{0, 0}, {1, 1}, {2, 2}}; - auto result = kClosestPoints(points, 1); - EXPECT_EQ(result.size(), 1u); - EXPECT_EQ(result[0].x, 0); - EXPECT_EQ(result[0].y, 0); + std::vector points = {{0, 0}, {1, 1}, {2, 2}}; + auto result = kClosestPoints(points, 1); + EXPECT_EQ(result.size(), 1u); + EXPECT_EQ(result[0].x, 0); + EXPECT_EQ(result[0].y, 0); +} + +TEST(KClosestPointsTest, DoesNotMutateInput) { + std::vector points = {{5, 5}, {1, 1}, {3, 3}}; + auto result = kClosestPoints(points, 1); + EXPECT_EQ(points[0].x, 5); + EXPECT_EQ(points[1].x, 1); + EXPECT_EQ(points[2].x, 3); + ASSERT_EQ(result.size(), 1u); + EXPECT_EQ(result[0].x, 1); +} + +TEST(KClosestPointsTest, KGreaterThanSizeIsSorted) { + std::vector points = {{3, 3}, {1, 1}, {2, 2}}; + auto result = kClosestPoints(points, 10); + ASSERT_EQ(result.size(), 3u); + EXPECT_EQ(result[0].x, 1); + EXPECT_EQ(result[1].x, 2); + EXPECT_EQ(result[2].x, 3); +} + +TEST(KClosestPointsTest, LargeCoordinatesNoOverflow) { + // 40000^2 * 2 overflows int; (1, 1) must still be the closest + std::vector points = {{40000, 40000}, {1, 1}, {50000, 0}}; + auto result = kClosestPoints(points, 1); + ASSERT_EQ(result.size(), 1u); + EXPECT_EQ(result[0].x, 1); + EXPECT_EQ(result[0].y, 1); } diff --git a/src/brain_teasers/cpp/lowest_common_ancestor/CMakeLists.txt b/src/brain_teasers/cpp/lowest_common_ancestor/CMakeLists.txt index f139f706..313dbf5c 100644 --- a/src/brain_teasers/cpp/lowest_common_ancestor/CMakeLists.txt +++ b/src/brain_teasers/cpp/lowest_common_ancestor/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(lowest-common-ancestor CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/lowest_common_ancestor/src/lca.cpp b/src/brain_teasers/cpp/lowest_common_ancestor/src/lca.cpp index c797006b..84aa4e17 100644 --- a/src/brain_teasers/cpp/lowest_common_ancestor/src/lca.cpp +++ b/src/brain_teasers/cpp/lowest_common_ancestor/src/lca.cpp @@ -1,31 +1,41 @@ #include "lca.h" // Helper function to find if a value exists in the tree -static bool findNode(TreeNode* root, int val) { - if (root == nullptr) return false; - if (root->val == val) return true; - return findNode(root->left, val) || findNode(root->right, val); +static bool findNode(TreeNode *root, int val) { + if (root == nullptr) + return false; + if (root->val == val) + return true; + return findNode(root->left, val) || findNode(root->right, val); } -TreeNode* lowestCommonAncestor(TreeNode* root, int p, int q) { - if (root == nullptr) { - return nullptr; - } - - // If current node matches either p or q, it could be the LCA - if (root->val == p || root->val == q) { - return root; - } - - // Recursively search left and right subtrees - TreeNode* leftLCA = lowestCommonAncestor(root->left, p, q); - TreeNode* rightLCA = lowestCommonAncestor(root->right, p, q); - - // If both sides found something, current node is LCA - if (leftLCA != nullptr && rightLCA != nullptr) { - return root; - } - - // Return whichever side found something - return (leftLCA != nullptr) ? leftLCA : rightLCA; +static TreeNode *lcaHelper(TreeNode *root, int p, int q) { + if (root == nullptr) { + return nullptr; + } + + // If current node matches either p or q, it could be the LCA + if (root->val == p || root->val == q) { + return root; + } + + // Recursively search left and right subtrees + TreeNode *leftLCA = lcaHelper(root->left, p, q); + TreeNode *rightLCA = lcaHelper(root->right, p, q); + + // If both sides found something, current node is LCA + if (leftLCA != nullptr && rightLCA != nullptr) { + return root; + } + + // Return whichever side found something + return (leftLCA != nullptr) ? leftLCA : rightLCA; +} + +TreeNode *lowestCommonAncestor(TreeNode *root, int p, int q) { + // Both values must exist in the tree, otherwise there is no LCA + if (!findNode(root, p) || !findNode(root, q)) { + return nullptr; + } + return lcaHelper(root, p, q); } diff --git a/src/brain_teasers/cpp/lowest_common_ancestor/test/test_lca.cpp b/src/brain_teasers/cpp/lowest_common_ancestor/test/test_lca.cpp index 2eeae457..e88476b4 100644 --- a/src/brain_teasers/cpp/lowest_common_ancestor/test/test_lca.cpp +++ b/src/brain_teasers/cpp/lowest_common_ancestor/test/test_lca.cpp @@ -2,66 +2,77 @@ #include "gtest/gtest.h" // Helper to delete tree -void deleteTree(TreeNode* root) { - if (root) { - deleteTree(root->left); - deleteTree(root->right); - delete root; - } +void deleteTree(TreeNode *root) { + if (root) { + deleteTree(root->left); + deleteTree(root->right); + delete root; + } } TEST(LCATest, BasicTree) { - // 3 - // / \ + // 3 + // / \ // 5 1 - // / \ / \ + // / \ / \ // 6 2 0 8 - // / \ + // / \ // 7 4 - TreeNode* root = new TreeNode(3); - root->left = new TreeNode(5); - root->right = new TreeNode(1); - root->left->left = new TreeNode(6); - root->left->right = new TreeNode(2); - root->right->left = new TreeNode(0); - root->right->right = new TreeNode(8); - root->left->right->left = new TreeNode(7); - root->left->right->right = new TreeNode(4); - - TreeNode* lca = lowestCommonAncestor(root, 5, 1); - EXPECT_EQ(lca->val, 3); - - lca = lowestCommonAncestor(root, 5, 4); - EXPECT_EQ(lca->val, 5); - - lca = lowestCommonAncestor(root, 6, 4); - EXPECT_EQ(lca->val, 5); - - deleteTree(root); + TreeNode *root = new TreeNode(3); + root->left = new TreeNode(5); + root->right = new TreeNode(1); + root->left->left = new TreeNode(6); + root->left->right = new TreeNode(2); + root->right->left = new TreeNode(0); + root->right->right = new TreeNode(8); + root->left->right->left = new TreeNode(7); + root->left->right->right = new TreeNode(4); + + TreeNode *lca = lowestCommonAncestor(root, 5, 1); + EXPECT_EQ(lca->val, 3); + + lca = lowestCommonAncestor(root, 5, 4); + EXPECT_EQ(lca->val, 5); + + lca = lowestCommonAncestor(root, 6, 4); + EXPECT_EQ(lca->val, 5); + + deleteTree(root); } TEST(LCATest, SimpleTree) { - TreeNode* root = new TreeNode(1); - root->left = new TreeNode(2); - - TreeNode* lca = lowestCommonAncestor(root, 1, 2); - EXPECT_EQ(lca->val, 1); - - deleteTree(root); + TreeNode *root = new TreeNode(1); + root->left = new TreeNode(2); + + TreeNode *lca = lowestCommonAncestor(root, 1, 2); + EXPECT_EQ(lca->val, 1); + + deleteTree(root); } TEST(LCATest, NullTree) { - TreeNode* lca = lowestCommonAncestor(nullptr, 1, 2); - EXPECT_EQ(lca, nullptr); + TreeNode *lca = lowestCommonAncestor(nullptr, 1, 2); + EXPECT_EQ(lca, nullptr); } TEST(LCATest, SameNode) { - TreeNode* root = new TreeNode(1); - root->left = new TreeNode(2); - root->right = new TreeNode(3); - - TreeNode* lca = lowestCommonAncestor(root, 2, 2); - EXPECT_EQ(lca->val, 2); - - deleteTree(root); + TreeNode *root = new TreeNode(1); + root->left = new TreeNode(2); + root->right = new TreeNode(3); + + TreeNode *lca = lowestCommonAncestor(root, 2, 2); + EXPECT_EQ(lca->val, 2); + + deleteTree(root); +} + +TEST(LCATest, MissingValueReturnsNull) { + TreeNode *root = new TreeNode(1); + root->left = new TreeNode(2); + root->right = new TreeNode(3); + + EXPECT_EQ(lowestCommonAncestor(root, 2, 99), nullptr); + EXPECT_EQ(lowestCommonAncestor(root, 99, 2), nullptr); + + deleteTree(root); } diff --git a/src/brain_teasers/cpp/lru_cache/CMakeLists.txt b/src/brain_teasers/cpp/lru_cache/CMakeLists.txt index 04bb893c..5a83f1e5 100644 --- a/src/brain_teasers/cpp/lru_cache/CMakeLists.txt +++ b/src/brain_teasers/cpp/lru_cache/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(lru-cache CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/lru_cache/src/lru_cache.cpp b/src/brain_teasers/cpp/lru_cache/src/lru_cache.cpp index 3e4e6903..fca65b0e 100644 --- a/src/brain_teasers/cpp/lru_cache/src/lru_cache.cpp +++ b/src/brain_teasers/cpp/lru_cache/src/lru_cache.cpp @@ -3,40 +3,43 @@ LRUCache::LRUCache(int capacity) : capacity_(capacity) {} void LRUCache::moveToFront(std::list>::iterator it) { - if (it != cache_.begin()) { - cache_.splice(cache_.begin(), cache_, it); - } + if (it != cache_.begin()) { + cache_.splice(cache_.begin(), cache_, it); + } } int LRUCache::get(int key) { - auto it = map_.find(key); - if (it == map_.end()) { - return -1; - } - - // Move to front (most recently used) - moveToFront(it->second); - return it->second->second; + auto it = map_.find(key); + if (it == map_.end()) { + return -1; + } + + // Move to front (most recently used) + moveToFront(it->second); + return it->second->second; } void LRUCache::put(int key, int value) { - auto it = map_.find(key); - - if (it != map_.end()) { - // Key exists, update value and move to front - it->second->second = value; - moveToFront(it->second); - } else { - // Key doesn't exist - if (static_cast(cache_.size()) >= capacity_) { - // Evict least recently used (back of list) - int evictKey = cache_.back().first; - cache_.pop_back(); - map_.erase(evictKey); - } - - // Add new entry to front - cache_.push_front({key, value}); - map_[key] = cache_.begin(); + if (capacity_ <= 0) { + return; + } + auto it = map_.find(key); + + if (it != map_.end()) { + // Key exists, update value and move to front + it->second->second = value; + moveToFront(it->second); + } else { + // Key doesn't exist + if (static_cast(cache_.size()) >= capacity_) { + // Evict least recently used (back of list) + int evictKey = cache_.back().first; + cache_.pop_back(); + map_.erase(evictKey); } + + // Add new entry to front + cache_.push_front({key, value}); + map_[key] = cache_.begin(); + } } diff --git a/src/brain_teasers/cpp/lru_cache/test/test_lru_cache.cpp b/src/brain_teasers/cpp/lru_cache/test/test_lru_cache.cpp index ef632d24..14d899c8 100644 --- a/src/brain_teasers/cpp/lru_cache/test/test_lru_cache.cpp +++ b/src/brain_teasers/cpp/lru_cache/test/test_lru_cache.cpp @@ -2,70 +2,78 @@ #include "gtest/gtest.h" TEST(LRUCacheTest, BasicOperations) { - LRUCache cache(2); - - cache.put(1, 1); - cache.put(2, 2); - EXPECT_EQ(cache.get(1), 1); // returns 1 - - cache.put(3, 3); // evicts key 2 - EXPECT_EQ(cache.get(2), -1); // returns -1 (not found) - - cache.put(4, 4); // evicts key 1 - EXPECT_EQ(cache.get(1), -1); // returns -1 (not found) - EXPECT_EQ(cache.get(3), 3); // returns 3 - EXPECT_EQ(cache.get(4), 4); // returns 4 + LRUCache cache(2); + + cache.put(1, 1); + cache.put(2, 2); + EXPECT_EQ(cache.get(1), 1); // returns 1 + + cache.put(3, 3); // evicts key 2 + EXPECT_EQ(cache.get(2), -1); // returns -1 (not found) + + cache.put(4, 4); // evicts key 1 + EXPECT_EQ(cache.get(1), -1); // returns -1 (not found) + EXPECT_EQ(cache.get(3), 3); // returns 3 + EXPECT_EQ(cache.get(4), 4); // returns 4 } TEST(LRUCacheTest, UpdateExistingKey) { - LRUCache cache(2); - - cache.put(1, 1); - cache.put(2, 2); - cache.put(1, 10); // update value - EXPECT_EQ(cache.get(1), 10); // returns updated value - EXPECT_EQ(cache.get(2), 2); // key 2 still exists + LRUCache cache(2); + + cache.put(1, 1); + cache.put(2, 2); + cache.put(1, 10); // update value + EXPECT_EQ(cache.get(1), 10); // returns updated value + EXPECT_EQ(cache.get(2), 2); // key 2 still exists } TEST(LRUCacheTest, GetMakesRecent) { - LRUCache cache(2); - - cache.put(1, 1); - cache.put(2, 2); - cache.get(1); // access key 1, making it most recent - cache.put(3, 3); // should evict key 2, not key 1 - - EXPECT_EQ(cache.get(1), 1); // key 1 still exists - EXPECT_EQ(cache.get(2), -1); // key 2 was evicted - EXPECT_EQ(cache.get(3), 3); // key 3 exists + LRUCache cache(2); + + cache.put(1, 1); + cache.put(2, 2); + cache.get(1); // access key 1, making it most recent + cache.put(3, 3); // should evict key 2, not key 1 + + EXPECT_EQ(cache.get(1), 1); // key 1 still exists + EXPECT_EQ(cache.get(2), -1); // key 2 was evicted + EXPECT_EQ(cache.get(3), 3); // key 3 exists } TEST(LRUCacheTest, SingleCapacity) { - LRUCache cache(1); - - cache.put(1, 1); - EXPECT_EQ(cache.get(1), 1); - - cache.put(2, 2); // evicts key 1 - EXPECT_EQ(cache.get(1), -1); - EXPECT_EQ(cache.get(2), 2); + LRUCache cache(1); + + cache.put(1, 1); + EXPECT_EQ(cache.get(1), 1); + + cache.put(2, 2); // evicts key 1 + EXPECT_EQ(cache.get(1), -1); + EXPECT_EQ(cache.get(2), 2); } TEST(LRUCacheTest, GetNonExistentKey) { - LRUCache cache(2); - EXPECT_EQ(cache.get(1), -1); - EXPECT_EQ(cache.get(100), -1); + LRUCache cache(2); + EXPECT_EQ(cache.get(1), -1); + EXPECT_EQ(cache.get(100), -1); } TEST(LRUCacheTest, PutMakesRecent) { - LRUCache cache(2); - - cache.put(1, 1); - cache.put(2, 2); - cache.put(1, 100); // update makes key 1 most recent - cache.put(3, 3); // should evict key 2 - - EXPECT_EQ(cache.get(1), 100); - EXPECT_EQ(cache.get(2), -1); - EXPECT_EQ(cache.get(3), 3); + LRUCache cache(2); + + cache.put(1, 1); + cache.put(2, 2); + cache.put(1, 100); // update makes key 1 most recent + cache.put(3, 3); // should evict key 2 + + EXPECT_EQ(cache.get(1), 100); + EXPECT_EQ(cache.get(2), -1); + EXPECT_EQ(cache.get(3), 3); +} + +TEST(LRUCacheTest, ZeroCapacityStoresNothing) { + LRUCache cache(0); + cache.put(1, 1); + EXPECT_EQ(cache.get(1), -1); + cache.put(2, 2); + EXPECT_EQ(cache.get(2), -1); } diff --git a/src/brain_teasers/cpp/randomize_array/CMakeLists.txt b/src/brain_teasers/cpp/randomize_array/CMakeLists.txt index 2abd1ca6..cefb0da2 100644 --- a/src/brain_teasers/cpp/randomize_array/CMakeLists.txt +++ b/src/brain_teasers/cpp/randomize_array/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(randomize-array CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/range_sum_of_bst/CMakeLists.txt b/src/brain_teasers/cpp/range_sum_of_bst/CMakeLists.txt index 5c3f6503..7be21c8f 100644 --- a/src/brain_teasers/cpp/range_sum_of_bst/CMakeLists.txt +++ b/src/brain_teasers/cpp/range_sum_of_bst/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(range-sum-bst CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/score_after_flipping_matrix/CMakeLists.txt b/src/brain_teasers/cpp/score_after_flipping_matrix/CMakeLists.txt index 4e109a0b..13759d47 100644 --- a/src/brain_teasers/cpp/score_after_flipping_matrix/CMakeLists.txt +++ b/src/brain_teasers/cpp/score_after_flipping_matrix/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(matrix-score CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/sparse_vectors_product/CMakeLists.txt b/src/brain_teasers/cpp/sparse_vectors_product/CMakeLists.txt index 40e8240a..e2e8b076 100644 --- a/src/brain_teasers/cpp/sparse_vectors_product/CMakeLists.txt +++ b/src/brain_teasers/cpp/sparse_vectors_product/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(sparse-vectors CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/cpp/subarray_sum/CMakeLists.txt b/src/brain_teasers/cpp/subarray_sum/CMakeLists.txt index 75a69a7a..c43ac515 100644 --- a/src/brain_teasers/cpp/subarray_sum/CMakeLists.txt +++ b/src/brain_teasers/cpp/subarray_sum/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(subarray-sum CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/brain_teasers/python/array_product/tests/test_array_product.py b/src/brain_teasers/python/array_product/tests/test_array_product.py index d8f8bce5..c1542121 100644 --- a/src/brain_teasers/python/array_product/tests/test_array_product.py +++ b/src/brain_teasers/python/array_product/tests/test_array_product.py @@ -22,6 +22,12 @@ def test_array_with_one_element(self): actual = product_of_array_except_self(input_array) self.assertEqual(actual, expected) + def test_zeros_negatives_and_no_mutation(self): + arr = [1, 0, -3, 4] + self.assertEqual(product_of_array_except_self(arr), [0, -12, 0, 0]) + self.assertEqual(arr, [1, 0, -3, 4]) + self.assertEqual(product_of_array_except_self([0, 0, 2]), [0, 0, 0]) + if __name__ == "__main__": unittest.main() diff --git a/src/brain_teasers/python/bst_to_list/src/bst_to_list.py b/src/brain_teasers/python/bst_to_list/src/bst_to_list.py index 48df55d3..c833ea23 100644 --- a/src/brain_teasers/python/bst_to_list/src/bst_to_list.py +++ b/src/brain_teasers/python/bst_to_list/src/bst_to_list.py @@ -118,7 +118,7 @@ def bst_to_double_linked_list(root: Optional[TreeNode]) -> DoubleLinkedList: node = node.left else: node = stack.pop() - double_linked_list.insert_before(double_linked_list.head, node.value) + double_linked_list.insert_after(double_linked_list.tail, node.value) node = node.right return double_linked_list diff --git a/src/brain_teasers/python/bst_to_list/tests/test_bst_to_list.py b/src/brain_teasers/python/bst_to_list/tests/test_bst_to_list.py index 39d2cb47..19aba481 100644 --- a/src/brain_teasers/python/bst_to_list/tests/test_bst_to_list.py +++ b/src/brain_teasers/python/bst_to_list/tests/test_bst_to_list.py @@ -19,7 +19,7 @@ def test_full_tree(self): root.left.right = TreeNode(5) root.right.right = TreeNode(11) - expected = [11, 10, 8, 5, 4, 3] + expected = [3, 4, 5, 8, 10, 11] actual = bst_to_double_linked_list(root) self.assertEqual(list(actual), expected) @@ -35,6 +35,17 @@ def test_single_node_tree(self): actual = bst_to_double_linked_list(root) self.assertEqual(list(actual), expected) + def test_result_is_ascending_and_doubly_linked(self): + root = TreeNode(2) + root.left = TreeNode(1) + root.right = TreeNode(3) + result = bst_to_double_linked_list(root) + self.assertEqual(result.to_list(), [1, 2, 3]) + self.assertEqual(result.head.value, 1) + self.assertEqual(result.tail.value, 3) + self.assertEqual(result.tail.prev.value, 2) + self.assertEqual(result.head.next.prev.value, 1) + if __name__ == "__main__": unittest.main() diff --git a/src/brain_teasers/python/design_browser_history/tests/test_design_browser_history.py b/src/brain_teasers/python/design_browser_history/tests/test_design_browser_history.py index b0d14963..6be89cf6 100644 --- a/src/brain_teasers/python/design_browser_history/tests/test_design_browser_history.py +++ b/src/brain_teasers/python/design_browser_history/tests/test_design_browser_history.py @@ -34,6 +34,15 @@ def test_3(self): self.assertEqual(browser_history.back(1), "e.com") self.assertEqual(browser_history.back(1), "d.com") + def test_visit_clears_forward_history(self): + b = BrowserHistory("a") + b.visit("b") + b.visit("c") + b.back(1) + b.visit("d") + self.assertEqual(b.forward(5), "d") + self.assertEqual(b.back(1), "b") + if __name__ == "__main__": unittest.main() diff --git a/src/brain_teasers/python/is_palindrome_after_char_deletion/src/is_palindrome_after_char_deletion.py b/src/brain_teasers/python/is_palindrome_after_char_deletion/src/is_palindrome_after_char_deletion.py index a075796f..211680d6 100644 --- a/src/brain_teasers/python/is_palindrome_after_char_deletion/src/is_palindrome_after_char_deletion.py +++ b/src/brain_teasers/python/is_palindrome_after_char_deletion/src/is_palindrome_after_char_deletion.py @@ -17,24 +17,23 @@ def is_palindrome_after_char_deletion(s: str) -> bool: True if the string is already a palindrome or can become one by deleting at most one character. """ + + def is_palindrome(left: int, right: int) -> bool: + while left < right: + if s[left] != s[right]: + return False + left += 1 + right -= 1 + return True + left = 0 right = len(s) - 1 - deleted = False while left < right: - if s[left] == s[right]: - left += 1 - right -= 1 - else: - if deleted: - return False - deleted = True - # Try deleting one of the characters - if s[left + 1] == s[right]: - left += 1 - elif s[left] == s[right - 1]: - right -= 1 - else: - return False + if s[left] != s[right]: + # Try deleting either mismatching character + return is_palindrome(left + 1, right) or is_palindrome(left, right - 1) + left += 1 + right -= 1 return True diff --git a/src/brain_teasers/python/is_palindrome_after_char_deletion/tests/test_is_palindrome_after_char_deletion.py b/src/brain_teasers/python/is_palindrome_after_char_deletion/tests/test_is_palindrome_after_char_deletion.py index 6d4176cc..7e6ce950 100644 --- a/src/brain_teasers/python/is_palindrome_after_char_deletion/tests/test_is_palindrome_after_char_deletion.py +++ b/src/brain_teasers/python/is_palindrome_after_char_deletion/tests/test_is_palindrome_after_char_deletion.py @@ -46,6 +46,16 @@ def test_7(self): actual = is_palindrome_after_char_deletion(input_string) self.assertEqual(actual, expected) + def test_delete_right_character_when_left_also_matches(self): + self.assertTrue(is_palindrome_after_char_deletion("cuucu")) + self.assertTrue(is_palindrome_after_char_deletion("ucuuc")) + + def test_empty_and_short(self): + self.assertTrue(is_palindrome_after_char_deletion("")) + self.assertTrue(is_palindrome_after_char_deletion("a")) + self.assertTrue(is_palindrome_after_char_deletion("ab")) + self.assertFalse(is_palindrome_after_char_deletion("abc")) + if __name__ == "__main__": unittest.main() diff --git a/src/brain_teasers/python/k_closest_points/src/k_closest_points.py b/src/brain_teasers/python/k_closest_points/src/k_closest_points.py index fe673484..bfcd9073 100644 --- a/src/brain_teasers/python/k_closest_points/src/k_closest_points.py +++ b/src/brain_teasers/python/k_closest_points/src/k_closest_points.py @@ -2,12 +2,8 @@ from __future__ import annotations -import math - -def k_closest_points( - points: list[tuple[int, int]], k: int -) -> list[tuple[int, int]]: +def k_closest_points(points: list[tuple[int, int]], k: int) -> list[tuple[int, int]]: """ Return the K closest points to the origin (0, 0). @@ -20,8 +16,10 @@ def k_closest_points( Returns: List of the K closest points to the origin. """ - # Create a list of (distance, point) tuples - distances = [(math.sqrt(x**2 + y**2), (x, y)) for x, y in points] + if k <= 0: + return [] + # Squared integer distance avoids floating-point rounding for large values + distances = [(x * x + y * y, (x, y)) for x, y in points] # Sort by distance distances.sort() # Return the K closest points diff --git a/src/brain_teasers/python/k_closest_points/tests/test_k_closest_points.py b/src/brain_teasers/python/k_closest_points/tests/test_k_closest_points.py index 2482f78f..6d0767ad 100644 --- a/src/brain_teasers/python/k_closest_points/tests/test_k_closest_points.py +++ b/src/brain_teasers/python/k_closest_points/tests/test_k_closest_points.py @@ -32,6 +32,24 @@ def test_4(self): actual = k_closest_points(input_points, input_K) self.assertEqual(actual, expected) + def test_k_zero_and_negative(self): + points = [(1, 1), (2, 2)] + self.assertEqual(k_closest_points(points, 0), []) + self.assertEqual(k_closest_points(points, -1), []) + + def test_k_larger_than_size_and_empty(self): + self.assertEqual(k_closest_points([(3, 3), (1, 1)], 5), [(1, 1), (3, 3)]) + self.assertEqual(k_closest_points([], 3), []) + + def test_large_coordinates_exact_ordering(self): + a = (10**9, 1) + b = (10**9 - 1, 10**5) + # squared distances differ by far less than float precision allows + self.assertEqual( + k_closest_points([a, b], 1), + [min([a, b], key=lambda p: p[0] ** 2 + p[1] ** 2)], + ) + if __name__ == "__main__": unittest.main() diff --git a/src/brain_teasers/python/lowest_common_ancestor/tests/test_lowest_common_ancestor.py b/src/brain_teasers/python/lowest_common_ancestor/tests/test_lowest_common_ancestor.py index 34a5caed..fd79d948 100644 --- a/src/brain_teasers/python/lowest_common_ancestor/tests/test_lowest_common_ancestor.py +++ b/src/brain_teasers/python/lowest_common_ancestor/tests/test_lowest_common_ancestor.py @@ -51,6 +51,11 @@ def test_right_skewed_tree(self): actual = lowest_common_ancestor(root, 10, 11) self.assertEqual(actual, expected) + def test_missing_value_returns_none(self): + root = TreeNode(1, TreeNode(2), TreeNode(3)) + self.assertIsNone(lowest_common_ancestor(root, 2, 99)) + self.assertIsNone(lowest_common_ancestor(root, 99, 2)) + if __name__ == "__main__": unittest.main() diff --git a/src/brain_teasers/python/lru_cache/src/lru_cache.py b/src/brain_teasers/python/lru_cache/src/lru_cache.py index 70b819c1..db7fe9e6 100644 --- a/src/brain_teasers/python/lru_cache/src/lru_cache.py +++ b/src/brain_teasers/python/lru_cache/src/lru_cache.py @@ -8,9 +8,7 @@ class ListNode: """A node in a doubly linked list storing key-value pairs.""" - def __init__( - self, key: int, value: int, next: Optional[ListNode] = None - ) -> None: + def __init__(self, key: int, value: int, next: Optional[ListNode] = None) -> None: self.key = key self.value = value self.next = next @@ -24,9 +22,7 @@ def __init__(self) -> None: self.head: Optional[ListNode] = None self.tail: Optional[ListNode] = None - def insert_before( - self, node: Optional[ListNode], key: int, value: int - ) -> ListNode: + def insert_before(self, node: Optional[ListNode], key: int, value: int) -> ListNode: """Insert a new node with the given key-value pair before the given node.""" new_node = ListNode(key, value) if not node: @@ -115,6 +111,8 @@ def put(self, key: int, value: int) -> None: key: The key to store. value: The value to store. """ + if self.capacity <= 0: + return if key in self.cache: node = self.cache[key] self.list.delete(node) diff --git a/src/brain_teasers/python/lru_cache/tests/test_lru_cache.py b/src/brain_teasers/python/lru_cache/tests/test_lru_cache.py index e37f0255..d5eba384 100644 --- a/src/brain_teasers/python/lru_cache/tests/test_lru_cache.py +++ b/src/brain_teasers/python/lru_cache/tests/test_lru_cache.py @@ -23,6 +23,20 @@ def test_2(self): lrucache.put(4, 1) self.assertEqual(lrucache.get(2), -1) + def test_zero_capacity_stores_nothing(self): + cache = LRUCache(0) + cache.put(1, 1) + self.assertEqual(cache.get(1), -1) + cache.put(2, 2) + self.assertEqual(cache.get(2), -1) + + def test_capacity_one(self): + cache = LRUCache(1) + cache.put(1, 1) + cache.put(2, 2) + self.assertEqual(cache.get(1), -1) + self.assertEqual(cache.get(2), 2) + if __name__ == "__main__": unittest.main() diff --git a/src/brain_teasers/python/randomize_array/src/randomize_array.py b/src/brain_teasers/python/randomize_array/src/randomize_array.py index 73b9ab39..7e0e0477 100644 --- a/src/brain_teasers/python/randomize_array/src/randomize_array.py +++ b/src/brain_teasers/python/randomize_array/src/randomize_array.py @@ -3,6 +3,7 @@ from __future__ import annotations import random +from collections import Counter def randomize_array(array: list[int]) -> list[int]: @@ -18,15 +19,30 @@ def randomize_array(array: list[int]) -> list[int]: A new randomized list of integers. """ n = len(array) - # Check if all elements are identical or array is of length 1 - if n == 1 or all(x == array[0] for x in array): - return array - - new_array = array.copy() - for i in range(n): - # Select a random index that's not equal to the current one - swap_candidates = [x for x in range(n) if x != i and new_array[x] != array[i]] - if swap_candidates: - swap_with = random.choice(swap_candidates) - new_array[i], new_array[swap_with] = new_array[swap_with], new_array[i] - return new_array + # Nothing can move if there is at most one element or all are identical + if n <= 1 or all(x == array[0] for x in array): + return array.copy() + + counts = Counter(array) + max_count = max(counts.values()) + + if 2 * max_count <= n: + # A derangement by value exists. Group equal values together (random + # order inside each group) and rotate by a random shift in + # [max_count, n - max_count]; equal values are never mapped together. + order = list(range(n)) + random.shuffle(order) + order.sort(key=lambda idx: array[idx]) + shift = random.randint(max_count, n - max_count) + result = [None] * n + for k in range(n): + result[order[k]] = array[order[(k + shift) % n]] + return result + + # Too many duplicates for a full derangement: use a random cyclic + # permutation (Sattolo), which at least moves every position. + indices = list(range(n)) + for i in range(n - 1, 0, -1): + j = random.randrange(i) + indices[i], indices[j] = indices[j], indices[i] + return [array[indices[i]] for i in range(n)] diff --git a/src/brain_teasers/python/randomize_array/tests/test_randomize_array.py b/src/brain_teasers/python/randomize_array/tests/test_randomize_array.py index e6cb8021..ba58b54b 100644 --- a/src/brain_teasers/python/randomize_array/tests/test_randomize_array.py +++ b/src/brain_teasers/python/randomize_array/tests/test_randomize_array.py @@ -20,6 +20,28 @@ def test_randomize_array_single(self): randomized_array = randomize_array(original_array.copy()) self.assertEqual(original_array, randomized_array) + def test_no_element_stays_in_place(self): + original = [1, 2, 3, 4, 5] + for _ in range(200): + result = randomize_array(original) + self.assertEqual(sorted(result), original) + self.assertTrue(all(a != b for a, b in zip(original, result))) + + def test_duplicates_with_possible_derangement(self): + original = [1, 1, 2, 2, 3, 3] + for _ in range(200): + result = randomize_array(original) + self.assertEqual(sorted(result), original) + self.assertTrue(all(a != b for a, b in zip(original, result))) + + def test_returns_new_list_and_keeps_input(self): + for original in ([], [7], [4, 4, 4]): + snapshot = list(original) + result = randomize_array(original) + self.assertEqual(result, snapshot) + self.assertIsNot(result, original) + self.assertEqual(original, snapshot) + if __name__ == "__main__": unittest.main() diff --git a/src/brain_teasers/python/score_after_flipping_matrix/src/score_after_flipping_matrix.py b/src/brain_teasers/python/score_after_flipping_matrix/src/score_after_flipping_matrix.py index 17f6317f..8ea1afb5 100644 --- a/src/brain_teasers/python/score_after_flipping_matrix/src/score_after_flipping_matrix.py +++ b/src/brain_teasers/python/score_after_flipping_matrix/src/score_after_flipping_matrix.py @@ -62,26 +62,18 @@ def score_after_flipping_matrix(matrix: list[list[int]]) -> int: Returns: The maximum possible score after any number of flips. """ + if not matrix or not matrix[0]: + return 0 + rows = len(matrix) columns = len(matrix[0]) - max_score = matrix_score(matrix) - - for i in range(rows - 1, -1, -1): - for j in range(columns): - # Try toggling the current row - toggle_row(matrix, i) - score = matrix_score(matrix) - if score > max_score: - max_score = score - else: - toggle_row(matrix, i) - - # Try toggling the current column - toggle_column(matrix, j) - score = matrix_score(matrix) - if score > max_score: - max_score = score - else: - toggle_column(matrix, j) - - return max_score + score = 0 + + # Greedy: flip every row so its leading bit is 1, then flip each column + # independently if that gives more ones. Works on a virtual view so the + # input matrix is not mutated. + for j in range(columns): + ones = sum(1 for i in range(rows) if matrix[i][j] == matrix[i][0]) + score += max(ones, rows - ones) * (1 << (columns - j - 1)) + + return score diff --git a/src/brain_teasers/python/score_after_flipping_matrix/tests/test_score_after_flipping_matrix.py b/src/brain_teasers/python/score_after_flipping_matrix/tests/test_score_after_flipping_matrix.py index 9c8ef69b..1e458763 100644 --- a/src/brain_teasers/python/score_after_flipping_matrix/tests/test_score_after_flipping_matrix.py +++ b/src/brain_teasers/python/score_after_flipping_matrix/tests/test_score_after_flipping_matrix.py @@ -22,6 +22,23 @@ def test_3(self): actual = score_after_flipping_matrix(input_matrix) self.assertEqual(actual, expected) + def test_optimal_where_local_search_fails(self): + # Optimal: flip rows to start with 1, then flip columns -> 19 + self.assertEqual( + score_after_flipping_matrix([[0, 0, 0], [1, 1, 1], [1, 0, 1]]), 19 + ) + self.assertEqual( + score_after_flipping_matrix( + [[1, 0, 0], [1, 0, 1], [0, 0, 1], [0, 1, 1], [0, 0, 1]] + ), + 30, + ) + + def test_does_not_mutate_input(self): + matrix = [[0, 0, 1, 1], [1, 0, 1, 0], [1, 1, 0, 0]] + score_after_flipping_matrix(matrix) + self.assertEqual(matrix, [[0, 0, 1, 1], [1, 0, 1, 0], [1, 1, 0, 0]]) + if __name__ == "__main__": unittest.main() diff --git a/src/collections_and_containers/cpp/avl_tree/CMakeLists.txt b/src/collections_and_containers/cpp/avl_tree/CMakeLists.txt index a9d43fd9..1bae2f8e 100644 --- a/src/collections_and_containers/cpp/avl_tree/CMakeLists.txt +++ b/src/collections_and_containers/cpp/avl_tree/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-avl-tree CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/collections_and_containers/cpp/avl_tree/test/test_avl_tree.cpp b/src/collections_and_containers/cpp/avl_tree/test/test_avl_tree.cpp index 5fed3266..ef473291 100644 --- a/src/collections_and_containers/cpp/avl_tree/test/test_avl_tree.cpp +++ b/src/collections_and_containers/cpp/avl_tree/test/test_avl_tree.cpp @@ -1,4 +1,13 @@ +#include +#include +#include +#include +#include +#include +#include +#define private public #include "avl_tree.h" +#undef private #include "gtest/gtest.h" TEST(AvlTreeTest, DefaultConstructor) { @@ -200,3 +209,50 @@ TEST(AvlTreeTest, IsTreeAlwaysBalanced) { EXPECT_EQ(tree.height(), 7); EXPECT_FALSE(tree.empty()); } + +namespace { +// Returns the height of p, or -1 when some node is unbalanced. +template int checkedHeight(const NodePtr &p) { + if (p == nullptr) { + return 0; + } + int l = checkedHeight(p->left); + int r = checkedHeight(p->right); + if (l < 0 || r < 0 || std::abs(l - r) > 1) { + return -1; + } + return 1 + std::max(l, r); +} +} // namespace + +TEST(AvlTreeTest, StaysBalancedAfterRandomInsertsAndRemoves) { + std::mt19937 gen(4242); + for (int round = 0; round < 30; round++) { + AvlTree tree; + std::set reference; + for (int step = 0; step < 300; step++) { + int v = static_cast(gen() % 100); + if (gen() % 2 == 0) { + if (reference.insert(v).second) { + tree.insert(v); + } + } else if (reference.erase(v) > 0) { + tree.remove(v); + } + ASSERT_GE(checkedHeight(tree.root), 0); + ASSERT_EQ(tree.size(), static_cast(reference.size())); + } + EXPECT_EQ(tree.in_order_traversal(), + std::vector(reference.begin(), reference.end())); + tree.clear(); + EXPECT_EQ(tree.size(), 0); + EXPECT_TRUE(tree.empty()); + } +} + +TEST(AvlTreeTest, RemoveMissingValueThrowsAndKeepsSize) { + AvlTree tree; + tree.insert(1); + EXPECT_THROW(tree.remove(2), std::invalid_argument); + EXPECT_EQ(tree.size(), 1); +} diff --git a/src/collections_and_containers/cpp/binary_search_tree/CMakeLists.txt b/src/collections_and_containers/cpp/binary_search_tree/CMakeLists.txt index 4c1b1e7a..552aaeb3 100644 --- a/src/collections_and_containers/cpp/binary_search_tree/CMakeLists.txt +++ b/src/collections_and_containers/cpp/binary_search_tree/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-binary_search_tree CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/collections_and_containers/cpp/binary_search_tree/test/test_binary_search_tree.cpp b/src/collections_and_containers/cpp/binary_search_tree/test/test_binary_search_tree.cpp index ab3f7b3d..4639b816 100644 --- a/src/collections_and_containers/cpp/binary_search_tree/test/test_binary_search_tree.cpp +++ b/src/collections_and_containers/cpp/binary_search_tree/test/test_binary_search_tree.cpp @@ -1,5 +1,9 @@ #include "binary_search_tree.h" #include "gtest/gtest.h" +#include +#include +#include +#include TEST(BinarySearchTreeTest, DefaultConstructor) { BinarySearchTree bst; @@ -174,3 +178,28 @@ TEST(BinarySearchTreeTest, PostOrderTraversal) { EXPECT_EQ(actual, expected); } + +TEST(BinarySearchTreeTest, RandomInsertsAndRemovesMatchStdSet) { + std::mt19937 gen(777); + for (int round = 0; round < 30; round++) { + BinarySearchTree tree; + std::set reference; + for (int step = 0; step < 300; step++) { + int v = static_cast(gen() % 100); + if (gen() % 2 == 0) { + if (reference.insert(v).second) { + tree.insert(v); + } else { + EXPECT_THROW(tree.insert(v), std::invalid_argument); + } + } else if (reference.erase(v) > 0) { + tree.remove(v); + } else { + EXPECT_THROW(tree.remove(v), std::invalid_argument); + } + ASSERT_EQ(tree.size(), static_cast(reference.size())); + ASSERT_EQ(tree.in_order_traversal(), + std::vector(reference.begin(), reference.end())); + } + } +} diff --git a/src/collections_and_containers/cpp/hash_table/CMakeLists.txt b/src/collections_and_containers/cpp/hash_table/CMakeLists.txt index ce6814c1..12bf9e26 100644 --- a/src/collections_and_containers/cpp/hash_table/CMakeLists.txt +++ b/src/collections_and_containers/cpp/hash_table/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-hash-table CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/collections_and_containers/cpp/hash_table/src/hash_table.cpp b/src/collections_and_containers/cpp/hash_table/src/hash_table.cpp index c002d918..d6a58936 100644 --- a/src/collections_and_containers/cpp/hash_table/src/hash_table.cpp +++ b/src/collections_and_containers/cpp/hash_table/src/hash_table.cpp @@ -1,13 +1,41 @@ #include "hash_table.h" #include #include +#include +#include template HashTable::HashTable(std::function hash_function, int size) - : hash_function(hash_function), _num_elements(0), _size(size), - load_factor(0.75f), - table(size) {} + : hash_function(hash_function), _num_elements(0), + _size(size > 0 ? size : 1), load_factor(0.75f), table(_size) {} + +template +HashTable::HashTable(const HashTable &other) + : hash_function(other.hash_function), _num_elements(other._num_elements), + _size(other._size), load_factor(other.load_factor), table(other._size) { + // Buckets are shared_ptrs: copy them so the tables do not alias. + for (int i = 0; i < _size; i++) { + if (other.table[i] != nullptr) { + table[i] = + std::make_shared>>(*other.table[i]); + } + } +} + +template +HashTable & +HashTable::operator=(const HashTable &other) { + if (this != &other) { + HashTable copy(other); + std::swap(hash_function, copy.hash_function); + std::swap(_num_elements, copy._num_elements); + std::swap(_size, copy._size); + std::swap(load_factor, copy.load_factor); + std::swap(table, copy.table); + } + return *this; +} template HashTable::~HashTable() { table.clear(); @@ -16,8 +44,12 @@ template HashTable::~HashTable() { template int HashTable::hash_index(Key key) const { // Handle negative hash values - int hash = hash_function(key); - return std::abs(hash) % _size; + // Widen before abs(): std::abs(INT_MIN) is undefined behaviour. + long long hash = hash_function(key); + if (hash < 0) { + hash = -hash; + } + return static_cast(hash % _size); } template @@ -26,7 +58,7 @@ void HashTable::add(Key key, Value value) { if (table[index] == nullptr) { table[index] = std::make_shared>>(); } - + // Check if key already exists and update it for (auto &pair : *table[index]) { if (pair.first == key) { @@ -34,10 +66,10 @@ void HashTable::add(Key key, Value value) { return; } } - + table[index]->push_back(std::make_pair(key, value)); _num_elements++; - + if (static_cast(_num_elements) / _size > load_factor) { grow(); } @@ -138,10 +170,10 @@ template void HashTable::grow() { int new_size = _size * 2; std::vector>>> new_table( new_size); - + int old_size = _size; _size = new_size; - + for (int i = 0; i < old_size; i++) { if (table[i] != nullptr) { for (const auto &pair : *table[i]) { @@ -162,13 +194,13 @@ template void HashTable::shrink() { if (new_size < 10) { new_size = 10; } - + std::vector>>> new_table( new_size); - + int old_size = _size; _size = new_size; - + for (int i = 0; i < old_size; i++) { if (table[i] != nullptr) { for (const auto &pair : *table[i]) { diff --git a/src/collections_and_containers/cpp/hash_table/src/hash_table.h b/src/collections_and_containers/cpp/hash_table/src/hash_table.h index 420fda16..20296c6f 100644 --- a/src/collections_and_containers/cpp/hash_table/src/hash_table.h +++ b/src/collections_and_containers/cpp/hash_table/src/hash_table.h @@ -9,6 +9,8 @@ template class HashTable { public: HashTable(std::function hash_function, int size = 10); + HashTable(const HashTable &other); + HashTable &operator=(const HashTable &other); ~HashTable(); void add(Key key, Value value); Value get(Key key) const; diff --git a/src/collections_and_containers/cpp/hash_table/test/test_hash_table.cpp b/src/collections_and_containers/cpp/hash_table/test/test_hash_table.cpp index 516280be..008921fb 100644 --- a/src/collections_and_containers/cpp/hash_table/test/test_hash_table.cpp +++ b/src/collections_and_containers/cpp/hash_table/test/test_hash_table.cpp @@ -1,5 +1,6 @@ #include "hash_table.h" #include "gtest/gtest.h" +#include TEST(HashTableTest, SimpleConstructor) { // int key @@ -258,3 +259,53 @@ TEST(HashTableTest, Clear) { hash_table2.clear(); EXPECT_EQ(hash_table2.size(), 0); } + +TEST(HashTableTest, CopyIsDeep) { + HashTable a([](int key) { return key; }); + for (int i = 0; i < 30; i++) { + a.add(i, i * 2); + } + HashTable b(a); + a.add(5, 1000); + a.remove(6); + a.add(1000, 1); + EXPECT_EQ(b.get(5), 10); + EXPECT_TRUE(b.contains(6)); + EXPECT_FALSE(b.contains(1000)); + EXPECT_EQ(b.size(), 30u); + + HashTable c([](int key) { return key; }); + c.add(1, 1); + c = a; + a.clear(); + EXPECT_EQ(c.get(5), 1000); + EXPECT_EQ(c.size(), 30u); + EXPECT_FALSE(c.contains(6)); +} + +TEST(HashTableTest, ManyInsertsAndRemovesAcrossGrowAndShrink) { + HashTable table([](int key) { return key * 7 - 100; }); + for (int i = 0; i < 1000; i++) { + table.add(i, i); + } + EXPECT_EQ(table.size(), 1000u); + for (int i = 0; i < 1000; i++) { + ASSERT_EQ(table.get(i), i); + } + for (int i = 0; i < 990; i++) { + table.remove(i); + } + EXPECT_EQ(table.size(), 10u); + for (int i = 990; i < 1000; i++) { + ASSERT_EQ(table[i], i); + } + for (int i = 0; i < 990; i++) { + ASSERT_FALSE(table.contains(i)); + } +} + +TEST(HashTableTest, ExtremeNegativeHash) { + HashTable table([](int) { return INT_MIN; }); + table.add(1, 5); + EXPECT_EQ(table.get(1), 5); +} diff --git a/src/collections_and_containers/cpp/heap/CMakeLists.txt b/src/collections_and_containers/cpp/heap/CMakeLists.txt index 18de8777..324b429e 100644 --- a/src/collections_and_containers/cpp/heap/CMakeLists.txt +++ b/src/collections_and_containers/cpp/heap/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-heap CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/collections_and_containers/cpp/heap/test/test_heap.cpp b/src/collections_and_containers/cpp/heap/test/test_heap.cpp index fdce5495..4a1a2c64 100644 --- a/src/collections_and_containers/cpp/heap/test/test_heap.cpp +++ b/src/collections_and_containers/cpp/heap/test/test_heap.cpp @@ -1,5 +1,9 @@ #include "heap.h" #include "gtest/gtest.h" +#include +#include +#include +#include TEST(MaxHeapTest, EmptyHeap) { MaxHeap heap; @@ -186,3 +190,40 @@ TEST(MinHeapTest, IsMinHeap) { ASSERT_TRUE(heap.isMinHeap()); } + +TEST(HeapTest, RandomPushPopMatchesSortedOrder) { + std::mt19937 gen(99); + MinHeap min_heap; + MaxHeap max_heap; + std::vector values; + for (int i = 0; i < 500; i++) { + int v = static_cast(gen() % 100) - 50; + values.push_back(v); + min_heap.push(v); + max_heap.push(v); + } + std::sort(values.begin(), values.end()); + for (size_t i = 0; i < values.size(); i++) { + EXPECT_EQ(min_heap.pop(), values[i]); + EXPECT_EQ(max_heap.pop(), values[values.size() - 1 - i]); + } + EXPECT_TRUE(min_heap.empty()); + EXPECT_THROW(min_heap.pop(), std::out_of_range); + EXPECT_THROW(max_heap.peek(), std::out_of_range); +} + +TEST(HeapTest, CopyIsIndependentAndClearWorks) { + MinHeap a; + a.push(3); + a.push(1); + a.push(2); + MinHeap b = a; + a.pop(); + a.clear(); + EXPECT_TRUE(a.empty()); + EXPECT_EQ(b.size(), 3u); + EXPECT_EQ(b.peek(), 1); + a = b; + EXPECT_EQ(a.pop(), 1); + EXPECT_EQ(b.size(), 3u); +} diff --git a/src/collections_and_containers/cpp/linked_list/CMakeLists.txt b/src/collections_and_containers/cpp/linked_list/CMakeLists.txt index a13bc7db..ee556499 100644 --- a/src/collections_and_containers/cpp/linked_list/CMakeLists.txt +++ b/src/collections_and_containers/cpp/linked_list/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-linked-list CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/collections_and_containers/cpp/linked_list/src/linked_list.cpp b/src/collections_and_containers/cpp/linked_list/src/linked_list.cpp index fb4f9c11..59b3b5d9 100644 --- a/src/collections_and_containers/cpp/linked_list/src/linked_list.cpp +++ b/src/collections_and_containers/cpp/linked_list/src/linked_list.cpp @@ -32,7 +32,7 @@ template LinkedList::~LinkedList() { clear(); } template void LinkedList::push_back(const T &v) { auto newNode = std::make_unique(); newNode->data = v; - + if (empty()) { tail = newNode.get(); head = std::move(newNode); @@ -46,7 +46,7 @@ template void LinkedList::push_back(const T &v) { template void LinkedList::push_front(const T &v) { auto newNode = std::make_unique(); newNode->data = v; - + if (empty()) { tail = newNode.get(); head = std::move(newNode); @@ -119,7 +119,11 @@ template const T &LinkedList::back() const { template bool LinkedList::empty() const { return n == 0; } template void LinkedList::clear() { - head = nullptr; + // Unlink iteratively: destroying a long unique_ptr chain recursively + // would overflow the stack. + while (head != nullptr) { + head = std::move(head->next); + } tail = nullptr; n = 0; } @@ -134,16 +138,16 @@ template void LinkedList::remove(const T &v) { pop_front(); return; } - + Node *p = head.get(); while (p->next != nullptr && p->next->data != v) { p = p->next.get(); } - + if (p->next == nullptr) { throw std::out_of_range("remove() called on non-existent value"); } - + if (p->next.get() == tail) { tail = p; p->next = nullptr; diff --git a/src/collections_and_containers/cpp/linked_list/test/test_linked_list.cpp b/src/collections_and_containers/cpp/linked_list/test/test_linked_list.cpp index af6f5ba1..dd24025f 100644 --- a/src/collections_and_containers/cpp/linked_list/test/test_linked_list.cpp +++ b/src/collections_and_containers/cpp/linked_list/test/test_linked_list.cpp @@ -94,3 +94,58 @@ TEST(LinkedListTest, Remove) { EXPECT_EQ(linked_list.front(), 3); EXPECT_EQ(linked_list.back(), 3); } + +TEST(LinkedListTest, ClearAndDestroyVeryLongListDoesNotOverflowStack) { + { + LinkedList linked_list; + for (int i = 0; i < 1000000; i++) { + linked_list.push_back(i); + } + EXPECT_EQ(linked_list.size(), 1000000); + linked_list.clear(); + EXPECT_TRUE(linked_list.empty()); + for (int i = 0; i < 1000000; i++) { + linked_list.push_front(i); + } + // Destructor releases the long chain here. + } +} + +TEST(LinkedListTest, PushBackAfterClearAndPopAll) { + LinkedList l; + l.push_back(1); + l.push_back(2); + l.pop_back(); + l.pop_back(); + EXPECT_TRUE(l.empty()); + l.push_back(3); + EXPECT_EQ(l.front(), 3); + EXPECT_EQ(l.back(), 3); + l.clear(); + l.push_back(4); + l.push_back(5); + EXPECT_EQ(l.back(), 5); + l.remove(5); + EXPECT_EQ(l.back(), 4); + l.push_back(6); + EXPECT_EQ(l.back(), 6); + EXPECT_EQ(l.size(), 2); +} + +TEST(LinkedListTest, CopyIsDeep) { + LinkedList a; + a.push_back(1); + a.push_back(2); + LinkedList b(a); + a.pop_front(); + a.push_back(9); + EXPECT_EQ(b.size(), 2); + EXPECT_EQ(b.front(), 1); + EXPECT_EQ(b.back(), 2); + LinkedList c; + c.push_back(7); + c = b; + b.clear(); + EXPECT_EQ(c.size(), 2); + EXPECT_EQ(c.back(), 2); +} diff --git a/src/collections_and_containers/cpp/queue/CMakeLists.txt b/src/collections_and_containers/cpp/queue/CMakeLists.txt index 053359d3..d3d5737e 100644 --- a/src/collections_and_containers/cpp/queue/CMakeLists.txt +++ b/src/collections_and_containers/cpp/queue/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-stack CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/collections_and_containers/cpp/queue/test/test_queue.cpp b/src/collections_and_containers/cpp/queue/test/test_queue.cpp index a249407e..bba5d3b7 100644 --- a/src/collections_and_containers/cpp/queue/test/test_queue.cpp +++ b/src/collections_and_containers/cpp/queue/test/test_queue.cpp @@ -75,3 +75,46 @@ TEST(QueueTest, AssignmentOperator) { EXPECT_EQ(queue2.dequeue(), 2); EXPECT_EQ(queue2.dequeue(), 3); } + +TEST(QueueTest, WrapAroundAndExpandKeepOrder) { + Queue queue; + int next_in = 0, next_out = 0; + // Keep head away from 0 so the buffer wraps before it expands. + for (int i = 0; i < 10; i++) + queue.enqueue(next_in++); + for (int i = 0; i < 10; i++) + EXPECT_EQ(queue.dequeue(), next_out++); + for (int i = 0; i < 12; i++) + queue.enqueue(next_in++); + for (int i = 0; i < 5; i++) + EXPECT_EQ(queue.dequeue(), next_out++); + for (int i = 0; i < 100; i++) { + queue.enqueue(next_in++); + EXPECT_EQ(queue.back(), next_in - 1); + } + EXPECT_EQ(queue.front(), next_out); + EXPECT_EQ(queue.size(), next_in - next_out); + while (!queue.isEmpty()) { + EXPECT_EQ(queue.dequeue(), next_out++); + } + EXPECT_EQ(next_out, next_in); + EXPECT_THROW(queue.dequeue(), std::out_of_range); +} + +TEST(QueueTest, CopyIsIndependentAndSelfAssignmentSafe) { + Queue a; + for (int i = 0; i < 20; i++) + a.enqueue(i); + for (int i = 0; i < 7; i++) + a.dequeue(); + Queue b(a); + a.clear(); + a.enqueue(99); + EXPECT_EQ(b.size(), 13); + EXPECT_EQ(b.front(), 7); + EXPECT_EQ(b.back(), 19); + b = b; + EXPECT_EQ(b.size(), 13); + EXPECT_THROW(Queue().front(), std::out_of_range); + EXPECT_THROW(Queue().back(), std::out_of_range); +} diff --git a/src/collections_and_containers/cpp/red_black_tree/CMakeLists.txt b/src/collections_and_containers/cpp/red_black_tree/CMakeLists.txt index d077fe38..d3cbc763 100644 --- a/src/collections_and_containers/cpp/red_black_tree/CMakeLists.txt +++ b/src/collections_and_containers/cpp/red_black_tree/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-red-black-tree CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/collections_and_containers/cpp/red_black_tree/src/red_black_tree.cpp b/src/collections_and_containers/cpp/red_black_tree/src/red_black_tree.cpp index 0e91ef2d..81731e65 100644 --- a/src/collections_and_containers/cpp/red_black_tree/src/red_black_tree.cpp +++ b/src/collections_and_containers/cpp/red_black_tree/src/red_black_tree.cpp @@ -112,39 +112,112 @@ void RedBlackTree::insert(const T &v, std::unique_ptr &p) { } template -void RedBlackTree::remove(const T &v, std::unique_ptr &p) { +bool RedBlackTree::remove(const T &v, std::unique_ptr &p) { if (p == nullptr) { - return; + return false; } - + if (v < p->data) { - remove(v, p->left); - } else if (v > p->data) { - remove(v, p->right); + if (remove(v, p->left)) { + return fix_left_deficit(p); + } + return false; + } + if (v > p->data) { + if (remove(v, p->right)) { + return fix_right_deficit(p); + } + return false; + } + + if (p->left != nullptr && p->right != nullptr) { + // Replace by the in-order successor and remove that node instead. + Node *successor = p->right.get(); + while (successor->left != nullptr) { + successor = successor->left.get(); + } + p->data = successor->data; + if (remove(successor->data, p->right)) { + return fix_right_deficit(p); + } + return false; + } + + // At most one child. A red node has none; a black node's only child is red. + bool was_black = !p->is_red; + if (p->left != nullptr || p->right != nullptr) { + std::unique_ptr child = + std::move(p->left != nullptr ? p->left : p->right); + child->is_red = false; + p = std::move(child); + n--; + return false; + } + p = nullptr; + n--; + return was_black; +} + +// The right subtree of p is one black node shorter than the left one. +template +bool RedBlackTree::fix_right_deficit(std::unique_ptr &p) { + Node *s = p->left.get(); + if (s->is_red) { + rotate_right(p); + p->is_red = false; + p->right->is_red = true; + fix_right_deficit(p->right); + return false; + } + bool parent_red = p->is_red; + if (s->left != nullptr && s->left->is_red) { + rotate_right(p); + } else if (s->right != nullptr && s->right->is_red) { + rotate_left(p->left); + rotate_right(p); } else { - if (p->left == nullptr && p->right == nullptr) { - p = nullptr; - n--; - } else if (p->left == nullptr) { - p = std::move(p->right); - n--; - } else if (p->right == nullptr) { - p = std::move(p->left); - n--; - } else { - // Find in-order successor - Node *successor = p->right.get(); - while (successor->left != nullptr) { - successor = successor->left.get(); - } - p->data = successor->data; - remove(successor->data, p->right); + s->is_red = true; + if (parent_red) { + p->is_red = false; + return false; } + return true; } - - if (p != nullptr) { - balance(p); + p->is_red = parent_red; + p->left->is_red = false; + p->right->is_red = false; + return false; +} + +// The left subtree of p is one black node shorter than the right one. +template +bool RedBlackTree::fix_left_deficit(std::unique_ptr &p) { + Node *s = p->right.get(); + if (s->is_red) { + rotate_left(p); + p->is_red = false; + p->left->is_red = true; + fix_left_deficit(p->left); + return false; + } + bool parent_red = p->is_red; + if (s->right != nullptr && s->right->is_red) { + rotate_left(p); + } else if (s->left != nullptr && s->left->is_red) { + rotate_right(p->right); + rotate_left(p); + } else { + s->is_red = true; + if (parent_red) { + p->is_red = false; + return false; + } + return true; } + p->is_red = parent_red; + p->left->is_red = false; + p->right->is_red = false; + return false; } template diff --git a/src/collections_and_containers/cpp/red_black_tree/src/red_black_tree.h b/src/collections_and_containers/cpp/red_black_tree/src/red_black_tree.h index 57d7d0c6..642468eb 100644 --- a/src/collections_and_containers/cpp/red_black_tree/src/red_black_tree.h +++ b/src/collections_and_containers/cpp/red_black_tree/src/red_black_tree.h @@ -37,7 +37,10 @@ template class RedBlackTree { int n; void insert(const T &v, std::unique_ptr &p); - void remove(const T &v, std::unique_ptr &p); + // Returns true when the black height of the subtree rooted at p shrank. + bool remove(const T &v, std::unique_ptr &p); + bool fix_left_deficit(std::unique_ptr &p); + bool fix_right_deficit(std::unique_ptr &p); bool contains(const T &v, const std::unique_ptr &p) const; void clear(std::unique_ptr &p); int height(const std::unique_ptr &p) const; diff --git a/src/collections_and_containers/cpp/red_black_tree/test/test_red_black_tree.cpp b/src/collections_and_containers/cpp/red_black_tree/test/test_red_black_tree.cpp index cd097bbe..5dcd10af 100644 --- a/src/collections_and_containers/cpp/red_black_tree/test/test_red_black_tree.cpp +++ b/src/collections_and_containers/cpp/red_black_tree/test/test_red_black_tree.cpp @@ -1,6 +1,40 @@ +#include +#include +#include +#include +#include +#include +// Tests inspect node colours, which are private. +#define private public #include "red_black_tree.h" +#undef private #include "gtest/gtest.h" +namespace { +// Returns the black height of p, or -1 when a red-black rule is violated. +template int blackHeight(const NodePtr &p, bool parentRed) { + if (p == nullptr) { + return 1; + } + if (p->is_red && parentRed) { + return -1; + } + int l = blackHeight(p->left, p->is_red); + int r = blackHeight(p->right, p->is_red); + if (l < 0 || r < 0 || l != r) { + return -1; + } + return l + (p->is_red ? 0 : 1); +} + +template bool isValidRedBlack(const Tree &t) { + if (t.root != nullptr && t.root->is_red) { + return false; + } + return blackHeight(t.root, false) > 0; +} +} // namespace + TEST(RedBlackTreeTest, DefaultConstructor) { RedBlackTree tree; EXPECT_EQ(tree.size(), 0); @@ -228,3 +262,54 @@ TEST(RedBlackTreeTest, IsTreeAlwaysBalanced) { EXPECT_EQ(tree.height(), 9); EXPECT_FALSE(tree.empty()); } + +TEST(RedBlackTreeTest, InvariantsAfterSequentialInsertAndRemove) { + RedBlackTree tree; + for (int i = 0; i < 200; i++) { + tree.insert(i); + ASSERT_TRUE(isValidRedBlack(tree)) << "after insert " << i; + } + for (int i = 0; i < 200; i += 2) { + tree.remove(i); + ASSERT_TRUE(isValidRedBlack(tree)) << "after remove " << i; + } + EXPECT_EQ(tree.size(), 100); + for (int i = 199; i >= 0; i--) { + if (i % 2 == 1) { + tree.remove(i); + ASSERT_TRUE(isValidRedBlack(tree)) << "after remove " << i; + } + } + EXPECT_TRUE(tree.empty()); +} + +TEST(RedBlackTreeTest, InvariantsAfterRandomInsertsAndRemoves) { + std::mt19937 gen(12345); + for (int round = 0; round < 30; round++) { + RedBlackTree tree; + std::set reference; + for (int step = 0; step < 300; step++) { + int v = static_cast(gen() % 100); + if (gen() % 3 != 0) { + if (reference.insert(v).second) { + tree.insert(v); + } + } else if (reference.erase(v) > 0) { + tree.remove(v); + } + ASSERT_TRUE(isValidRedBlack(tree)); + ASSERT_EQ(tree.size(), static_cast(reference.size())); + } + EXPECT_EQ(tree.in_order_traversal(), + std::vector(reference.begin(), reference.end())); + } +} + +TEST(RedBlackTreeTest, RemoveMissingValueLeavesTreeUntouched) { + RedBlackTree tree; + tree.insert(1); + tree.insert(2); + tree.remove(42); + EXPECT_EQ(tree.size(), 2); + EXPECT_TRUE(isValidRedBlack(tree)); +} diff --git a/src/collections_and_containers/cpp/stack/CMakeLists.txt b/src/collections_and_containers/cpp/stack/CMakeLists.txt index c635d9eb..08002647 100644 --- a/src/collections_and_containers/cpp/stack/CMakeLists.txt +++ b/src/collections_and_containers/cpp/stack/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(project-stack CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/collections_and_containers/cpp/stack/test/test_stack.cpp b/src/collections_and_containers/cpp/stack/test/test_stack.cpp index cbcd6fb6..8c1bf9a2 100644 --- a/src/collections_and_containers/cpp/stack/test/test_stack.cpp +++ b/src/collections_and_containers/cpp/stack/test/test_stack.cpp @@ -72,3 +72,30 @@ TEST(StackTest, AssignmentOperator) { EXPECT_EQ(stack2.pop(), 2); EXPECT_EQ(stack2.pop(), 1); } + +TEST(StackTest, GrowsPastInitialCapacityAndKeepsOrder) { + Stack stack; + for (int i = 0; i < 1000; i++) + stack.push(i); + EXPECT_EQ(stack.size(), 1000); + EXPECT_EQ(stack.top(), 999); + for (int i = 999; i >= 0; i--) + EXPECT_EQ(stack.pop(), i); + EXPECT_TRUE(stack.isEmpty()); + EXPECT_THROW(stack.top(), std::out_of_range); +} + +TEST(StackTest, CopyIsIndependentAndSelfAssignmentSafe) { + Stack a; + for (int i = 0; i < 40; i++) + a.push(i); + Stack b(a); + a.pop(); + a.clear(); + a.push(5); + EXPECT_EQ(b.size(), 40); + EXPECT_EQ(b.top(), 39); + b = b; + EXPECT_EQ(b.size(), 40); + EXPECT_EQ(b.top(), 39); +} diff --git a/src/collections_and_containers/cpp/vector/CMakeLists.txt b/src/collections_and_containers/cpp/vector/CMakeLists.txt index b20f3cba..c0f268c2 100644 --- a/src/collections_and_containers/cpp/vector/CMakeLists.txt +++ b/src/collections_and_containers/cpp/vector/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-vector CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/collections_and_containers/cpp/vector/src/vector.cpp b/src/collections_and_containers/cpp/vector/src/vector.cpp index 1fa1dad1..00477d4c 100644 --- a/src/collections_and_containers/cpp/vector/src/vector.cpp +++ b/src/collections_and_containers/cpp/vector/src/vector.cpp @@ -43,8 +43,7 @@ template typename Vector::iterator Vector::end() { return data + n; } -template -typename Vector::const_iterator Vector::begin() const { +template typename Vector::const_iterator Vector::begin() const { return data; } @@ -78,8 +77,12 @@ template const T &Vector::back() const { template void Vector::push_back(const T &v) { if (n >= _capacity) { - // Double capacity (or start with 1 if empty) + // v may refer to an element of this vector, which reserve() invalidates, + // so copy it first. Double capacity (or start with 1 if empty). + const T copy = v; reserve(_capacity == 0 ? 1 : _capacity * 2); + data[n++] = copy; + return; } data[n++] = v; } @@ -115,6 +118,10 @@ template void Vector::resize(unsigned int newSize) { if (static_cast(newSize) > _capacity) { reserve(newSize); } + // Elements exposed by growing are value-initialised, not stale/garbage. + for (int i = n; i < static_cast(newSize); i++) { + data[i] = T(); + } n = static_cast(newSize); } @@ -142,9 +149,7 @@ template const T &Vector::at(unsigned int index) const { return data[index]; } -template void Vector::clear() { - n = 0; -} +template void Vector::clear() { n = 0; } template class Vector; template class Vector; diff --git a/src/collections_and_containers/cpp/vector/test/test_vector.cpp b/src/collections_and_containers/cpp/vector/test/test_vector.cpp index 2f8fcb38..34089c3b 100644 --- a/src/collections_and_containers/cpp/vector/test/test_vector.cpp +++ b/src/collections_and_containers/cpp/vector/test/test_vector.cpp @@ -114,4 +114,61 @@ TEST(VectorTest, AcessingIllegalIndex) { ASSERT_THROW(vector[3], std::out_of_range); ASSERT_THROW(vector[-1], std::out_of_range); -} \ No newline at end of file +} +TEST(VectorTest, ResizeGrowValueInitialisesNewElements) { + Vector vector; + for (int i = 1; i <= 4; i++) { + vector.push_back(i); + } + vector.resize(1); + vector.resize(4); + EXPECT_EQ(vector[0], 1); + EXPECT_EQ(vector[1], 0); + EXPECT_EQ(vector[2], 0); + EXPECT_EQ(vector[3], 0); + + Vector fresh; + fresh.resize(3); + EXPECT_EQ(fresh.size(), 3); + EXPECT_EQ(fresh[2], 0); +} + +TEST(VectorTest, PushBackOfOwnElementAcrossReallocation) { + Vector vector; + vector.push_back(7); + for (int i = 0; i < 20; i++) { + vector.push_back(vector[0]); + } + EXPECT_EQ(vector.size(), 21); + for (int i = 0; i < vector.size(); i++) { + EXPECT_EQ(vector[i], 7); + } +} + +TEST(VectorTest, CopyIsDeepAndAssignable) { + Vector a; + for (int i = 0; i < 5; i++) { + a.push_back(i); + } + Vector b(a); + Vector c; + c.push_back(99); + c = a; + a[0] = 100; + EXPECT_EQ(b[0], 0); + EXPECT_EQ(c[0], 0); + EXPECT_EQ(c.size(), 5); + a = a; + EXPECT_EQ(a.size(), 5); +} + +TEST(VectorTest, ReserveKeepsElementsAndNeverShrinks) { + Vector vector; + vector.push_back(1); + vector.push_back(2); + vector.reserve(50); + EXPECT_EQ(vector.capacity(), 50); + vector.reserve(1); + EXPECT_EQ(vector.capacity(), 50); + EXPECT_EQ(vector[1], 2); +} diff --git a/src/collections_and_containers/python/avl_tree/src/avl_tree.py b/src/collections_and_containers/python/avl_tree/src/avl_tree.py index e376a000..2b69e2fe 100644 --- a/src/collections_and_containers/python/avl_tree/src/avl_tree.py +++ b/src/collections_and_containers/python/avl_tree/src/avl_tree.py @@ -24,6 +24,7 @@ def __init__(self) -> None: def insert(self, value: Any) -> None: """Insert a value into the tree.""" + def insert_helper(p: Optional[Node]) -> Node: if not p: return Node(value) @@ -43,11 +44,12 @@ def insert_helper(p: Optional[Node]) -> Node: def remove(self, value: Any) -> None: """Remove a value from the tree.""" - def remove_helper(p: Optional[Node]) -> Optional[Node]: + + def remove_helper(p: Optional[Node], target: Any) -> Optional[Node]: if not p: return None - if value == p.value: + if target == p.value: if not p.left: return p.right if not p.right: @@ -55,22 +57,25 @@ def remove_helper(p: Optional[Node]) -> Optional[Node]: min_value = self.find_min(p.right) p.value = min_value - p.right = remove_helper(p.right) - elif value < p.value: - p.left = remove_helper(p.left) + p.right = remove_helper(p.right, min_value) + elif target < p.value: + p.left = remove_helper(p.left, target) else: - p.right = remove_helper(p.right) + p.right = remove_helper(p.right, target) right_height = self._height(p.right) if p.right else 0 left_height = self._height(p.left) if p.left else 0 p.balance_factor = right_height - left_height return self.balance(p) - self.root = remove_helper(self.root) + if not self.contains(value): + return + self.root = remove_helper(self.root, value) self.n -= 1 def contains(self, value: Any) -> bool: """Check if the tree contains a value.""" + def contains_helper(p: Optional[Node]) -> bool: if not p: return False @@ -107,6 +112,7 @@ def empty(self) -> bool: def in_order_traversal(self) -> list[Any]: """Return elements in in-order traversal (left, root, right).""" + def in_order_helper(p: Optional[Node], v: list[Any]) -> None: if p: in_order_helper(p.left, v) @@ -119,6 +125,7 @@ def in_order_helper(p: Optional[Node], v: list[Any]) -> None: def pre_order_traversal(self) -> list[Any]: """Return elements in pre-order traversal (root, left, right).""" + def pre_order_helper(p: Optional[Node], v: list[Any]) -> None: if p: v.append(p.value) @@ -131,6 +138,7 @@ def pre_order_helper(p: Optional[Node], v: list[Any]) -> None: def post_order_traversal(self) -> list[Any]: """Return elements in post-order traversal (left, right, root).""" + def post_order_helper(p: Optional[Node], v: list[Any]) -> None: if p: post_order_helper(p.left, v) @@ -165,6 +173,8 @@ def rotate_left(self, p: Node) -> Node: q = p.right p.right = q.left q.left = p + self._update_balance_factor(p) + self._update_balance_factor(q) return q def rotate_right(self, p: Node) -> Node: @@ -172,8 +182,13 @@ def rotate_right(self, p: Node) -> Node: q = p.left p.left = q.right q.right = p + self._update_balance_factor(p) + self._update_balance_factor(q) return q + def _update_balance_factor(self, p: Node) -> None: + p.balance_factor = self._height(p.right) - self._height(p.left) + def find_min(self, p: Node) -> Any: """Find the minimum value in the subtree rooted at p.""" while p.left: diff --git a/src/collections_and_containers/python/avl_tree/test/test_avl_tree.py b/src/collections_and_containers/python/avl_tree/test/test_avl_tree.py index b0e0363f..837d617e 100644 --- a/src/collections_and_containers/python/avl_tree/test/test_avl_tree.py +++ b/src/collections_and_containers/python/avl_tree/test/test_avl_tree.py @@ -1,3 +1,4 @@ +import random from src.avl_tree import AvlTree from unittest import TestCase import unittest @@ -125,5 +126,60 @@ def test_is_tree_always_balanced(self): self.assertFalse(tree.empty()) +class TestAvlTreeInvariants(TestCase): + def _check(self, node): + """Return the height of node, asserting AVL and cached balance factors.""" + if node is None: + return 0 + left = self._check(node.left) + right = self._check(node.right) + self.assertLessEqual(abs(left - right), 1) + return 1 + max(left, right) + + def test_remove_two_children_removes_exactly_one_value(self): + tree = AvlTree() + for v in [5, 3, 8, 1, 4, 7, 9]: + tree.insert(v) + tree.remove(5) + self.assertEqual(tree.in_order_traversal(), [1, 3, 4, 7, 8, 9]) + self.assertEqual(tree.size(), 6) + self.assertFalse(tree.contains(5)) + + def test_remove_missing_value_is_noop(self): + tree = AvlTree() + tree.insert(1) + tree.remove(2) + self.assertEqual(tree.size(), 1) + self.assertEqual(tree.in_order_traversal(), [1]) + + def test_balanced_after_sequential_operations(self): + tree = AvlTree() + for i in range(200): + tree.insert(i) + self._check(tree.root) + for i in range(0, 200, 3): + tree.remove(i) + self._check(tree.root) + self.assertEqual(tree.in_order_traversal(), [i for i in range(200) if i % 3]) + + def test_random_operations_stay_balanced(self): + rng = random.Random(7) + for _ in range(20): + tree = AvlTree() + ref = set() + for _ in range(200): + v = rng.randrange(60) + if rng.random() < 0.5: + if v not in ref: + tree.insert(v) + ref.add(v) + else: + tree.remove(v) + ref.discard(v) + self._check(tree.root) + self.assertEqual(tree.size(), len(ref)) + self.assertEqual(tree.in_order_traversal(), sorted(ref)) + + if __name__ == "__main__": unittest.main() diff --git a/src/collections_and_containers/python/binary_search_tree/src/binary_search_tree.py b/src/collections_and_containers/python/binary_search_tree/src/binary_search_tree.py index 7bc65494..62f438b3 100644 --- a/src/collections_and_containers/python/binary_search_tree/src/binary_search_tree.py +++ b/src/collections_and_containers/python/binary_search_tree/src/binary_search_tree.py @@ -43,10 +43,9 @@ def _insert(self, v: Any, p: Node) -> None: def remove(self, v: Any) -> None: """Remove a value from the tree.""" - if self.root.data == v and self.root.left is None and self.root.right is None: - self.root = None - else: - self._remove(v, self.root) + if not self.contains(v): + return + self.root = self._remove(v, self.root) self.n -= 1 def _remove(self, v: Any, p: Optional[Node]) -> Optional[Node]: diff --git a/src/collections_and_containers/python/binary_search_tree/test/test_binary_search_tree.py b/src/collections_and_containers/python/binary_search_tree/test/test_binary_search_tree.py index bb3ca8b6..167ae939 100644 --- a/src/collections_and_containers/python/binary_search_tree/test/test_binary_search_tree.py +++ b/src/collections_and_containers/python/binary_search_tree/test/test_binary_search_tree.py @@ -1,3 +1,4 @@ +import random from src.binary_search_tree import BinarySearchTree from unittest import TestCase import unittest @@ -189,5 +190,52 @@ def test_post_order_traversal(self): self.assertEqual(actual, expected) +class TestBinarySearchTreeRemoval(TestCase): + def test_remove_root_with_only_right_child(self): + bst = BinarySearchTree() + bst.insert(1) + bst.insert(2) + bst.insert(3) + bst.remove(1) + self.assertEqual(bst.in_order_traversal(), [2, 3]) + self.assertFalse(bst.contains(1)) + self.assertEqual(bst.size(), 2) + + def test_remove_root_with_only_left_child(self): + bst = BinarySearchTree() + bst.insert(3) + bst.insert(2) + bst.insert(1) + bst.remove(3) + self.assertEqual(bst.in_order_traversal(), [1, 2]) + self.assertEqual(bst.size(), 2) + + def test_remove_from_empty_or_missing_value_is_noop(self): + bst = BinarySearchTree() + bst.remove(1) + self.assertEqual(bst.size(), 0) + bst.insert(5) + bst.remove(7) + self.assertEqual(bst.size(), 1) + self.assertEqual(bst.in_order_traversal(), [5]) + + def test_random_operations_match_set(self): + rng = random.Random(1) + for _ in range(30): + bst = BinarySearchTree() + ref = set() + for _ in range(200): + v = rng.randrange(60) + if rng.random() < 0.5: + if v not in ref: + bst.insert(v) + ref.add(v) + else: + bst.remove(v) + ref.discard(v) + self.assertEqual(bst.in_order_traversal(), sorted(ref)) + self.assertEqual(bst.size(), len(ref)) + + if __name__ == "__main__": unittest.main() diff --git a/src/collections_and_containers/python/hash_table/src/hash_table.py b/src/collections_and_containers/python/hash_table/src/hash_table.py index c6f63a7e..960e13af 100644 --- a/src/collections_and_containers/python/hash_table/src/hash_table.py +++ b/src/collections_and_containers/python/hash_table/src/hash_table.py @@ -1,105 +1,111 @@ -"""Hash table implementation with separate chaining.""" - -from __future__ import annotations - -from typing import Any, Callable - - -class HashTable: - """A hash table using separate chaining for collision resolution.""" - - def __init__(self, hash_function: Callable[[Any], int], size: int = 10) -> None: - self.hash_function = hash_function - self._num_elements = 0 - self._size = size - self.load_factor = 0.7 - self.table: list[list[tuple[Any, Any]] | None] = [None] * size - - def add(self, key: Any, value: Any) -> None: - """Add or update a key-value pair.""" - index = self.hash_function(key) % self._size - if self.table[index] is None: - self.table[index] = [] - # Handle overwriting existing keys - for i, (k, _) in enumerate(self.table[index]): - if k == key: - self.table[index][i] = (key, value) - return - self.table[index].append((key, value)) - self._num_elements += 1 - if self._num_elements / self._size > self.load_factor: - self.grow() - - def get(self, key: Any) -> Any: - """Get the value for a key.""" - index = self.hash_function(key) % self._size - if self.table[index] is None: - raise KeyError("Key not found") - for k, v in self.table[index]: - if k == key: - return v - return None - - def remove(self, key: Any) -> None: - """Remove a key-value pair.""" - index = self.hash_function(key) % self._size - if self.table[index] is None: - raise KeyError("Key not found") - for i, (k, _) in enumerate(self.table[index]): - if k == key: - del self.table[index][i] - self._num_elements -= 1 - break - if self._num_elements / self._size < (1 - self.load_factor) / 2: - self.shrink() - - def size(self) -> int: - """Return the number of elements in the hash table.""" - return self._num_elements - - def clear(self) -> None: - """Remove all elements from the hash table.""" - self.table = [None] * self._size - self._num_elements = 0 - - def contains(self, key: Any) -> bool: - """Check if the hash table contains a key.""" - try: - self.get(key) - return True - except KeyError: - return False - - def __getitem__(self, key: Any) -> Any: - return self.get(key) - - def __setitem__(self, key: Any, value: Any) -> None: - self.add(key, value) - - def grow(self) -> None: - """Double the capacity of the hash table.""" - self._size = self._size * 2 + 1 - new_table: list[list[tuple[Any, Any]] | None] = [None] * self._size - for bucket in self.table: - if bucket is None: - continue - for key, value in bucket: - index = self.hash_function(key) % self._size - if new_table[index] is None: - new_table[index] = [] - new_table[index].append((key, value)) - self.table = new_table - - def shrink(self) -> None: - """Halve the capacity of the hash table.""" - self._size = (self._size - 1) // 2 - new_table: list[list[tuple[Any, Any]] | None] = [None] * self._size - for bucket in self.table: - if bucket is None: - continue - for key, value in bucket: - index = self.hash_function(key) % self._size - if new_table[index] is None: - new_table[index] = [] - new_table[index].append((key, value)) - self.table = new_table +"""Hash table implementation with separate chaining.""" + +from __future__ import annotations + +from typing import Any, Callable + + +class HashTable: + """A hash table using separate chaining for collision resolution.""" + + def __init__(self, hash_function: Callable[[Any], int], size: int = 10) -> None: + self.hash_function = hash_function + self._num_elements = 0 + self._size = max(1, size) + self.load_factor = 0.7 + self._min_size = max(1, size) + self.table: list[list[tuple[Any, Any]] | None] = [None] * self._size + + def add(self, key: Any, value: Any) -> None: + """Add or update a key-value pair.""" + index = self.hash_function(key) % self._size + if self.table[index] is None: + self.table[index] = [] + # Handle overwriting existing keys + for i, (k, _) in enumerate(self.table[index]): + if k == key: + self.table[index][i] = (key, value) + return + self.table[index].append((key, value)) + self._num_elements += 1 + if self._num_elements / self._size > self.load_factor: + self.grow() + + def get(self, key: Any) -> Any: + """Get the value for a key.""" + index = self.hash_function(key) % self._size + if self.table[index] is None: + raise KeyError("Key not found") + for k, v in self.table[index]: + if k == key: + return v + raise KeyError("Key not found") + + def remove(self, key: Any) -> None: + """Remove a key-value pair.""" + index = self.hash_function(key) % self._size + if self.table[index] is None: + raise KeyError("Key not found") + for i, (k, _) in enumerate(self.table[index]): + if k == key: + del self.table[index][i] + self._num_elements -= 1 + break + else: + raise KeyError("Key not found") + if ( + self._size > self._min_size + and self._num_elements / self._size < (1 - self.load_factor) / 2 + ): + self.shrink() + + def size(self) -> int: + """Return the number of elements in the hash table.""" + return self._num_elements + + def clear(self) -> None: + """Remove all elements from the hash table.""" + self.table = [None] * self._size + self._num_elements = 0 + + def contains(self, key: Any) -> bool: + """Check if the hash table contains a key.""" + try: + self.get(key) + return True + except KeyError: + return False + + def __getitem__(self, key: Any) -> Any: + return self.get(key) + + def __setitem__(self, key: Any, value: Any) -> None: + self.add(key, value) + + def grow(self) -> None: + """Double the capacity of the hash table.""" + self._size = self._size * 2 + 1 + new_table: list[list[tuple[Any, Any]] | None] = [None] * self._size + for bucket in self.table: + if bucket is None: + continue + for key, value in bucket: + index = self.hash_function(key) % self._size + if new_table[index] is None: + new_table[index] = [] + new_table[index].append((key, value)) + self.table = new_table + + def shrink(self) -> None: + """Halve the capacity of the hash table.""" + self._size = max((self._size - 1) // 2, self._min_size) + new_table: list[list[tuple[Any, Any]] | None] = [None] * self._size + for bucket in self.table: + if bucket is None: + continue + for key, value in bucket: + index = self.hash_function(key) % self._size + if new_table[index] is None: + new_table[index] = [] + new_table[index].append((key, value)) + self.table = new_table diff --git a/src/collections_and_containers/python/hash_table/test/test_hash_table.py b/src/collections_and_containers/python/hash_table/test/test_hash_table.py index a573158e..c7362170 100644 --- a/src/collections_and_containers/python/hash_table/test/test_hash_table.py +++ b/src/collections_and_containers/python/hash_table/test/test_hash_table.py @@ -90,5 +90,46 @@ def test_string_key(self): self.assertEqual(hash_table["c"], 10) +class TestHashTableRegressions(TestCase): + def test_contains_missing_key_in_occupied_bucket(self): + hash_table = HashTable(lambda key: key) + hash_table.add(1, 1) + # 11 % 10 == 1: same bucket as key 1, but absent. + self.assertFalse(hash_table.contains(11)) + self.assertRaises(KeyError, lambda: hash_table[11]) + + def test_remove_missing_key_in_occupied_bucket(self): + hash_table = HashTable(lambda key: key) + hash_table.add(1, 1) + self.assertRaises(KeyError, hash_table.remove, 11) + self.assertEqual(hash_table.size(), 1) + + def test_none_value_is_stored(self): + hash_table = HashTable(lambda key: key) + hash_table.add(1, None) + self.assertTrue(hash_table.contains(1)) + + def test_grow_and_shrink_down_to_empty(self): + hash_table = HashTable(lambda key: key * 7 - 100) + for i in range(500): + hash_table.add(i, i) + for i in range(500): + self.assertEqual(hash_table[i], i) + for i in range(500): + hash_table.remove(i) + self.assertEqual(hash_table.size(), 0) + hash_table.add(3, 3) + self.assertEqual(hash_table[3], 3) + + def test_tiny_table(self): + hash_table = HashTable(lambda key: key, size=1) + for i in range(20): + hash_table.add(i, i) + for i in range(20): + hash_table.remove(i) + hash_table.add(1, 1) + self.assertEqual(hash_table.size(), 1) + + if __name__ == "__main__": unittest.main() diff --git a/src/collections_and_containers/python/heap/test/test_heap.py b/src/collections_and_containers/python/heap/test/test_heap.py index d016de5d..a78be8f3 100644 --- a/src/collections_and_containers/python/heap/test/test_heap.py +++ b/src/collections_and_containers/python/heap/test/test_heap.py @@ -1,3 +1,4 @@ +import random from src.heap import Heap, MaxHeap, MinHeap from unittest import TestCase import unittest @@ -123,5 +124,19 @@ def test_is_min_heap(self): self.assertTrue(heap.is_min_heap()) +class TestHeapRandomized(TestCase): + def test_pop_order_matches_sorted(self): + rng = random.Random(5) + values = [rng.randrange(-50, 50) for _ in range(500)] + min_heap, max_heap = MinHeap(), MaxHeap() + for v in values: + min_heap.push(v) + max_heap.push(v) + self.assertEqual([min_heap.pop() for _ in values], sorted(values)) + self.assertEqual([max_heap.pop() for _ in values], sorted(values, reverse=True)) + self.assertRaises(IndexError, min_heap.pop) + self.assertRaises(IndexError, max_heap.peek) + + if __name__ == "__main__": unittest.main() diff --git a/src/collections_and_containers/python/red_black_tree/src/red_black_tree.py b/src/collections_and_containers/python/red_black_tree/src/red_black_tree.py index 0949abe2..b83aa1ec 100644 --- a/src/collections_and_containers/python/red_black_tree/src/red_black_tree.py +++ b/src/collections_and_containers/python/red_black_tree/src/red_black_tree.py @@ -38,16 +38,17 @@ def __init__(self) -> None: def insert(self, value: Any) -> None: """Insert a value into the tree.""" + if self.contains(value): + return + def insert_helper(p: Optional[Node]) -> Node: if not p: return Node(value, is_red=True) if value < p.value: p.left = insert_helper(p.left) - elif value > p.value: - p.right = insert_helper(p.right) else: - p.value = value + p.right = insert_helper(p.right) if p.is_right_red() and not p.is_left_red(): p = self.rotate_left(p) @@ -63,27 +64,42 @@ def insert_helper(p: Optional[Node]) -> Node: self.n += 1 def remove(self, value: Any) -> None: - """Remove a value from the tree.""" - def remove_helper(p: Optional[Node]) -> Optional[Node]: - if not p: + """Remove a value from the tree (no-op if it is absent).""" + if not self.contains(value): + return + + def is_red(p: Optional[Node]) -> bool: + return p is not None and p.is_red + + def remove_min(p: Node) -> Optional[Node]: + if p.left is None: return None + if not is_red(p.left) and not is_red(p.left.left): + p = self.move_red_left(p) + p.left = remove_min(p.left) + return self.balance(p) + def remove_helper(p: Node) -> Optional[Node]: if value < p.value: + if not is_red(p.left) and not is_red(p.left.left): + p = self.move_red_left(p) p.left = remove_helper(p.left) - elif value > p.value: - p.right = remove_helper(p.right) else: - if not p.left: - return p.right - if not p.right: - return p.left - - min_value = self.find_min(p.right) - p.value = min_value - p.right = remove_helper(p.right) - + if is_red(p.left): + p = self.rotate_right(p) + if value == p.value and p.right is None: + return None + if not is_red(p.right) and not is_red(p.right.left): + p = self.move_red_right(p) + if value == p.value: + p.value = self.find_min(p.right) + p.right = remove_min(p.right) + else: + p.right = remove_helper(p.right) return self.balance(p) + if not is_red(self.root.left) and not is_red(self.root.right): + self.root.is_red = True self.root = remove_helper(self.root) if self.root: self.root.is_red = False @@ -91,6 +107,7 @@ def remove_helper(p: Optional[Node]) -> Optional[Node]: def contains(self, value: Any) -> bool: """Check if the tree contains a value.""" + def contains_helper(p: Optional[Node]) -> bool: if not p: return False @@ -110,6 +127,7 @@ def clear(self) -> None: def height(self) -> int: """Return the height of the tree.""" + def height_helper(p: Optional[Node]) -> int: if not p: return 0 @@ -127,6 +145,7 @@ def empty(self) -> bool: def in_order_traversal(self) -> list[Any]: """Return elements in in-order traversal (left, root, right).""" + def in_order_helper(p: Optional[Node], v: list[Any]) -> None: if not p: return @@ -140,6 +159,7 @@ def in_order_helper(p: Optional[Node], v: list[Any]) -> None: def pre_order_traversal(self) -> list[Any]: """Return elements in pre-order traversal (root, left, right).""" + def pre_order_helper(p: Optional[Node], v: list[Any]) -> None: if not p: return @@ -153,6 +173,7 @@ def pre_order_helper(p: Optional[Node], v: list[Any]) -> None: def post_order_traversal(self) -> list[Any]: """Return elements in post-order traversal (left, right, root).""" + def post_order_helper(p: Optional[Node], v: list[Any]) -> None: if not p: return @@ -191,7 +212,8 @@ def flip_colors(self, p: Node) -> None: def move_red_left(self, p: Node) -> Node: """Move red to the left child.""" self.flip_colors(p) - if p.right and p.right.right and p.right.right.is_red: + if p.right and p.right.left and p.right.left.is_red: + p.right = self.rotate_right(p.right) p = self.rotate_left(p) self.flip_colors(p) return p diff --git a/src/collections_and_containers/python/red_black_tree/test/test_red_black_tree.py b/src/collections_and_containers/python/red_black_tree/test/test_red_black_tree.py index 3a7a2393..a0f32797 100644 --- a/src/collections_and_containers/python/red_black_tree/test/test_red_black_tree.py +++ b/src/collections_and_containers/python/red_black_tree/test/test_red_black_tree.py @@ -1,3 +1,4 @@ +import random from src.red_black_tree import RedBlackTree from unittest import TestCase import unittest @@ -125,5 +126,75 @@ def test_is_tree_always_balanced(self): self.assertFalse(tree.empty()) +class TestRedBlackTreeInvariants(TestCase): + def _black_height(self, node, parent_red=False): + if node is None: + return 1 + if node.is_red and parent_red: + self.fail("two consecutive red nodes") + left = self._black_height(node.left, node.is_red) + right = self._black_height(node.right, node.is_red) + self.assertEqual(left, right, "black heights differ") + return left + (0 if node.is_red else 1) + + def _check(self, tree): + if tree.root is not None: + self.assertFalse(tree.root.is_red) + self._black_height(tree.root) + + def test_remove_two_children_removes_exactly_one_value(self): + tree = RedBlackTree() + for v in [5, 3, 8, 1, 4, 7, 9]: + tree.insert(v) + tree.remove(5) + self.assertEqual(tree.in_order_traversal(), [1, 3, 4, 7, 8, 9]) + self.assertEqual(tree.size(), 6) + self._check(tree) + + def test_duplicate_insert_does_not_change_size(self): + tree = RedBlackTree() + tree.insert(1) + tree.insert(1) + self.assertEqual(tree.size(), 1) + self.assertEqual(tree.in_order_traversal(), [1]) + + def test_remove_missing_value_is_noop(self): + tree = RedBlackTree() + tree.remove(1) + tree.insert(1) + tree.remove(2) + self.assertEqual(tree.size(), 1) + + def test_sequential_insert_then_remove_all(self): + tree = RedBlackTree() + for i in range(100): + tree.insert(i) + self._check(tree) + for i in range(0, 100, 2): + tree.remove(i) + self._check(tree) + for i in range(99, 0, -2): + tree.remove(i) + self._check(tree) + self.assertTrue(tree.empty()) + + def test_random_operations_keep_red_black_properties(self): + rng = random.Random(3) + for _ in range(20): + tree = RedBlackTree() + ref = set() + for _ in range(200): + v = rng.randrange(60) + if rng.random() < 0.5: + tree.insert(v) + ref.add(v) + else: + tree.remove(v) + ref.discard(v) + self._check(tree) + self.assertEqual(tree.size(), len(ref)) + self.assertEqual(tree.in_order_traversal(), sorted(ref)) + + if __name__ == "__main__": unittest.main() diff --git a/src/collections_and_containers/python/simple_queue/src/simple_queue.py b/src/collections_and_containers/python/simple_queue/src/simple_queue.py index b64b50c0..7668af56 100644 --- a/src/collections_and_containers/python/simple_queue/src/simple_queue.py +++ b/src/collections_and_containers/python/simple_queue/src/simple_queue.py @@ -1,67 +1,74 @@ -"""Simple queue implementation using a circular buffer.""" - -from __future__ import annotations - -from typing import Any, Optional - - -class Queue: - """A FIFO (First In, First Out) queue data structure.""" - - def __init__(self) -> None: - self.data: list[Any] = [] - self.head = 0 - self.tail = 0 - self.n = 0 - self.max_size = 10 - - def enqueue(self, element: Any) -> None: - """Add an element to the back of the queue.""" - if self.n == self.max_size: - self.expand() - self.data.append(element) - self.n += 1 - - def dequeue(self) -> Optional[Any]: - """Remove and return the front element.""" - if self.empty(): - return None - element = self.data[self.head] - self.head = (self.head + 1) % self.max_size - self.n -= 1 - return element - - def front(self) -> Any: - """Return the front element without removing it.""" - if self.empty(): - raise IndexError("Queue is empty") - return self.data[self.head] - - def back(self) -> Any: - """Return the back element without removing it.""" - if self.empty(): - raise IndexError("Queue is empty") - return self.data[self.tail - 1] - - def size(self) -> int: - """Return the number of elements in the queue.""" - return self.n - - def empty(self) -> bool: - """Check if the queue is empty.""" - return self.n == 0 - - def expand(self) -> None: - """Double the capacity of the queue.""" - new_data = [0] * (self.max_size * 2) - for i in range(self.n): - new_data[i] = self.data[(self.head + i) % self.max_size] - self.head = 0 - self.tail = self.n - self.max_size *= 2 - self.data = new_data[:] - - def __eq__(self, other: object) -> bool: - if isinstance(other, self.__class__): - return self.__dict__ == other.__dict__ - return False +"""Simple queue implementation using a circular buffer.""" + +from __future__ import annotations + +from typing import Any, Optional + + +class Queue: + """A FIFO (First In, First Out) queue data structure.""" + + def __init__(self) -> None: + self.max_size = 10 + self.data: list[Any] = [None] * self.max_size + self.head = 0 + self.tail = 0 + self.n = 0 + + def enqueue(self, element: Any) -> None: + """Add an element to the back of the queue.""" + if self.n == self.max_size: + self.expand() + self.data[self.tail] = element + self.tail = (self.tail + 1) % self.max_size + self.n += 1 + + def dequeue(self) -> Optional[Any]: + """Remove and return the front element.""" + if self.empty(): + return None + element = self.data[self.head] + self.data[self.head] = None + self.head = (self.head + 1) % self.max_size + self.n -= 1 + return element + + def front(self) -> Any: + """Return the front element without removing it.""" + if self.empty(): + raise IndexError("Queue is empty") + return self.data[self.head] + + def back(self) -> Any: + """Return the back element without removing it.""" + if self.empty(): + raise IndexError("Queue is empty") + return self.data[(self.tail - 1) % self.max_size] + + def size(self) -> int: + """Return the number of elements in the queue.""" + return self.n + + def empty(self) -> bool: + """Check if the queue is empty.""" + return self.n == 0 + + def expand(self) -> None: + """Double the capacity of the queue.""" + new_data: list[Any] = [None] * (self.max_size * 2) + for i in range(self.n): + new_data[i] = self.data[(self.head + i) % self.max_size] + self.head = 0 + self.tail = self.n + self.max_size *= 2 + self.data = new_data[:] + + def __eq__(self, other: object) -> bool: + if isinstance(other, self.__class__): + # Compare logical contents, not the internal ring-buffer layout. + return self.n == other.n and all( + self.data[(self.head + i) % self.max_size] + == other.data[(other.head + i) % other.max_size] + for i in range(self.n) + ) + return False diff --git a/src/collections_and_containers/python/simple_queue/test/test_simple_queue.py b/src/collections_and_containers/python/simple_queue/test/test_simple_queue.py index bd45af7a..5f527d7a 100644 --- a/src/collections_and_containers/python/simple_queue/test/test_simple_queue.py +++ b/src/collections_and_containers/python/simple_queue/test/test_simple_queue.py @@ -52,5 +52,44 @@ def test_dequeue_when_empty(self): self.assertEqual(queue.dequeue(), None) +class TestSimpleQueueWrapAround(TestCase): + def test_interleaved_operations_preserve_fifo_order(self): + queue = Queue() + expected = [] + counter = 0 + for round_ in range(50): + for _ in range(7): + queue.enqueue(counter) + expected.append(counter) + counter += 1 + self.assertEqual(queue.back(), counter - 1) + for _ in range(3 + round_ % 3): + self.assertEqual(queue.front(), expected[0]) + self.assertEqual(queue.dequeue(), expected.pop(0)) + self.assertEqual(queue.size(), len(expected)) + while expected: + self.assertEqual(queue.dequeue(), expected.pop(0)) + self.assertTrue(queue.empty()) + + def test_fill_exactly_to_capacity_after_dequeue(self): + queue = Queue() + for i in range(10): + queue.enqueue(i) + self.assertEqual(queue.dequeue(), 0) + queue.enqueue(10) + queue.enqueue(11) + self.assertEqual([queue.dequeue() for _ in range(11)], list(range(1, 12))) + + def test_equality_ignores_internal_layout(self): + a = Queue() + b = Queue() + for i in range(5): + a.enqueue(i) + a.dequeue() + for i in range(1, 5): + b.enqueue(i) + self.assertEqual(a, b) + + if __name__ == "__main__": unittest.main() diff --git a/src/collections_and_containers/python/vector/src/vector.py b/src/collections_and_containers/python/vector/src/vector.py index 00df1dda..169d7d78 100644 --- a/src/collections_and_containers/python/vector/src/vector.py +++ b/src/collections_and_containers/python/vector/src/vector.py @@ -10,7 +10,7 @@ class Vector: def __init__(self, capacity: int = 10000) -> None: self.n = 0 - self.capacity = capacity + self.capacity = max(0, capacity) self.data: list[Any] = [None] * self.capacity def __iter__(self) -> Iterator[Any]: @@ -26,6 +26,8 @@ def __getitem__(self, i: int) -> Any: return self.data[i] def __setitem__(self, i: int, value: Any) -> None: + if i < 0 or i >= self.n: + raise IndexError("Index out of range") self.data[i] = value def size(self) -> int: @@ -51,7 +53,7 @@ def back(self) -> Any: def push_back(self, value: Any) -> None: """Add an element to the end of the vector.""" if self.n == self.capacity: - self.reserve(self.capacity * 2) + self.reserve(max(1, self.capacity * 2)) self.data[self.n] = value self.n += 1 @@ -66,6 +68,9 @@ def resize(self, size: int) -> None: """Resize the vector to contain 'size' elements.""" if size > self.capacity: self.reserve(size) + # Elements exposed by growing must not be stale values from earlier use. + for i in range(self.n, size): + self.data[i] = None self.n = size def __eq__(self, other: object) -> bool: @@ -86,6 +91,8 @@ def clear(self) -> None: def reserve(self, size: int) -> None: """Reserve capacity for at least 'size' elements.""" + if size <= self.capacity: + return self.capacity = size new_data: list[Any] = [None] * self.capacity for i in range(self.n): diff --git a/src/collections_and_containers/python/vector/test/test_vector.py b/src/collections_and_containers/python/vector/test/test_vector.py index c56972cf..1f484229 100644 --- a/src/collections_and_containers/python/vector/test/test_vector.py +++ b/src/collections_and_containers/python/vector/test/test_vector.py @@ -84,3 +84,40 @@ def test_clear(self): vector.push_back(3) vector.clear() self.assertTrue(vector.empty()) + + +class TestVectorRegressions(TestCase): + def test_zero_capacity_push_back(self): + vector = Vector(0) + vector.push_back(1) + vector.push_back(2) + self.assertEqual(list(vector), [1, 2]) + + def test_growth_beyond_initial_capacity(self): + vector = Vector(2) + for i in range(50): + vector.push_back(i) + self.assertEqual(list(vector), list(range(50))) + + def test_setitem_is_bounds_checked(self): + vector = Vector() + vector.push_back(1) + vector[0] = 5 + self.assertEqual(vector[0], 5) + self.assertRaises(IndexError, vector.__setitem__, 1, 2) + self.assertRaises(IndexError, vector.__setitem__, -1, 2) + + def test_reserve_smaller_keeps_elements(self): + vector = Vector(4) + for i in range(4): + vector.push_back(i) + vector.reserve(2) + self.assertEqual(list(vector), [0, 1, 2, 3]) + + def test_resize_grow_after_shrink_gives_none(self): + vector = Vector() + for i in range(4): + vector.push_back(i) + vector.resize(1) + vector.resize(3) + self.assertEqual(list(vector), [0, None, None]) diff --git a/src/dynamic_programming/cpp/all_construct/CMakeLists.txt b/src/dynamic_programming/cpp/all_construct/CMakeLists.txt index 9b677d93..69467530 100644 --- a/src/dynamic_programming/cpp/all_construct/CMakeLists.txt +++ b/src/dynamic_programming/cpp/all_construct/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-all-construct CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/cpp/all_construct/src/all_construct.cpp b/src/dynamic_programming/cpp/all_construct/src/all_construct.cpp index 89cf5e21..660a3af9 100644 --- a/src/dynamic_programming/cpp/all_construct/src/all_construct.cpp +++ b/src/dynamic_programming/cpp/all_construct/src/all_construct.cpp @@ -15,10 +15,10 @@ allConstructBasic(const std::string &target, std::vector> result; for (const auto &word : word_bank) { - if (target.find(word) == 0) { + if (!word.empty() && target.compare(0, word.size(), word) == 0) { auto suffix_ways = recurse(target.substr(word.size())); for (auto &way : suffix_ways) { - way.push_back(word); + way.insert(way.begin(), word); } result.insert(result.end(), suffix_ways.begin(), suffix_ways.end()); } @@ -48,10 +48,10 @@ allConstructMemo(const std::string &target, std::vector> result; for (const auto &word : word_bank) { - if (target.find(word) == 0) { + if (!word.empty() && target.compare(0, word.size(), word) == 0) { auto suffix_ways = recurse(target.substr(word.size())); for (auto &way : suffix_ways) { - way.push_back(word); + way.insert(way.begin(), word); } result.insert(result.end(), suffix_ways.begin(), suffix_ways.end()); } @@ -72,7 +72,7 @@ allConstructTable(const std::string &target, for (size_t i = 0; i <= target.size(); ++i) { for (const auto &word : word_bank) { - if (target.substr(i, word.size()) == word) { + if (!word.empty() && target.substr(i, word.size()) == word) { auto new_combination = table[i]; for (auto &combination : new_combination) { combination.push_back(word); diff --git a/src/dynamic_programming/cpp/all_construct/test/test_all_construct.cpp b/src/dynamic_programming/cpp/all_construct/test/test_all_construct.cpp index 87f779f6..d51ae809 100644 --- a/src/dynamic_programming/cpp/all_construct/test/test_all_construct.cpp +++ b/src/dynamic_programming/cpp/all_construct/test/test_all_construct.cpp @@ -132,3 +132,38 @@ TEST(AllConstructTest, TestPositiveTable2) { ASSERT_TRUE(equal(result, expectedResult)); } + +static std::string join(const std::vector &words) { + std::string s; + for (const auto &w : words) + s += w; + return s; +} + +TEST(AllConstructEdgeCases, WordsAreInTargetOrder) { + std::string target = "abcdef"; + std::vector bank = {"a", "b", "c", "d", "e", "f", + "ab", "cd", "ef", "abc", "def"}; + for (auto f : {allConstructBasic, allConstructMemo, allConstructTable}) { + auto result = f(target, bank); + ASSERT_EQ(result.size(), 13u); + for (const auto &combination : result) + ASSERT_EQ(join(combination), target); + } +} + +TEST(AllConstructEdgeCases, EmptyWordIsIgnored) { + std::vector bank = {"", "a", "b"}; + std::vector> expected = {{"a", "b"}}; + ASSERT_EQ(allConstructBasic("ab", bank), expected); + ASSERT_EQ(allConstructMemo("ab", bank), expected); + ASSERT_EQ(allConstructTable("ab", bank), expected); +} + +TEST(AllConstructEdgeCases, EmptyTarget) { + std::vector bank = {"a"}; + std::vector> expected = {{}}; + ASSERT_EQ(allConstructBasic("", bank), expected); + ASSERT_EQ(allConstructMemo("", bank), expected); + ASSERT_EQ(allConstructTable("", bank), expected); +} diff --git a/src/dynamic_programming/cpp/best_sum/CMakeLists.txt b/src/dynamic_programming/cpp/best_sum/CMakeLists.txt index f67e188e..e8e9e9ea 100644 --- a/src/dynamic_programming/cpp/best_sum/CMakeLists.txt +++ b/src/dynamic_programming/cpp/best_sum/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-best-sum CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/cpp/best_sum/src/best_sum.cpp b/src/dynamic_programming/cpp/best_sum/src/best_sum.cpp index b0edc709..a46bcd0c 100644 --- a/src/dynamic_programming/cpp/best_sum/src/best_sum.cpp +++ b/src/dynamic_programming/cpp/best_sum/src/best_sum.cpp @@ -18,7 +18,7 @@ bestSumBasic(unsigned int target, const std::vector &numbers) { std::vector shortestCombination; for (const auto &num : numbers) { - if (num > target) { + if (num == 0 || num > target) { continue; } @@ -71,7 +71,7 @@ bestSumMemo(unsigned int target, const std::vector &numbers) { std::vector shortestCombination; for (const auto &num : numbers) { - if (num > target) { + if (num == 0 || num > target) { continue; } @@ -114,10 +114,10 @@ bestSumTabulation(unsigned int target, table[0] = {{}}; - for (auto i = 0; i <= target; ++i) { + for (unsigned int i = 0; i <= target; ++i) { if (!table[i].empty()) { for (const auto &num : numbers) { - if (i + num <= target) { + if (num > 0 && num <= target - i) { auto combination = table[i]; for (auto &comb : combination) { diff --git a/src/dynamic_programming/cpp/best_sum/test/test_best_sum.cpp b/src/dynamic_programming/cpp/best_sum/test/test_best_sum.cpp index 47d1889a..6e1341ff 100644 --- a/src/dynamic_programming/cpp/best_sum/test/test_best_sum.cpp +++ b/src/dynamic_programming/cpp/best_sum/test/test_best_sum.cpp @@ -100,3 +100,48 @@ TEST(BestSumTest, TestPositiveTabulation2) { std::vector expectedResult = {3, 6}; ASSERT_TRUE(equal(bestSumTabulation(target, numbers), expectedResult)); } + +#include +#include +#include + +TEST(BestSumEdgeCases, ZeroIsIgnored) { + std::vector numbers = {0, 5, 1}; + for (auto f : {bestSumBasic, bestSumMemo, bestSumTabulation}) { + auto r = f(5, numbers); + ASSERT_EQ(r.size(), 1u); + ASSERT_EQ(r[0], 5u); + } +} + +TEST(BestSumEdgeCases, HugeNumberDoesNotOverflow) { + std::vector numbers = {UINT_MAX, 4}; + ASSERT_TRUE(bestSumTabulation(10, numbers).empty()); + ASSERT_EQ(bestSumTabulation(12, numbers).size(), 3u); +} + +TEST(BestSumEdgeCases, AgainstBruteForce) { + std::mt19937 rng(7); + for (int iter = 0; iter < 200; ++iter) { + unsigned int target = rng() % 15 + 1; + std::vector numbers(rng() % 4 + 1); + for (auto &n : numbers) + n = rng() % 9 + 1; + std::vector best(target + 1, -1); + best[0] = 0; + for (unsigned int t = 1; t <= target; ++t) + for (auto n : numbers) + if (n <= t && best[t - n] >= 0 && + (best[t] < 0 || best[t - n] + 1 < best[t])) + best[t] = best[t - n] + 1; + for (auto f : {bestSumBasic, bestSumMemo, bestSumTabulation}) { + auto r = f(target, numbers); + if (best[target] < 0) { + ASSERT_TRUE(r.empty()); + } else { + ASSERT_EQ(static_cast(r.size()), best[target]); + ASSERT_EQ(std::accumulate(r.begin(), r.end(), 0u), target); + } + } + } +} diff --git a/src/dynamic_programming/cpp/can_construct/CMakeLists.txt b/src/dynamic_programming/cpp/can_construct/CMakeLists.txt index aa804719..e808c6f6 100644 --- a/src/dynamic_programming/cpp/can_construct/CMakeLists.txt +++ b/src/dynamic_programming/cpp/can_construct/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-can-construct CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/cpp/can_construct/src/can_construct.cpp b/src/dynamic_programming/cpp/can_construct/src/can_construct.cpp index 02492fe5..f57049a9 100644 --- a/src/dynamic_programming/cpp/can_construct/src/can_construct.cpp +++ b/src/dynamic_programming/cpp/can_construct/src/can_construct.cpp @@ -10,7 +10,8 @@ bool canConstructBasic(const std::string &target, for (const auto &word : wordBank) { - if (target.size() >= word.size() && target.substr(0, word.size()) == word) { + if (!word.empty() && target.size() >= word.size() && + target.substr(0, word.size()) == word) { auto remainder = target.substr(word.size()); if (canConstructBasic(remainder, wordBank)) @@ -38,7 +39,7 @@ bool canConstructMemo(const std::string &target, for (const auto &word : wordBank) { - if (target.size() >= word.size() && + if (!word.empty() && target.size() >= word.size() && target.substr(0, word.size()) == word) { auto remainder = target.substr(word.size()); @@ -64,12 +65,12 @@ bool canConstructTable(const std::string &target, std::vector table(target.size() + 1, false); table[0] = true; - for (int i = 0; i < target.size(); i++) { + for (size_t i = 0; i < target.size(); i++) { if (table[i]) { for (const auto &word : wordBank) { - if (target.substr(i, word.size()) == word) { + if (!word.empty() && target.substr(i, word.size()) == word) { table[i + word.size()] = true; } diff --git a/src/dynamic_programming/cpp/can_construct/test/test_can_construct.cpp b/src/dynamic_programming/cpp/can_construct/test/test_can_construct.cpp index d8fd7144..1a352bba 100644 --- a/src/dynamic_programming/cpp/can_construct/test/test_can_construct.cpp +++ b/src/dynamic_programming/cpp/can_construct/test/test_can_construct.cpp @@ -88,3 +88,21 @@ TEST(CanConstructTest, TestPositiveTable2) { ASSERT_TRUE(canConstructBasic(target, wordBank)); } + +TEST(CanConstructEdgeCases, EmptyTarget) { + std::vector bank = {"a"}; + ASSERT_TRUE(canConstructBasic("", bank)); + ASSERT_TRUE(canConstructMemo("", bank)); + ASSERT_TRUE(canConstructTable("", bank)); +} + +TEST(CanConstructEdgeCases, EmptyWordIsIgnored) { + std::vector bank = {"", "ab"}; + ASSERT_FALSE(canConstructBasic("abc", bank)); + ASSERT_FALSE(canConstructMemo("abc", bank)); + ASSERT_FALSE(canConstructTable("abc", bank)); + bank.push_back("c"); + ASSERT_TRUE(canConstructBasic("abc", bank)); + ASSERT_TRUE(canConstructMemo("abc", bank)); + ASSERT_TRUE(canConstructTable("abc", bank)); +} diff --git a/src/dynamic_programming/cpp/can_sum/CMakeLists.txt b/src/dynamic_programming/cpp/can_sum/CMakeLists.txt index 3fb23909..f6ee1de0 100644 --- a/src/dynamic_programming/cpp/can_sum/CMakeLists.txt +++ b/src/dynamic_programming/cpp/can_sum/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-can-sum CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/cpp/can_sum/src/can_sum.cpp b/src/dynamic_programming/cpp/can_sum/src/can_sum.cpp index dcb98f32..54d343ee 100644 --- a/src/dynamic_programming/cpp/can_sum/src/can_sum.cpp +++ b/src/dynamic_programming/cpp/can_sum/src/can_sum.cpp @@ -10,8 +10,8 @@ bool canSumBasic(const unsigned int target, for (const auto &num : numbers) { - if (num > target) - return false; + if (num == 0 || num > target) + continue; auto remainder = target - num; @@ -39,8 +39,8 @@ bool canSumMemo(const unsigned int target, for (const auto &num : numbers) { - if (num > target) - return false; + if (num == 0 || num > target) + continue; auto remainder = target - num; @@ -68,7 +68,7 @@ bool canSumTable(const unsigned int target, for (unsigned int i = 0; i < target; i++) { if (table[i]) { for (const auto &number : numbers) { - if (i + number <= target) + if (number > 0 && number <= target - i) table[i + number] = true; } } diff --git a/src/dynamic_programming/cpp/can_sum/test/test_can_sum.cpp b/src/dynamic_programming/cpp/can_sum/test/test_can_sum.cpp index 50b36658..0104e701 100644 --- a/src/dynamic_programming/cpp/can_sum/test/test_can_sum.cpp +++ b/src/dynamic_programming/cpp/can_sum/test/test_can_sum.cpp @@ -84,3 +84,59 @@ TEST(CanSumTest, TestPositiveTable2) { ASSERT_TRUE(canSumTable(target, numbers)); } + +#include +#include + +TEST(CanSumEdgeCases, LargeNumberBeforeUsableOne) { + // A number larger than the target must be skipped, not end the search. + std::vector numbers = {8, 7}; + ASSERT_TRUE(canSumBasic(7, numbers)); + ASSERT_TRUE(canSumMemo(7, numbers)); + ASSERT_TRUE(canSumTable(7, numbers)); +} + +TEST(CanSumEdgeCases, ZeroTargetAndEmptyNumbers) { + std::vector empty; + ASSERT_TRUE(canSumBasic(0, empty)); + ASSERT_TRUE(canSumMemo(0, empty)); + ASSERT_TRUE(canSumTable(0, empty)); + ASSERT_FALSE(canSumBasic(5, empty)); + ASSERT_FALSE(canSumMemo(5, empty)); + ASSERT_FALSE(canSumTable(5, empty)); +} + +TEST(CanSumEdgeCases, ZeroIsIgnored) { + std::vector numbers = {0, 3}; + ASSERT_TRUE(canSumBasic(6, numbers)); + ASSERT_TRUE(canSumMemo(6, numbers)); + ASSERT_TRUE(canSumTable(6, numbers)); + ASSERT_FALSE(canSumBasic(7, numbers)); + ASSERT_FALSE(canSumMemo(7, numbers)); + ASSERT_FALSE(canSumTable(7, numbers)); +} + +TEST(CanSumEdgeCases, HugeNumberDoesNotOverflow) { + std::vector numbers = {UINT_MAX, 4}; + ASSERT_FALSE(canSumTable(10, numbers)); + ASSERT_TRUE(canSumTable(12, numbers)); +} + +TEST(CanSumEdgeCases, AgainstBruteForce) { + std::mt19937 rng(42); + for (int iter = 0; iter < 300; ++iter) { + unsigned int target = rng() % 30; + std::vector numbers(rng() % 5); + for (auto &n : numbers) + n = rng() % 10 + 1; + std::vector reachable(target + 1, false); + reachable[0] = true; + for (unsigned int t = 1; t <= target; ++t) + for (auto n : numbers) + if (n <= t && reachable[t - n]) + reachable[t] = true; + ASSERT_EQ(canSumBasic(target, numbers), reachable[target]); + ASSERT_EQ(canSumMemo(target, numbers), reachable[target]); + ASSERT_EQ(canSumTable(target, numbers), reachable[target]); + } +} diff --git a/src/dynamic_programming/cpp/climb_stairs/CMakeLists.txt b/src/dynamic_programming/cpp/climb_stairs/CMakeLists.txt index 0cc4e59a..301d8442 100644 --- a/src/dynamic_programming/cpp/climb_stairs/CMakeLists.txt +++ b/src/dynamic_programming/cpp/climb_stairs/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-climb CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/cpp/climb_stairs/src/climb.cpp b/src/dynamic_programming/cpp/climb_stairs/src/climb.cpp index f4c3bf6a..da9df61a 100644 --- a/src/dynamic_programming/cpp/climb_stairs/src/climb.cpp +++ b/src/dynamic_programming/cpp/climb_stairs/src/climb.cpp @@ -35,6 +35,8 @@ int countWaysToClimbMemo(int n) { } int countWaysToClimbTable(int n) { + if (n < 0) + return 0; std::vector table(n + 1, 0); table[0] = 1; diff --git a/src/dynamic_programming/cpp/climb_stairs/test/test_climb.cpp b/src/dynamic_programming/cpp/climb_stairs/test/test_climb.cpp index 0f316f6b..d425101f 100644 --- a/src/dynamic_programming/cpp/climb_stairs/test/test_climb.cpp +++ b/src/dynamic_programming/cpp/climb_stairs/test/test_climb.cpp @@ -90,3 +90,17 @@ TEST(TestClimbTable, TestWith20) { int result = 121415; EXPECT_EQ(countWaysToClimbTable(num), result); } + +TEST(ClimbEdgeCases, NegativeSteps) { + ASSERT_EQ(countWaysToClimbBasic(-1), 0); + ASSERT_EQ(countWaysToClimbMemo(-1), 0); + ASSERT_EQ(countWaysToClimbTable(-1), 0); + ASSERT_EQ(countWaysToClimbTable(-5), 0); +} + +TEST(ClimbEdgeCases, SmallValuesAgree) { + for (int n = 0; n <= 20; ++n) { + ASSERT_EQ(countWaysToClimbBasic(n), countWaysToClimbMemo(n)); + ASSERT_EQ(countWaysToClimbBasic(n), countWaysToClimbTable(n)); + } +} diff --git a/src/dynamic_programming/cpp/coins/CMakeLists.txt b/src/dynamic_programming/cpp/coins/CMakeLists.txt index cec93613..1ca7cee3 100644 --- a/src/dynamic_programming/cpp/coins/CMakeLists.txt +++ b/src/dynamic_programming/cpp/coins/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-coinChange CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/cpp/coins/src/coin_change.cpp b/src/dynamic_programming/cpp/coins/src/coin_change.cpp index 36e87769..51a8626d 100644 --- a/src/dynamic_programming/cpp/coins/src/coin_change.cpp +++ b/src/dynamic_programming/cpp/coins/src/coin_change.cpp @@ -1,17 +1,20 @@ #include "coin_change.h" +#include #include #include #include int coinChangeBasic(int amount, const std::vector &coins) { - if (amount <= 0) + if (amount < 0) + return INT_MAX; + if (amount == 0) return 0; auto minCoins = INT_MAX; for (const auto &coin : coins) { - if (coin <= amount) { + if (coin > 0 && coin <= amount) { auto temp = coinChangeBasic(amount - coin, coins); if (temp != INT_MAX) @@ -29,7 +32,9 @@ int coinChangeMemo(int amount, const std::vector &coins) { _coinChangeMemo; _coinChangeMemo = [&](int amount, const std::vector &coins, std::unordered_map &memo) -> int { - if (amount <= 0) + if (amount < 0) + return INT_MAX; + if (amount == 0) return 0; if (memo.count(amount)) @@ -38,7 +43,7 @@ int coinChangeMemo(int amount, const std::vector &coins) { auto minCoins = INT_MAX; for (const auto &coin : coins) { - if (coin <= amount) { + if (coin > 0 && coin <= amount) { auto temp = _coinChangeMemo(amount - coin, coins, memo); if (temp != INT_MAX) @@ -55,23 +60,18 @@ int coinChangeMemo(int amount, const std::vector &coins) { } int coinChangeTable(int amount, const std::vector &coins) { + if (amount < 0) + return INT_MAX; - std::vector> table(coins.size() + 1, - std::vector(amount + 1)); + std::vector table(amount + 1, INT_MAX); + table[0] = 0; - for (int i = 0; i <= amount; i++) - table[0][i] = i; - - for (int i = 1; i < coins.size(); i++) { - - for (int j = 0; j <= amount; j++) { - if (j < coins[i]) - table[i][j] = table[i - 1][j]; - - else - table[i][j] = std::min(table[i - 1][j], table[i][j - coins[i]] + 1); + for (int i = 1; i <= amount; i++) { + for (const auto &coin : coins) { + if (coin > 0 && coin <= i && table[i - coin] != INT_MAX) + table[i] = std::min(table[i], table[i - coin] + 1); } } - return table[coins.size() - 1][amount]; + return table[amount]; } diff --git a/src/dynamic_programming/cpp/coins/src/coin_change.h b/src/dynamic_programming/cpp/coins/src/coin_change.h index 99a1e153..f94d32af 100644 --- a/src/dynamic_programming/cpp/coins/src/coin_change.h +++ b/src/dynamic_programming/cpp/coins/src/coin_change.h @@ -20,6 +20,6 @@ int coinChangeMemo(int amount, const std::vector &coins); * @brief Finds the minimum number of coins needed using tabulation. * @param amount The target amount. * @param coins Vector of coin denominations. - * @return Minimum number of coins needed. + * @return Minimum number of coins needed, or INT_MAX if impossible. */ int coinChangeTable(int amount, const std::vector &coins); diff --git a/src/dynamic_programming/cpp/coins/test/test_coin_change.cpp b/src/dynamic_programming/cpp/coins/test/test_coin_change.cpp index d265b0a8..176012b9 100644 --- a/src/dynamic_programming/cpp/coins/test/test_coin_change.cpp +++ b/src/dynamic_programming/cpp/coins/test/test_coin_change.cpp @@ -10,9 +10,10 @@ TEST(CoinChangeTest, TestBasicNegative) { } TEST(CoinChangeTest, TestBasicPositive) { - int num = 67; + // Kept small: the plain recursion is exponential in the amount. + int num = 41; std::vector coins = {1, 5, 10, 25}; - int result = 6; + int result = 4; ASSERT_EQ(coinChangeBasic(num, coins), result); } @@ -47,3 +48,62 @@ TEST(CoinChangeTest, TestTablePositive) { ASSERT_EQ(coinChangeTable(num, coins), result); } + +#include +#include + +TEST(CoinChangeEdgeCases, FirstCoinIsNotOne) { + // The old table implementation implicitly assumed a coin of value 1. + std::vector coins = {2, 5}; + ASSERT_EQ(coinChangeTable(3, coins), INT_MAX); + ASSERT_EQ(coinChangeBasic(3, coins), INT_MAX); + ASSERT_EQ(coinChangeMemo(3, coins), INT_MAX); + ASSERT_EQ(coinChangeTable(9, coins), 3); +} + +TEST(CoinChangeEdgeCases, FirstCoinIsUsed) { + std::vector coins = {5, 2, 1}; + ASSERT_EQ(coinChangeTable(10, coins), 2); + ASSERT_EQ(coinChangeBasic(10, coins), 2); +} + +TEST(CoinChangeEdgeCases, EmptyCoins) { + std::vector coins; + ASSERT_EQ(coinChangeTable(0, coins), 0); + ASSERT_EQ(coinChangeTable(4, coins), INT_MAX); + ASSERT_EQ(coinChangeBasic(4, coins), INT_MAX); + ASSERT_EQ(coinChangeMemo(4, coins), INT_MAX); +} + +TEST(CoinChangeEdgeCases, NegativeAmountIsImpossible) { + std::vector coins = {1, 2}; + ASSERT_EQ(coinChangeBasic(-1, coins), INT_MAX); + ASSERT_EQ(coinChangeMemo(-1, coins), INT_MAX); + ASSERT_EQ(coinChangeTable(-1, coins), INT_MAX); +} + +TEST(CoinChangeEdgeCases, NonPositiveCoinsAreIgnored) { + std::vector coins = {0, -1, 2}; + ASSERT_EQ(coinChangeBasic(4, coins), 2); + ASSERT_EQ(coinChangeMemo(4, coins), 2); + ASSERT_EQ(coinChangeTable(4, coins), 2); +} + +TEST(CoinChangeEdgeCases, AgainstBruteForce) { + std::mt19937 rng(99); + for (int iter = 0; iter < 200; ++iter) { + int amount = rng() % 25; + std::vector coins(rng() % 4); + for (auto &c : coins) + c = rng() % 7 + 1; + std::vector best(amount + 1, INT_MAX); + best[0] = 0; + for (int a = 1; a <= amount; ++a) + for (auto c : coins) + if (c <= a && best[a - c] != INT_MAX) + best[a] = std::min(best[a], best[a - c] + 1); + ASSERT_EQ(coinChangeBasic(amount, coins), best[amount]); + ASSERT_EQ(coinChangeMemo(amount, coins), best[amount]); + ASSERT_EQ(coinChangeTable(amount, coins), best[amount]); + } +} diff --git a/src/dynamic_programming/cpp/count_construct/CMakeLists.txt b/src/dynamic_programming/cpp/count_construct/CMakeLists.txt index d8e196a8..015b6300 100644 --- a/src/dynamic_programming/cpp/count_construct/CMakeLists.txt +++ b/src/dynamic_programming/cpp/count_construct/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-count-construct CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/cpp/count_construct/src/count_construct.cpp b/src/dynamic_programming/cpp/count_construct/src/count_construct.cpp index e667a22a..3621b364 100644 --- a/src/dynamic_programming/cpp/count_construct/src/count_construct.cpp +++ b/src/dynamic_programming/cpp/count_construct/src/count_construct.cpp @@ -11,7 +11,8 @@ int countConstructBasic(const std::string &target, for (const auto &word : wordBank) { - if (target.size() >= word.size() && target.substr(0, word.size()) == word) { + if (!word.empty() && target.size() >= word.size() && + target.substr(0, word.size()) == word) { auto suffix = target.substr(word.size()); totalCount += countConstructBasic(suffix, wordBank); } @@ -39,7 +40,7 @@ int countConstructMemo(const std::string &target, for (const auto &word : wordBank) { - if (target.size() >= word.size() && + if (!word.empty() && target.size() >= word.size() && target.substr(0, word.size()) == word) { auto suffix = target.substr(word.size()); totalCount += _countConstructMemo(suffix, wordBank, memo); @@ -65,7 +66,7 @@ int countConstructTable(const std::string &target, for (const auto &word : wordBank) { - if (target.substr(i, word.size()) == word) { + if (!word.empty() && target.substr(i, word.size()) == word) { table[i + word.size()] += table[i]; } diff --git a/src/dynamic_programming/cpp/count_construct/test/test_count_construct.cpp b/src/dynamic_programming/cpp/count_construct/test/test_count_construct.cpp index 514a3ee3..9a7f5cec 100644 --- a/src/dynamic_programming/cpp/count_construct/test/test_count_construct.cpp +++ b/src/dynamic_programming/cpp/count_construct/test/test_count_construct.cpp @@ -100,3 +100,24 @@ TEST(CountConstructTest, TestPositiveTable2) { EXPECT_EQ(result, countConstructTable(target, wordBank)); } + +TEST(CountConstructEdgeCases, EmptyWordIsIgnored) { + std::vector bank = {"", "a", "b"}; + ASSERT_EQ(countConstructBasic("ab", bank), 1); + ASSERT_EQ(countConstructMemo("ab", bank), 1); + ASSERT_EQ(countConstructTable("ab", bank), 1); +} + +TEST(CountConstructEdgeCases, OverlappingWords) { + std::vector bank = {"a", "aa"}; + ASSERT_EQ(countConstructBasic("aaaa", bank), 5); + ASSERT_EQ(countConstructMemo("aaaa", bank), 5); + ASSERT_EQ(countConstructTable("aaaa", bank), 5); +} + +TEST(CountConstructEdgeCases, EmptyTarget) { + std::vector bank = {"a"}; + ASSERT_EQ(countConstructBasic("", bank), 1); + ASSERT_EQ(countConstructMemo("", bank), 1); + ASSERT_EQ(countConstructTable("", bank), 1); +} diff --git a/src/dynamic_programming/cpp/fibonacci/CMakeLists.txt b/src/dynamic_programming/cpp/fibonacci/CMakeLists.txt index 261b924f..2e1586c9 100644 --- a/src/dynamic_programming/cpp/fibonacci/CMakeLists.txt +++ b/src/dynamic_programming/cpp/fibonacci/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-fibonacci CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/cpp/grid_traveler/CMakeLists.txt b/src/dynamic_programming/cpp/grid_traveler/CMakeLists.txt index 561b6b12..7817de3b 100644 --- a/src/dynamic_programming/cpp/grid_traveler/CMakeLists.txt +++ b/src/dynamic_programming/cpp/grid_traveler/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-gridTravelers CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/cpp/grid_traveler/src/grid_travelers.cpp b/src/dynamic_programming/cpp/grid_traveler/src/grid_travelers.cpp index 4a364962..a70049e3 100644 --- a/src/dynamic_programming/cpp/grid_traveler/src/grid_travelers.cpp +++ b/src/dynamic_programming/cpp/grid_traveler/src/grid_travelers.cpp @@ -16,7 +16,7 @@ struct hash { int gridTravelersBasic(int n, int m) { - if (n == 0 || m == 0) + if (n <= 0 || m <= 0) return 0; if (n < 2 && m < 2) @@ -33,7 +33,7 @@ int gridTravelersMemo(int n, int m) { _gridTravelersMemo = [&](int n, int m, std::unordered_map, int, hash> &memo) -> int { - if (n == 0 || m == 0) + if (n <= 0 || m <= 0) return 0; if (n < 2 && m < 2) @@ -55,6 +55,8 @@ int gridTravelersMemo(int n, int m) { } int gridTravelersTable(int n, int m) { + if (n <= 0 || m <= 0) + return 0; std::vector> table(m + 1, std::vector(n + 1)); table[1][1] = 1; diff --git a/src/dynamic_programming/cpp/grid_traveler/test/test_grid_travelers.cpp b/src/dynamic_programming/cpp/grid_traveler/test/test_grid_travelers.cpp index 22e707dd..a266a14f 100644 --- a/src/dynamic_programming/cpp/grid_traveler/test/test_grid_travelers.cpp +++ b/src/dynamic_programming/cpp/grid_traveler/test/test_grid_travelers.cpp @@ -96,3 +96,29 @@ TEST(GridTravelersTest, TestTable4) { ASSERT_EQ(gridTravelersTable(m, n), result); } + +TEST(GridTravelersEdgeCases, NonPositiveDimensions) { + for (auto f : {gridTravelersBasic, gridTravelersMemo, gridTravelersTable}) { + ASSERT_EQ(f(0, 3), 0); + ASSERT_EQ(f(3, 0), 0); + ASSERT_EQ(f(-1, 3), 0); + ASSERT_EQ(f(3, -2), 0); + ASSERT_EQ(f(0, 0), 0); + } +} + +TEST(GridTravelersEdgeCases, SingleRowOrColumn) { + for (auto f : {gridTravelersBasic, gridTravelersMemo, gridTravelersTable}) { + ASSERT_EQ(f(1, 1), 1); + ASSERT_EQ(f(1, 9), 1); + ASSERT_EQ(f(9, 1), 1); + } +} + +TEST(GridTravelersEdgeCases, AsymmetricGrid) { + for (auto f : {gridTravelersBasic, gridTravelersMemo, gridTravelersTable}) { + ASSERT_EQ(f(2, 5), 5); + ASSERT_EQ(f(5, 2), 5); + ASSERT_EQ(f(3, 7), 28); + } +} diff --git a/src/dynamic_programming/cpp/how_sum/CMakeLists.txt b/src/dynamic_programming/cpp/how_sum/CMakeLists.txt index d36ad1d6..41ed49a2 100644 --- a/src/dynamic_programming/cpp/how_sum/CMakeLists.txt +++ b/src/dynamic_programming/cpp/how_sum/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-how-sum CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/cpp/how_sum/src/how_sum.cpp b/src/dynamic_programming/cpp/how_sum/src/how_sum.cpp index af5ae38c..9347ceec 100644 --- a/src/dynamic_programming/cpp/how_sum/src/how_sum.cpp +++ b/src/dynamic_programming/cpp/how_sum/src/how_sum.cpp @@ -16,7 +16,7 @@ howSumBasic(const unsigned int target, return {{}}; for (const auto &num : numbers) { - if (num > target) + if (num == 0 || num > target) continue; auto remainder = target - num; @@ -59,7 +59,7 @@ std::vector howSumMemo(const unsigned int target, return memo[target]; for (const auto &num : numbers) { - if (num > target) + if (num == 0 || num > target) continue; auto remainder = target - num; @@ -96,7 +96,7 @@ howSumTable(const unsigned int target, for (unsigned int i = 0; i < target; i++) { if (!table[i].empty()) { for (const auto &number : numbers) { - if (i + number <= target) { + if (number > 0 && number <= target - i) { table[i + number] = table[i]; table[i + number][0].push_back(number); } diff --git a/src/dynamic_programming/cpp/how_sum/test/test_how_sum.cpp b/src/dynamic_programming/cpp/how_sum/test/test_how_sum.cpp index b789a346..1459df7e 100644 --- a/src/dynamic_programming/cpp/how_sum/test/test_how_sum.cpp +++ b/src/dynamic_programming/cpp/how_sum/test/test_how_sum.cpp @@ -100,3 +100,30 @@ TEST(HowSumTest, TestPositiveTable2) { std::vector expectedResult = {3, 3, 3}; ASSERT_TRUE(equal(howSumTable(target, numbers), expectedResult)); } + +#include +#include + +TEST(HowSumEdgeCases, ZeroIsIgnored) { + std::vector numbers = {0, 5}; + for (auto f : {howSumBasic, howSumMemo, howSumTable}) { + auto r = f(10, numbers); + ASSERT_EQ(std::accumulate(r.begin(), r.end(), 0u), 10u); + ASSERT_EQ(r.size(), 2u); + ASSERT_TRUE(f(7, numbers).empty()); + } +} + +TEST(HowSumEdgeCases, HugeNumberDoesNotOverflow) { + std::vector numbers = {UINT_MAX, 4}; + auto r = howSumTable(12, numbers); + ASSERT_EQ(r.size(), 3u); + ASSERT_TRUE(howSumTable(10, numbers).empty()); +} + +TEST(HowSumEdgeCases, ZeroTarget) { + std::vector numbers = {1, 2}; + ASSERT_TRUE(howSumBasic(0, numbers).empty()); + ASSERT_TRUE(howSumMemo(0, numbers).empty()); + ASSERT_TRUE(howSumTable(0, numbers).empty()); +} diff --git a/src/dynamic_programming/cpp/kmp/CMakeLists.txt b/src/dynamic_programming/cpp/kmp/CMakeLists.txt index fdd92706..121dfa3b 100644 --- a/src/dynamic_programming/cpp/kmp/CMakeLists.txt +++ b/src/dynamic_programming/cpp/kmp/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-kmp CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/cpp/kmp/src/kmp.cpp b/src/dynamic_programming/cpp/kmp/src/kmp.cpp index fa77281d..0c4a871b 100644 --- a/src/dynamic_programming/cpp/kmp/src/kmp.cpp +++ b/src/dynamic_programming/cpp/kmp/src/kmp.cpp @@ -25,16 +25,23 @@ std::vector search(const std::string &text, const std::string &pattern) { return result; }; - auto longest_prefix_suffix = find_longest_prefix_suffix(pattern); std::vector result; + if (pattern.empty()) { + // The empty pattern matches at every position (like std::string::find). + for (size_t i = 0; i <= text.size(); i++) { + result.push_back(static_cast(i)); + } + return result; + } + + auto longest_prefix_suffix = find_longest_prefix_suffix(pattern); int j = 0; - for (int i = 0; i < text.size(); i++) { + for (int i = 0; i < static_cast(text.size()); i++) { + while (j != 0 && text[i] != pattern[j]) { + j = longest_prefix_suffix[j - 1]; + } if (text[i] == pattern[j]) { j += 1; - } else { - if (j != 0) { - j = longest_prefix_suffix[j - 1]; - } } if (j == pattern.size()) { result.push_back(i - j + 1); diff --git a/src/dynamic_programming/cpp/kmp/test/test_kmp.cpp b/src/dynamic_programming/cpp/kmp/test/test_kmp.cpp index 01a9b461..ab15204b 100644 --- a/src/dynamic_programming/cpp/kmp/test/test_kmp.cpp +++ b/src/dynamic_programming/cpp/kmp/test/test_kmp.cpp @@ -56,3 +56,43 @@ TEST(TestKmp, TestMultipleMatch) { ASSERT_EQ(expected, actual); } + +#include + +TEST(TestKmpEdgeCases, FallbackNeedsMoreThanOneStep) { + std::vector expected = {4}; + ASSERT_EQ(search("aaabaaaab", "aaaab"), expected); +} + +TEST(TestKmpEdgeCases, OverlappingMatches) { + std::vector expected = {0, 1, 2}; + ASSERT_EQ(search("aaaa", "aa"), expected); +} + +TEST(TestKmpEdgeCases, EmptyPattern) { + std::vector expected = {0, 1, 2, 3}; + ASSERT_EQ(search("abc", ""), expected); + std::vector expectedEmpty = {0}; + ASSERT_EQ(search("", ""), expectedEmpty); +} + +TEST(TestKmpEdgeCases, EmptyTextAndLongPattern) { + ASSERT_TRUE(search("", "a").empty()); + ASSERT_TRUE(search("ab", "abc").empty()); +} + +TEST(TestKmpEdgeCases, AgainstBruteForce) { + std::mt19937 rng(1234); + for (int iter = 0; iter < 500; ++iter) { + std::string text, pattern; + for (size_t i = 0, n = rng() % 21; i < n; ++i) + text += "ab"[rng() % 2]; + for (size_t i = 0, n = rng() % 6 + 1; i < n; ++i) + pattern += "ab"[rng() % 2]; + std::vector expected; + for (size_t i = 0; i + pattern.size() <= text.size(); ++i) + if (text.compare(i, pattern.size(), pattern) == 0) + expected.push_back(static_cast(i)); + ASSERT_EQ(search(text, pattern), expected); + } +} diff --git a/src/dynamic_programming/cpp/longest_common_subsequence/CMakeLists.txt b/src/dynamic_programming/cpp/longest_common_subsequence/CMakeLists.txt index 35d682cc..7e82018f 100644 --- a/src/dynamic_programming/cpp/longest_common_subsequence/CMakeLists.txt +++ b/src/dynamic_programming/cpp/longest_common_subsequence/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-longestCommonSubsequence CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/cpp/longest_increasing_subarray/CMakeLists.txt b/src/dynamic_programming/cpp/longest_increasing_subarray/CMakeLists.txt index dba5f5bb..84757f8c 100644 --- a/src/dynamic_programming/cpp/longest_increasing_subarray/CMakeLists.txt +++ b/src/dynamic_programming/cpp/longest_increasing_subarray/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-longestIncreasingSubarray CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/cpp/longest_increasing_subarray/src/longest_increasing_subarray.cpp b/src/dynamic_programming/cpp/longest_increasing_subarray/src/longest_increasing_subarray.cpp index 9bf9d1a4..5d791963 100644 --- a/src/dynamic_programming/cpp/longest_increasing_subarray/src/longest_increasing_subarray.cpp +++ b/src/dynamic_programming/cpp/longest_increasing_subarray/src/longest_increasing_subarray.cpp @@ -16,10 +16,10 @@ struct hash { int longestIncreasingSubarrayBasic(const std::vector &array) { - std::function &, unsigned int, int)> + std::function &, unsigned int, long long)> _longestIncreasingSubarrayBasic; _longestIncreasingSubarrayBasic = [&](const std::vector &array, - unsigned int i, int prev) -> int { + unsigned int i, long long prev) -> int { if (i == array.size()) return 0; @@ -33,21 +33,22 @@ int longestIncreasingSubarrayBasic(const std::vector &array) { return std::max(incl, excl); }; - return _longestIncreasingSubarrayBasic(array, 0, INT_MIN); + return _longestIncreasingSubarrayBasic(array, 0, LLONG_MIN); } int longestIncreasingSubarrayMemo(const std::vector &array) { - std::function &, unsigned int, int, - std::unordered_map, int, hash> &)> + std::function &, unsigned int, long long, + std::unordered_map, int, hash> &)> _longestIncreasingSubarrayMemo; _longestIncreasingSubarrayMemo = - [&](const std::vector &array, unsigned int i, int prev, - std::unordered_map, int, hash> &memo) -> int { + [&](const std::vector &array, unsigned int i, long long prev, + std::unordered_map, int, hash> &memo) + -> int { if (i == array.size()) return 0; - std::pair key(i, prev); + std::pair key(i, prev); if (memo.count(key)) return memo[key]; @@ -63,13 +64,16 @@ int longestIncreasingSubarrayMemo(const std::vector &array) { return memo[key]; }; - std::unordered_map, int, hash> memo; + std::unordered_map, int, hash> memo; - return _longestIncreasingSubarrayMemo(array, 0, INT_MIN, memo); + return _longestIncreasingSubarrayMemo(array, 0, LLONG_MIN, memo); } int longestIncreasingSubarrayTable(const std::vector &array) { + if (array.empty()) + return 0; + std::vector> table(array.size(), std::vector()); table[0].emplace_back(array[0]); diff --git a/src/dynamic_programming/cpp/longest_increasing_subarray/test/test_longest_increasing_subarray.cpp b/src/dynamic_programming/cpp/longest_increasing_subarray/test/test_longest_increasing_subarray.cpp index 0383be4d..b2949402 100644 --- a/src/dynamic_programming/cpp/longest_increasing_subarray/test/test_longest_increasing_subarray.cpp +++ b/src/dynamic_programming/cpp/longest_increasing_subarray/test/test_longest_increasing_subarray.cpp @@ -42,3 +42,40 @@ TEST(LongestIncreasingSubarrayTest, TestTable2) { ASSERT_EQ(longestIncreasingSubarrayTable(array), result); } + +#include +#include + +TEST(LisEdgeCases, EmptyArray) { + std::vector a; + ASSERT_EQ(longestIncreasingSubarrayBasic(a), 0); + ASSERT_EQ(longestIncreasingSubarrayMemo(a), 0); + ASSERT_EQ(longestIncreasingSubarrayTable(a), 0); +} + +TEST(LisEdgeCases, IntMinElement) { + std::vector a = {INT_MIN, 0, 1}; + ASSERT_EQ(longestIncreasingSubarrayBasic(a), 3); + ASSERT_EQ(longestIncreasingSubarrayMemo(a), 3); + ASSERT_EQ(longestIncreasingSubarrayTable(a), 3); +} + +TEST(LisEdgeCases, AgainstBruteForce) { + std::mt19937 rng(5); + for (int iter = 0; iter < 200; ++iter) { + std::vector a(rng() % 10 + 1); + for (auto &x : a) + x = static_cast(rng() % 8) - 4; + std::vector best(a.size(), 1); + int expected = 0; + for (size_t i = 0; i < a.size(); ++i) { + for (size_t j = 0; j < i; ++j) + if (a[j] < a[i]) + best[i] = std::max(best[i], best[j] + 1); + expected = std::max(expected, best[i]); + } + ASSERT_EQ(longestIncreasingSubarrayBasic(a), expected); + ASSERT_EQ(longestIncreasingSubarrayMemo(a), expected); + ASSERT_EQ(longestIncreasingSubarrayTable(a), expected); + } +} diff --git a/src/dynamic_programming/cpp/minimum_insertions_for_palindrome/CMakeLists.txt b/src/dynamic_programming/cpp/minimum_insertions_for_palindrome/CMakeLists.txt index bda7e710..dc81bc06 100644 --- a/src/dynamic_programming/cpp/minimum_insertions_for_palindrome/CMakeLists.txt +++ b/src/dynamic_programming/cpp/minimum_insertions_for_palindrome/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-minimum_insertions_for_palindrome CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/dynamic_programming/python/all_construct/src/all_construct.py b/src/dynamic_programming/python/all_construct/src/all_construct.py index 90c6aba8..b6b5a5a4 100644 --- a/src/dynamic_programming/python/all_construct/src/all_construct.py +++ b/src/dynamic_programming/python/all_construct/src/all_construct.py @@ -21,9 +21,9 @@ def recurse(target: str) -> List[List[str]]: result: List[List[str]] = [] for word in word_bank: - if target.startswith(word): + if word and target.startswith(word): suffix_ways = recurse(target[len(word) :]) - result.extend([way + [word] for way in suffix_ways]) + result.extend([[word] + way for way in suffix_ways]) return result @@ -53,9 +53,9 @@ def recurse(target: str, memo: dict) -> List[List[str]]: result: List[List[str]] = [] for word in word_bank: - if target.startswith(word): + if word and target.startswith(word): suffix_ways = recurse(target[len(word) :], memo) - result.extend([way + [word] for way in suffix_ways]) + result.extend([[word] + way for way in suffix_ways]) memo[target] = result return result @@ -81,7 +81,7 @@ def all_construct_table(target: str, word_bank: List[str]) -> List[List[str]]: for i in range(len(target)): for word in word_bank: - if target[i : i + len(word)] == word: + if word and target[i : i + len(word)] == word: new_combinations = [combination + [word] for combination in table[i]] table[i + len(word)].extend(new_combinations) diff --git a/src/dynamic_programming/python/all_construct/tests/test_all_construct.py b/src/dynamic_programming/python/all_construct/tests/test_all_construct.py index 2f40ad46..cca089c9 100644 --- a/src/dynamic_programming/python/all_construct/tests/test_all_construct.py +++ b/src/dynamic_programming/python/all_construct/tests/test_all_construct.py @@ -96,5 +96,39 @@ def test_multiple_combinations_possible(self): self.assertTrue(compare_2d_lists(result, [["abc", "def"]])) +class TestAllConstructEdgeCases(unittest.TestCase): + FUNCS = (all_construct_basic, all_construct_memo, all_construct_table) + + def test_empty_target(self): + for f in self.FUNCS: + self.assertEqual(f("", ["a"]), [[]], f.__name__) + + def test_empty_word_is_ignored(self): + for f in self.FUNCS: + self.assertEqual(f("ab", ["", "a", "b"]), [["a", "b"]], f.__name__) + + def test_impossible(self): + for f in self.FUNCS: + self.assertEqual(f("abc", ["a", "b"]), [], f.__name__) + + def test_words_are_in_target_order(self): + # Every returned combination must concatenate to the target. + target = "abcdef" + word_bank = ["a", "b", "c", "d", "e", "f", "ab", "cd", "ef", "abc", "def"] + for f in self.FUNCS: + result = f(target, word_bank) + self.assertTrue(result, f.__name__) + for combination in result: + self.assertEqual("".join(combination), target, f.__name__) + self.assertEqual(len(result), 13, f.__name__) + + def test_all_variants_agree(self): + word_bank = ["a", "ab", "b", "ba"] + target = "abababa" + expected = sorted(all_construct_table(target, word_bank)) + self.assertEqual(sorted(all_construct_basic(target, word_bank)), expected) + self.assertEqual(sorted(all_construct_memo(target, word_bank)), expected) + + if __name__ == "__main__": unittest.main() diff --git a/src/dynamic_programming/python/best_sum/src/best_sum.py b/src/dynamic_programming/python/best_sum/src/best_sum.py index d1d571db..35454235 100644 --- a/src/dynamic_programming/python/best_sum/src/best_sum.py +++ b/src/dynamic_programming/python/best_sum/src/best_sum.py @@ -26,6 +26,8 @@ def recurse(target: int) -> float: min_length = float("inf") for number in numbers: + if number <= 0: + continue combination_length = recurse(target - number) + 1 min_length = min(min_length, combination_length) @@ -38,7 +40,7 @@ def recurse(target: int) -> float: combination: List[int] = [] while target > 0: for number in numbers: - if recurse(target - number) == min_length - 1: + if number > 0 and recurse(target - number) == min_length - 1: combination.append(number) target -= number min_length -= 1 @@ -75,6 +77,8 @@ def recurse(target: int, memo: dict) -> float: min_length = float("inf") for number in numbers: + if number <= 0: + continue combination_length = recurse(target - number, memo) + 1 min_length = min(min_length, combination_length) @@ -89,7 +93,7 @@ def recurse(target: int, memo: dict) -> float: combination: List[int] = [] while target > 0: for number in numbers: - if recurse(target - number, memo) == min_length - 1: + if number > 0 and recurse(target - number, memo) == min_length - 1: combination.append(number) target -= number min_length -= 1 @@ -122,7 +126,7 @@ def find_shortest_combination_sum_table( for i in range(target + 1): if table[i] is not None: for number in numbers: - if i + number <= target: + if 0 < number <= target - i: if table[i + number] is None or len(table[i]) < len( table[i + number] ): diff --git a/src/dynamic_programming/python/best_sum/tests/test_best_sum.py b/src/dynamic_programming/python/best_sum/tests/test_best_sum.py index 59ec241b..c5bc69f0 100644 --- a/src/dynamic_programming/python/best_sum/tests/test_best_sum.py +++ b/src/dynamic_programming/python/best_sum/tests/test_best_sum.py @@ -1,3 +1,4 @@ +import random import unittest from src.best_sum import ( @@ -103,5 +104,49 @@ def test_multiple_combinations_possible(self): self.assertCountEqual(result, [3, 6]) +class TestBestSumEdgeCases(unittest.TestCase): + FUNCS = ( + find_shortest_combination_sum_basic, + find_shortest_combination_sum_memo, + find_shortest_combination_sum_table, + ) + + def test_negative_target(self): + for f in self.FUNCS: + self.assertIsNone(f(-4, [1, 2]), f.__name__) + + def test_zero_target(self): + for f in self.FUNCS: + self.assertEqual(f(0, [1]), [], f.__name__) + + def test_non_positive_numbers_are_ignored(self): + for f in self.FUNCS: + self.assertIsNone(f(5, [0, -1]), f.__name__) + self.assertEqual(f(5, [0, 5, 1]), [5], f.__name__) + + def test_against_brute_force(self): + rng = random.Random(21) + for _ in range(150): + target = rng.randint(0, 20) + numbers = [rng.randint(1, 8) for _ in range(rng.randint(0, 4))] + best = [0] + [None] * target + for t in range(1, target + 1): + options = [ + best[t - n] + 1 + for n in numbers + if n <= t and best[t - n] is not None + ] + best[t] = min(options) if options else None + for f in self.FUNCS: + result = f(target, numbers) + if best[target] is None: + self.assertIsNone(result, (f.__name__, target, numbers)) + else: + self.assertEqual( + len(result), best[target], (f.__name__, target, numbers) + ) + self.assertEqual(sum(result), target) + + if __name__ == "__main__": unittest.main() diff --git a/src/dynamic_programming/python/can_construct/src/can_construct.py b/src/dynamic_programming/python/can_construct/src/can_construct.py index 5b050175..5796fd11 100644 --- a/src/dynamic_programming/python/can_construct/src/can_construct.py +++ b/src/dynamic_programming/python/can_construct/src/can_construct.py @@ -18,7 +18,8 @@ def can_construct_basic(target: str, word_bank: List[str]) -> bool: return True return any( - len(target) >= len(word) + word + and len(target) >= len(word) and target[: len(word)] == word and can_construct_basic(target[len(word) :], word_bank) for word in word_bank @@ -51,7 +52,8 @@ def can_construct_memo( return memo[target] can_construct = any( - len(target) >= len(word) + word + and len(target) >= len(word) and target[: len(word)] == word and can_construct_memo(target[len(word) :], word_bank, memo) for word in word_bank @@ -80,7 +82,7 @@ def can_construct_table(target: str, word_bank: List[str]) -> bool: for i in range(len(target)): if table[i]: for word in word_bank: - if target[i : i + len(word)] == word: + if word and target[i : i + len(word)] == word: table[i + len(word)] = True return table[-1] diff --git a/src/dynamic_programming/python/can_construct/tests/test_can_construct.py b/src/dynamic_programming/python/can_construct/tests/test_can_construct.py index 94e1e2df..abc9cf56 100644 --- a/src/dynamic_programming/python/can_construct/tests/test_can_construct.py +++ b/src/dynamic_programming/python/can_construct/tests/test_can_construct.py @@ -68,5 +68,23 @@ def test_positive(self): self.assertTrue(can_construct_table(target, word_bank)) +class TestCanConstructEdgeCases(unittest.TestCase): + FUNCS = (can_construct_basic, can_construct_memo, can_construct_table) + + def test_empty_target(self): + for f in self.FUNCS: + self.assertTrue(f("", []), f.__name__) + self.assertTrue(f("", ["a"]), f.__name__) + + def test_empty_word_is_ignored(self): + for f in self.FUNCS: + self.assertFalse(f("abc", ["", "ab"]), f.__name__) + self.assertTrue(f("abc", ["", "ab", "c"]), f.__name__) + + def test_empty_word_bank(self): + for f in self.FUNCS: + self.assertFalse(f("a", []), f.__name__) + + if __name__ == "__main__": unittest.main() diff --git a/src/dynamic_programming/python/can_sum/src/can_sum.py b/src/dynamic_programming/python/can_sum/src/can_sum.py index 6d4d2349..a579998b 100644 --- a/src/dynamic_programming/python/can_sum/src/can_sum.py +++ b/src/dynamic_programming/python/can_sum/src/can_sum.py @@ -20,12 +20,10 @@ def can_sum_basic(target: int, numbers: List[int]) -> bool: if target < 0: return False - return any(can_sum_basic(target - i, numbers) for i in numbers) + return any(can_sum_basic(target - i, numbers) for i in numbers if i > 0) -def can_sum_memo( - target: int, numbers: List[int], memo: Optional[dict] = None -) -> bool: +def can_sum_memo(target: int, numbers: List[int], memo: Optional[dict] = None) -> bool: """ Determine if it's possible to generate target sum using numbers from the list. @@ -51,7 +49,7 @@ def can_sum_memo( if target in memo: return memo[target] - can_sum = any(can_sum_memo(target - i, numbers, memo) for i in numbers) + can_sum = any(can_sum_memo(target - i, numbers, memo) for i in numbers if i > 0) memo[target] = can_sum return can_sum @@ -70,13 +68,16 @@ def can_sum_table(target: int, numbers: List[int]) -> bool: Returns: True if target sum is achievable, False otherwise. """ + if target < 0: + return False + table = [False] * (target + 1) table[0] = True for i in range(target): if table[i]: for number in numbers: - if i + number <= target: + if 0 < number <= target - i: table[i + number] = True return table[-1] diff --git a/src/dynamic_programming/python/can_sum/tests/test_can_sum.py b/src/dynamic_programming/python/can_sum/tests/test_can_sum.py index fe175cbd..f5751168 100644 --- a/src/dynamic_programming/python/can_sum/tests/test_can_sum.py +++ b/src/dynamic_programming/python/can_sum/tests/test_can_sum.py @@ -1,3 +1,4 @@ +import random import unittest @@ -64,5 +65,36 @@ def test_positive(self): self.assertTrue(can_sum_table(target, numbers)) +class TestCanSumEdgeCases(unittest.TestCase): + FUNCS = (can_sum_basic, can_sum_memo, can_sum_table) + + def test_negative_target(self): + for f in self.FUNCS: + self.assertFalse(f(-3, [1, 2]), f.__name__) + + def test_zero_target_and_empty_numbers(self): + for f in self.FUNCS: + self.assertTrue(f(0, []), f.__name__) + self.assertFalse(f(5, []), f.__name__) + + def test_non_positive_numbers_are_ignored(self): + for f in self.FUNCS: + self.assertFalse(f(7, [0, -2]), f.__name__) + self.assertTrue(f(7, [0, -1, 7]), f.__name__) + + def test_against_brute_force(self): + rng = random.Random(7) + for _ in range(200): + target = rng.randint(0, 25) + numbers = [rng.randint(1, 9) for _ in range(rng.randint(0, 4))] + reachable = [True] + [False] * target + for t in range(1, target + 1): + reachable[t] = any(n <= t and reachable[t - n] for n in numbers) + for f in self.FUNCS: + self.assertEqual( + f(target, numbers), reachable[target], (target, numbers) + ) + + if __name__ == "__main__": unittest.main() diff --git a/src/dynamic_programming/python/coins/src/coins.py b/src/dynamic_programming/python/coins/src/coins.py index 56ebcc51..9dd5cc4f 100644 --- a/src/dynamic_programming/python/coins/src/coins.py +++ b/src/dynamic_programming/python/coins/src/coins.py @@ -18,6 +18,8 @@ def coin_change_basic(amount: int, coins: List[int]) -> float: return 0 min_coins = float("inf") for coin in coins: + if coin <= 0: + continue num_coins = coin_change_basic(amount - coin, coins) + 1 min_coins = min(min_coins, num_coins) return min_coins @@ -49,6 +51,8 @@ def coin_change_memo( min_coins = float("inf") for coin in coins: + if coin <= 0: + continue num_coins = coin_change_memo(amount - coin, coins, memo) + 1 min_coins = min(min_coins, num_coins) @@ -67,10 +71,13 @@ def coin_change_tab(amount: int, coins: List[int]) -> float: Returns: Minimum number of coins needed, or float('inf') if impossible. """ + if amount < 0: + return float("inf") + table = [float("inf")] * (amount + 1) table[0] = 0 for i in range(amount + 1): for coin in coins: - if i >= coin: + if 0 < coin <= i: table[i] = min(table[i], table[i - coin] + 1) return table[amount] diff --git a/src/dynamic_programming/python/coins/tests/test_coins.py b/src/dynamic_programming/python/coins/tests/test_coins.py index 7a610dbc..16865fe0 100644 --- a/src/dynamic_programming/python/coins/tests/test_coins.py +++ b/src/dynamic_programming/python/coins/tests/test_coins.py @@ -82,5 +82,30 @@ def test_with_12(self): self.assertEqual(coin_change_tab(amount, coins), result) +class TestCoinChangeEdgeCases(unittest.TestCase): + FUNCS = (coin_change_basic, coin_change_memo, coin_change_tab) + + def test_negative_amount_is_impossible(self): + for f in self.FUNCS: + self.assertEqual(f(-5, [1, 2]), float("inf"), f.__name__) + + def test_impossible_amount(self): + for f in self.FUNCS: + self.assertEqual(f(3, [2]), float("inf"), f.__name__) + self.assertEqual(f(3, []), float("inf"), f.__name__) + + def test_zero_amount(self): + for f in self.FUNCS: + self.assertEqual(f(0, []), 0, f.__name__) + + def test_non_positive_coins_are_ignored(self): + for f in self.FUNCS: + self.assertEqual(f(4, [0, -1, 2]), 2, f.__name__) + + def test_greedy_fails(self): + for f in self.FUNCS: + self.assertEqual(f(6, [1, 3, 4]), 2, f.__name__) + + if __name__ == "__main__": unittest.main() diff --git a/src/dynamic_programming/python/count_construct/src/count_construct.py b/src/dynamic_programming/python/count_construct/src/count_construct.py index 833da832..be8e7a38 100644 --- a/src/dynamic_programming/python/count_construct/src/count_construct.py +++ b/src/dynamic_programming/python/count_construct/src/count_construct.py @@ -19,7 +19,7 @@ def count_construct_basic(target: str, word_bank: List[str]) -> int: count = 0 for word in word_bank: - if target.startswith(word): + if word and target.startswith(word): count += count_construct_basic(target[len(word) :], word_bank) return count @@ -50,7 +50,7 @@ def count_construct_memo( count = 0 for word in word_bank: - if target.startswith(word): + if word and target.startswith(word): count += count_construct_memo(target[len(word) :], word_bank, memo) memo[target] = count return count @@ -80,6 +80,6 @@ def count_construct_table(target: str, word_bank: List[str]) -> int: if counts[i] == 0: continue for word in word_bank: - if target[i : i + len(word)] == word: + if word and target[i : i + len(word)] == word: counts[i + len(word)] += counts[i] return counts[n] diff --git a/src/dynamic_programming/python/count_construct/tests/test_count_construct.py b/src/dynamic_programming/python/count_construct/tests/test_count_construct.py index 8cff1084..ca7c211a 100644 --- a/src/dynamic_programming/python/count_construct/tests/test_count_construct.py +++ b/src/dynamic_programming/python/count_construct/tests/test_count_construct.py @@ -86,5 +86,21 @@ def test_positive_2(self): self.assertEqual(count_construct_table(target, word_bank), result) +class TestCountConstructEdgeCases(unittest.TestCase): + FUNCS = (count_construct_basic, count_construct_memo, count_construct_table) + + def test_empty_target(self): + for f in self.FUNCS: + self.assertEqual(f("", ["a"]), 1, f.__name__) + + def test_empty_word_is_ignored(self): + for f in self.FUNCS: + self.assertEqual(f("ab", ["", "a", "b"]), 1, f.__name__) + + def test_overlapping_words(self): + for f in self.FUNCS: + self.assertEqual(f("aaaa", ["a", "aa"]), 5, f.__name__) + + if __name__ == "__main__": unittest.main() diff --git a/src/dynamic_programming/python/grid_traveler/src/grid_traveler.py b/src/dynamic_programming/python/grid_traveler/src/grid_traveler.py index 3f437dcf..acc5bdf9 100644 --- a/src/dynamic_programming/python/grid_traveler/src/grid_traveler.py +++ b/src/dynamic_programming/python/grid_traveler/src/grid_traveler.py @@ -14,7 +14,7 @@ def grid_traveller_basic(num_rows: int, num_columns: int) -> int: Returns: Number of unique paths from top-left to bottom-right. """ - if not num_rows or not num_columns: + if num_rows <= 0 or num_columns <= 0: return 0 if num_rows < 2 and num_columns < 2: @@ -44,7 +44,7 @@ def grid_traveller_memo( if memo is None: memo = {} - if not num_rows or not num_columns: + if num_rows <= 0 or num_columns <= 0: return 0 if num_rows < 2 and num_columns < 2: @@ -82,8 +82,8 @@ def grid_traveller_table(n: int, m: int) -> int: table = [[0 for _ in range(m + 1)] for _ in range(n + 1)] table[1][1] = 1 - for i in range(n + 1): - for j in range(m + 1): + for i in range(1, n + 1): + for j in range(1, m + 1): if i == 1 and j == 1: continue table[i][j] = table[i - 1][j] + table[i][j - 1] diff --git a/src/dynamic_programming/python/grid_traveler/tests/test_grid_traveler.py b/src/dynamic_programming/python/grid_traveler/tests/test_grid_traveler.py index fd17c703..d1c38781 100644 --- a/src/dynamic_programming/python/grid_traveler/tests/test_grid_traveler.py +++ b/src/dynamic_programming/python/grid_traveler/tests/test_grid_traveler.py @@ -85,5 +85,25 @@ def test_with_10_and_10(self): self.assertEqual(grid_traveller_table(m, n), result) +class TestGridTravelerEdgeCases(unittest.TestCase): + FUNCS = (grid_traveller_basic, grid_traveller_memo, grid_traveller_table) + + def test_non_positive_dimensions(self): + for f in self.FUNCS: + self.assertEqual(f(-1, 3), 0, f.__name__) + self.assertEqual(f(3, -2), 0, f.__name__) + self.assertEqual(f(0, 3), 0, f.__name__) + self.assertEqual(f(-1, -1), 0, f.__name__) + + def test_single_row_or_column(self): + for f in self.FUNCS: + self.assertEqual(f(1, 9), 1, f.__name__) + self.assertEqual(f(9, 1), 1, f.__name__) + + def test_large_grid_does_not_overflow(self): + self.assertEqual(grid_traveller_memo(18, 18), 2333606220) + self.assertEqual(grid_traveller_table(18, 18), 2333606220) + + if __name__ == "__main__": unittest.main() diff --git a/src/dynamic_programming/python/how_sum/src/how_sum.py b/src/dynamic_programming/python/how_sum/src/how_sum.py index 695e092b..6f3561f8 100644 --- a/src/dynamic_programming/python/how_sum/src/how_sum.py +++ b/src/dynamic_programming/python/how_sum/src/how_sum.py @@ -20,7 +20,7 @@ def how_sum_basic(target: int, numbers: List[int]) -> Optional[List[int]]: for number in numbers: remainder = target - number - if remainder >= 0: + if number > 0 and remainder >= 0: combination = how_sum_basic(remainder, numbers) if combination is not None: @@ -57,7 +57,7 @@ def how_sum_memo( for number in numbers: remainder = target - number - if remainder >= 0: + if number > 0 and remainder >= 0: combination = how_sum_memo(remainder, numbers, memo) if combination is not None: @@ -81,13 +81,16 @@ def how_sum_table(target: int, numbers: List[int]) -> Optional[List[int]]: Returns: A list of numbers that sum to target, or None if impossible. """ + if target < 0: + return None + table: List[Optional[List[int]]] = [None] * (target + 1) table[0] = [] for i in range(target): if table[i] is not None: for number in numbers: - if i + number <= target: + if 0 < number <= target - i: table[i + number] = table[i] + [number] return table[-1] diff --git a/src/dynamic_programming/python/how_sum/tests/test_how_sum.py b/src/dynamic_programming/python/how_sum/tests/test_how_sum.py index 75eff1eb..2827d8af 100644 --- a/src/dynamic_programming/python/how_sum/tests/test_how_sum.py +++ b/src/dynamic_programming/python/how_sum/tests/test_how_sum.py @@ -1,3 +1,4 @@ +import random import unittest @@ -94,5 +95,38 @@ def test_positive_2(self): self.assertTrue(any([result == elem for elem in expected])) +class TestHowSumEdgeCases(unittest.TestCase): + FUNCS = (how_sum_basic, how_sum_memo, how_sum_table) + + def test_negative_target(self): + for f in self.FUNCS: + self.assertIsNone(f(-1, [1, 2]), f.__name__) + + def test_zero_target(self): + for f in self.FUNCS: + self.assertEqual(f(0, []), [], f.__name__) + + def test_non_positive_numbers_are_ignored(self): + for f in self.FUNCS: + self.assertIsNone(f(5, [0, -1]), f.__name__) + self.assertEqual(sum(f(5, [0, 5])), 5, f.__name__) + + def test_against_brute_force(self): + rng = random.Random(11) + for _ in range(200): + target = rng.randint(0, 20) + numbers = [rng.randint(1, 8) for _ in range(rng.randint(0, 4))] + reachable = [True] + [False] * target + for t in range(1, target + 1): + reachable[t] = any(n <= t and reachable[t - n] for n in numbers) + for f in self.FUNCS: + result = f(target, numbers) + if reachable[target]: + self.assertEqual(sum(result), target, (f.__name__, target, numbers)) + self.assertTrue(all(n in numbers for n in result)) + else: + self.assertIsNone(result, (f.__name__, target, numbers)) + + if __name__ == "__main__": unittest.main() diff --git a/src/dynamic_programming/python/kmp/src/kmp.py b/src/dynamic_programming/python/kmp/src/kmp.py index 6ddb7359..7d56fe15 100644 --- a/src/dynamic_programming/python/kmp/src/kmp.py +++ b/src/dynamic_programming/python/kmp/src/kmp.py @@ -34,15 +34,18 @@ def find_longest_prefix_suffix(word: str) -> List[int]: i += 1 return result + if not pattern: + # The empty pattern matches at every position (like str.find). + return list(range(len(text) + 1)) + longest_prefix_suffix = find_longest_prefix_suffix(pattern) result: List[int] = [] j = 0 for i, c in enumerate(text): + while j != 0 and c != pattern[j]: + j = longest_prefix_suffix[j - 1] if c == pattern[j]: j += 1 - else: - if j != 0: - j = longest_prefix_suffix[j - 1] if j == len(pattern): result.append(i - j + 1) j = longest_prefix_suffix[j - 1] diff --git a/src/dynamic_programming/python/kmp/tests/test_kmp.py b/src/dynamic_programming/python/kmp/tests/test_kmp.py index c370bbb0..f094f660 100644 --- a/src/dynamic_programming/python/kmp/tests/test_kmp.py +++ b/src/dynamic_programming/python/kmp/tests/test_kmp.py @@ -1,3 +1,4 @@ +import random import unittest @@ -24,5 +25,37 @@ def test_multiple_match(self): self.assertEqual(search(text, pattern), result) +class TestKMPEdgeCases(unittest.TestCase): + def test_fallback_needs_more_than_one_step(self): + # After the mismatch at the final "b" the match state must fall back + # more than once before the current character can be compared. + self.assertEqual(search("aaabaaaab", "aaaab"), [4]) + + def test_overlapping_matches(self): + self.assertEqual(search("aaaa", "aa"), [0, 1, 2]) + + def test_empty_pattern_matches_everywhere(self): + self.assertEqual(search("abc", ""), [0, 1, 2, 3]) + self.assertEqual(search("", ""), [0]) + + def test_empty_text(self): + self.assertEqual(search("", "a"), []) + + def test_pattern_longer_than_text(self): + self.assertEqual(search("ab", "abc"), []) + + def test_against_brute_force(self): + rng = random.Random(1234) + for _ in range(500): + text = "".join(rng.choice("ab") for _ in range(rng.randint(0, 20))) + pattern = "".join(rng.choice("ab") for _ in range(rng.randint(1, 6))) + expected = [ + i + for i in range(len(text) - len(pattern) + 1) + if text.startswith(pattern, i) + ] + self.assertEqual(search(text, pattern), expected, (text, pattern)) + + if __name__ == "__main__": unittest.main() diff --git a/src/graphs/cpp/a_star/CMakeLists.txt b/src/graphs/cpp/a_star/CMakeLists.txt index 2ccb6a0b..4ae01729 100644 --- a/src/graphs/cpp/a_star/CMakeLists.txt +++ b/src/graphs/cpp/a_star/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(project-aStar CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/graphs/cpp/a_star/src/a_star.cpp b/src/graphs/cpp/a_star/src/a_star.cpp index 363dea45..f877253e 100644 --- a/src/graphs/cpp/a_star/src/a_star.cpp +++ b/src/graphs/cpp/a_star/src/a_star.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include // A* search algorithm with heuristic-guided exploration @@ -29,22 +30,22 @@ int aStar(const Graph &graph, Vertex source, Vertex destination, gScore[source] = 0; fScore[source] = heuristic(source.value, destination.value); - // Min-heap ordered by fScore - auto cmp = [&](const Vertex &l, const Vertex &r) { - return fScore[l] > fScore[r]; - }; + // Min-heap of (f, vertex). Entries are never updated in place; when a better + // path is found a new entry is pushed and the outdated one is skipped on pop. + using Entry = std::pair>; + auto cmp = [](const Entry &l, const Entry &r) { return l.first > r.first; }; - std::priority_queue, std::vector>, decltype(cmp)> openSet( - cmp); - - std::unordered_map, bool, HashFunction> inOpenSet; - openSet.push(source); - inOpenSet[source] = true; + std::priority_queue, decltype(cmp)> openSet(cmp); + openSet.push({fScore[source], source}); while (!openSet.empty()) { - Vertex current = openSet.top(); + Entry top = openSet.top(); openSet.pop(); - inOpenSet[current] = false; + const Vertex current = top.second; + + // Skip stale entries + if (top.first > fScore[current]) + continue; // Goal reached if (current == destination) @@ -58,11 +59,7 @@ int aStar(const Graph &graph, Vertex source, Vertex destination, gScore[neighbor] = tentativeGScore; fScore[neighbor] = tentativeGScore + heuristic(neighbor.value, destination.value); - - if (!inOpenSet[neighbor]) { - openSet.push(neighbor); - inOpenSet[neighbor] = true; - } + openSet.push({fScore[neighbor], neighbor}); } } } diff --git a/src/graphs/cpp/a_star/src/graph.cpp b/src/graphs/cpp/a_star/src/graph.cpp index ac9025d3..afbc6741 100644 --- a/src/graphs/cpp/a_star/src/graph.cpp +++ b/src/graphs/cpp/a_star/src/graph.cpp @@ -15,7 +15,7 @@ bool Point::operator!=(const Point &other) const { } bool Point::operator<(const Point &other) const { - return x < other.x && y < other.y; + return x < other.x || (x == other.x && y < other.y); } template Vertex::Vertex(T _value) : value(_value) {} @@ -139,6 +139,12 @@ bool Graph::connected(const Vertex source, return true; } + // Edges are stored one-way, but "connected" is symmetric (as in Python). + for (auto &edge : adjcDict.at(destination)) { + if (edge.destination == source) + return true; + } + return false; } diff --git a/src/graphs/cpp/a_star/test/test_a_star.cpp b/src/graphs/cpp/a_star/test/test_a_star.cpp index d45825d4..84d27dde 100644 --- a/src/graphs/cpp/a_star/test/test_a_star.cpp +++ b/src/graphs/cpp/a_star/test/test_a_star.cpp @@ -30,3 +30,37 @@ TEST(TestAStar, PointVertices) { EXPECT_EQ(aStar(graph, Vertex(b), Vertex(e), distance), INT_MAX); } + +namespace { +double zero(int, int) { return 0.0; } +double zeroPoint(Point, Point) { return 0.0; } +} // namespace + +TEST(TestAStar, SourceEqualsDestinationAndUnknownVertex) { + Graph graph; + graph.addEdge(Vertex(Point(0, 0)), Vertex(Point(1, 0)), 2); + EXPECT_EQ(aStar(graph, Vertex(Point(0, 0)), Vertex(Point(0, 0)), + zeroPoint), + 0); + EXPECT_EQ(aStar(graph, Vertex(Point(0, 0)), Vertex(Point(9, 9)), + zeroPoint), + INT_MAX); +} + +TEST(TestAStar, ImprovedPathToQueuedVertex) { + Graph graph; + auto V = [](int x) { return Vertex(Point(x, 0)); }; + graph.addEdge(V(0), V(1), 1); + graph.addEdge(V(0), V(2), 10); + graph.addEdge(V(1), V(2), 1); + graph.addEdge(V(2), V(3), 1); + graph.addEdge(V(0), V(3), 20); + EXPECT_EQ(aStar(graph, V(0), V(3), zeroPoint), 3); +} + +TEST(TestAStar, PointOrderingIsStrictWeak) { + EXPECT_TRUE(Point(1, 1) < Point(1, 2)); + EXPECT_TRUE(Point(1, 5) < Point(2, 0)); + EXPECT_FALSE(Point(2, 0) < Point(1, 5)); + EXPECT_FALSE(Point(1, 1) < Point(1, 1)); +} diff --git a/src/graphs/cpp/bellman_ford/CMakeLists.txt b/src/graphs/cpp/bellman_ford/CMakeLists.txt index b19cc24e..7634a633 100644 --- a/src/graphs/cpp/bellman_ford/CMakeLists.txt +++ b/src/graphs/cpp/bellman_ford/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(project-bellman-ford CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/graphs/cpp/bellman_ford/src/bellman_ford.cpp b/src/graphs/cpp/bellman_ford/src/bellman_ford.cpp index e0fd2c02..caa6734f 100644 --- a/src/graphs/cpp/bellman_ford/src/bellman_ford.cpp +++ b/src/graphs/cpp/bellman_ford/src/bellman_ford.cpp @@ -2,7 +2,8 @@ #include #include -// Bellman-Ford algorithm: handles negative edge weights and detects negative cycles +// Bellman-Ford algorithm: handles negative edge weights and detects negative +// cycles template int bellmanFord(const Graph &graph, Vertex source, Vertex destination) { @@ -11,9 +12,6 @@ int bellmanFord(const Graph &graph, Vertex source, if (!graph.contains(source) || !graph.contains(destination)) return INF; - if (source == destination) - return 0; - std::unordered_map, int, HashFunction> distances; for (const auto &vertex : graph.vertices()) { diff --git a/src/graphs/cpp/bellman_ford/src/graph.cpp b/src/graphs/cpp/bellman_ford/src/graph.cpp index ab2af74d..b755c889 100644 --- a/src/graphs/cpp/bellman_ford/src/graph.cpp +++ b/src/graphs/cpp/bellman_ford/src/graph.cpp @@ -117,6 +117,12 @@ bool Graph::connected(const Vertex source, return true; } + // Edges are stored one-way, but "connected" is symmetric (as in Python). + for (auto &edge : adjcDict.at(destination)) { + if (edge.destination == source) + return true; + } + return false; } diff --git a/src/graphs/cpp/bellman_ford/test/test_bellman_ford.cpp b/src/graphs/cpp/bellman_ford/test/test_bellman_ford.cpp index 709c2505..9cc24d0a 100644 --- a/src/graphs/cpp/bellman_ford/test/test_bellman_ford.cpp +++ b/src/graphs/cpp/bellman_ford/test/test_bellman_ford.cpp @@ -1,6 +1,9 @@ #include "bellman_ford.h" #include "gtest/gtest.h" +#include #include +#include +#include TEST(TestBellmanFord, charVertices) { Graph graph; @@ -51,3 +54,83 @@ TEST(TestBellmanFord, nonExistentVertices) { EXPECT_EQ(bellmanFord(graph, Vertex(1), Vertex(4)), INT_MAX); EXPECT_EQ(bellmanFord(graph, Vertex(1), Vertex(5)), INT_MAX); } + +namespace { +struct RawEdge { + int u, v, w; +}; + +std::vector> floyd(int n, + const std::vector &edges) { + const long long INF = 1LL << 50; + std::vector> d(n, std::vector(n, INF)); + for (int i = 0; i < n; ++i) + d[i][i] = 0; + for (const auto &e : edges) + d[e.u][e.v] = std::min(d[e.u][e.v], e.w); + for (int k = 0; k < n; ++k) + for (int i = 0; i < n; ++i) + for (int j = 0; j < n; ++j) + if (d[i][k] + d[k][j] < d[i][j]) + d[i][j] = d[i][k] + d[k][j]; + return d; +} +} // namespace + +TEST(TestBellmanFord, NegativeEdgesWithoutCycle) { + Graph graph; + graph.addEdge(Vertex(0), Vertex(1), 4); + graph.addEdge(Vertex(0), Vertex(2), 5); + graph.addEdge(Vertex(2), Vertex(1), -3); + EXPECT_EQ(bellmanFord(graph, Vertex(0), Vertex(1)), 2); +} + +TEST(TestBellmanFord, NegativeCycleThroughSourceIsDetected) { + Graph graph; + graph.addEdge(Vertex(0), Vertex(1), -2); + graph.addEdge(Vertex(1), Vertex(0), 1); + EXPECT_EQ(bellmanFord(graph, Vertex(0), Vertex(0)), INT_MAX); + EXPECT_EQ(bellmanFord(graph, Vertex(0), Vertex(1)), INT_MAX); +} + +TEST(TestBellmanFord, UnreachableNegativeCycleIsIgnored) { + Graph graph; + graph.addEdge(Vertex(0), Vertex(1), 3); + graph.addEdge(Vertex(5), Vertex(6), -4); + graph.addEdge(Vertex(6), Vertex(5), 1); + EXPECT_EQ(bellmanFord(graph, Vertex(0), Vertex(1)), 3); + EXPECT_EQ(bellmanFord(graph, Vertex(0), Vertex(0)), 0); + EXPECT_EQ(bellmanFord(graph, Vertex(0), Vertex(6)), INT_MAX); +} + +TEST(TestBellmanFord, RandomGraphsMatchFloydWarshall) { + const long long INF = 1LL << 50; + std::mt19937 rng(5); + for (int iter = 0; iter < 150; ++iter) { + int n = 1 + rng() % 6; + int m = rng() % 13; + std::vector edges; + Graph graph; + for (int v = 0; v < n; ++v) + graph.addVertex(Vertex(v)); + for (int i = 0; i < m; ++i) { + RawEdge e{int(rng() % n), int(rng() % n), int(rng() % 13) - 3}; + edges.push_back(e); + graph.addEdge(Vertex(e.u), Vertex(e.v), e.w); + } + // Floyd-Warshall values can blow up on negative cycles; clamp to detect + // them. + auto d = floyd(n, edges); + for (int s = 0; s < n; ++s) { + bool negCycle = false; + for (int v = 0; v < n; ++v) + if (d[s][v] < INF / 2 && d[v][v] < 0) + negCycle = true; + for (int t = 0; t < n; ++t) { + int expected = + (negCycle || d[s][t] >= INF / 2) ? INT_MAX : int(d[s][t]); + EXPECT_EQ(bellmanFord(graph, Vertex(s), Vertex(t)), expected); + } + } + } +} diff --git a/src/graphs/cpp/bfs/CMakeLists.txt b/src/graphs/cpp/bfs/CMakeLists.txt index d0fc6b3e..90f15d62 100644 --- a/src/graphs/cpp/bfs/CMakeLists.txt +++ b/src/graphs/cpp/bfs/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(project-bfs CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/graphs/cpp/bfs/src/graph.cpp b/src/graphs/cpp/bfs/src/graph.cpp index ab2af74d..b755c889 100644 --- a/src/graphs/cpp/bfs/src/graph.cpp +++ b/src/graphs/cpp/bfs/src/graph.cpp @@ -117,6 +117,12 @@ bool Graph::connected(const Vertex source, return true; } + // Edges are stored one-way, but "connected" is symmetric (as in Python). + for (auto &edge : adjcDict.at(destination)) { + if (edge.destination == source) + return true; + } + return false; } diff --git a/src/graphs/cpp/dfs/CMakeLists.txt b/src/graphs/cpp/dfs/CMakeLists.txt index 415e5592..17a6177a 100644 --- a/src/graphs/cpp/dfs/CMakeLists.txt +++ b/src/graphs/cpp/dfs/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(project-dfs CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/graphs/cpp/dfs/src/dfs.cpp b/src/graphs/cpp/dfs/src/dfs.cpp index 0ea0b3db..337a628f 100644 --- a/src/graphs/cpp/dfs/src/dfs.cpp +++ b/src/graphs/cpp/dfs/src/dfs.cpp @@ -1,7 +1,7 @@ #include "dfs.h" -#include #include #include +#include // Depth-First Search using recursive approach template @@ -24,29 +24,42 @@ int dfs(const Graph &graph, Vertex source, Vertex destination) { distances[source] = 0; - // Recursive DFS helper with early termination support - std::function)> search; - search = [&](Vertex u) -> bool { - visited[u] = true; + // Iterative DFS with an explicit stack (same visiting order as the recursive + // version) so that long paths cannot overflow the call stack. + struct Frame { + Vertex vertex; + std::vector> edges; + size_t next; + }; - // Early termination when destination is found - if (u == destination) - return true; + std::vector stack; + visited[source] = true; + stack.push_back({source, graph.edgesFromVertex(source), 0}); - for (const auto &edge : graph.edgesFromVertex(u)) { - const auto &v = edge.destination; + while (!stack.empty()) { + Frame &frame = stack.back(); - if (!visited[v]) { - distances[v] = distances[u] + edge.distance; - if (search(v)) - return true; - } + if (frame.next == frame.edges.size()) { + stack.pop_back(); + continue; } - return false; - }; + const Edge edge = frame.edges[frame.next++]; + const Vertex u = frame.vertex; + const Vertex v = edge.destination; + + if (visited[v]) + continue; - search(source); + visited[v] = true; + distances[v] = distances[u] + edge.distance; + + // Early termination when destination is found + if (v == destination) + break; + + stack.push_back({v, graph.edgesFromVertex(v), 0}); + } return distances[destination]; } diff --git a/src/graphs/cpp/dfs/src/graph.cpp b/src/graphs/cpp/dfs/src/graph.cpp index ab2af74d..b755c889 100644 --- a/src/graphs/cpp/dfs/src/graph.cpp +++ b/src/graphs/cpp/dfs/src/graph.cpp @@ -117,6 +117,12 @@ bool Graph::connected(const Vertex source, return true; } + // Edges are stored one-way, but "connected" is symmetric (as in Python). + for (auto &edge : adjcDict.at(destination)) { + if (edge.destination == source) + return true; + } + return false; } diff --git a/src/graphs/cpp/dfs/test/test_dfs.cpp b/src/graphs/cpp/dfs/test/test_dfs.cpp index bfba44a4..6b7b7cb2 100644 --- a/src/graphs/cpp/dfs/test/test_dfs.cpp +++ b/src/graphs/cpp/dfs/test/test_dfs.cpp @@ -1,6 +1,9 @@ #include "dfs.h" #include "gtest/gtest.h" +#include #include +#include +#include TEST(TestDfs, charVertices) { Graph graph; @@ -50,3 +53,48 @@ TEST(TestDfs, nonExistentVertices) { EXPECT_EQ(dfs(graph, Vertex(1), Vertex(4)), INT_MAX); EXPECT_EQ(dfs(graph, Vertex(1), Vertex(5)), INT_MAX); } + +TEST(TestDFS, DeepPathDoesNotOverflowStack) { + Graph graph; + const int n = 200000; + for (int i = 0; i < n; ++i) + graph.addEdge(Vertex(i), Vertex(i + 1), 1); + EXPECT_EQ(dfs(graph, Vertex(0), Vertex(n)), n); +} + +TEST(TestDFS, CycleAndSourceEqualsDestination) { + Graph graph; + graph.addEdge(Vertex(0), Vertex(1), 1); + graph.addEdge(Vertex(1), Vertex(0), 1); + EXPECT_EQ(dfs(graph, Vertex(0), Vertex(0)), 0); + EXPECT_EQ(dfs(graph, Vertex(0), Vertex(1)), 1); + EXPECT_EQ(dfs(graph, Vertex(0), Vertex(5)), INT_MAX); +} + +TEST(TestDFS, ReachabilityMatchesBruteForce) { + std::mt19937 rng(3); + for (int iter = 0; iter < 100; ++iter) { + int n = 1 + rng() % 7; + int m = rng() % 13; + Graph graph; + std::vector> reach(n, std::vector(n, false)); + for (int v = 0; v < n; ++v) { + graph.addVertex(Vertex(v)); + reach[v][v] = true; + } + for (int i = 0; i < m; ++i) { + int u = rng() % n, v = rng() % n; + graph.addEdge(Vertex(u), Vertex(v), 1 + rng() % 5); + reach[u][v] = true; + } + for (int k = 0; k < n; ++k) + for (int i = 0; i < n; ++i) + for (int j = 0; j < n; ++j) + if (reach[i][k] && reach[k][j]) + reach[i][j] = true; + for (int s = 0; s < n; ++s) + for (int t = 0; t < n; ++t) + EXPECT_EQ(dfs(graph, Vertex(s), Vertex(t)) != INT_MAX, + bool(reach[s][t])); + } +} diff --git a/src/graphs/cpp/dijkstra/CMakeLists.txt b/src/graphs/cpp/dijkstra/CMakeLists.txt index 8f79e6bb..88f3820b 100644 --- a/src/graphs/cpp/dijkstra/CMakeLists.txt +++ b/src/graphs/cpp/dijkstra/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(project-dijkstra CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/graphs/cpp/dijkstra/src/graph.cpp b/src/graphs/cpp/dijkstra/src/graph.cpp index ab2af74d..b755c889 100644 --- a/src/graphs/cpp/dijkstra/src/graph.cpp +++ b/src/graphs/cpp/dijkstra/src/graph.cpp @@ -117,6 +117,12 @@ bool Graph::connected(const Vertex source, return true; } + // Edges are stored one-way, but "connected" is symmetric (as in Python). + for (auto &edge : adjcDict.at(destination)) { + if (edge.destination == source) + return true; + } + return false; } diff --git a/src/graphs/cpp/dijkstra/test/test_dijkstra.cpp b/src/graphs/cpp/dijkstra/test/test_dijkstra.cpp index 0cb3f543..f095652b 100644 --- a/src/graphs/cpp/dijkstra/test/test_dijkstra.cpp +++ b/src/graphs/cpp/dijkstra/test/test_dijkstra.cpp @@ -1,6 +1,9 @@ #include "dijkstra.h" #include "gtest/gtest.h" +#include #include +#include +#include TEST(TestDijkstra, charVertices) { Graph graph; @@ -51,3 +54,63 @@ TEST(TestDijkstra, nonExistentVertices) { EXPECT_EQ(dijkstra(graph, Vertex(1), Vertex(4)), INT_MAX); EXPECT_EQ(dijkstra(graph, Vertex(1), Vertex(5)), INT_MAX); } + +namespace { +struct RawEdge { + int u, v, w; +}; + +std::vector> floyd(int n, + const std::vector &edges) { + const long long INF = 1LL << 50; + std::vector> d(n, std::vector(n, INF)); + for (int i = 0; i < n; ++i) + d[i][i] = 0; + for (const auto &e : edges) + d[e.u][e.v] = std::min(d[e.u][e.v], e.w); + for (int k = 0; k < n; ++k) + for (int i = 0; i < n; ++i) + for (int j = 0; j < n; ++j) + if (d[i][k] + d[k][j] < d[i][j]) + d[i][j] = d[i][k] + d[k][j]; + return d; +} +} // namespace + +TEST(TestDijkstra, SourceEqualsDestinationAndUnknownVertex) { + Graph graph; + graph.addEdge(Vertex(1), Vertex(2), 3); + EXPECT_EQ(dijkstra(graph, Vertex(1), Vertex(1)), 0); + EXPECT_EQ(dijkstra(graph, Vertex(1), Vertex(7)), INT_MAX); +} + +TEST(TestDijkstra, ParallelEdgesAndZeroWeights) { + Graph graph; + graph.addEdge(Vertex(1), Vertex(2), 5); + graph.addEdge(Vertex(1), Vertex(2), 2); + graph.addEdge(Vertex(2), Vertex(3), 0); + EXPECT_EQ(dijkstra(graph, Vertex(1), Vertex(3)), 2); +} + +TEST(TestDijkstra, RandomGraphsMatchFloydWarshall) { + std::mt19937 rng(11); + for (int iter = 0; iter < 100; ++iter) { + int n = 1 + rng() % 7; + int m = rng() % 16; + std::vector edges; + Graph graph; + for (int v = 0; v < n; ++v) + graph.addVertex(Vertex(v)); + for (int i = 0; i < m; ++i) { + RawEdge e{int(rng() % n), int(rng() % n), int(rng() % 10)}; + edges.push_back(e); + graph.addEdge(Vertex(e.u), Vertex(e.v), e.w); + } + auto d = floyd(n, edges); + for (int s = 0; s < n; ++s) + for (int t = 0; t < n; ++t) { + int expected = d[s][t] >= (1LL << 50) ? INT_MAX : int(d[s][t]); + EXPECT_EQ(dijkstra(graph, Vertex(s), Vertex(t)), expected); + } + } +} diff --git a/src/graphs/cpp/graph/CMakeLists.txt b/src/graphs/cpp/graph/CMakeLists.txt index 2735e85d..86d11975 100644 --- a/src/graphs/cpp/graph/CMakeLists.txt +++ b/src/graphs/cpp/graph/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-graph CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/graphs/cpp/graph/src/graph.cpp b/src/graphs/cpp/graph/src/graph.cpp index ab2af74d..b755c889 100644 --- a/src/graphs/cpp/graph/src/graph.cpp +++ b/src/graphs/cpp/graph/src/graph.cpp @@ -117,6 +117,12 @@ bool Graph::connected(const Vertex source, return true; } + // Edges are stored one-way, but "connected" is symmetric (as in Python). + for (auto &edge : adjcDict.at(destination)) { + if (edge.destination == source) + return true; + } + return false; } diff --git a/src/graphs/cpp/graph/test/test_graph.cpp b/src/graphs/cpp/graph/test/test_graph.cpp index 5d66eb63..687a391f 100644 --- a/src/graphs/cpp/graph/test/test_graph.cpp +++ b/src/graphs/cpp/graph/test/test_graph.cpp @@ -197,3 +197,18 @@ TEST(Graph, size) { graph.addEdge(Vertex(3), Vertex(1), 10); EXPECT_EQ(graph.size(), 5); } + +TEST(Graph, ConnectedIsSymmetric) { + Graph graph; + graph.addEdge(Vertex(1), Vertex(9), 8); + graph.addEdge(Vertex(5), Vertex(4), 5); + graph.addEdge(Vertex(3), Vertex(1), -5); + + EXPECT_TRUE(graph.connected(Vertex(1), Vertex(9))); + EXPECT_TRUE(graph.connected(Vertex(9), Vertex(1))); + EXPECT_TRUE(graph.connected(Vertex(1), Vertex(3))); + EXPECT_TRUE(graph.connected(Vertex(3), Vertex(1))); + EXPECT_FALSE(graph.connected(Vertex(1), Vertex(5))); + EXPECT_FALSE(graph.connected(Vertex(9), Vertex(4))); + EXPECT_FALSE(graph.connected(Vertex(1), Vertex(100))); +} diff --git a/src/graphs/cpp/kruskal/CMakeLists.txt b/src/graphs/cpp/kruskal/CMakeLists.txt index 6d95e7a1..bc35cee9 100644 --- a/src/graphs/cpp/kruskal/CMakeLists.txt +++ b/src/graphs/cpp/kruskal/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(project-kruskal CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/graphs/cpp/kruskal/src/graph.cpp b/src/graphs/cpp/kruskal/src/graph.cpp index 6fb299a2..5be5136d 100644 --- a/src/graphs/cpp/kruskal/src/graph.cpp +++ b/src/graphs/cpp/kruskal/src/graph.cpp @@ -120,6 +120,12 @@ bool Graph::connected(const Vertex source, return true; } + // Edges are stored one-way, but "connected" is symmetric (as in Python). + for (auto &edge : adjcDict.at(destination)) { + if (edge.destination == source) + return true; + } + return false; } diff --git a/src/graphs/cpp/kruskal/test/test_kruskal.cpp b/src/graphs/cpp/kruskal/test/test_kruskal.cpp index a7916502..576bdce4 100644 --- a/src/graphs/cpp/kruskal/test/test_kruskal.cpp +++ b/src/graphs/cpp/kruskal/test/test_kruskal.cpp @@ -1,5 +1,6 @@ #include "kruskal.h" #include "gtest/gtest.h" +#include TEST(TestKruskal, nonEmptyGraph) { Graph graph; @@ -31,3 +32,25 @@ TEST(TestKruskal, nonEmptyGraph) { EXPECT_EQ(kruskal(graph), 27); } + +TEST(TestKruskal, EmptyGraph) { + Graph graph; + EXPECT_EQ(kruskal(graph), 0); +} + +TEST(TestKruskal, DisconnectedGraphReturnsForestWeight) { + Graph graph; + graph.addEdge(Vertex(0), Vertex(1), 2); + graph.addEdge(Vertex(2), Vertex(3), 5); + graph.addVertex(Vertex(9)); + EXPECT_EQ(kruskal(graph), 7); +} + +TEST(TestKruskal, SelfLoopsDirectionAndNegativeWeights) { + Graph graph; + graph.addEdge(Vertex(0), Vertex(0), 4); + graph.addEdge(Vertex(1), Vertex(0), 3); + graph.addEdge(Vertex(1), Vertex(2), -1); + graph.addEdge(Vertex(2), Vertex(0), 4); + EXPECT_EQ(kruskal(graph), 2); +} diff --git a/src/graphs/cpp/prim/CMakeLists.txt b/src/graphs/cpp/prim/CMakeLists.txt index a9c8f587..fc443d25 100644 --- a/src/graphs/cpp/prim/CMakeLists.txt +++ b/src/graphs/cpp/prim/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.14 FATAL_ERROR) project(project-prim CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/graphs/cpp/prim/src/graph.cpp b/src/graphs/cpp/prim/src/graph.cpp index 8e2b0d28..5b517ac4 100644 --- a/src/graphs/cpp/prim/src/graph.cpp +++ b/src/graphs/cpp/prim/src/graph.cpp @@ -118,6 +118,12 @@ bool Graph::connected(const Vertex source, return true; } + // Edges are stored one-way, but "connected" is symmetric (as in Python). + for (auto &edge : adjcDict.at(destination)) { + if (edge.destination == source) + return true; + } + return false; } diff --git a/src/graphs/cpp/prim/src/prim.cpp b/src/graphs/cpp/prim/src/prim.cpp index 70cc6bfd..2f718e27 100644 --- a/src/graphs/cpp/prim/src/prim.cpp +++ b/src/graphs/cpp/prim/src/prim.cpp @@ -1,61 +1,72 @@ #include "prim.h" #include #include +#include #include -// Prim's algorithm using a priority queue for efficient minimum edge selection +// Prim's algorithm using a priority queue for efficient minimum edge selection. +// Edges are treated as undirected and, for a disconnected graph, the weight of +// the minimum spanning forest is returned (consistent with Kruskal). template int prim(const Graph &graph) { if (graph.empty()) return 0; - auto vertices = graph.vertices(); - if (vertices.empty()) - return 0; + const auto vertices = graph.vertices(); + + // Undirected adjacency view, since edges are stored one-way + using Neighbor = std::pair>; + std::unordered_map, std::vector, HashFunction> + adjacency; + + for (const auto &vertex : vertices) + adjacency[vertex] = {}; + + for (const auto &edge : graph.edges()) { + adjacency[edge.source].push_back({edge.distance, edge.destination}); + adjacency[edge.destination].push_back({edge.distance, edge.source}); + } std::unordered_map, bool, HashFunction> inMST; for (const auto &vertex : vertices) inMST[vertex] = false; - // Start from the first vertex - Vertex source = vertices.front(); - inMST[source] = true; - - // Min-heap of edges ordered by weight - auto cmp = [](const Edge &a, const Edge &b) { - return a.distance > b.distance; + // Min-heap of (weight, vertex) + auto cmp = [](const Neighbor &a, const Neighbor &b) { + return a.first > b.first; }; - std::priority_queue, std::vector>, decltype(cmp)> minHeap( - cmp); + int mstWeight = 0; - // Add all edges from source to the heap - for (const auto &edge : graph.edgesFromVertex(source)) - minHeap.push(edge); + for (const auto &start : vertices) { + if (inMST[start]) + continue; - int mstWeight = 0; - size_t verticesInMST = 1; - const size_t totalVertices = vertices.size(); + inMST[start] = true; - while (!minHeap.empty() && verticesInMST < totalVertices) { - Edge minEdge = minHeap.top(); - minHeap.pop(); + std::priority_queue, decltype(cmp)> minHeap( + cmp); - const auto &v = minEdge.destination; + for (const auto &neighbor : adjacency[start]) + minHeap.push(neighbor); - // Skip if vertex is already in MST - if (inMST[v]) - continue; + while (!minHeap.empty()) { + const Neighbor next = minHeap.top(); + minHeap.pop(); + + const auto &v = next.second; + + // Skip if vertex is already in MST + if (inMST[v]) + continue; - // Add vertex to MST - inMST[v] = true; - mstWeight += minEdge.distance; - verticesInMST++; + inMST[v] = true; + mstWeight += next.first; - // Add all edges from newly added vertex - for (const auto &edge : graph.edgesFromVertex(v)) { - if (!inMST[edge.destination]) - minHeap.push(edge); + for (const auto &neighbor : adjacency[v]) { + if (!inMST[neighbor.second]) + minHeap.push(neighbor); + } } } diff --git a/src/graphs/cpp/prim/test/test_prim.cpp b/src/graphs/cpp/prim/test/test_prim.cpp index 8ce9826a..c4b057e7 100644 --- a/src/graphs/cpp/prim/test/test_prim.cpp +++ b/src/graphs/cpp/prim/test/test_prim.cpp @@ -1,5 +1,6 @@ #include "prim.h" #include "gtest/gtest.h" +#include TEST(TestPrim, nonEmptyGraph) { Graph graph; @@ -31,3 +32,33 @@ TEST(TestPrim, nonEmptyGraph) { EXPECT_EQ(prim(graph), 27); } + +TEST(TestPrim, EmptyGraph) { + Graph graph; + EXPECT_EQ(prim(graph), 0); +} + +TEST(TestPrim, DisconnectedGraphReturnsForestWeight) { + Graph graph; + graph.addEdge(Vertex(0), Vertex(1), 2); + graph.addEdge(Vertex(2), Vertex(3), 5); + graph.addVertex(Vertex(9)); + EXPECT_EQ(prim(graph), 7); +} + +TEST(TestPrim, EdgesPointingIntoAVertexAreUsed) { + Graph graph; + graph.addEdge(Vertex(0), Vertex(1), 1); + graph.addEdge(Vertex(2), Vertex(1), 2); + graph.addEdge(Vertex(3), Vertex(2), 4); + EXPECT_EQ(prim(graph), 7); +} + +TEST(TestPrim, SelfLoopsAndNegativeWeights) { + Graph graph; + graph.addEdge(Vertex(0), Vertex(0), 4); + graph.addEdge(Vertex(1), Vertex(0), 3); + graph.addEdge(Vertex(1), Vertex(2), -1); + graph.addEdge(Vertex(2), Vertex(0), 4); + EXPECT_EQ(prim(graph), 2); +} diff --git a/src/graphs/python/a_star/src/a_star.py b/src/graphs/python/a_star/src/a_star.py index 82c02099..cf39997f 100644 --- a/src/graphs/python/a_star/src/a_star.py +++ b/src/graphs/python/a_star/src/a_star.py @@ -1,73 +1,80 @@ -"""A* search algorithm implementation.""" - -from __future__ import annotations - -import math -from heapq import heappush, heappop -from typing import Callable, Any - -from graph import Graph, Vertex - - -def a_star( - graph: Graph, - source: Vertex, - destination: Vertex, - heuristic: Callable[[Any, Any], float], -) -> float: - """ - Find the shortest path between source and destination using A* algorithm. - - Args: - graph: The graph to search. - source: The starting vertex. - destination: The target vertex. - heuristic: A function that estimates the distance between two vertex values. - - Returns: - The shortest distance from source to destination, or infinity if no path exists. - """ - if not (graph.contains(source) and graph.contains(destination)): - return math.inf - - distance_from_source: dict[Vertex, float] = {} - distance_till_destination: dict[Vertex, float] = {} - - for vertex in graph.vertices(): - distance_from_source[vertex] = math.inf - distance_till_destination[vertex] = math.inf - - distance_from_source[source] = 0 - distance_till_destination[source] = heuristic(source.value, destination.value) - - open_set: list[tuple[float, Vertex]] = [] - heappush(open_set, (distance_till_destination[source], source)) - in_open_set: set[Vertex] = {source} - - while open_set: - current = heappop(open_set)[1] - in_open_set.discard(current) - - if current == destination: - return distance_from_source[destination] - - for edge in graph.edges_from_vertex(current): - tentative_distance_from_source = ( - distance_from_source[current] + edge.distance - ) - - if tentative_distance_from_source < distance_from_source[edge.destination]: - distance_from_source[edge.destination] = tentative_distance_from_source - distance_till_destination[edge.destination] = ( - distance_from_source[edge.destination] - + heuristic(edge.destination.value, destination.value) - ) - - if edge.destination not in in_open_set: - heappush( - open_set, - (distance_till_destination[edge.destination], edge.destination), - ) - in_open_set.add(edge.destination) - - return math.inf +"""A* search algorithm implementation.""" + +from __future__ import annotations + +import math +from heapq import heappush, heappop +from itertools import count +from typing import Callable, Any + +from graph import Graph, Vertex + + +def a_star( + graph: Graph, + source: Vertex, + destination: Vertex, + heuristic: Callable[[Any, Any], float], +) -> float: + """ + Find the shortest path between source and destination using A* algorithm. + + Args: + graph: The graph to search. + source: The starting vertex. + destination: The target vertex. + heuristic: A function that estimates the distance between two vertex values. + + Returns: + The shortest distance from source to destination, or infinity if no path exists. + """ + if not (graph.contains(source) and graph.contains(destination)): + return math.inf + + distance_from_source: dict[Vertex, float] = {} + distance_till_destination: dict[Vertex, float] = {} + + for vertex in graph.vertices(): + distance_from_source[vertex] = math.inf + distance_till_destination[vertex] = math.inf + + distance_from_source[source] = 0 + distance_till_destination[source] = heuristic(source.value, destination.value) + + # The counter breaks ties so vertices themselves never need to be comparable. + counter = count() + open_set: list[tuple[float, int, Vertex]] = [] + heappush(open_set, (distance_till_destination[source], next(counter), source)) + + while open_set: + priority, _, current = heappop(open_set) + + # Skip stale entries: a better path to this vertex was found after the push. + if priority > distance_till_destination[current]: + continue + + if current == destination: + return distance_from_source[destination] + + for edge in graph.edges_from_vertex(current): + tentative_distance_from_source = ( + distance_from_source[current] + edge.distance + ) + + if tentative_distance_from_source < distance_from_source[edge.destination]: + distance_from_source[edge.destination] = tentative_distance_from_source + distance_till_destination[edge.destination] = distance_from_source[ + edge.destination + ] + heuristic(edge.destination.value, destination.value) + + # Always push: the previous entry (if any) is now stale and skipped. + heappush( + open_set, + ( + distance_till_destination[edge.destination], + next(counter), + edge.destination, + ), + ) + + return math.inf diff --git a/src/graphs/python/a_star/tests/test_a_star.py b/src/graphs/python/a_star/tests/test_a_star.py index 31e9ae81..e03c892d 100644 --- a/src/graphs/python/a_star/tests/test_a_star.py +++ b/src/graphs/python/a_star/tests/test_a_star.py @@ -1,3 +1,4 @@ +import random import unittest import math @@ -11,6 +12,19 @@ from a_star import a_star +def floyd_warshall(vertices, edges): + """Brute-force all-pairs shortest paths over plain (u, v, w) tuples.""" + dist = {(u, v): (0 if u == v else math.inf) for u in vertices for v in vertices} + for u, v, w in edges: + dist[(u, v)] = min(dist[(u, v)], w) + for k in vertices: + for i in vertices: + for j in vertices: + if dist[(i, k)] + dist[(k, j)] < dist[(i, j)]: + dist[(i, j)] = dist[(i, k)] + dist[(k, j)] + return dist + + class TestAStar(unittest.TestCase): def test_a_star_points(self): class Point: @@ -41,12 +55,63 @@ def distance(point_a, point_b): self.assertEqual(a_star(graph, Vertex(a), Vertex(c), Point.distance), 18) self.assertEqual(a_star(graph, Vertex(a), Vertex(b), Point.distance), 6) - self.assertEqual( - a_star(graph, Vertex(d), Vertex(b), Point.distance), math.inf - ) - self.assertEqual( - a_star(graph, Vertex(b), Vertex(e), Point.distance), math.inf - ) + self.assertEqual(a_star(graph, Vertex(d), Vertex(b), Point.distance), math.inf) + self.assertEqual(a_star(graph, Vertex(b), Vertex(e), Point.distance), math.inf) + + def test_a_star_source_equals_destination_and_unknown_vertex(self): + graph = Graph() + graph.add_edge(Edge(Vertex(0), Vertex(1), 2)) + self.assertEqual(a_star(graph, Vertex(0), Vertex(0), lambda a, b: 0), 0) + self.assertEqual(a_star(graph, Vertex(0), Vertex(9), lambda a, b: 0), math.inf) + + def test_a_star_unorderable_values_with_equal_priorities(self): + class Opaque: + pass + + a, b, c, d = Opaque(), Opaque(), Opaque(), Opaque() + graph = Graph() + graph.add_edge(Edge(Vertex(a), Vertex(b), 1)) + graph.add_edge(Edge(Vertex(a), Vertex(c), 1)) + graph.add_edge(Edge(Vertex(b), Vertex(d), 1)) + graph.add_edge(Edge(Vertex(c), Vertex(d), 1)) + self.assertEqual(a_star(graph, Vertex(a), Vertex(d), lambda x, y: 0), 2) + + def test_a_star_improved_path_to_queued_vertex(self): + # Vertex 3 is queued with a stale priority before a cheaper path to it + # is discovered; the answer must still be optimal. + graph = Graph() + graph.add_edge(Edge(Vertex(0), Vertex(1), 1)) + graph.add_edge(Edge(Vertex(0), Vertex(2), 10)) + graph.add_edge(Edge(Vertex(1), Vertex(2), 1)) + graph.add_edge(Edge(Vertex(2), Vertex(3), 1)) + graph.add_edge(Edge(Vertex(0), Vertex(3), 20)) + self.assertEqual(a_star(graph, Vertex(0), Vertex(3), lambda a, b: 0), 3) + + def test_a_star_random_graphs_with_inconsistent_heuristic(self): + rng = random.Random(21) + for _ in range(200): + n = rng.randint(1, 7) + edges = [ + (rng.randrange(n), rng.randrange(n), rng.randint(0, 9)) + for _ in range(rng.randint(0, 16)) + ] + graph = Graph() + for v in range(n): + graph.add_vertex(Vertex(v)) + for u, v, w in edges: + graph.add_edge(Edge(Vertex(u), Vertex(v), w)) + dist = floyd_warshall(range(n), edges) + for t in range(n): + # Admissible but inconsistent: a random fraction of the true distance. + h = { + v: (0 if dist[(v, t)] == math.inf else dist[(v, t)] * rng.random()) + for v in range(n) + } + for s in range(n): + self.assertAlmostEqual( + a_star(graph, Vertex(s), Vertex(t), lambda x, y: h[x]), + dist[(s, t)], + ) if __name__ == "__main__": diff --git a/src/graphs/python/bellman_ford/src/bellman_ford.py b/src/graphs/python/bellman_ford/src/bellman_ford.py index 8d934db6..70736f66 100644 --- a/src/graphs/python/bellman_ford/src/bellman_ford.py +++ b/src/graphs/python/bellman_ford/src/bellman_ford.py @@ -1,56 +1,53 @@ -"""Bellman-Ford algorithm for shortest paths with negative edges.""" - -from __future__ import annotations - -import math - -from graph import Graph, Vertex - - -def bellman_ford(graph: Graph, source: Vertex, destination: Vertex) -> float: - """ - Find the shortest path between source and destination using Bellman-Ford algorithm. - - This algorithm handles negative edge weights and detects negative cycles. - - Args: - graph: The graph to search. - source: The starting vertex. - destination: The target vertex. - - Returns: - The shortest distance from source to destination, or infinity if no path exists - or if a negative cycle is detected. - """ - if not (graph.contains(source) and graph.contains(destination)): - return math.inf - - if source == destination: - return 0 - - distances: dict[Vertex, float] = {} - - for vertex in graph.vertices(): - distances[vertex] = math.inf - - distances[source] = 0 - - for _ in range(graph.size() - 1): - for edge in graph.edges(): - new_distance = distances[edge.source] + edge.distance - - if ( - distances[edge.source] != math.inf - and new_distance < distances[edge.destination] - ): - distances[edge.destination] = new_distance - - for edge in graph.edges(): - if ( - distances[edge.source] != math.inf - and distances[edge.source] + edge.distance < distances[edge.destination] - ): - # Graph contains negative weight cycle - return math.inf - - return distances[destination] +"""Bellman-Ford algorithm for shortest paths with negative edges.""" + +from __future__ import annotations + +import math + +from graph import Graph, Vertex + + +def bellman_ford(graph: Graph, source: Vertex, destination: Vertex) -> float: + """ + Find the shortest path between source and destination using Bellman-Ford algorithm. + + This algorithm handles negative edge weights and detects negative cycles. + + Args: + graph: The graph to search. + source: The starting vertex. + destination: The target vertex. + + Returns: + The shortest distance from source to destination, or infinity if no path exists + or if a negative cycle is detected. + """ + if not (graph.contains(source) and graph.contains(destination)): + return math.inf + + distances: dict[Vertex, float] = {} + + for vertex in graph.vertices(): + distances[vertex] = math.inf + + distances[source] = 0 + + for _ in range(graph.size() - 1): + for edge in graph.edges(): + new_distance = distances[edge.source] + edge.distance + + if ( + distances[edge.source] != math.inf + and new_distance < distances[edge.destination] + ): + distances[edge.destination] = new_distance + + for edge in graph.edges(): + if ( + distances[edge.source] != math.inf + and distances[edge.source] + edge.distance < distances[edge.destination] + ): + # Graph contains negative weight cycle + return math.inf + + return distances[destination] diff --git a/src/graphs/python/bellman_ford/tests/test_bellman_ford.py b/src/graphs/python/bellman_ford/tests/test_bellman_ford.py index e4343e4e..4f3a5ac7 100644 --- a/src/graphs/python/bellman_ford/tests/test_bellman_ford.py +++ b/src/graphs/python/bellman_ford/tests/test_bellman_ford.py @@ -1,4 +1,5 @@ import math +import random import unittest import os @@ -11,6 +12,19 @@ from bellman_ford import bellman_ford +def floyd_warshall(vertices, edges): + """Brute-force all-pairs shortest paths over plain (u, v, w) tuples.""" + dist = {(u, v): (0 if u == v else math.inf) for u in vertices for v in vertices} + for u, v, w in edges: + dist[(u, v)] = min(dist[(u, v)], w) + for k in vertices: + for i in vertices: + for j in vertices: + if dist[(i, k)] + dist[(k, j)] < dist[(i, j)]: + dist[(i, j)] = dist[(i, k)] + dist[(k, j)] + return dist + + class TestBellmanFord(unittest.TestCase): def test_bellman_ford_str_vertices_negative(self): graph = Graph() @@ -62,6 +76,58 @@ def test_bellman_ford_int_vertices_negative(self): self.assertEqual(bellman_ford(graph, Vertex(5), Vertex(0)), math.inf) self.assertEqual(bellman_ford(graph, Vertex(1), Vertex(5)), math.inf) + def test_negative_edges_without_cycle(self): + graph = Graph() + graph.add_edge(Edge(Vertex(0), Vertex(1), 4)) + graph.add_edge(Edge(Vertex(0), Vertex(2), 5)) + graph.add_edge(Edge(Vertex(2), Vertex(1), -3)) + self.assertEqual(bellman_ford(graph, Vertex(0), Vertex(1)), 2) + + def test_negative_cycle_reachable_returns_inf(self): + graph = Graph() + graph.add_edge(Edge(Vertex(0), Vertex(1), 1)) + graph.add_edge(Edge(Vertex(1), Vertex(2), -2)) + graph.add_edge(Edge(Vertex(2), Vertex(1), 1)) + self.assertEqual(bellman_ford(graph, Vertex(0), Vertex(2)), math.inf) + + def test_negative_cycle_through_source_is_detected(self): + graph = Graph() + graph.add_edge(Edge(Vertex(0), Vertex(1), -2)) + graph.add_edge(Edge(Vertex(1), Vertex(0), 1)) + self.assertEqual(bellman_ford(graph, Vertex(0), Vertex(0)), math.inf) + + def test_unreachable_negative_cycle_is_ignored(self): + graph = Graph() + graph.add_edge(Edge(Vertex(0), Vertex(1), 3)) + graph.add_edge(Edge(Vertex(5), Vertex(6), -4)) + graph.add_edge(Edge(Vertex(6), Vertex(5), 1)) + self.assertEqual(bellman_ford(graph, Vertex(0), Vertex(1)), 3) + self.assertEqual(bellman_ford(graph, Vertex(0), Vertex(0)), 0) + self.assertEqual(bellman_ford(graph, Vertex(0), Vertex(6)), math.inf) + + def test_random_graphs_match_floyd_warshall(self): + rng = random.Random(5) + for _ in range(150): + n = rng.randint(1, 6) + edges = [ + (rng.randrange(n), rng.randrange(n), rng.randint(-3, 9)) + for _ in range(rng.randint(0, 12)) + ] + graph = Graph() + for v in range(n): + graph.add_vertex(Vertex(v)) + for u, v, w in edges: + graph.add_edge(Edge(Vertex(u), Vertex(v), w)) + dist = floyd_warshall(range(n), edges) + for s in range(n): + reachable = [v for v in range(n) if dist[(s, v)] < math.inf] + has_neg_cycle = any(dist[(v, v)] < 0 for v in reachable) + for t in range(n): + expected = math.inf if has_neg_cycle else dist[(s, t)] + self.assertEqual( + bellman_ford(graph, Vertex(s), Vertex(t)), expected + ) + if __name__ == "__main__": unittest.main() diff --git a/src/graphs/python/dfs/src/dfs.py b/src/graphs/python/dfs/src/dfs.py index 4f74c9c9..94831845 100644 --- a/src/graphs/python/dfs/src/dfs.py +++ b/src/graphs/python/dfs/src/dfs.py @@ -1,50 +1,59 @@ -"""Depth-First Search (DFS) algorithm for graph traversal.""" - -from __future__ import annotations - -import math - -from graph import Graph, Vertex - - -def dfs(graph: Graph, source: Vertex, destination: Vertex) -> float: - """ - Find a path between source and destination using DFS. - - Note: DFS does not guarantee the shortest path. For shortest paths, - use BFS (unweighted) or Dijkstra/Bellman-Ford (weighted). - - Args: - graph: The graph to search. - source: The starting vertex. - destination: The target vertex. - - Returns: - The distance from source to destination, or infinity if no path exists. - """ - if not (graph.contains(source) and graph.contains(destination)): - return math.inf - - distances: dict[Vertex, float] = {} - visited: dict[Vertex, bool] = {} - - for vertex in graph.vertices(): - distances[vertex] = math.inf - visited[vertex] = False - - distances[source] = 0 - visited[source] = True - - def search(u: Vertex) -> None: - visited[u] = True - - for edge in graph.edges_from_vertex(u): - v = edge.destination - - if not visited[v]: - distances[v] = distances[u] + edge.distance - search(v) - - search(source) - - return distances[destination] +"""Depth-First Search (DFS) algorithm for graph traversal.""" + +from __future__ import annotations + +import math + +from graph import Graph, Vertex + + +def dfs(graph: Graph, source: Vertex, destination: Vertex) -> float: + """ + Find a path between source and destination using DFS. + + Note: DFS does not guarantee the shortest path. For shortest paths, + use BFS (unweighted) or Dijkstra/Bellman-Ford (weighted). + + Args: + graph: The graph to search. + source: The starting vertex. + destination: The target vertex. + + Returns: + The distance from source to destination, or infinity if no path exists. + """ + if not (graph.contains(source) and graph.contains(destination)): + return math.inf + + distances: dict[Vertex, float] = {} + visited: dict[Vertex, bool] = {} + + for vertex in graph.vertices(): + distances[vertex] = math.inf + visited[vertex] = False + + distances[source] = 0 + visited[source] = True + + # Iterative DFS (same visiting order as the recursive version) so that + # long paths cannot hit Python's recursion limit. + stack = [iter(graph.edges_from_vertex(source))] + sources = [source] + + while stack: + u = sources[-1] + + for edge in stack[-1]: + v = edge.destination + + if not visited[v]: + visited[v] = True + distances[v] = distances[u] + edge.distance + stack.append(iter(graph.edges_from_vertex(v))) + sources.append(v) + break + else: + stack.pop() + sources.pop() + + return distances[destination] diff --git a/src/graphs/python/dfs/tests/test_dfs.py b/src/graphs/python/dfs/tests/test_dfs.py index 97498760..0bdc6724 100644 --- a/src/graphs/python/dfs/tests/test_dfs.py +++ b/src/graphs/python/dfs/tests/test_dfs.py @@ -1,4 +1,5 @@ import math +import random import unittest import os @@ -62,6 +63,44 @@ def test_dfs_int_vertices_negative(self): self.assertEqual(dfs(graph, Vertex(5), Vertex(0)), math.inf) self.assertEqual(dfs(graph, Vertex(1), Vertex(5)), math.inf) + def test_dfs_deep_path_does_not_overflow_stack(self): + graph = Graph() + n = 5000 + for i in range(n): + graph.add_edge(Edge(Vertex(i), Vertex(i + 1), 1)) + self.assertEqual(dfs(graph, Vertex(0), Vertex(n)), n) + + def test_dfs_cycle_and_source_equals_destination(self): + graph = Graph() + graph.add_edge(Edge(Vertex(0), Vertex(1), 1)) + graph.add_edge(Edge(Vertex(1), Vertex(0), 1)) + self.assertEqual(dfs(graph, Vertex(0), Vertex(0)), 0) + self.assertEqual(dfs(graph, Vertex(0), Vertex(1)), 1) + + def test_dfs_reachability_matches_brute_force(self): + rng = random.Random(3) + for _ in range(100): + n = rng.randint(1, 7) + edges = [ + (rng.randrange(n), rng.randrange(n), rng.randint(1, 5)) + for _ in range(rng.randint(0, 12)) + ] + graph = Graph() + for v in range(n): + graph.add_vertex(Vertex(v)) + for u, v, w in edges: + graph.add_edge(Edge(Vertex(u), Vertex(v), w)) + reach = {(v, v) for v in range(n)} | {(u, v) for u, v, _ in edges} + for k in range(n): + for i in range(n): + for j in range(n): + if (i, k) in reach and (k, j) in reach: + reach.add((i, j)) + for s in range(n): + for t in range(n): + result = dfs(graph, Vertex(s), Vertex(t)) + self.assertEqual((s, t) in reach, result != math.inf) + if __name__ == "__main__": unittest.main() diff --git a/src/graphs/python/dijkstra/src/dijkstra.py b/src/graphs/python/dijkstra/src/dijkstra.py index a49a8bfa..1d18a337 100644 --- a/src/graphs/python/dijkstra/src/dijkstra.py +++ b/src/graphs/python/dijkstra/src/dijkstra.py @@ -1,59 +1,62 @@ -"""Dijkstra's algorithm for shortest paths in graphs with non-negative weights.""" - -from __future__ import annotations - -import math -from heapq import heappush, heappop - -from graph import Graph, Vertex - - -def dijkstra(graph: Graph, source: Vertex, destination: Vertex) -> float: - """ - Find the shortest path between source and destination using Dijkstra's algorithm. - - This implementation uses a priority queue (min-heap) for efficient O((V+E) log V) - performance. - - Args: - graph: The graph to search (must have non-negative edge weights). - source: The starting vertex. - destination: The target vertex. - - Returns: - The shortest distance from source to destination, or infinity if no path exists. - """ - if not (graph.contains(source) and graph.contains(destination)): - return math.inf - - distances: dict[Vertex, float] = {} - visited: dict[Vertex, bool] = {} - - for vertex in graph.vertices(): - distances[vertex] = math.inf - visited[vertex] = False - - distances[source] = 0 - - priority_queue: list[tuple[float, Vertex]] = [(0, source)] - - while priority_queue: - current_distance, u = heappop(priority_queue) - - if visited[u]: - continue - - visited[u] = True - - if u == destination: - return distances[destination] - - for edge in graph.edges_from_vertex(u): - v = edge.destination - new_distance = distances[u] + edge.distance - - if new_distance < distances[v]: - distances[v] = new_distance - heappush(priority_queue, (new_distance, v)) - - return distances[destination] +"""Dijkstra's algorithm for shortest paths in graphs with non-negative weights.""" + +from __future__ import annotations + +import math +from heapq import heappush, heappop +from itertools import count + +from graph import Graph, Vertex + + +def dijkstra(graph: Graph, source: Vertex, destination: Vertex) -> float: + """ + Find the shortest path between source and destination using Dijkstra's algorithm. + + This implementation uses a priority queue (min-heap) for efficient O((V+E) log V) + performance. + + Args: + graph: The graph to search (must have non-negative edge weights). + source: The starting vertex. + destination: The target vertex. + + Returns: + The shortest distance from source to destination, or infinity if no path exists. + """ + if not (graph.contains(source) and graph.contains(destination)): + return math.inf + + distances: dict[Vertex, float] = {} + visited: dict[Vertex, bool] = {} + + for vertex in graph.vertices(): + distances[vertex] = math.inf + visited[vertex] = False + + distances[source] = 0 + + # The counter breaks ties so vertices themselves never need to be comparable. + counter = count() + priority_queue: list[tuple[float, int, Vertex]] = [(0, next(counter), source)] + + while priority_queue: + _, _, u = heappop(priority_queue) + + if visited[u]: + continue + + visited[u] = True + + if u == destination: + return distances[destination] + + for edge in graph.edges_from_vertex(u): + v = edge.destination + new_distance = distances[u] + edge.distance + + if new_distance < distances[v]: + distances[v] = new_distance + heappush(priority_queue, (new_distance, next(counter), v)) + + return distances[destination] diff --git a/src/graphs/python/dijkstra/tests/test_dijkstra.py b/src/graphs/python/dijkstra/tests/test_dijkstra.py index 8a168533..30f47bcc 100644 --- a/src/graphs/python/dijkstra/tests/test_dijkstra.py +++ b/src/graphs/python/dijkstra/tests/test_dijkstra.py @@ -1,4 +1,5 @@ import math +import random import unittest import os @@ -11,6 +12,19 @@ from dijkstra import dijkstra +def floyd_warshall(vertices, edges): + """Brute-force all-pairs shortest paths over plain (u, v, w) tuples.""" + dist = {(u, v): (0 if u == v else math.inf) for u in vertices for v in vertices} + for u, v, w in edges: + dist[(u, v)] = min(dist[(u, v)], w) + for k in vertices: + for i in vertices: + for j in vertices: + if dist[(i, k)] + dist[(k, j)] < dist[(i, j)]: + dist[(i, j)] = dist[(i, k)] + dist[(k, j)] + return dist + + class TestDijkstra(unittest.TestCase): def test_dijkstra_str_vertices_negative(self): graph = Graph() @@ -62,6 +76,50 @@ def test_dijkstra_int_vertices_negative(self): self.assertEqual(dijkstra(graph, Vertex(5), Vertex(0)), math.inf) self.assertEqual(dijkstra(graph, Vertex(1), Vertex(5)), math.inf) + def test_dijkstra_source_equals_destination(self): + graph = Graph() + graph.add_edge(Edge(Vertex("A"), Vertex("B"), 3)) + self.assertEqual(dijkstra(graph, Vertex("A"), Vertex("A")), 0) + + def test_dijkstra_unorderable_vertex_values_with_ties(self): + class Opaque: + pass + + a, b, c, d = Opaque(), Opaque(), Opaque(), Opaque() + graph = Graph() + graph.add_edge(Edge(Vertex(a), Vertex(b), 1)) + graph.add_edge(Edge(Vertex(a), Vertex(c), 1)) + graph.add_edge(Edge(Vertex(b), Vertex(d), 1)) + graph.add_edge(Edge(Vertex(c), Vertex(d), 1)) + self.assertEqual(dijkstra(graph, Vertex(a), Vertex(d)), 2) + + def test_dijkstra_unknown_vertex_and_parallel_edges(self): + graph = Graph() + graph.add_edge(Edge(Vertex(1), Vertex(2), 5)) + graph.add_edge(Edge(Vertex(1), Vertex(2), 2)) + self.assertEqual(dijkstra(graph, Vertex(1), Vertex(2)), 2) + self.assertEqual(dijkstra(graph, Vertex(1), Vertex(99)), math.inf) + + def test_dijkstra_random_graphs_match_floyd_warshall(self): + rng = random.Random(11) + for _ in range(100): + n = rng.randint(1, 7) + edges = [ + (rng.randrange(n), rng.randrange(n), rng.randint(0, 9)) + for _ in range(rng.randint(0, 15)) + ] + graph = Graph() + for v in range(n): + graph.add_vertex(Vertex(v)) + for u, v, w in edges: + graph.add_edge(Edge(Vertex(u), Vertex(v), w)) + dist = floyd_warshall(range(n), edges) + for u in range(n): + for v in range(n): + self.assertEqual( + dijkstra(graph, Vertex(u), Vertex(v)), dist[(u, v)] + ) + if __name__ == "__main__": unittest.main() diff --git a/src/graphs/python/eulerian_path/src/eulerian_path.py b/src/graphs/python/eulerian_path/src/eulerian_path.py index 57b2d5a9..e2f312f0 100644 --- a/src/graphs/python/eulerian_path/src/eulerian_path.py +++ b/src/graphs/python/eulerian_path/src/eulerian_path.py @@ -58,6 +58,21 @@ def edges_from_adjacency(graph: AdjacencyGraph) -> list[tuple[int, int]]: return [(u, v) for u in graph for v in graph[u]] +def _all_edges_reachable_from(graph: AdjacencyGraph, start: int) -> bool: + """Check that every remaining edge is reachable from ``start``.""" + seen = {start} + stack = [start] + + while stack: + u = stack.pop() + for v in graph[u]: + if v not in seen: + seen.add(v) + stack.append(v) + + return all(v in seen for v in graph if graph[v]) + + def fleury(graph: AdjacencyGraph) -> list[tuple[int, int]] | bool: """ Find an Eulerian path using Fleury's algorithm. @@ -81,18 +96,30 @@ def fleury(graph: AdjacencyGraph) -> list[tuple[int, int]] | bool: return False g = {k: list(v) for k, v in graph.items()} # Deep copy + + # All edges must lie in one connected component (isolated vertices are fine) + first_with_edges = next((k for k in g if g[k]), None) + if first_with_edges is not None and not _all_edges_reachable_from( + g, first_with_edges + ): + return False + trail: list[tuple[int, int]] = [] # Start from odd-degree vertex if exists, otherwise from any vertex - u = odd_degree_nodes[0] if len(odd_degree_nodes) == 2 else list(g)[0] + u = ( + odd_degree_nodes[0] + if len(odd_degree_nodes) == 2 + else (first_with_edges if first_with_edges is not None else list(g)[0]) + ) while edges_from_adjacency(g): current_vertex = u - for u in g[current_vertex]: + for u in list(g[current_vertex]): g[current_vertex].remove(u) g[u].remove(current_vertex) - bridge = not is_connected(g) + bridge = not _all_edges_reachable_from(g, u) if bridge: g[current_vertex].append(u) @@ -103,7 +130,6 @@ def fleury(graph: AdjacencyGraph) -> list[tuple[int, int]] | bool: if bridge: g[current_vertex].remove(u) g[u].remove(current_vertex) - g.pop(current_vertex) trail.append((current_vertex, u)) diff --git a/src/graphs/python/eulerian_path/tests/test_eulerian_path.py b/src/graphs/python/eulerian_path/tests/test_eulerian_path.py index 9c771284..cf91560e 100644 --- a/src/graphs/python/eulerian_path/tests/test_eulerian_path.py +++ b/src/graphs/python/eulerian_path/tests/test_eulerian_path.py @@ -1,6 +1,8 @@ """Tests for the Eulerian Path algorithm.""" +import random import unittest +from collections import Counter import os import sys @@ -11,6 +13,34 @@ from eulerian_path import fleury, is_connected, edges_from_adjacency +def is_valid_trail(graph, trail): + """Check that trail is a walk using every undirected edge exactly once.""" + remaining = Counter((u, v) for u in graph for v in graph[u]) + if 2 * len(trail) != sum(remaining.values()): + return False + for i, (a, b) in enumerate(trail): + if i > 0 and trail[i - 1][1] != a: + return False + if remaining[(a, b)] <= 0 or remaining[(b, a)] <= 0: + return False + remaining[(a, b)] -= 1 + remaining[(b, a)] -= 1 + return True + + +def edges_connected(graph): + start = next((v for v in graph if graph[v]), None) + if start is None: + return True + seen, stack = {start}, [start] + while stack: + for v in graph[stack.pop()]: + if v not in seen: + seen.add(v) + stack.append(v) + return all(v in seen for v in graph if graph[v]) + + class TestEulerianPath(unittest.TestCase): def test_no_eulerian_path_odd_degree_vertices(self): """Graph with 4 odd-degree vertices has no Eulerian path.""" @@ -46,13 +76,17 @@ def test_eulerian_path_complex_graph(self): (7, 6), (6, 0), ] - self.assertEqual(fleury(graph), expected) + result = fleury(graph) + self.assertEqual(len(expected), len(result)) + self.assertTrue(is_valid_trail(graph, result)) def test_eulerian_path_multigraph(self): """Test Eulerian path on a multigraph with parallel edges.""" graph = {1: [2, 3, 4, 4], 2: [1, 3, 3, 4], 3: [1, 2, 2, 4], 4: [1, 1, 2, 3]} expected = [(1, 2), (2, 3), (3, 1), (1, 4), (4, 3), (3, 2), (2, 4), (4, 1)] - self.assertEqual(fleury(graph), expected) + result = fleury(graph) + self.assertEqual(len(expected), len(result)) + self.assertTrue(is_valid_trail(graph, result)) def test_eulerian_path_simple_graph(self): """Test Eulerian path on a simple graph with two odd-degree vertices.""" @@ -60,6 +94,40 @@ def test_eulerian_path_simple_graph(self): expected = [(2, 1), (1, 3), (3, 2), (2, 4), (4, 3)] self.assertEqual(fleury(graph), expected) + def test_isolated_vertices_are_ignored(self): + """Vertices without edges must not break the search.""" + graph = {0: [], 1: [2, 3, 2, 3], 2: [1, 1], 3: [1, 1], 4: []} + result = fleury(graph) + self.assertTrue(is_valid_trail(graph, result)) + + def test_disconnected_edges_have_no_path(self): + """Edges in two components cannot form one trail.""" + graph = {0: [1, 1], 1: [0, 0], 2: [3, 3], 3: [2, 2]} + self.assertFalse(fleury(graph)) + + def test_no_edges(self): + self.assertEqual(fleury({0: [], 1: []}), []) + + def test_fleury_random_graphs_against_criterion(self): + """Eulerian path exists iff edges are connected and 0 or 2 odd vertices.""" + rng = random.Random(7) + for _ in range(500): + n = rng.randint(1, 6) + graph = {i: [] for i in range(n)} + for _ in range(rng.randint(0, 9)): + a, b = rng.randrange(n), rng.randrange(n) + if a != b: + graph[a].append(b) + graph[b].append(a) + odd = sum(len(graph[u]) % 2 for u in graph) + exists = edges_connected(graph) and odd in (0, 2) + result = fleury(graph) + if exists: + self.assertIsNot(result, False, graph) + self.assertTrue(is_valid_trail(graph, result), graph) + else: + self.assertIs(result, False, graph) + def test_is_connected_single_vertex(self): """Single vertex graph is connected.""" graph = {0: []} diff --git a/src/graphs/python/kruskal/tests/test_kruskal.py b/src/graphs/python/kruskal/tests/test_kruskal.py index 1eedc03e..5c084827 100644 --- a/src/graphs/python/kruskal/tests/test_kruskal.py +++ b/src/graphs/python/kruskal/tests/test_kruskal.py @@ -1,3 +1,5 @@ +import random +import itertools import unittest import os @@ -10,6 +12,30 @@ from kruskal import kruskal +def brute_force_msf(n, edges): + """Minimum spanning forest weight by trying every edge subset.""" + + def components(subset): + parent = list(range(n)) + + def find(x): + while parent[x] != x: + x = parent[x] + return x + + for u, v, _ in subset: + parent[find(u)] = find(v) + return len({find(x) for x in range(n)}) + + target = components(edges) + best = float("inf") + for size in range(len(edges) + 1): + for subset in itertools.combinations(edges, size): + if components(subset) == target: + best = min(best, sum(w for _, _, w in subset)) + return best + + class TestKruskal(unittest.TestCase): def test_kruskal_str_vertices(self): graph = Graph() @@ -42,6 +68,42 @@ def test_kruskal_int_vertices(self): self.assertEqual(kruskal(graph), 4) + def test_kruskal_disconnected_graph_returns_forest_weight(self): + graph = Graph() + graph.add_edge(Edge(Vertex(0), Vertex(1), 2)) + graph.add_edge(Edge(Vertex(2), Vertex(3), 5)) + graph.add_vertex(Vertex(9)) + self.assertEqual(kruskal(graph), 7) + + def test_kruskal_empty_graph_and_self_loops(self): + self.assertEqual(kruskal(Graph()), 0) + graph = Graph() + graph.add_edge(Edge(Vertex(0), Vertex(0), 4)) + graph.add_edge(Edge(Vertex(0), Vertex(1), 1)) + self.assertEqual(kruskal(graph), 1) + + def test_kruskal_direction_does_not_matter_and_negative_weights(self): + graph = Graph() + graph.add_edge(Edge(Vertex(1), Vertex(0), 3)) + graph.add_edge(Edge(Vertex(1), Vertex(2), -1)) + graph.add_edge(Edge(Vertex(2), Vertex(0), 4)) + self.assertEqual(kruskal(graph), 2) + + def test_kruskal_random_graphs_match_brute_force(self): + rng = random.Random(9) + for _ in range(100): + n = rng.randint(1, 6) + edges = [ + (rng.randrange(n), rng.randrange(n), rng.randint(0, 9)) + for _ in range(rng.randint(0, 9)) + ] + graph = Graph() + for v in range(n): + graph.add_vertex(Vertex(v)) + for u, v, w in edges: + graph.add_edge(Edge(Vertex(u), Vertex(v), w)) + self.assertEqual(kruskal(graph), brute_force_msf(n, edges)) + if __name__ == "__main__": unittest.main() diff --git a/src/graphs/python/prim/src/prim.py b/src/graphs/python/prim/src/prim.py index 7a253816..10d16993 100644 --- a/src/graphs/python/prim/src/prim.py +++ b/src/graphs/python/prim/src/prim.py @@ -1,49 +1,58 @@ -"""Prim's algorithm for Minimum Spanning Tree (MST).""" - -from __future__ import annotations - -from heapq import heappush, heappop - -from graph import Graph, Vertex - - -def prim(graph: Graph) -> float: - """ - Find the minimum spanning tree weight using Prim's algorithm. - - Uses a priority queue (min-heap) for efficient O((V+E) log V) performance. - - Args: - graph: The graph to find the MST for. - - Returns: - The total weight of the minimum spanning tree. - """ - visited: dict[Vertex, bool] = {} - - vertices = graph.vertices() - - for vertex in vertices: - visited[vertex] = False - - # Pick first vertex as starting point - vertex = vertices[0] - visited[vertex] = True - - unvisited: list[tuple[float, Vertex]] = [ - (edge.distance, edge.destination) for edge in graph.edges_from_vertex(vertex) - ] - - result = 0.0 - - while unvisited: - distance, vertex = heappop(unvisited) - - if not visited[vertex]: - visited[vertex] = True - result += distance - for edge in graph.edges_from_vertex(vertex): - if not visited[edge.destination]: - heappush(unvisited, (edge.distance, edge.destination)) - - return result +"""Prim's algorithm for Minimum Spanning Tree (MST).""" + +from __future__ import annotations + +from heapq import heappush, heappop +from itertools import count + +from graph import Graph, Vertex + + +def prim(graph: Graph) -> float: + """ + Find the minimum spanning tree weight using Prim's algorithm. + + Edges are treated as undirected. For a disconnected graph the weight of the + minimum spanning forest is returned (consistent with Kruskal's algorithm). + Uses a priority queue (min-heap) for efficient O((V+E) log V) performance. + + Args: + graph: The graph to find the MST for. + + Returns: + The total weight of the minimum spanning tree (or forest). + """ + # Build an undirected adjacency view, since edges are stored one-way. + adjacency: dict[Vertex, list[tuple[float, Vertex]]] = { + vertex: [] for vertex in graph.vertices() + } + for edge in graph.edges(): + adjacency[edge.source].append((edge.distance, edge.destination)) + adjacency[edge.destination].append((edge.distance, edge.source)) + + visited: set[Vertex] = set() + result = 0.0 + counter = count() # tie-breaker so vertices never need to be comparable + + for start in graph.vertices(): + if start in visited: + continue + + visited.add(start) + heap: list[tuple[float, int, Vertex]] = [] + for distance, neighbor in adjacency[start]: + heappush(heap, (distance, next(counter), neighbor)) + + while heap: + distance, _, vertex = heappop(heap) + + if vertex in visited: + continue + + visited.add(vertex) + result += distance + for next_distance, neighbor in adjacency[vertex]: + if neighbor not in visited: + heappush(heap, (next_distance, next(counter), neighbor)) + + return result diff --git a/src/graphs/python/prim/tests/test_prim.py b/src/graphs/python/prim/tests/test_prim.py index 6b8c8ec7..096359dc 100644 --- a/src/graphs/python/prim/tests/test_prim.py +++ b/src/graphs/python/prim/tests/test_prim.py @@ -1,3 +1,5 @@ +import random +import itertools import unittest import os @@ -10,6 +12,30 @@ from prim import prim +def brute_force_msf(n, edges): + """Minimum spanning forest weight by trying every edge subset.""" + + def components(subset): + parent = list(range(n)) + + def find(x): + while parent[x] != x: + x = parent[x] + return x + + for u, v, _ in subset: + parent[find(u)] = find(v) + return len({find(x) for x in range(n)}) + + target = components(edges) + best = float("inf") + for size in range(len(edges) + 1): + for subset in itertools.combinations(edges, size): + if components(subset) == target: + best = min(best, sum(w for _, _, w in subset)) + return best + + class TestPrim(unittest.TestCase): def test_prim_str_vertices(self): graph = Graph() @@ -42,6 +68,49 @@ def test_prim_int_vertices(self): self.assertEqual(prim(graph), 4) + def test_prim_disconnected_graph_returns_forest_weight(self): + graph = Graph() + graph.add_edge(Edge(Vertex(0), Vertex(1), 2)) + graph.add_edge(Edge(Vertex(2), Vertex(3), 5)) + graph.add_vertex(Vertex(9)) + self.assertEqual(prim(graph), 7) + + def test_prim_empty_graph_and_self_loops(self): + self.assertEqual(prim(Graph()), 0) + graph = Graph() + graph.add_edge(Edge(Vertex(0), Vertex(0), 4)) + graph.add_edge(Edge(Vertex(0), Vertex(1), 1)) + self.assertEqual(prim(graph), 1) + + def test_prim_direction_does_not_matter_and_negative_weights(self): + graph = Graph() + graph.add_edge(Edge(Vertex(1), Vertex(0), 3)) + graph.add_edge(Edge(Vertex(1), Vertex(2), -1)) + graph.add_edge(Edge(Vertex(2), Vertex(0), 4)) + self.assertEqual(prim(graph), 2) + + def test_prim_random_graphs_match_brute_force(self): + rng = random.Random(9) + for _ in range(100): + n = rng.randint(1, 6) + edges = [ + (rng.randrange(n), rng.randrange(n), rng.randint(0, 9)) + for _ in range(rng.randint(0, 9)) + ] + graph = Graph() + for v in range(n): + graph.add_vertex(Vertex(v)) + for u, v, w in edges: + graph.add_edge(Edge(Vertex(u), Vertex(v), w)) + self.assertEqual(prim(graph), brute_force_msf(n, edges)) + + def test_prim_edges_only_pointing_into_first_vertex(self): + # Only incoming edges at the first vertex: edges are undirected for MST. + graph = Graph() + graph.add_edge(Edge(Vertex("A"), Vertex("B"), 1)) + graph.add_edge(Edge(Vertex("C"), Vertex("B"), 2)) + self.assertEqual(prim(graph), 3) + if __name__ == "__main__": unittest.main() diff --git a/src/searching/cpp/binary_search/CMakeLists.txt b/src/searching/cpp/binary_search/CMakeLists.txt index 41c73ecb..86565f16 100644 --- a/src/searching/cpp/binary_search/CMakeLists.txt +++ b/src/searching/cpp/binary_search/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-binary-search CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/searching/cpp/exponential_search/CMakeLists.txt b/src/searching/cpp/exponential_search/CMakeLists.txt index 90e53511..546f9c05 100644 --- a/src/searching/cpp/exponential_search/CMakeLists.txt +++ b/src/searching/cpp/exponential_search/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-exponential-search CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/searching/cpp/interpolation_search/CMakeLists.txt b/src/searching/cpp/interpolation_search/CMakeLists.txt index b1737d09..27940e19 100644 --- a/src/searching/cpp/interpolation_search/CMakeLists.txt +++ b/src/searching/cpp/interpolation_search/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-interpolation-search CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/searching/cpp/interpolation_search/src/interpolation_search.cpp b/src/searching/cpp/interpolation_search/src/interpolation_search.cpp index 0efc0b62..e404c595 100644 --- a/src/searching/cpp/interpolation_search/src/interpolation_search.cpp +++ b/src/searching/cpp/interpolation_search/src/interpolation_search.cpp @@ -5,7 +5,8 @@ #include #include -std::optional interpolation_search(const std::vector &arr, int target) { +std::optional interpolation_search(const std::vector &arr, + int target) { if (arr.empty()) { return std::nullopt; } @@ -23,9 +24,12 @@ std::optional interpolation_search(const std::vector &arr, int } // Estimate position using interpolation formula - std::size_t pos = low + static_cast( - (static_cast(target - arr[low]) / (arr[high] - arr[low])) * (high - low) - ); + std::size_t pos = + low + + static_cast( + (static_cast(static_cast(target) - arr[low]) / + (static_cast(arr[high]) - arr[low])) * + static_cast(high - low)); // Ensure pos is within bounds pos = std::max(low, std::min(pos, high)); diff --git a/src/searching/cpp/interpolation_search/test/test_interpolation_search.cpp b/src/searching/cpp/interpolation_search/test/test_interpolation_search.cpp index 4b88388d..abc159fa 100644 --- a/src/searching/cpp/interpolation_search/test/test_interpolation_search.cpp +++ b/src/searching/cpp/interpolation_search/test/test_interpolation_search.cpp @@ -5,6 +5,7 @@ #include "interpolation_search.h" #include "gtest/gtest.h" +#include /// @brief Test searching an empty array. TEST(InterpolationSearchTest, EmptyArray) { @@ -71,3 +72,12 @@ TEST(InterpolationSearchTest, AllSameElementsNotFound) { std::vector arr{5, 5, 5, 5}; EXPECT_EQ(interpolation_search(arr, 3), std::nullopt); } + +/// @brief Test extreme int values that would overflow naive int subtraction. +TEST(InterpolationSearchTest, ExtremeValuesNoOverflow) { + std::vector arr{INT_MIN, -1, 0, 1, INT_MAX}; + EXPECT_EQ(interpolation_search(arr, INT_MIN), 0); + EXPECT_EQ(interpolation_search(arr, 0), 2); + EXPECT_EQ(interpolation_search(arr, INT_MAX), 4); + EXPECT_EQ(interpolation_search(arr, 2), std::nullopt); +} diff --git a/src/searching/cpp/jump_search/CMakeLists.txt b/src/searching/cpp/jump_search/CMakeLists.txt index 115f4f5a..8d583f4e 100644 --- a/src/searching/cpp/jump_search/CMakeLists.txt +++ b/src/searching/cpp/jump_search/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-jump-search CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/searching/cpp/linear_search/CMakeLists.txt b/src/searching/cpp/linear_search/CMakeLists.txt index f1c6478d..fba4302b 100644 --- a/src/searching/cpp/linear_search/CMakeLists.txt +++ b/src/searching/cpp/linear_search/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-linear-search CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/sorting/cpp/heap_sort/CMakeLists.txt b/src/sorting/cpp/heap_sort/CMakeLists.txt index 4fc74646..8916f6a4 100644 --- a/src/sorting/cpp/heap_sort/CMakeLists.txt +++ b/src/sorting/cpp/heap_sort/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-heap-sort CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/sorting/cpp/heap_sort/src/heap_sort.cpp b/src/sorting/cpp/heap_sort/src/heap_sort.cpp index be580275..9587de14 100644 --- a/src/sorting/cpp/heap_sort/src/heap_sort.cpp +++ b/src/sorting/cpp/heap_sort/src/heap_sort.cpp @@ -7,38 +7,47 @@ namespace { /** - * @brief Recursively heapify a subtree rooted at the given index. - * - * @tparam T The type of elements in the vector. - * @param arr The vector to heapify. - * @param curr Current node index (1-based). - * @param offset Starting position in the array; elements before this - * index are already in their final sorted position. + * @brief Sift the element at `root` down until the max-heap property holds + * for the heap occupying arr[0, size). */ template -void heapify(std::vector &arr, std::size_t curr, std::size_t offset) { - auto parent = [](std::size_t i) { return i >> 1; }; - auto left_child = [](std::size_t i) { return i << 1; }; - auto right_child = [](std::size_t i) { return (i << 1) + 1; }; - - if (curr + offset > arr.size()) { - return; +void sift_down(std::vector &arr, std::size_t root, std::size_t size) { + while (true) { + std::size_t largest = root; + const std::size_t left = 2 * root + 1; + const std::size_t right = left + 1; + + if (left < size && arr[largest] < arr[left]) { + largest = left; + } + if (right < size && arr[largest] < arr[right]) { + largest = right; + } + if (largest == root) { + return; + } + std::swap(arr[root], arr[largest]); + root = largest; } +} - heapify(arr, left_child(curr), offset); - heapify(arr, right_child(curr), offset); +} // namespace - if (curr > 1 && arr[curr + offset - 1] < arr[parent(curr) + offset - 1]) { - std::swap(arr[curr + offset - 1], arr[parent(curr) + offset - 1]); +template void heap_sort(std::vector &arr) { + const std::size_t n = arr.size(); + if (n < 2) { + return; } -} -} // namespace + // Build a max-heap in O(n). + for (std::size_t i = n / 2; i-- > 0;) { + sift_down(arr, i, n); + } -template -void heap_sort(std::vector &arr) { - for (std::size_t i = 0; i < arr.size(); ++i) { - heapify(arr, 1, i); + // Move the maximum to the end and restore the heap on the remainder. + for (std::size_t end = n - 1; end > 0; --end) { + std::swap(arr[0], arr[end]); + sift_down(arr, 0, end); } } diff --git a/src/sorting/cpp/heap_sort/src/heap_sort.h b/src/sorting/cpp/heap_sort/src/heap_sort.h index 70c5d3bf..9d8c857e 100644 --- a/src/sorting/cpp/heap_sort/src/heap_sort.h +++ b/src/sorting/cpp/heap_sort/src/heap_sort.h @@ -4,18 +4,17 @@ #include /** - * @brief Sort an array using the heap sort algorithm. + * @brief Sort a vector in ascending order using heap sort. * - * This implementation repeatedly heapifies portions of the array, moving - * smaller elements toward the front during each pass through the array. + * Builds a max-heap in place, then repeatedly swaps the maximum to the end of + * the shrinking heap and sifts the new root down. * - * @tparam T The type of elements in the vector (must be comparable). + * @tparam T The type of elements in the vector (must be comparable with <). * @param arr The vector to sort in ascending order. * * @note Time complexity: O(n log n) - * @note Space complexity: O(log n) due to recursion stack + * @note Space complexity: O(1) */ -template -void heap_sort(std::vector &arr); +template void heap_sort(std::vector &arr); #endif // HEAP_SORT_H diff --git a/src/sorting/cpp/heap_sort/test/test_heap_sort.cpp b/src/sorting/cpp/heap_sort/test/test_heap_sort.cpp index c7eaca93..932001b2 100644 --- a/src/sorting/cpp/heap_sort/test/test_heap_sort.cpp +++ b/src/sorting/cpp/heap_sort/test/test_heap_sort.cpp @@ -6,6 +6,9 @@ #include "heap_sort.h" #include "gtest/gtest.h" +#include +#include + /// @brief Test sorting an empty array. TEST(HeapSortTest, EmptyArray) { std::vector arr; @@ -85,3 +88,31 @@ TEST(HeapSortTest, AllSameElements) { heap_sort(arr); EXPECT_EQ(expected, arr); } + +TEST(HeapSortTest, MatchesStdSortOnRandomInputs) { + std::mt19937 rng(1234); + std::uniform_int_distribution value(-20, 20); + std::uniform_int_distribution length(0, 40); + for (int round = 0; round < 300; ++round) { + std::vector data(length(rng)); + for (auto &x : data) { + x = value(rng); + } + std::vector expected = data; + std::sort(expected.begin(), expected.end()); + heap_sort(data); + ASSERT_EQ(expected, data); + } +} + +TEST(HeapSortTest, LargeInput) { + std::mt19937 rng(7); + std::vector data(200000); + for (auto &x : data) { + x = static_cast(rng()); + } + std::vector expected = data; + std::sort(expected.begin(), expected.end()); + heap_sort(data); + EXPECT_EQ(expected, data); +} diff --git a/src/sorting/cpp/insertion_sort/CMakeLists.txt b/src/sorting/cpp/insertion_sort/CMakeLists.txt index acb1bcb0..13d9f91d 100644 --- a/src/sorting/cpp/insertion_sort/CMakeLists.txt +++ b/src/sorting/cpp/insertion_sort/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-insertion-sort CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/sorting/cpp/merge_sort/CMakeLists.txt b/src/sorting/cpp/merge_sort/CMakeLists.txt index 8eedaea8..46d19ac3 100644 --- a/src/sorting/cpp/merge_sort/CMakeLists.txt +++ b/src/sorting/cpp/merge_sort/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-merge-sort CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/sorting/cpp/quick_sort/CMakeLists.txt b/src/sorting/cpp/quick_sort/CMakeLists.txt index 4fc74646..8916f6a4 100644 --- a/src/sorting/cpp/quick_sort/CMakeLists.txt +++ b/src/sorting/cpp/quick_sort/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-heap-sort CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/sorting/cpp/selection_sort/CMakeLists.txt b/src/sorting/cpp/selection_sort/CMakeLists.txt index d56d167f..cd208057 100644 --- a/src/sorting/cpp/selection_sort/CMakeLists.txt +++ b/src/sorting/cpp/selection_sort/CMakeLists.txt @@ -2,6 +2,10 @@ cmake_minimum_required(VERSION 3.15 FATAL_ERROR) project(project-selection-sort CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) diff --git a/src/sorting/python/heap_sort/src/heap_sort.py b/src/sorting/python/heap_sort/src/heap_sort.py index 42976802..fe560bb1 100644 --- a/src/sorting/python/heap_sort/src/heap_sort.py +++ b/src/sorting/python/heap_sort/src/heap_sort.py @@ -7,13 +7,13 @@ def heap_sort(array: List[T]) -> List[T]: """ - Sort an array using the heap sort algorithm. + Sort a list in place in ascending order using heap sort. - This implementation builds a min-heap and repeatedly extracts the minimum - element to produce a sorted array in-place. + Builds a max-heap in place, then repeatedly swaps the maximum to the end + of the shrinking heap and sifts the new root down. Time complexity: O(n log n) - Space complexity: O(log n) due to recursion stack + Space complexity: O(1) Args: array: The list of comparable elements to sort. @@ -22,29 +22,30 @@ def heap_sort(array: List[T]) -> List[T]: The same list, sorted in ascending order. """ - def parent(i: int) -> int: - return i >> 1 - - def left_child(i: int) -> int: - return i << 1 - - def right_child(i: int) -> int: - return (i << 1) + 1 - - def heapify(arr: List[T], curr: int, offset: int) -> None: - if curr + offset > len(arr): - return - - heapify(arr, left_child(curr), offset) - heapify(arr, right_child(curr), offset) - - if curr > 1 and arr[curr + offset - 1] < arr[parent(curr) + offset - 1]: - arr[curr + offset - 1], arr[parent(curr) + offset - 1] = ( - arr[parent(curr) + offset - 1], - arr[curr + offset - 1], - ) - - for i in range(len(array)): - heapify(array, 1, i) + def sift_down(root: int, size: int) -> None: + while True: + largest = root + left = 2 * root + 1 + right = left + 1 + + if left < size and array[largest] < array[left]: + largest = left + if right < size and array[largest] < array[right]: + largest = right + if largest == root: + return + array[root], array[largest] = array[largest], array[root] + root = largest + + n = len(array) + + # Build a max-heap in O(n). + for i in range(n // 2 - 1, -1, -1): + sift_down(i, n) + + # Move the maximum to the end and restore the heap on the remainder. + for end in range(n - 1, 0, -1): + array[0], array[end] = array[end], array[0] + sift_down(0, end) return array diff --git a/src/sorting/python/heap_sort/tests/test_heap_sort.py b/src/sorting/python/heap_sort/tests/test_heap_sort.py index 10b9df15..4a91d6c1 100644 --- a/src/sorting/python/heap_sort/tests/test_heap_sort.py +++ b/src/sorting/python/heap_sort/tests/test_heap_sort.py @@ -42,7 +42,9 @@ def test_random_order(self): def test_with_duplicates(self): """Sorting an array with duplicate values.""" - self.assertEqual(heap_sort([3, 1, 4, 1, 5, 9, 2, 6, 5]), [1, 1, 2, 3, 4, 5, 5, 6, 9]) + self.assertEqual( + heap_sort([3, 1, 4, 1, 5, 9, 2, 6, 5]), [1, 1, 2, 3, 4, 5, 5, 6, 9] + ) def test_with_negative_numbers(self): """Sorting an array containing negative numbers.""" @@ -52,6 +54,23 @@ def test_all_same_elements(self): """Sorting an array where all elements are the same.""" self.assertEqual(heap_sort([5, 5, 5, 5]), [5, 5, 5, 5]) + def test_matches_sorted_on_random_inputs(self): + """Agrees with sorted() on many random lists, including duplicates.""" + import random + + rng = random.Random(1234) + for _ in range(300): + data = [rng.randint(-20, 20) for _ in range(rng.randint(0, 40))] + self.assertEqual(heap_sort(list(data)), sorted(data)) + + def test_large_input(self): + """A 50,000-element input finishes quickly (O(n log n)).""" + import random + + rng = random.Random(7) + data = [rng.randint(0, 10**6) for _ in range(50_000)] + self.assertEqual(heap_sort(list(data)), sorted(data)) + if __name__ == "__main__": unittest.main() diff --git a/src/sorting/python/merge_sort/src/merge_sort.py b/src/sorting/python/merge_sort/src/merge_sort.py index 2c6f5f4b..798fbfdd 100644 --- a/src/sorting/python/merge_sort/src/merge_sort.py +++ b/src/sorting/python/merge_sort/src/merge_sort.py @@ -41,6 +41,6 @@ def merge(array_a: List[T], array_b: List[T]) -> List[T]: n = len(array) if n < 2: - return array + return list(array) return merge(merge_sort(array[: n // 2]), merge_sort(array[n // 2 :])) diff --git a/src/sorting/python/merge_sort/tests/test_merge_sort.py b/src/sorting/python/merge_sort/tests/test_merge_sort.py index 5535e186..1145dc08 100644 --- a/src/sorting/python/merge_sort/tests/test_merge_sort.py +++ b/src/sorting/python/merge_sort/tests/test_merge_sort.py @@ -42,7 +42,9 @@ def test_random_order(self): def test_with_duplicates(self): """Sorting an array with duplicate values.""" - self.assertEqual(merge_sort([3, 1, 4, 1, 5, 9, 2, 6, 5]), [1, 1, 2, 3, 4, 5, 5, 6, 9]) + self.assertEqual( + merge_sort([3, 1, 4, 1, 5, 9, 2, 6, 5]), [1, 1, 2, 3, 4, 5, 5, 6, 9] + ) def test_with_negative_numbers(self): """Sorting an array containing negative numbers.""" @@ -52,6 +54,17 @@ def test_all_same_elements(self): """Sorting an array where all elements are the same.""" self.assertEqual(merge_sort([5, 5, 5, 5]), [5, 5, 5, 5]) + def test_does_not_alias_input(self): + """The result is a new list, even for 0 or 1 elements.""" + for data in ([], [1]): + self.assertIsNot(merge_sort(data), data) + + def test_does_not_mutate_input(self): + """The input list is left unchanged.""" + data = [3, 1, 2] + merge_sort(data) + self.assertEqual(data, [3, 1, 2]) + if __name__ == "__main__": unittest.main() diff --git a/src/sorting/python/quick_sort/src/quick_sort.py b/src/sorting/python/quick_sort/src/quick_sort.py index d4093419..357ad5a7 100644 --- a/src/sorting/python/quick_sort/src/quick_sort.py +++ b/src/sorting/python/quick_sort/src/quick_sort.py @@ -42,14 +42,18 @@ def partition(arr: List[T], start: int, stop: int) -> int: return i def _quick_sort(arr: List[T], start: int, stop: int) -> None: - if stop - start < 2: - return - - pivot_idx = random.randint(start, stop - 1) - arr[pivot_idx], arr[stop - 1] = arr[stop - 1], arr[pivot_idx] - pivot_pos = partition(arr, start, stop) - _quick_sort(arr, start, pivot_pos) - _quick_sort(arr, pivot_pos + 1, stop) + # Recurse into the smaller side and loop on the larger one so the + # recursion depth stays O(log n) even with many duplicate values. + while stop - start >= 2: + pivot_idx = random.randint(start, stop - 1) + arr[pivot_idx], arr[stop - 1] = arr[stop - 1], arr[pivot_idx] + pivot_pos = partition(arr, start, stop) + if pivot_pos - start < stop - (pivot_pos + 1): + _quick_sort(arr, start, pivot_pos) + start = pivot_pos + 1 + else: + _quick_sort(arr, pivot_pos + 1, stop) + stop = pivot_pos _quick_sort(array, 0, len(array)) return array diff --git a/src/sorting/python/quick_sort/tests/test_quick_sort.py b/src/sorting/python/quick_sort/tests/test_quick_sort.py index 5985fa92..cb11b776 100644 --- a/src/sorting/python/quick_sort/tests/test_quick_sort.py +++ b/src/sorting/python/quick_sort/tests/test_quick_sort.py @@ -42,7 +42,9 @@ def test_random_order(self): def test_with_duplicates(self): """Sorting an array with duplicate values.""" - self.assertEqual(quick_sort([3, 1, 4, 1, 5, 9, 2, 6, 5]), [1, 1, 2, 3, 4, 5, 5, 6, 9]) + self.assertEqual( + quick_sort([3, 1, 4, 1, 5, 9, 2, 6, 5]), [1, 1, 2, 3, 4, 5, 5, 6, 9] + ) def test_with_negative_numbers(self): """Sorting an array containing negative numbers.""" @@ -52,6 +54,16 @@ def test_all_same_elements(self): """Sorting an array where all elements are the same.""" self.assertEqual(quick_sort([5, 5, 5, 5]), [5, 5, 5, 5]) + def test_many_duplicates_no_recursion_error(self): + """Many equal elements must not exhaust the recursion limit.""" + data = [7] * 3000 + self.assertEqual(quick_sort(data), [7] * 3000) + + def test_mostly_duplicates(self): + """Sorting a large array with few distinct values.""" + data = [i % 3 for i in range(2000)] + self.assertEqual(quick_sort(list(data)), sorted(data)) + class TestQuickSortThreeWay(unittest.TestCase): """Test cases for the quick_sort_three_way function (3-way partitioning).""" @@ -86,7 +98,10 @@ def test_random_order(self): def test_with_duplicates(self): """Sorting an array with duplicate values.""" - self.assertEqual(quick_sort_three_way([3, 1, 4, 1, 5, 9, 2, 6, 5]), [1, 1, 2, 3, 4, 5, 5, 6, 9]) + self.assertEqual( + quick_sort_three_way([3, 1, 4, 1, 5, 9, 2, 6, 5]), + [1, 1, 2, 3, 4, 5, 5, 6, 9], + ) def test_with_negative_numbers(self): """Sorting an array containing negative numbers."""