From f5a9edeb55fcd85edf857d45e10df04290c8710d Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 01/26] fix(c): support negative values in radix sort Radix sort indexed its digit counts with (value / exp) % 10, which is negative for negative input and wrote before the stack count array. AddressSanitizer reported it as a stack-buffer-underflow. Take digits from each value's distance to the minimum, computed in unsigned arithmetic so INT_MIN..INT_MAX cannot overflow, and allocate the output buffer once instead of on every digit pass. Add tests for negative, all-negative, and extreme inputs, and a seeded randomized test that checks every sort against qsort. --- by-language/c/algorithms/sorting.c | 43 +++++++---- by-language/c/algorithms/sorting.h | 4 +- by-language/c/tests/test_sorting.c | 110 +++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 16 deletions(-) diff --git a/by-language/c/algorithms/sorting.c b/by-language/c/algorithms/sorting.c index c4f7cb9..86ef3f1 100644 --- a/by-language/c/algorithms/sorting.c +++ b/by-language/c/algorithms/sorting.c @@ -294,17 +294,22 @@ void counting_sort(int *arr, size_t n) { /* ============== Radix Sort (LSD) ============== */ -static void counting_sort_digit(int *arr, size_t n, int exp) { - int *output = malloc(n * sizeof(int)); - int count[10] = {0}; +/* + * Digits are taken from the distance to the minimum, computed in unsigned + * arithmetic, so negative values sort correctly and INT_MIN..INT_MAX cannot + * overflow. + */ +static unsigned int radix_key(int value, int min_val) { + return (unsigned int)value - (unsigned int)min_val; +} - if (output == NULL) { - return; - } +static void counting_sort_digit(int *arr, int *output, size_t n, int min_val, + unsigned int exp) { + size_t count[10] = {0}; /* Count occurrences of each digit */ for (size_t i = 0; i < n; i++) { - count[(arr[i] / exp) % 10]++; + count[(radix_key(arr[i], min_val) / exp) % 10]++; } /* Cumulative count */ @@ -314,14 +319,13 @@ static void counting_sort_digit(int *arr, size_t n, int exp) { /* Build output (stable) */ for (size_t i = n; i > 0; i--) { - int digit = (arr[i - 1] / exp) % 10; + unsigned int digit = (radix_key(arr[i - 1], min_val) / exp) % 10; output[count[digit] - 1] = arr[i - 1]; count[digit]--; } /* Copy back */ memcpy(arr, output, n * sizeof(int)); - free(output); } void radix_sort(int *arr, size_t n) { @@ -329,20 +333,31 @@ void radix_sort(int *arr, size_t n) { return; } - /* Find max to know number of digits */ + /* Find the range to know the number of digits */ + int min_val = arr[0]; int max_val = arr[0]; for (size_t i = 1; i < n; i++) { + if (arr[i] < min_val) { + min_val = arr[i]; + } if (arr[i] > max_val) { max_val = arr[i]; } } + int *output = malloc(n * sizeof(int)); + if (output == NULL) { + return; + } + /* Sort by each digit */ - for (int exp = 1; max_val / exp > 0; ) { - counting_sort_digit(arr, n, exp); - if (exp > max_val / 10) break; - exp *= 10; + unsigned int max_key = radix_key(max_val, min_val); + for (unsigned int exp = 1; max_key / exp > 0; exp *= 10) { + counting_sort_digit(arr, output, n, min_val, exp); + if (exp > max_key / 10) break; } + + free(output); } /* ============== Shell Sort ============== */ diff --git a/by-language/c/algorithms/sorting.h b/by-language/c/algorithms/sorting.h index b141155..fb665f8 100644 --- a/by-language/c/algorithms/sorting.h +++ b/by-language/c/algorithms/sorting.h @@ -100,10 +100,10 @@ void counting_sort(int *arr, size_t n); /** * Radix Sort (LSD) - O(d * n) - * Non-comparison sort for non-negative integers. + * Non-comparison sort for integers. Digits are taken from each value's + * distance to the minimum, so negative values are supported. * @param arr Array to sort * @param n Size of array - * Note: Only works for non-negative integers */ void radix_sort(int *arr, size_t n); diff --git a/by-language/c/tests/test_sorting.c b/by-language/c/tests/test_sorting.c index 7878c3b..b497221 100644 --- a/by-language/c/tests/test_sorting.c +++ b/by-language/c/tests/test_sorting.c @@ -5,6 +5,7 @@ #include "test_framework.h" #include "../algorithms/sorting.h" #include +#include #include #include @@ -286,6 +287,111 @@ TEST(radix_sort_max_int_exponent) { ASSERT_EQ(INT_MAX, arr[2]); } +TEST(radix_sort_negative) { + int arr[] = {3, -12, 7, -5, 1, 0, -12}; + radix_sort(arr, 7); + ASSERT_TRUE(is_sorted(arr, 7)); + ASSERT_EQ(-12, arr[0]); + ASSERT_EQ(-12, arr[1]); + ASSERT_EQ(7, arr[6]); +} + +TEST(radix_sort_all_negative) { + int arr[] = {-3, -1, -2}; + radix_sort(arr, 3); + ASSERT_EQ(-3, arr[0]); + ASSERT_EQ(-2, arr[1]); + ASSERT_EQ(-1, arr[2]); +} + +TEST(radix_sort_extreme_range) { + int arr[] = {INT_MAX, -1, INT_MIN, 0, 1}; + radix_sort(arr, 5); + ASSERT_EQ(INT_MIN, arr[0]); + ASSERT_EQ(-1, arr[1]); + ASSERT_EQ(0, arr[2]); + ASSERT_EQ(1, arr[3]); + ASSERT_EQ(INT_MAX, arr[4]); +} + +/* ============== Randomized Tests ============== */ + +static int compare_ints(const void *a, const void *b) { + int x = *(const int *)a; + int y = *(const int *)b; + return (x > y) - (x < y); +} + +/* xorshift32: a fixed seed reproduces the same inputs on every platform */ +static uint32_t next_random(uint32_t *state) { + uint32_t x = *state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *state = x; + return x; +} + +static int random_value(uint32_t *state, int trial) { + uint32_t r = next_random(state); + switch (trial % 3) { + case 0: + return (int)(r % 41) - 20; + case 1: + /* Full int range, built without overflow */ + return (r & 1u) ? INT_MIN + (int)(r >> 1) : INT_MAX - (int)(r >> 1); + default: + return (int)(r % 2001) - 1000; + } +} + +TEST(randomized_sorts_match_qsort) { + static const struct { + const char *name; + void (*sort)(int *, size_t); + } sorts[] = { + {"bubble_sort", bubble_sort}, + {"selection_sort", selection_sort}, + {"insertion_sort", insertion_sort}, + {"merge_sort", merge_sort}, + {"quick_sort", quick_sort}, + {"quick_sort_median", quick_sort_median}, + {"counting_sort", counting_sort}, + {"radix_sort", radix_sort}, + {"shell_sort", shell_sort}, + }; + const uint32_t seed = 20261023u; + uint32_t state = seed; + int input[64]; + int expected[64]; + int actual[64]; + + for (int trial = 0; trial < 300; trial++) { + size_t n = next_random(&state) % 64; + for (size_t i = 0; i < n; i++) { + input[i] = random_value(&state, trial); + } + memcpy(expected, input, n * sizeof(int)); + qsort(expected, n, sizeof(int), compare_ints); + + for (size_t s = 0; s < sizeof(sorts) / sizeof(sorts[0]); s++) { + memcpy(actual, input, n * sizeof(int)); + sorts[s].sort(actual, n); + if (!arrays_equal(expected, actual, n)) { + printf(" %s mismatch: seed=%u trial=%d n=%zu\n", + sorts[s].name, (unsigned)seed, trial, n); + } + ASSERT_TRUE(arrays_equal(expected, actual, n)); + } + + if (n > 0) { + size_t k = next_random(&state) % n; + memcpy(actual, input, n * sizeof(int)); + ASSERT_EQ(expected[k], quick_select(actual, n, k)); + } + } +} + /* ============== Shell Sort Tests ============== */ TEST(shell_sort_basic) { @@ -525,6 +631,10 @@ int main(void) { RUN_TEST(radix_sort_large_numbers); RUN_TEST(radix_sort_duplicates); RUN_TEST(radix_sort_max_int_exponent); + RUN_TEST(radix_sort_negative); + RUN_TEST(radix_sort_all_negative); + RUN_TEST(radix_sort_extreme_range); + RUN_TEST(randomized_sorts_match_qsort); /* Shell Sort */ RUN_TEST(shell_sort_basic); From 68b0fd9eed7ad167b51e5a5e9ead63cdfbc7dcb9 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 02/26] test(c): add randomized BST model test Compare insert, delete, search, successor, predecessor, and k-th smallest against a presence array over 200 seeded trials. The xorshift generator makes failures reproducible on every platform. --- by-language/c/tests/test_bst.c | 85 ++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/by-language/c/tests/test_bst.c b/by-language/c/tests/test_bst.c index 1700392..de873f2 100644 --- a/by-language/c/tests/test_bst.c +++ b/by-language/c/tests/test_bst.c @@ -4,6 +4,7 @@ #include "test_framework.h" #include "../data-structures/binary_search_tree.h" +#include #include /* ============== Basic Tests ============== */ @@ -440,6 +441,87 @@ TEST(bst_clear_test) { bst_destroy(tree); } +/* ============== Randomized Test ============== */ + +/* xorshift32: a fixed seed reproduces the same operations on every platform */ +static uint32_t next_random(uint32_t *state) { + uint32_t x = *state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *state = x; + return x; +} + +#define KEY_MIN (-20) +#define KEY_COUNT 41 + +TEST(bst_randomized_against_model) { + const uint32_t seed = 20261023u; + uint32_t state = seed; + + for (int trial = 0; trial < 200; trial++) { + BST *tree = bst_create(); + bool present[KEY_COUNT] = {false}; + int values[KEY_COUNT] = {0}; + size_t model_size = 0; + + for (int step = 0; step < 60; step++) { + int slot = (int)(next_random(&state) % KEY_COUNT); + int key = KEY_MIN + slot; + if (next_random(&state) % 10 < 6) { + int value = key * 100 + step; + ASSERT_TRUE(bst_insert(tree, key, value)); + if (!present[slot]) { + present[slot] = true; + model_size++; + } + values[slot] = value; + } else { + bool deleted = bst_delete(tree, key); + if (deleted != present[slot]) { + printf(" delete mismatch: seed=%u trial=%d step=%d key=%d\n", + (unsigned)seed, trial, step, key); + } + ASSERT_EQ(present[slot], deleted); + if (present[slot]) { + present[slot] = false; + model_size--; + } + } + ASSERT_EQ(model_size, bst_size(tree)); + } + + ASSERT_TRUE(bst_is_valid(tree)); + size_t rank = 0; + for (int slot = 0; slot < KEY_COUNT; slot++) { + int key = KEY_MIN + slot; + int found = 0; + ASSERT_EQ(present[slot], bst_contains(tree, key)); + if (!present[slot]) { + continue; + } + ASSERT_TRUE(bst_search(tree, key, &found)); + ASSERT_EQ(values[slot], found); + rank++; + ASSERT_TRUE(bst_kth_smallest(tree, rank, &found)); + ASSERT_EQ(key, found); + + int next = slot + 1; + while (next < KEY_COUNT && !present[next]) next++; + ASSERT_EQ(next < KEY_COUNT, bst_successor(tree, key, &found)); + if (next < KEY_COUNT) ASSERT_EQ(KEY_MIN + next, found); + + int prev = slot - 1; + while (prev >= 0 && !present[prev]) prev--; + ASSERT_EQ(prev >= 0, bst_predecessor(tree, key, &found)); + if (prev >= 0) ASSERT_EQ(KEY_MIN + prev, found); + } + ASSERT_EQ(model_size, rank); + bst_destroy(tree); + } +} + /* ============== Null Safety Test ============== */ TEST(bst_null_safety) { @@ -507,6 +589,9 @@ int main(void) { /* Null safety test */ RUN_TEST(bst_null_safety); + /* Randomized test */ + RUN_TEST(bst_randomized_against_model); + TEST_SUITE_END(); return GET_FAILED_COUNT() > 0 ? 1 : 0; From ccc4d4c08b30fdeab7999fe0969aefe7db936d56 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 03/26] build(c): track header dependencies in the Makefile Object rules depended only on their own source file, and the algorithm rule ignored headers entirely, so editing a header left stale objects. Generate dependency files with -MMD -MP and include them, which makes the hand-written graph object rules unnecessary. Declare test-bst, test-ht, and test-heap as phony targets. --- by-language/c/Makefile | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/by-language/c/Makefile b/by-language/c/Makefile index 1b87671..081a46f 100644 --- a/by-language/c/Makefile +++ b/by-language/c/Makefile @@ -10,6 +10,8 @@ CC = gcc CFLAGS = -Wall -Wextra -std=c11 -g -I. LDFLAGS = +# Emit build/*.d files so objects rebuild when an included header changes +DEPFLAGS = -MMD -MP # Directories SRC_DS = data-structures @@ -25,7 +27,7 @@ ALG_SOURCES = $(wildcard $(SRC_ALG)/*.c) DS_OBJECTS = $(patsubst $(SRC_DS)/%.c,$(BUILD_DIR)/ds_%.o,$(DS_SOURCES)) ALG_OBJECTS = $(patsubst $(SRC_ALG)/%.c,$(BUILD_DIR)/alg_%.o,$(ALG_SOURCES)) -.PHONY: all clean test test-da test-sll test-dll test-stack test-queue test-sort test-graph test-search test-dp test-string test-trie test-avl test-cache test-range test-bloom test-skiplist test-rb test-btree test-adjmat test-uf test-mq test-ttl +.PHONY: all clean test test-da test-sll test-dll test-stack test-queue test-bst test-ht test-heap test-sort test-graph test-search test-dp test-string test-trie test-avl test-cache test-range test-bloom test-skiplist test-rb test-btree test-adjmat test-uf test-mq test-ttl all: $(BUILD_DIR) $(DS_OBJECTS) $(ALG_OBJECTS) @@ -33,12 +35,14 @@ $(BUILD_DIR): mkdir -p $(BUILD_DIR) # Data structure object files -$(BUILD_DIR)/ds_%.o: $(SRC_DS)/%.c $(SRC_DS)/%.h | $(BUILD_DIR) - $(CC) $(CFLAGS) -c $< -o $@ +$(BUILD_DIR)/ds_%.o: $(SRC_DS)/%.c | $(BUILD_DIR) + $(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@ # Algorithm object files $(BUILD_DIR)/alg_%.o: $(SRC_ALG)/%.c | $(BUILD_DIR) - $(CC) $(CFLAGS) -c $< -o $@ + $(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@ + +-include $(wildcard $(BUILD_DIR)/*.d) # Individual test targets test-da: $(BUILD_DIR) $(BUILD_DIR)/ds_dynamic_array.o @@ -78,12 +82,6 @@ test-sort: $(BUILD_DIR) $(BUILD_DIR)/alg_sorting.o ./$(BUILD_DIR)/test_sort # Graph is split into core and algorithms -$(BUILD_DIR)/ds_graph_core.o: $(SRC_DS)/graph_core.c $(SRC_DS)/graph_core.h | $(BUILD_DIR) - $(CC) $(CFLAGS) -c $< -o $@ - -$(BUILD_DIR)/ds_graph_algorithms.o: $(SRC_DS)/graph_algorithms.c $(SRC_DS)/graph_algorithms.h $(SRC_DS)/graph_core.h | $(BUILD_DIR) - $(CC) $(CFLAGS) -c $< -o $@ - test-graph: $(BUILD_DIR) $(BUILD_DIR)/ds_graph_core.o $(BUILD_DIR)/ds_graph_algorithms.o $(CC) $(CFLAGS) $(TEST_DIR)/test_graph.c $(BUILD_DIR)/ds_graph_core.o $(BUILD_DIR)/ds_graph_algorithms.o -o $(BUILD_DIR)/test_graph $(LDFLAGS) ./$(BUILD_DIR)/test_graph From 7427e59916a86bfb10f0e3fdc328d0a25c3b4123 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 04/26] style(go): apply gofmt Six files had drifted from gofmt output. Formatting only. --- .../go/algorithms/dynamic_programming.go | 4 ++-- by-language/go/datastructures/bloom_filter.go | 20 +++++++++---------- .../go/datastructures/dynamic_array.go | 6 +++--- by-language/go/datastructures/graph_core.go | 15 +++++++++++--- by-language/go/datastructures/heap.go | 6 +++--- by-language/go/datastructures/skip_list.go | 2 +- 6 files changed, 31 insertions(+), 22 deletions(-) diff --git a/by-language/go/algorithms/dynamic_programming.go b/by-language/go/algorithms/dynamic_programming.go index 4ca0f66..75893c8 100644 --- a/by-language/go/algorithms/dynamic_programming.go +++ b/by-language/go/algorithms/dynamic_programming.go @@ -396,8 +396,8 @@ func LongestPalindromicSubstring(s string) string { } for i := 0; i < len(s); i++ { - expand(i, i) // odd length - expand(i, i+1) // even length + expand(i, i) // odd length + expand(i, i+1) // even length } return s[start : start+maxLen] } diff --git a/by-language/go/datastructures/bloom_filter.go b/by-language/go/datastructures/bloom_filter.go index e3f490a..d1ddc2d 100644 --- a/by-language/go/datastructures/bloom_filter.go +++ b/by-language/go/datastructures/bloom_filter.go @@ -9,10 +9,10 @@ import ( // BloomFilter is a space-efficient probabilistic data structure. type BloomFilter struct { - bits []bool - size int - numHash int - count int + bits []bool + size int + numHash int + count int } func NewBloomFilter(expectedItems int, falsePositiveRate float64) *BloomFilter { @@ -172,12 +172,12 @@ func (cbf *CountingBloomFilter) FalsePositiveRate() float64 { // ScalableBloomFilter grows automatically when the current filter becomes too full. type ScalableBloomFilter struct { - filters []*BloomFilter - targetFPRate float64 - growthFactor float64 - initialCapacity int - totalCount int - maxFillRatio float64 + filters []*BloomFilter + targetFPRate float64 + growthFactor float64 + initialCapacity int + totalCount int + maxFillRatio float64 } func NewScalableBloomFilter(initialCapacity int, targetFPRate float64) *ScalableBloomFilter { diff --git a/by-language/go/datastructures/dynamic_array.go b/by-language/go/datastructures/dynamic_array.go index dd7ca88..ddfe749 100644 --- a/by-language/go/datastructures/dynamic_array.go +++ b/by-language/go/datastructures/dynamic_array.go @@ -1,9 +1,9 @@ package datastructures const ( - defaultCapacity = 8 - growthFactor = 2 - shrinkThreshold = 0.25 + defaultCapacity = 8 + growthFactor = 2 + shrinkThreshold = 0.25 ) // DynamicArray is a resizable array implementation. diff --git a/by-language/go/datastructures/graph_core.go b/by-language/go/datastructures/graph_core.go index 8be9f3c..6c91e8d 100644 --- a/by-language/go/datastructures/graph_core.go +++ b/by-language/go/datastructures/graph_core.go @@ -105,11 +105,20 @@ func (g *GraphCore) Neighbors(v int) []int { return nil } -func (g *GraphCore) NeighborsWeighted(v int) []struct{ Vertex int; Weight float64 } { +func (g *GraphCore) NeighborsWeighted(v int) []struct { + Vertex int + Weight float64 +} { if neighbors, ok := g.adj[v]; ok { - result := make([]struct{ Vertex int; Weight float64 }, 0, len(neighbors)) + result := make([]struct { + Vertex int + Weight float64 + }, 0, len(neighbors)) for u, w := range neighbors { - result = append(result, struct{ Vertex int; Weight float64 }{u, w}) + result = append(result, struct { + Vertex int + Weight float64 + }{u, w}) } return result } diff --git a/by-language/go/datastructures/heap.go b/by-language/go/datastructures/heap.go index e12f0a7..ee2cdbc 100644 --- a/by-language/go/datastructures/heap.go +++ b/by-language/go/datastructures/heap.go @@ -18,7 +18,7 @@ func MinHeapFromSlice[T Ordered](items []T) *MinHeap[T] { return h } -func (h *MinHeap[T]) Size() int { return len(h.data) } +func (h *MinHeap[T]) Size() int { return len(h.data) } func (h *MinHeap[T]) IsEmpty() bool { return len(h.data) == 0 } func (h *MinHeap[T]) Peek() (T, bool) { @@ -129,7 +129,7 @@ func MaxHeapFromSlice[T Ordered](items []T) *MaxHeap[T] { return h } -func (h *MaxHeap[T]) Size() int { return len(h.data) } +func (h *MaxHeap[T]) Size() int { return len(h.data) } func (h *MaxHeap[T]) IsEmpty() bool { return len(h.data) == 0 } func (h *MaxHeap[T]) Peek() (T, bool) { @@ -277,7 +277,7 @@ func NewPriorityQueue[T any]() *PriorityQueue[T] { return &PriorityQueue[T]{} } -func (pq *PriorityQueue[T]) Size() int { return len(pq.items) } +func (pq *PriorityQueue[T]) Size() int { return len(pq.items) } func (pq *PriorityQueue[T]) IsEmpty() bool { return len(pq.items) == 0 } func (pq *PriorityQueue[T]) Enqueue(value T, priority int) { diff --git a/by-language/go/datastructures/skip_list.go b/by-language/go/datastructures/skip_list.go index 3e3911d..df88923 100644 --- a/by-language/go/datastructures/skip_list.go +++ b/by-language/go/datastructures/skip_list.go @@ -28,7 +28,7 @@ func NewSkipList[K Ordered, V any]() *SkipList[K, V] { return &SkipList[K, V]{head: head, level: 0} } -func (sl *SkipList[K, V]) Size() int { return sl.size } +func (sl *SkipList[K, V]) Size() int { return sl.size } func (sl *SkipList[K, V]) IsEmpty() bool { return sl.size == 0 } func (sl *SkipList[K, V]) randomLevel() int { From f7746c7801c8af4f750c91c5b018bb02e751a65e Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 05/26] fix(go): accept the full int range in counting and radix sort RadixSort indexed its digit counts with a negative value for negative input and panicked. CountingSort computed max-min+1 in int, which overflows for wide ranges, and allocated the whole range. Both now work on unsigned distances from the minimum. CountingSort falls back to slices.Sort when the range is more than 16 times the input length, matching the C implementation. --- by-language/go/algorithms/sorting.go | 80 ++++++++++++++++------- by-language/go/algorithms/sorting_test.go | 65 ++++++++++++++++++ 2 files changed, 120 insertions(+), 25 deletions(-) diff --git a/by-language/go/algorithms/sorting.go b/by-language/go/algorithms/sorting.go index 1ecd596..166a23e 100644 --- a/by-language/go/algorithms/sorting.go +++ b/by-language/go/algorithms/sorting.go @@ -2,6 +2,7 @@ package algorithms import ( "math/rand" + "slices" ) // Ordered constraint for comparable types. @@ -165,64 +166,93 @@ func ShellSort[T Ordered](arr []T) { } } -// CountingSort sorts integers in O(n + k) where k is the range. +// CountingSort sorts integers in O(n + k) where k is the range of values. +// The range is max-min+1, computed with unsigned subtraction so every int +// value is accepted and MinInt..MaxInt cannot overflow. When that range is +// wider than 16*n, it falls back to slices.Sort. func CountingSort(arr []int) { - if len(arr) <= 1 { + n := len(arr) + if n <= 1 { return } - min, max := arr[0], arr[0] - for _, v := range arr { - if v < min { - min = v + minVal, maxVal := arr[0], arr[0] + for _, v := range arr[1:] { + if v < minVal { + minVal = v } - if v > max { - max = v + if v > maxVal { + maxVal = v } } - offset := min - count := make([]int, max-min+1) + // Two's-complement subtraction yields the mathematical difference for + // every pair of int values, including MinInt and MaxInt. + span := uint(maxVal) - uint(minVal) + tooWide := span == ^uint(0) || uint(n) > ^uint(0)/16 + var rangeSize uint + if !tooWide { + rangeSize = span + 1 + tooWide = rangeSize > uint(n)*16 || rangeSize > ^uint(0)>>1 + } + if tooWide { + slices.Sort(arr) + return + } + count := make([]int, rangeSize) for _, v := range arr { - count[v-offset]++ + count[uint(v)-uint(minVal)]++ } idx := 0 for i, c := range count { + value := int(uint(minVal) + uint(i)) for j := 0; j < c; j++ { - arr[idx] = i + offset + arr[idx] = value idx++ } } } -// RadixSort sorts non-negative integers in O(d*(n+10)). +// RadixSort sorts integers in O(d*(n+10)). Digits are taken from each +// value's distance to the minimum, computed as an unsigned key, so negative +// values are supported and the full int range cannot overflow. func RadixSort(arr []int) { if len(arr) <= 1 { return } - max := arr[0] + minVal, maxVal := arr[0], arr[0] for _, v := range arr { - if v > max { - max = v + if v < minVal { + minVal = v + } + if v > maxVal { + maxVal = v } } - for exp := 1; max/exp > 0; exp *= 10 { - countingSortByDigit(arr, exp) + output := make([]int, len(arr)) + maxKey := radixKey(maxVal, minVal) + for exp := uint(1); maxKey/exp > 0; exp *= 10 { + countingSortByDigit(arr, output, minVal, exp) + if exp > maxKey/10 { + break + } } } -func countingSortByDigit(arr []int, exp int) { - n := len(arr) - output := make([]int, n) - count := make([]int, 10) +func radixKey(v, minVal int) uint { + return uint(v) - uint(minVal) +} + +func countingSortByDigit(arr, output []int, minVal int, exp uint) { + var count [10]int for _, v := range arr { - count[(v/exp)%10]++ + count[(radixKey(v, minVal)/exp)%10]++ } for i := 1; i < 10; i++ { count[i] += count[i-1] } - for i := n - 1; i >= 0; i-- { - digit := (arr[i] / exp) % 10 + for i := len(arr) - 1; i >= 0; i-- { + digit := (radixKey(arr[i], minVal) / exp) % 10 count[digit]-- output[count[digit]] = arr[i] } diff --git a/by-language/go/algorithms/sorting_test.go b/by-language/go/algorithms/sorting_test.go index b6c05e5..3520697 100644 --- a/by-language/go/algorithms/sorting_test.go +++ b/by-language/go/algorithms/sorting_test.go @@ -1,6 +1,7 @@ package algorithms import ( + "math" "testing" ) @@ -92,16 +93,80 @@ func TestCountingSort(t *testing.T) { arr := []int{4, 2, 2, 8, 3, 3, 1} CountingSort(arr) verifySorted(t, "CountingSort", arr) + want := []int{1, 2, 2, 3, 3, 4, 8} + for i := range want { + if arr[i] != want[i] { + t.Fatalf("CountingSort = %v, want %v", arr, want) + } + } arr2 := []int{-3, -1, 0, 2, 5, -2} CountingSort(arr2) verifySorted(t, "CountingSort negative", arr2) + + // Narrow ranges around the int extremes still use counting sort. + nearMin := []int{math.MinInt + 2, math.MinInt, math.MinInt + 2, math.MinInt + 1} + CountingSort(nearMin) + wantNearMin := []int{math.MinInt, math.MinInt + 1, math.MinInt + 2, math.MinInt + 2} + for i := range wantNearMin { + if nearMin[i] != wantNearMin[i] { + t.Fatalf("CountingSort near MinInt = %v, want %v", nearMin, wantNearMin) + } + } + nearMax := []int{math.MaxInt, math.MaxInt - 2, math.MaxInt - 1} + CountingSort(nearMax) + wantNearMax := []int{math.MaxInt - 2, math.MaxInt - 1, math.MaxInt} + for i := range wantNearMax { + if nearMax[i] != wantNearMax[i] { + t.Fatalf("CountingSort near MaxInt = %v, want %v", nearMax, wantNearMax) + } + } + + // The full int domain and a sparse wide range must not overflow or allocate. + extremes := []int{math.MaxInt, -1, math.MinInt, 0, 1} + CountingSort(extremes) + wantExtremes := []int{math.MinInt, -1, 0, 1, math.MaxInt} + for i := range wantExtremes { + if extremes[i] != wantExtremes[i] { + t.Fatalf("CountingSort extremes = %v, want %v", extremes, wantExtremes) + } + } + + sparse := []int{1 << 40, 0, 1 << 40} + CountingSort(sparse) + wantSparse := []int{0, 1 << 40, 1 << 40} + for i := range wantSparse { + if sparse[i] != wantSparse[i] { + t.Fatalf("CountingSort sparse = %v, want %v", sparse, wantSparse) + } + } + + single := []int{math.MinInt} + CountingSort(single) + if single[0] != math.MinInt { + t.Fatalf("CountingSort single = %d", single[0]) + } + var empty []int + CountingSort(empty) } func TestRadixSort(t *testing.T) { arr := []int{170, 45, 75, 90, 802, 24, 2, 66} RadixSort(arr) verifySorted(t, "RadixSort", arr) + + negative := []int{3, -12, 7, -5, 1, 0, -12} + RadixSort(negative) + verifySorted(t, "RadixSort negative", negative) + + extremes := []int{math.MaxInt, -1, math.MinInt, 0, 1} + RadixSort(extremes) + want := []int{math.MinInt, -1, 0, 1, math.MaxInt} + for i := range want { + if extremes[i] != want[i] { + t.Fatalf("RadixSort extremes = %v, want %v", extremes, want) + } + } } func TestBucketSort(t *testing.T) { From 4002b5f6126635c9abdf58d1f4ec664add0e0505 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 06/26] fix(go): correct interpolation search, B-tree, and empty patterns Fuzzing against reference models found three edge cases: - InterpolationSearch converted an int difference to float64, which overflows across the full int range, and divided by zero when the endpoint values were equal. The probe now uses unsigned distances. - BTree.Insert checked the wrong slot for an existing key stored in an internal node, so re-inserting that key added a second copy instead of updating its value. - RabinKarpSearch returned [0] and ZAlgorithmSearch returned every index but the last for an empty pattern. Both now return no matches, like KMPSearch. --- by-language/go/algorithms/searching.go | 19 +++++++++---- by-language/go/algorithms/searching_test.go | 18 +++++++++++- .../go/algorithms/string_algorithms.go | 7 +++-- .../go/algorithms/string_algorithms_test.go | 10 +++++++ by-language/go/datastructures/b_tree.go | 5 ++-- by-language/go/datastructures/b_tree_test.go | 28 +++++++++++++++++++ 6 files changed, 77 insertions(+), 10 deletions(-) diff --git a/by-language/go/algorithms/searching.go b/by-language/go/algorithms/searching.go index 187fcc2..831ccd5 100644 --- a/by-language/go/algorithms/searching.go +++ b/by-language/go/algorithms/searching.go @@ -103,23 +103,32 @@ func min(a, b int) int { } // InterpolationSearch for uniformly distributed sorted slices. +// The probe is an integer ratio of unsigned distances, so the full int +// domain cannot overflow the way a float64 conversion of int subtraction can. func InterpolationSearch(arr []int, target int) int { low, high := 0, len(arr)-1 for low <= high && target >= arr[low] && target <= arr[high] { - if low == high { + if arr[low] == arr[high] { if arr[low] == target { return low } return -1 } - pos := low + int(float64(high-low)*float64(target-arr[low])/float64(arr[high]-arr[low])) - if pos < low || pos > high { - break + span := uint(arr[high]) - uint(arr[low]) + offset := uint(target) - uint(arr[low]) + dist := uint(high - low) + var step uint + if offset > ^uint(0)/dist { + step = dist / 2 + } else { + step = (dist * offset) / span } + pos := low + int(step) if arr[pos] == target { return pos - } else if arr[pos] < target { + } + if arr[pos] < target { low = pos + 1 } else { high = pos - 1 diff --git a/by-language/go/algorithms/searching_test.go b/by-language/go/algorithms/searching_test.go index fc0a22a..5f85acb 100644 --- a/by-language/go/algorithms/searching_test.go +++ b/by-language/go/algorithms/searching_test.go @@ -1,6 +1,9 @@ package algorithms -import "testing" +import ( + "math" + "testing" +) func TestLinearSearch(t *testing.T) { arr := []int{5, 3, 8, 1, 9} @@ -74,6 +77,19 @@ func TestInterpolationSearch(t *testing.T) { } } +func TestInterpolationSearchWideValues(t *testing.T) { + arr := []int{-1 << 62, 0, 1 << 62} + if got := InterpolationSearch(arr, 0); got != 1 { + t.Fatalf("InterpolationSearch wide = %d, want 1", got) + } + if got := InterpolationSearch(arr, 1); got != -1 { + t.Fatalf("InterpolationSearch missing wide = %d, want -1", got) + } + if got := InterpolationSearch([]int{math.MinInt, math.MaxInt}, math.MaxInt); got != 1 { + t.Fatalf("InterpolationSearch extremes = %d, want 1", got) + } +} + func TestExponentialSearch(t *testing.T) { arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} if ExponentialSearch(arr, 6) != 5 { diff --git a/by-language/go/algorithms/string_algorithms.go b/by-language/go/algorithms/string_algorithms.go index 0dfb123..c69d72e 100644 --- a/by-language/go/algorithms/string_algorithms.go +++ b/by-language/go/algorithms/string_algorithms.go @@ -77,7 +77,7 @@ func KMPSearch(text, pattern string) []int { func RabinKarpSearch(text, pattern string) []int { var result []int n, m := len(text), len(pattern) - if m > n { + if m == 0 || m > n { return result } @@ -141,9 +141,12 @@ func ComputeZArray(s string) []int { // ZAlgorithmSearch finds all occurrences using Z-algorithm. O(n+m) func ZAlgorithmSearch(text, pattern string) []int { var result []int + m := len(pattern) + if m == 0 || m > len(text) { + return result + } concat := pattern + "$" + text z := ComputeZArray(concat) - m := len(pattern) for i := m + 1; i < len(concat); i++ { if z[i] >= m { diff --git a/by-language/go/algorithms/string_algorithms_test.go b/by-language/go/algorithms/string_algorithms_test.go index 8d86e8c..4e6381b 100644 --- a/by-language/go/algorithms/string_algorithms_test.go +++ b/by-language/go/algorithms/string_algorithms_test.go @@ -66,6 +66,11 @@ func TestRabinKarpSearch(t *testing.T) { if len(result) != 0 { t.Errorf("expected empty when pattern longer than text, got %v", result) } + + result = RabinKarpSearch("abc", "") + if len(result) != 0 { + t.Errorf("expected empty for empty pattern, got %v", result) + } } func TestComputeZArray(t *testing.T) { @@ -90,6 +95,11 @@ func TestZAlgorithmSearch(t *testing.T) { if len(result) != 0 { t.Errorf("expected empty, got %v", result) } + + result = ZAlgorithmSearch("abc", "") + if len(result) != 0 { + t.Errorf("expected empty for empty pattern, got %v", result) + } } func TestAllSearchAlgorithmsAgree(t *testing.T) { diff --git a/by-language/go/datastructures/b_tree.go b/by-language/go/datastructures/b_tree.go index 0ff8c56..227bdea 100644 --- a/by-language/go/datastructures/b_tree.go +++ b/by-language/go/datastructures/b_tree.go @@ -87,11 +87,12 @@ func (bt *BTree[K, V]) insertNonFull(node *BTreeNode[K, V], key K, value V) { for i >= 0 && key < node.Keys[i] { i-- } - i++ - if i < len(node.Keys) && key == node.Keys[i] { + // i is the rightmost key that is <= key, or -1 when every key is larger. + if i >= 0 && key == node.Keys[i] { node.Values[i] = value return } + i++ if len(node.Children[i].Keys) == 2*bt.t-1 { bt.splitChild(node, i) if key > node.Keys[i] { diff --git a/by-language/go/datastructures/b_tree_test.go b/by-language/go/datastructures/b_tree_test.go index e6ff60e..8705290 100644 --- a/by-language/go/datastructures/b_tree_test.go +++ b/by-language/go/datastructures/b_tree_test.go @@ -99,6 +99,34 @@ func TestBTreeUpdate(t *testing.T) { } } +func TestBTreeUpdatePromotedKey(t *testing.T) { + bt := NewBTree[int, int](2) + bt.Insert(10, 1) + bt.Insert(20, 2) + bt.Insert(5, 3) + // The next insert splits a full root and promotes 10. Updating that + // internal key must not create a second copy. + bt.Insert(10, 4) + + if bt.Size() != 3 { + t.Fatalf("size = %d, want 3", bt.Size()) + } + val, ok := bt.Search(10) + if !ok || val != 4 { + t.Fatalf("search 10 = %d, %v", val, ok) + } + got := bt.Inorder() + want := []int{5, 10, 20} + if len(got) != len(want) { + t.Fatalf("inorder = %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("inorder = %v, want %v", got, want) + } + } +} + func TestBTreeLargeScale(t *testing.T) { bt := NewBTree[int, int](3) From 21878161f5f84b92341b3a4422140ce9d8c2058f Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 07/26] fix(go): keep repeated values once in BSTFromSortedSlice BST keys are unique, but BSTFromSortedSlice built nodes straight from the slice, so sorted input with repeats produced duplicate keys and a wrong size. Drop adjacent repeats before building. The tests also cover Successor and Predecessor at the minimum, maximum, and for missing values. --- .../go/datastructures/binary_search_tree.go | 12 ++- .../datastructures/binary_search_tree_test.go | 89 ++++++++++++++++++- 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/by-language/go/datastructures/binary_search_tree.go b/by-language/go/datastructures/binary_search_tree.go index 3fe12f3..9a42dd0 100644 --- a/by-language/go/datastructures/binary_search_tree.go +++ b/by-language/go/datastructures/binary_search_tree.go @@ -366,10 +366,18 @@ func (t *BST[T]) height(node *BSTNode[T]) int { return rh + 1 } +// BSTFromSortedSlice builds a balanced BST from a sorted slice. Repeated +// values are kept once. func BSTFromSortedSlice[T Ordered](arr []T) *BST[T] { + distinct := make([]T, 0, len(arr)) + for i, v := range arr { + if i == 0 || v != arr[i-1] { + distinct = append(distinct, v) + } + } tree := NewBST[T]() - tree.Root = buildBST(arr, 0, len(arr)-1, nil) - tree.size = len(arr) + tree.Root = buildBST(distinct, 0, len(distinct)-1, nil) + tree.size = len(distinct) return tree } diff --git a/by-language/go/datastructures/binary_search_tree_test.go b/by-language/go/datastructures/binary_search_tree_test.go index 570e8d7..6cbceb3 100644 --- a/by-language/go/datastructures/binary_search_tree_test.go +++ b/by-language/go/datastructures/binary_search_tree_test.go @@ -123,11 +123,47 @@ func TestBSTSuccessorPredecessor(t *testing.T) { if !ok || succ != 6 { t.Errorf("expected successor 6, got %d", succ) } + succ, ok = tree.Successor(4) + if !ok || succ != 5 { + t.Errorf("expected successor of 4 to be 5, got %d", succ) + } + succ, ok = tree.Successor(1) + if !ok || succ != 3 { + t.Errorf("expected successor of 1 to be 3, got %d", succ) + } + succ, ok = tree.Successor(6) + if !ok || succ != 7 { + t.Errorf("expected successor of 6 to be 7, got %d", succ) + } + if _, ok = tree.Successor(8); ok { + t.Error("maximum should have no successor") + } + if _, ok = tree.Successor(99); ok { + t.Error("missing value should have no successor") + } pred, ok := tree.Predecessor(5) if !ok || pred != 4 { t.Errorf("expected predecessor 4, got %d", pred) } + pred, ok = tree.Predecessor(6) + if !ok || pred != 5 { + t.Errorf("expected predecessor of 6 to be 5, got %d", pred) + } + pred, ok = tree.Predecessor(4) + if !ok || pred != 3 { + t.Errorf("expected predecessor of 4 to be 3, got %d", pred) + } + pred, ok = tree.Predecessor(7) + if !ok || pred != 6 { + t.Errorf("expected predecessor of 7 to be 6, got %d", pred) + } + if _, ok = tree.Predecessor(1); ok { + t.Error("minimum should have no predecessor") + } + if _, ok = tree.Predecessor(99); ok { + t.Error("missing value should have no predecessor") + } } func TestBSTKthSmallest(t *testing.T) { @@ -199,6 +235,17 @@ func TestBSTFromSorted(t *testing.T) { if tree.Height() > 3 { t.Error("tree from sorted should be balanced") } + + repeated := BSTFromSortedSlice([]int{1, 1, 2, 3, 3, 3}) + if repeated.Size() != 3 { + t.Errorf("expected repeated values to be kept once, got size %d", repeated.Size()) + } + if got := repeated.Inorder(); len(got) != 3 || got[0] != 1 || got[1] != 2 || got[2] != 3 { + t.Errorf("expected in-order [1 2 3], got %v", got) + } + if !repeated.IsValidBST() { + t.Error("tree built from repeated values should be valid") + } } func TestBSTLowestCommonAncestor(t *testing.T) { @@ -216,6 +263,19 @@ func TestBSTLowestCommonAncestor(t *testing.T) { if !ok || lca != 2 { t.Errorf("expected LCA 2, got %d", lca) } + + lca, ok = tree.LowestCommonAncestor(7, 9) + if !ok || lca != 8 { + t.Errorf("expected LCA 8, got %d", lca) + } + if _, ok = tree.LowestCommonAncestor(-2, -1); ok { + t.Error("keys past the minimum should have no ancestor") + } + + empty := NewBST[int]() + if _, ok = empty.LowestCommonAncestor(1, 2); ok { + t.Error("empty tree should have no ancestor") + } } func TestBSTCountNodes(t *testing.T) { @@ -426,10 +486,31 @@ func TestBSTMinMaxEmpty(t *testing.T) { func TestBSTDuplicate(t *testing.T) { tree := NewBST[int]() - tree.Insert(5) - tree.Insert(5) - if tree.Size() != 1 { - t.Errorf("expected 1 after duplicate insert, got %d", tree.Size()) + first := tree.Insert(5) + tree.Insert(3) + tree.Insert(7) + again := tree.Insert(5) + if again != first { + t.Fatal("duplicate insert should return the existing node") + } + if tree.Size() != 3 { + t.Fatalf("size = %d, want 3", tree.Size()) + } + if !tree.IsValidBST() { + t.Fatal("duplicate insert should leave a valid BST") + } + if tree.Root != first || tree.Root.Left.Value != 3 || tree.Root.Right.Value != 7 { + t.Fatalf("duplicate insert changed the tree shape: %+v", tree.Root) + } + got := tree.Inorder() + want := []int{3, 5, 7} + if len(got) != len(want) { + t.Fatalf("inorder = %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("inorder = %v, want %v", got, want) + } } } From a10f309f0bd06e5f729ca7435b3348c617335b89 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 08/26] test(go): add fuzz targets and fill coverage gaps Add native fuzz targets for the integer sorts, sorted-search helpers, string search, and the BST, AVL, red-black, B-tree, and skip-list implementations. Each is seeded with f.Add, so go test runs the seeds. Also cover the adjacency matrix, graph core accessors, LRU Clear, and empty queue paths. --- by-language/go/algorithms/fuzz_test.go | 215 +++++++++++++++++ .../datastructures/adjacency_matrix_test.go | 101 ++++++++ by-language/go/datastructures/cache_test.go | 26 ++ by-language/go/datastructures/graph_test.go | 98 ++++++++ by-language/go/datastructures/queue_test.go | 43 ++++ .../go/datastructures/tree_fuzz_test.go | 228 ++++++++++++++++++ 6 files changed, 711 insertions(+) create mode 100644 by-language/go/algorithms/fuzz_test.go create mode 100644 by-language/go/datastructures/adjacency_matrix_test.go create mode 100644 by-language/go/datastructures/tree_fuzz_test.go diff --git a/by-language/go/algorithms/fuzz_test.go b/by-language/go/algorithms/fuzz_test.go new file mode 100644 index 0000000..43f7604 --- /dev/null +++ b/by-language/go/algorithms/fuzz_test.go @@ -0,0 +1,215 @@ +package algorithms + +import ( + "encoding/binary" + "math" + "slices" + "sort" + "strings" + "testing" +) + +func encodeInts(vals ...int64) []byte { + buf := make([]byte, 8*len(vals)) + for i, v := range vals { + binary.LittleEndian.PutUint64(buf[i*8:], uint64(v)) + } + return buf +} + +func decodeInts(data []byte) []int { + n := len(data) / 8 + if n > 32 { + n = 32 + } + out := make([]int, n) + for i := 0; i < n; i++ { + out[i] = int(int64(binary.LittleEndian.Uint64(data[i*8 : i*8+8]))) + } + return out +} + +func FuzzIntSorts(f *testing.F) { + f.Add([]byte{}) + f.Add(encodeInts(1)) + f.Add(encodeInts(5, 3, 8, 1, 9, 2, 7, 4, 6)) + f.Add(encodeInts(3, 3, 3, 1, 1, 2, 2)) + f.Add(encodeInts(-4, -1, 0, 2, 2, -3)) + f.Add(encodeInts(math.MaxInt64, -1, math.MinInt64, 0, 1)) + f.Add(encodeInts(0, 1<<40, 1<<40)) + + f.Fuzz(func(t *testing.T, data []byte) { + base := decodeInts(data) + want := append([]int(nil), base...) + slices.Sort(want) + + check := func(name string, got []int) { + t.Helper() + if !slices.Equal(got, want) { + t.Fatalf("%s(%v) = %v, want %v", name, base, got, want) + } + } + sortCopy := func(name string, fn func([]int)) { + t.Helper() + arr := append([]int(nil), base...) + fn(arr) + check(name, arr) + } + + sortCopy("BubbleSort", func(a []int) { BubbleSort(a) }) + sortCopy("SelectionSort", func(a []int) { SelectionSort(a) }) + sortCopy("InsertionSort", func(a []int) { InsertionSort(a) }) + sortCopy("QuickSort", func(a []int) { QuickSort(a) }) + sortCopy("HeapSortAlgo", func(a []int) { HeapSortAlgo(a) }) + sortCopy("ShellSort", func(a []int) { ShellSort(a) }) + sortCopy("CountingSort", func(a []int) { CountingSort(a) }) + sortCopy("RadixSort", func(a []int) { RadixSort(a) }) + sortCopy("TimSort", func(a []int) { TimSort(a) }) + sortCopy("CombSort", func(a []int) { CombSort(a) }) + sortCopy("GnomeSort", func(a []int) { GnomeSort(a) }) + sortCopy("PancakeSort", func(a []int) { PancakeSort(a) }) + sortCopy("CycleSort", func(a []int) { CycleSort(a) }) + sortCopy("MergeSortIterative", func(a []int) { MergeSortIterative(a) }) + check("MergeSort", MergeSort(append([]int(nil), base...))) + }) +} + +func FuzzSortedSearch(f *testing.F) { + f.Add([]byte{}) + f.Add(encodeInts(1, 3, 3, 5, 8, 3)) + f.Add(encodeInts(0, 4, 9)) + f.Add(encodeInts(-1<<62, 0, 1<<62, 0)) + f.Add(encodeInts(math.MinInt64, -1, 0, 1, math.MaxInt64, 42)) + + f.Fuzz(func(t *testing.T, data []byte) { + vals := decodeInts(data) + target := 0 + var arr []int + if len(vals) > 0 { + target = vals[len(vals)-1] + arr = append([]int(nil), vals[:len(vals)-1]...) + } + slices.Sort(arr) + + checkSortedSearch(t, arr, target) + seen := make(map[int]struct{}, len(arr)) + for _, v := range arr { + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + checkSortedSearch(t, arr, v) + } + if len(arr) > 0 && arr[0] > math.MinInt { + checkSortedSearch(t, arr, arr[0]-1) + } + if len(arr) > 0 && arr[len(arr)-1] < math.MaxInt { + checkSortedSearch(t, arr, arr[len(arr)-1]+1) + } + }) +} + +func checkSortedSearch(t *testing.T, arr []int, target int) { + t.Helper() + wantLower := sort.SearchInts(arr, target) + if got := LowerBound(arr, target); got != wantLower { + t.Fatalf("LowerBound(%v, %d) = %d, want %d", arr, target, got, wantLower) + } + wantUpper := sort.Search(len(arr), func(i int) bool { return arr[i] > target }) + if got := UpperBound(arr, target); got != wantUpper { + t.Fatalf("UpperBound(%v, %d) = %d, want %d", arr, target, got, wantUpper) + } + + found := wantLower < len(arr) && arr[wantLower] == target + gotBin := BinarySearch(arr, target) + if found { + if gotBin < 0 || gotBin >= len(arr) || arr[gotBin] != target { + t.Fatalf("BinarySearch(%v, %d) = %d", arr, target, gotBin) + } + } else if gotBin != -1 { + t.Fatalf("BinarySearch(%v, %d) = %d, want -1", arr, target, gotBin) + } + + wantFirst, wantLast := -1, -1 + if found { + wantFirst = wantLower + wantLast = wantUpper - 1 + } + if got := FindFirstOccurrence(arr, target); got != wantFirst { + t.Fatalf("FindFirstOccurrence(%v, %d) = %d, want %d", arr, target, got, wantFirst) + } + if got := FindLastOccurrence(arr, target); got != wantLast { + t.Fatalf("FindLastOccurrence(%v, %d) = %d, want %d", arr, target, got, wantLast) + } + + checkIndex := func(name string, got int) { + t.Helper() + if found { + if got < 0 || got >= len(arr) || arr[got] != target { + t.Fatalf("%s(%v, %d) = %d", name, arr, target, got) + } + return + } + if got != -1 { + t.Fatalf("%s(%v, %d) = %d, want -1", name, arr, target, got) + } + } + checkIndex("JumpSearch", JumpSearch(arr, target)) + checkIndex("ExponentialSearch", ExponentialSearch(arr, target)) + checkIndex("InterpolationSearch", InterpolationSearch(arr, target)) +} + +func FuzzStringSearch(f *testing.F) { + f.Add("AABAACAADAABAABA", "AABA") + f.Add("aaaaaa", "aa") + f.Add("hello", "xyz") + f.Add("abc", "") + f.Add("", "") + f.Add("", "a") + f.Add("$$", "$") + f.Add("a$a$a", "a$") + f.Add("abababa", "aba") + + f.Fuzz(func(t *testing.T, text, pattern string) { + if len(text) > 48 { + text = text[:48] + } + if len(pattern) > 12 { + pattern = pattern[:12] + } + want := bruteForceSearch(text, pattern) + algs := []struct { + name string + fn func(string, string) []int + }{ + {"KMPSearch", KMPSearch}, + {"RabinKarpSearch", RabinKarpSearch}, + {"ZAlgorithmSearch", ZAlgorithmSearch}, + } + for _, alg := range algs { + got := alg.fn(text, pattern) + if !slices.Equal(got, want) { + t.Fatalf("%s(%q, %q) = %v, want %v", alg.name, text, pattern, got, want) + } + } + }) +} + +// bruteForceSearch reports every overlapping match. An empty pattern matches +// nothing, which is the contract already pinned by KMP and Boyer-Moore. +func bruteForceSearch(text, pattern string) []int { + if pattern == "" { + return nil + } + var idx []int + start := 0 + for start+len(pattern) <= len(text) { + rel := strings.Index(text[start:], pattern) + if rel < 0 { + break + } + idx = append(idx, start+rel) + start += rel + 1 + } + return idx +} diff --git a/by-language/go/datastructures/adjacency_matrix_test.go b/by-language/go/datastructures/adjacency_matrix_test.go new file mode 100644 index 0000000..88df8f8 --- /dev/null +++ b/by-language/go/datastructures/adjacency_matrix_test.go @@ -0,0 +1,101 @@ +package datastructures + +import "testing" + +func TestAdjacencyMatrixAccessors(t *testing.T) { + directed := NewAdjacencyMatrix(4, true, true) + if directed.NumVertices() != 4 { + t.Fatalf("NumVertices = %d, want 4", directed.NumVertices()) + } + if !directed.IsDirected() || !directed.IsWeighted() { + t.Fatalf("directed=%v weighted=%v", directed.IsDirected(), directed.IsWeighted()) + } + + plain := NewAdjacencyMatrix(2, false, false) + if plain.IsDirected() || plain.IsWeighted() { + t.Fatalf("undirected flags directed=%v weighted=%v", plain.IsDirected(), plain.IsWeighted()) + } + if plain.NumVertices() != 2 { + t.Fatalf("NumVertices = %d, want 2", plain.NumVertices()) + } + + if directed.AddEdge(-1, 0, 1) || directed.AddEdge(0, 4, 1) || directed.AddEdge(0, 1, 0) { + t.Fatal("invalid AddEdge should fail") + } + if !directed.AddEdge(0, 1, 5) || !directed.AddEdge(0, 2, 3) || !directed.AddEdge(1, 2, 7) { + t.Fatal("AddEdge should succeed") + } + + if directed.HasEdge(-1, 0) || directed.HasEdge(0, 4) || directed.HasEdge(2, 0) { + t.Fatal("HasEdge should reject invalid vertices and missing edges") + } + if !directed.HasEdge(0, 1) || !directed.HasEdge(1, 2) { + t.Fatal("HasEdge missed an inserted edge") + } + + if directed.GetWeight(-1, 0) != 0 || directed.GetWeight(0, 4) != 0 || directed.GetWeight(3, 0) != 0 { + t.Fatal("GetWeight should be 0 for invalid or missing edges") + } + if directed.GetWeight(0, 1) != 5 || directed.GetWeight(1, 2) != 7 { + t.Fatalf("weights = %d, %d", directed.GetWeight(0, 1), directed.GetWeight(1, 2)) + } + + if directed.Neighbors(-1) != nil || directed.OutDegree(-1) != 0 || directed.InDegree(9) != 0 { + t.Fatal("invalid vertex accessors") + } + if neighbors := directed.Neighbors(0); len(neighbors) != 2 || neighbors[0] != 1 || neighbors[1] != 2 { + t.Fatalf("neighbors of 0 = %v", neighbors) + } + if directed.OutDegree(0) != 2 || directed.InDegree(2) != 2 { + t.Fatalf("degrees out=%d in=%d", directed.OutDegree(0), directed.InDegree(2)) + } + if directed.BFS(-1) != nil || directed.DFS(-1) != nil { + t.Fatal("search from an invalid vertex should be nil") + } + if bfs := directed.BFS(0); len(bfs) != 3 || bfs[0] != 0 || bfs[1] != 1 || bfs[2] != 2 { + t.Fatalf("BFS = %v", bfs) + } + if dfs := directed.DFS(0); len(dfs) != 3 || dfs[0] != 0 { + t.Fatalf("DFS = %v", dfs) + } + + if directed.RemoveEdge(-1, 0) || directed.RemoveEdge(3, 3) { + t.Fatal("RemoveEdge should fail for an invalid vertex or a missing edge") + } + if !directed.RemoveEdge(0, 2) || directed.HasEdge(0, 2) { + t.Fatal("RemoveEdge should drop 0-2") + } + + if path, dist := directed.ShortestPathDijkstra(-1, 1); path != nil || dist != 0 { + t.Fatalf("invalid Dijkstra = %v, %d", path, dist) + } + + empty := NewAdjacencyMatrix(0, false, false) + if empty.NumVertices() != 0 { + t.Fatalf("empty NumVertices = %d", empty.NumVertices()) + } + if !empty.IsConnected() { + t.Fatal("empty graph is connected") + } + if empty.HasEdge(0, 0) || empty.GetWeight(0, 0) != 0 { + t.Fatal("empty graph has no edges") + } + + disconnected := NewAdjacencyMatrix(3, false, false) + if !disconnected.AddEdge(0, 1, 1) { + t.Fatal("undirected AddEdge") + } + if disconnected.IsConnected() { + t.Fatal("graph with an isolated vertex is not connected") + } + if !disconnected.HasEdge(1, 0) || disconnected.GetWeight(1, 0) != 1 { + t.Fatal("undirected edge should be stored both ways") + } + + connected := NewAdjacencyMatrix(3, false, false) + connected.AddEdge(0, 1, 1) + connected.AddEdge(1, 2, 1) + if !connected.IsConnected() { + t.Fatal("path graph should be connected") + } +} diff --git a/by-language/go/datastructures/cache_test.go b/by-language/go/datastructures/cache_test.go index a17fbcb..c16d40b 100644 --- a/by-language/go/datastructures/cache_test.go +++ b/by-language/go/datastructures/cache_test.go @@ -374,6 +374,32 @@ func TestLRUCacheEmpty(t *testing.T) { } } +func TestLRUCacheClear(t *testing.T) { + cache := NewLRUCache[int, string](2) + cache.Put(1, "one") + cache.Put(2, "two") + cache.Clear() + + if cache.Size() != 0 { + t.Fatalf("cleared cache size = %d, want 0", cache.Size()) + } + if _, ok := cache.Get(1); ok { + t.Fatal("cleared cache should not contain old keys") + } + if keys := cache.Keys(); len(keys) != 0 { + t.Fatalf("cleared cache keys = %v", keys) + } + + cache.Put(3, "three") + val, ok := cache.Get(3) + if !ok || val != "three" { + t.Fatalf("put after clear = %q, %v", val, ok) + } + if cache.Size() != 1 { + t.Fatalf("size after refill = %d", cache.Size()) + } +} + // --- LFU Cache Extended Tests --- func TestLFUCacheCapacityZero(t *testing.T) { diff --git a/by-language/go/datastructures/graph_test.go b/by-language/go/datastructures/graph_test.go index 0c9ef83..e1a18ed 100644 --- a/by-language/go/datastructures/graph_test.go +++ b/by-language/go/datastructures/graph_test.go @@ -1075,3 +1075,101 @@ func TestGraphDegreeDirected(t *testing.T) { t.Errorf("expected degree 2 (in=1, out=1), got %d", deg) } } + +func TestGraphCoreAccessors(t *testing.T) { + undirected := NewGraphCore(false) + if undirected.IsDirected() { + t.Fatal("undirected graph reports directed") + } + if _, ok := undirected.GetWeight(0, 1); ok { + t.Fatal("missing vertex should have no weight") + } + if undirected.NeighborsWeighted(0) != nil { + t.Fatal("missing vertex should have nil weighted neighbors") + } + + undirected.AddVertex(4) + if got := undirected.NeighborsWeighted(4); len(got) != 0 { + t.Fatalf("isolated vertex neighbors = %+v", got) + } + undirected.AddEdge(0, 1, 2.5) + undirected.AddEdge(0, 2, 4) + + w, ok := undirected.GetWeight(0, 1) + if !ok || w != 2.5 { + t.Fatalf("weight 0-1 = %v, %v", w, ok) + } + w, ok = undirected.GetWeight(1, 0) + if !ok || w != 2.5 { + t.Fatalf("undirected reverse weight = %v, %v", w, ok) + } + if _, ok = undirected.GetWeight(1, 2); ok { + t.Fatal("unexpected edge 1-2") + } + + nbrs := undirected.NeighborsWeighted(0) + gotW := map[int]float64{} + for _, n := range nbrs { + gotW[n.Vertex] = n.Weight + } + if len(gotW) != 2 || gotW[1] != 2.5 || gotW[2] != 4 { + t.Fatalf("weighted neighbors = %v", gotW) + } + + verts, matrix := undirected.ToAdjacencyMatrix() + if len(verts) != 4 || len(matrix) != 4 { + t.Fatalf("matrix shape vertices=%v rows=%d", verts, len(matrix)) + } + index := make(map[int]int, len(verts)) + for i, v := range verts { + index[v] = i + if len(matrix[i]) != len(verts) { + t.Fatalf("row %d len = %d", i, len(matrix[i])) + } + if matrix[i][i] != 0 { + t.Fatalf("diagonal %d = %v", i, matrix[i][i]) + } + } + if matrix[index[0]][index[1]] != 2.5 || matrix[index[1]][index[0]] != 2.5 { + t.Fatal("matrix missing undirected edge 0-1") + } + if !math.IsInf(matrix[index[1]][index[2]], 1) { + t.Fatal("missing edge should be +Inf") + } + if !math.IsInf(matrix[index[0]][index[4]], 1) { + t.Fatal("edge to isolated vertex should be +Inf") + } + + emptyVerts, emptyMatrix := NewGraphCore(true).ToAdjacencyMatrix() + if len(emptyVerts) != 0 || len(emptyMatrix) != 0 { + t.Fatalf("empty matrix = %v, %v", emptyVerts, emptyMatrix) + } + + directed := GraphCoreFromEdgeList([][3]int{{1, 2, 5}, {2, 3, -1}}, true) + if !directed.IsDirected() { + t.Fatal("edge list should build a directed graph") + } + if !directed.HasEdge(1, 2) || directed.HasEdge(2, 1) { + t.Fatal("directed edge list edges") + } + w, ok = directed.GetWeight(2, 3) + if !ok || w != -1 { + t.Fatalf("weight 2-3 = %v, %v", w, ok) + } + dVerts, dMatrix := directed.ToAdjacencyMatrix() + dIndex := make(map[int]int, len(dVerts)) + for i, v := range dVerts { + dIndex[v] = i + } + if dMatrix[dIndex[1]][dIndex[2]] != 5 { + t.Fatal("directed matrix weight") + } + if !math.IsInf(dMatrix[dIndex[2]][dIndex[1]], 1) { + t.Fatal("directed matrix should not mirror the edge") + } + + none := GraphCoreFromEdgeList(nil, false) + if none.IsDirected() || none.NumVertices() != 0 { + t.Fatalf("empty edge list directed=%v vertices=%d", none.IsDirected(), none.NumVertices()) + } +} diff --git a/by-language/go/datastructures/queue_test.go b/by-language/go/datastructures/queue_test.go index 3f17821..7c960d1 100644 --- a/by-language/go/datastructures/queue_test.go +++ b/by-language/go/datastructures/queue_test.go @@ -181,3 +181,46 @@ func TestSlidingWindowMaximum(t *testing.T) { t.Errorf("single element: expected [1], got %v", result) } } + +func TestQueueEmptyAccessors(t *testing.T) { + q := NewQueue[int]() + if q.Size() != 0 || !q.IsEmpty() { + t.Fatalf("empty queue size=%d empty=%v", q.Size(), q.IsEmpty()) + } + if _, ok := q.Peek(); ok { + t.Fatal("peek on empty queue should fail") + } + if _, ok := q.Rear(); ok { + t.Fatal("rear on empty queue should fail") + } + + cq := NewCircularQueue[int](2) + if cq.Size() != 0 || !cq.IsEmpty() { + t.Fatalf("empty circular queue size=%d empty=%v", cq.Size(), cq.IsEmpty()) + } + if _, ok := cq.Peek(); ok { + t.Fatal("peek on empty circular queue should fail") + } + if _, ok := cq.Rear(); ok { + t.Fatal("rear on empty circular queue should fail") + } +} + +func TestDequeEmptyAccessors(t *testing.T) { + d := NewDeque[int]() + if d.Size() != 0 || !d.IsEmpty() { + t.Fatalf("empty deque size=%d empty=%v", d.Size(), d.IsEmpty()) + } + if _, ok := d.PeekFront(); ok { + t.Fatal("peek front on empty deque should fail") + } + if _, ok := d.PeekRear(); ok { + t.Fatal("peek rear on empty deque should fail") + } + if _, ok := d.RemoveFront(); ok { + t.Fatal("remove front on empty deque should fail") + } + if _, ok := d.RemoveRear(); ok { + t.Fatal("remove rear on empty deque should fail") + } +} diff --git a/by-language/go/datastructures/tree_fuzz_test.go b/by-language/go/datastructures/tree_fuzz_test.go new file mode 100644 index 0000000..642e0bf --- /dev/null +++ b/by-language/go/datastructures/tree_fuzz_test.go @@ -0,0 +1,228 @@ +package datastructures + +import ( + "slices" + "strconv" + "strings" + "testing" +) + +func FuzzBST(f *testing.F) { + f.Add([]byte{}) + f.Add([]byte{0, 5, 0, 3, 0, 7, 0, 5, 1, 3, 2, 7, 1, 9}) + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > 96 { + data = data[:96] + } + tree := NewBST[int]() + ref := make(map[int]int) + var trace []string + for i := 0; i+1 < len(data); i += 2 { + op := data[i] % 3 + key := int(data[i+1]) + switch op { + case 0: + tree.Insert(key) + ref[key] = 0 + trace = append(trace, "insert "+strconv.Itoa(key)) + case 1: + deleted := tree.Delete(key) + _, exists := ref[key] + if deleted != exists { + t.Fatalf("BST delete %d = %v, exists %v; data %v after %s", key, deleted, exists, data, strings.Join(trace, ",")) + } + delete(ref, key) + trace = append(trace, "delete "+strconv.Itoa(key)) + default: + trace = append(trace, "contains "+strconv.Itoa(key)) + } + if !tree.IsValidBST() { + t.Fatalf("BST invalid; data %v after %s", data, strings.Join(trace, ",")) + } + checkOrderedMap(t, "BST", data, trace, tree.Size(), tree.Inorder(), tree.Contains, func(k int) (int, bool) { + if tree.Contains(k) { + return 0, true + } + return 0, false + }, key, ref) + } + }) +} + +func FuzzAVL(f *testing.F) { + f.Add([]byte{}) + f.Add([]byte{0, 5, 3, 5, 0, 1, 1, 5, 2, 1, 0, 9}) + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > 96 { + data = data[:96] + } + tree := NewAVLTree[int, int]() + ref := make(map[int]int) + var trace []string + for i := 0; i+1 < len(data); i += 2 { + raw := data[i] + op := raw % 3 + key := int(data[i+1]) + switch op { + case 0: + tree.Insert(key, int(raw)) + ref[key] = int(raw) + trace = append(trace, "insert "+strconv.Itoa(key)+"="+strconv.Itoa(int(raw))) + case 1: + deleted := tree.Delete(key) + _, exists := ref[key] + if deleted != exists { + t.Fatalf("AVL delete %d = %v, exists %v; data %v after %s", key, deleted, exists, data, strings.Join(trace, ",")) + } + delete(ref, key) + trace = append(trace, "delete "+strconv.Itoa(key)) + default: + trace = append(trace, "contains "+strconv.Itoa(key)) + } + if !tree.IsBalanced() { + t.Fatalf("AVL unbalanced; data %v after %s", data, strings.Join(trace, ",")) + } + checkOrderedMap(t, "AVL", data, trace, tree.Size(), tree.Inorder(), tree.Contains, tree.Search, key, ref) + } + }) +} + +func FuzzRedBlack(f *testing.F) { + f.Add([]byte{}) + f.Add([]byte{0, 4, 0, 2, 0, 6, 0, 4, 1, 2, 2, 6, 1, 8}) + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > 96 { + data = data[:96] + } + tree := NewRedBlackTree[int]() + ref := make(map[int]int) + var trace []string + for i := 0; i+1 < len(data); i += 2 { + op := data[i] % 3 + key := int(data[i+1]) + switch op { + case 0: + tree.Insert(key) + ref[key] = 0 + trace = append(trace, "insert "+strconv.Itoa(key)) + case 1: + deleted := tree.Delete(key) + _, exists := ref[key] + if deleted != exists { + t.Fatalf("RB delete %d = %v, exists %v; data %v after %s", key, deleted, exists, data, strings.Join(trace, ",")) + } + delete(ref, key) + trace = append(trace, "delete "+strconv.Itoa(key)) + default: + trace = append(trace, "contains "+strconv.Itoa(key)) + } + if !tree.IsValid() { + t.Fatalf("RB invalid; data %v after %s", data, strings.Join(trace, ",")) + } + checkOrderedMap(t, "RB", data, trace, tree.Size(), tree.Inorder(), tree.Contains, func(k int) (int, bool) { + if tree.Contains(k) { + return 0, true + } + return 0, false + }, key, ref) + } + }) +} + +func FuzzBTree(f *testing.F) { + f.Add([]byte{}) + f.Add([]byte{0, 10, 0, 20, 0, 5, 3, 10, 1, 20, 2, 5, 0, 15, 1, 10}) + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > 96 { + data = data[:96] + } + for _, degree := range []int{2, 3, 5} { + tree := NewBTree[int, int](degree) + ref := make(map[int]int) + var trace []string + for i := 0; i+1 < len(data); i += 2 { + raw := data[i] + op := raw % 3 + key := int(data[i+1]) + switch op { + case 0: + tree.Insert(key, int(raw)) + ref[key] = int(raw) + trace = append(trace, "insert "+strconv.Itoa(key)+"="+strconv.Itoa(int(raw))) + case 1: + deleted := tree.Delete(key) + _, exists := ref[key] + if deleted != exists { + t.Fatalf("BTree t=%d delete %d = %v, exists %v; data %v after %s", degree, key, deleted, exists, data, strings.Join(trace, ",")) + } + delete(ref, key) + trace = append(trace, "delete "+strconv.Itoa(key)) + default: + trace = append(trace, "contains "+strconv.Itoa(key)) + } + checkOrderedMap(t, "BTree t="+strconv.Itoa(degree), data, trace, tree.Size(), tree.Inorder(), tree.Contains, tree.Search, key, ref) + } + } + }) +} + +func FuzzSkipList(f *testing.F) { + f.Add([]byte{}) + f.Add([]byte{0, 4, 3, 4, 0, 1, 0, 8, 1, 1, 2, 8, 1, 4}) + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) > 96 { + data = data[:96] + } + list := NewSkipList[int, int]() + ref := make(map[int]int) + var trace []string + for i := 0; i+1 < len(data); i += 2 { + raw := data[i] + op := raw % 3 + key := int(data[i+1]) + switch op { + case 0: + list.Insert(key, int(raw)) + ref[key] = int(raw) + trace = append(trace, "insert "+strconv.Itoa(key)+"="+strconv.Itoa(int(raw))) + case 1: + deleted := list.Delete(key) + _, exists := ref[key] + if deleted != exists { + t.Fatalf("SkipList delete %d = %v, exists %v; data %v after %s", key, deleted, exists, data, strings.Join(trace, ",")) + } + delete(ref, key) + trace = append(trace, "delete "+strconv.Itoa(key)) + default: + trace = append(trace, "contains "+strconv.Itoa(key)) + } + checkOrderedMap(t, "SkipList", data, trace, list.Size(), list.Keys(), list.Contains, list.Search, key, ref) + } + }) +} + +func checkOrderedMap(t *testing.T, name string, data []byte, trace []string, size int, inorder []int, contains func(int) bool, search func(int) (int, bool), probe int, ref map[int]int) { + t.Helper() + done := strings.Join(trace, ",") + if size != len(ref) { + t.Fatalf("%s size=%d want %d; data %v after %s", name, size, len(ref), data, done) + } + want := make([]int, 0, len(ref)) + for k, v := range ref { + want = append(want, k) + got, ok := search(k) + if !ok || got != v { + t.Fatalf("%s search(%d)=(%d,%v) want %d; data %v after %s", name, k, got, ok, v, data, done) + } + if !contains(k) { + t.Fatalf("%s contains(%d)=false; data %v after %s", name, k, data, done) + } + } + slices.Sort(want) + if !slices.Equal(inorder, want) { + t.Fatalf("%s inorder=%v want %v; data %v after %s", name, inorder, want, data, done) + } + if _, exists := ref[probe]; contains(probe) != exists { + t.Fatalf("%s contains(%d)=%v want %v; data %v after %s", name, probe, contains(probe), exists, data, done) + } +} From 8921fb5ce5591a5a7b263d0395a4df96d927880e Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 09/26] fix(java): accept negative values in radix and counting sort Both sorts indexed their count arrays with negative values and threw ArrayIndexOutOfBoundsException. Counting sort also overflowed new int[max + 1] at Integer.MAX_VALUE. Radix sort now takes digits from each value's distance to the minimum, held in a long. Counting sort offsets by the minimum and falls back to Arrays.sort when the range is more than 16 times the input length. Every sort is now tested with negative and extreme inputs, and a seeded randomized test compares all of them with Arrays.sort. --- .../src/main/java/dsa/algorithms/Sorting.java | 40 ++++++++--- .../test/java/dsa/algorithms/SortingTest.java | 69 +++++++++++++++---- 2 files changed, 84 insertions(+), 25 deletions(-) diff --git a/by-language/java/src/main/java/dsa/algorithms/Sorting.java b/by-language/java/src/main/java/dsa/algorithms/Sorting.java index d48184e..61367a1 100644 --- a/by-language/java/src/main/java/dsa/algorithms/Sorting.java +++ b/by-language/java/src/main/java/dsa/algorithms/Sorting.java @@ -155,52 +155,72 @@ private static void heapify(int[] a, int n, int i) { } } + /** + * LSD radix sort. Digits are taken from each value's distance to the + * minimum, held in a long, so negative values and the full int range are + * supported. + */ public static int[] radixSort(int[] arr) { int[] a = arr.clone(); if (a.length <= 1) return a; + int min = a[0]; int max = a[0]; for (int v : a) { + if (v < min) min = v; if (v > max) max = v; } - for (int exp = 1; max / exp > 0; exp *= 10) { - countingSortByDigit(a, exp); + long maxKey = (long) max - min; + int[] output = new int[a.length]; + for (long exp = 1; maxKey / exp > 0; exp *= 10) { + countingSortByDigit(a, output, min, exp); } return a; } - private static void countingSortByDigit(int[] a, int exp) { + private static void countingSortByDigit(int[] a, int[] output, int min, long exp) { int n = a.length; - int[] output = new int[n]; int[] count = new int[10]; for (int v : a) { - count[(v / exp) % 10]++; + count[(int) ((((long) v - min) / exp) % 10)]++; } for (int i = 1; i < 10; i++) { count[i] += count[i - 1]; } for (int i = n - 1; i >= 0; i--) { - int digit = (a[i] / exp) % 10; + int digit = (int) ((((long) a[i] - min) / exp) % 10); output[count[digit] - 1] = a[i]; count[digit]--; } System.arraycopy(output, 0, a, 0, n); } + /** + * Counting sort over the range [min, max]. Falls back to a comparison sort + * when the range is more than 16 times the input length, which keeps the + * count array proportional to the input. + */ public static int[] countingSort(int[] arr) { int[] a = arr.clone(); if (a.length <= 1) return a; + int min = a[0]; int max = a[0]; for (int v : a) { + if (v < min) min = v; if (v > max) max = v; } - int[] count = new int[max + 1]; + long range = (long) max - min + 1; + if (range > 16L * a.length || range > Integer.MAX_VALUE) { + Arrays.sort(a); + return a; + } + int[] count = new int[(int) range]; for (int v : a) { - count[v]++; + count[v - min]++; } int idx = 0; - for (int i = 0; i <= max; i++) { + for (int i = 0; i < range; i++) { while (count[i] > 0) { - a[idx++] = i; + a[idx++] = min + i; count[i]--; } } diff --git a/by-language/java/src/test/java/dsa/algorithms/SortingTest.java b/by-language/java/src/test/java/dsa/algorithms/SortingTest.java index 95cb596..90d480c 100644 --- a/by-language/java/src/test/java/dsa/algorithms/SortingTest.java +++ b/by-language/java/src/test/java/dsa/algorithms/SortingTest.java @@ -3,6 +3,8 @@ import org.junit.jupiter.api.Test; import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Random; import java.util.function.Function; @@ -18,6 +20,9 @@ class SortingTest { private static final int[] RANDOM = {38, 27, 43, 3, 9, 82, 10}; private static final int[] ALL_SAME = {7, 7, 7, 7, 7}; private static final int[] TWO_ELEMENTS = {2, 1}; + private static final int[] NEGATIVES = {3, -12, 7, -5, 1, 0, -12}; + private static final int[] ALL_NEGATIVE = {-3, -1, -2}; + private static final int[] EXTREMES = {Integer.MAX_VALUE, -1, Integer.MIN_VALUE, 0, 1}; private void assertSortsCorrectly(Function sortFn, int[] input) { int[] original = input.clone(); @@ -43,18 +48,9 @@ private void testSortAlgorithm(Function sortFn) { assertSortsCorrectly(sortFn, RANDOM); assertSortsCorrectly(sortFn, ALL_SAME); assertSortsCorrectly(sortFn, TWO_ELEMENTS); - } - - // Non-negative only sort algorithms - private void testNonNegativeSortAlgorithm(Function sortFn) { - assertSortsCorrectly(sortFn, EMPTY); - assertSortsCorrectly(sortFn, SINGLE); - assertSortsCorrectly(sortFn, SORTED); - assertSortsCorrectly(sortFn, REVERSE); - assertSortsCorrectly(sortFn, DUPLICATES); - assertSortsCorrectly(sortFn, RANDOM); - assertSortsCorrectly(sortFn, ALL_SAME); - assertSortsCorrectly(sortFn, TWO_ELEMENTS); + assertSortsCorrectly(sortFn, NEGATIVES); + assertSortsCorrectly(sortFn, ALL_NEGATIVE); + assertSortsCorrectly(sortFn, EXTREMES); assertSortsCorrectly(sortFn, new int[]{0, 0, 0}); assertSortsCorrectly(sortFn, new int[]{100, 0, 50, 25, 75}); } @@ -91,12 +87,12 @@ void testHeapSort() { @Test void testRadixSort() { - testNonNegativeSortAlgorithm(Sorting::radixSort); + testSortAlgorithm(Sorting::radixSort); } @Test void testCountingSort() { - testNonNegativeSortAlgorithm(Sorting::countingSort); + testSortAlgorithm(Sorting::countingSort); } @Test @@ -124,6 +120,49 @@ void testBubbleSortDoesNotModifyInput() { assertArrayEquals(copy, input); } + @Test + void testRandomizedAgainstArraysSort() { + Map> sorts = new LinkedHashMap<>(); + sorts.put("bubbleSort", Sorting::bubbleSort); + sorts.put("selectionSort", Sorting::selectionSort); + sorts.put("insertionSort", Sorting::insertionSort); + sorts.put("mergeSort", Sorting::mergeSort); + sorts.put("quickSort", Sorting::quickSort); + sorts.put("heapSort", Sorting::heapSort); + sorts.put("radixSort", Sorting::radixSort); + sorts.put("countingSort", Sorting::countingSort); + sorts.put("shellSort", Sorting::shellSort); + sorts.put("bucketSort", Sorting::bucketSort); + sorts.put("timSort", Sorting::timSort); + sorts.put("stableSort", Sorting::stableSort); + + long seed = 20261023L; + Random rng = new Random(seed); + for (int trial = 0; trial < 300; trial++) { + int[] input = new int[rng.nextInt(60)]; + for (int i = 0; i < input.length; i++) { + switch (trial % 3) { + case 0 -> input[i] = rng.nextInt(41) - 20; + case 1 -> input[i] = rng.nextInt(); + default -> input[i] = rng.nextInt(2001) - 1000; + } + } + int[] expected = input.clone(); + Arrays.sort(expected); + for (Map.Entry> sort : sorts.entrySet()) { + int[] actual = sort.getValue().apply(input.clone()); + assertArrayEquals(expected, actual, sort.getKey() + " seed=" + seed + + " trial=" + trial + " input=" + Arrays.toString(input)); + } + if (input.length > 0) { + int k = rng.nextInt(input.length); + assertEquals(expected[k], Sorting.quickSelect(input.clone(), k), + "quickSelect seed=" + seed + " trial=" + trial + " k=" + k + + " input=" + Arrays.toString(input)); + } + } + } + @Test void testLargerRandomArray() { Random rng = new Random(12345); @@ -161,7 +200,7 @@ void testMixedPositiveNegative() { // --- bucketSort --- @Test void testBucketSort() { - testNonNegativeSortAlgorithm(Sorting::bucketSort); + testSortAlgorithm(Sorting::bucketSort); assertArrayEquals(new int[]{-1, 0}, Sorting.bucketSort(new int[]{0, -1})); assertArrayEquals(new int[]{Integer.MIN_VALUE, Integer.MAX_VALUE}, Sorting.bucketSort(new int[]{Integer.MAX_VALUE, Integer.MIN_VALUE})); From 8138b0c2408019222770c19c4b3a4e480edd6ee5 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 10/26] fix(java)!: keep binary search tree keys unique insert sent duplicates to the right subtree while isValidBst used strict bounds, so inserting the same value twice made the tree report itself invalid. Inserting a present value now returns the existing node and leaves the tree unchanged, and fromSortedList keeps repeated values once. A seeded test compares the tree with TreeSet. BREAKING CHANGE: inserting a value that is already present no longer adds a node or increases size(). Callers that relied on storing duplicates should keep a count per value instead. --- .../dsa/datastructures/BinarySearchTree.java | 24 ++++--- .../datastructures/BinarySearchTreeTest.java | 62 +++++++++++++++++++ 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/by-language/java/src/main/java/dsa/datastructures/BinarySearchTree.java b/by-language/java/src/main/java/dsa/datastructures/BinarySearchTree.java index 1234093..72068c1 100644 --- a/by-language/java/src/main/java/dsa/datastructures/BinarySearchTree.java +++ b/by-language/java/src/main/java/dsa/datastructures/BinarySearchTree.java @@ -49,14 +49,14 @@ public TreeNode getRoot() { } /** - * Insert a value into the BST. + * Insert a value into the BST. Keys are unique: inserting a value that is + * already present leaves the tree unchanged and returns the existing node. */ public TreeNode insert(T value) { - TreeNode newNode = new TreeNode(value); if (root == null) { - root = newNode; + root = new TreeNode(value); size++; - return newNode; + return root; } TreeNode current = root; TreeNode parent = null; @@ -68,10 +68,10 @@ public TreeNode insert(T value) { } else if (cmp > 0) { current = current.right; } else { - // Duplicate: go right - current = current.right; + return current; } } + TreeNode newNode = new TreeNode(value); newNode.parent = parent; if (value.compareTo(parent.value) < 0) { parent.left = newNode; @@ -421,12 +421,18 @@ public static > BinarySearchTree fromList(List lis } /** - * Build a balanced BST from a sorted list. + * Build a balanced BST from a sorted list. Repeated values are kept once. */ public static > BinarySearchTree fromSortedList(List sortedList) { + List distinct = new ArrayList<>(sortedList.size()); + for (T item : sortedList) { + if (distinct.isEmpty() || item.compareTo(distinct.get(distinct.size() - 1)) != 0) { + distinct.add(item); + } + } BinarySearchTree bst = new BinarySearchTree<>(); - bst.root = bst.buildFromSorted(sortedList, 0, sortedList.size() - 1, null); - bst.size = sortedList.size(); + bst.root = bst.buildFromSorted(distinct, 0, distinct.size() - 1, null); + bst.size = distinct.size(); return bst; } diff --git a/by-language/java/src/test/java/dsa/datastructures/BinarySearchTreeTest.java b/by-language/java/src/test/java/dsa/datastructures/BinarySearchTreeTest.java index bbcbd18..129c839 100644 --- a/by-language/java/src/test/java/dsa/datastructures/BinarySearchTreeTest.java +++ b/by-language/java/src/test/java/dsa/datastructures/BinarySearchTreeTest.java @@ -3,9 +3,12 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.Random; +import java.util.TreeSet; import static org.junit.jupiter.api.Assertions.*; @@ -45,6 +48,17 @@ void testInsertReturnsNode() { assertEquals(42, node.getValue()); } + @Test + void testInsertDuplicateKeepsTreeUnchanged() { + BinarySearchTree.TreeNode first = bst.insert(5); + bst.insert(10); + BinarySearchTree.TreeNode again = bst.insert(5); + assertSame(first, again); + assertEquals(2, bst.size()); + assertEquals(Arrays.asList(5, 10), bst.inorder()); + assertTrue(bst.isValidBst()); + } + // ======================== Search ======================== @Test @@ -421,6 +435,54 @@ void testFromSortedList() { assertEquals(2, tree.height()); } + @Test + void testFromSortedListKeepsRepeatedValuesOnce() { + BinarySearchTree tree = BinarySearchTree.fromSortedList(Arrays.asList(1, 1, 2, 3, 3, 3)); + assertEquals(3, tree.size()); + assertEquals(Arrays.asList(1, 2, 3), tree.inorder()); + assertTrue(tree.isValidBst()); + } + + @Test + void testRandomizedAgainstTreeSet() { + long seed = 20261023L; + Random rng = new Random(seed); + for (int trial = 0; trial < 200; trial++) { + BinarySearchTree tree = new BinarySearchTree<>(); + TreeSet model = new TreeSet<>(); + List ops = new ArrayList<>(); + for (int step = 0; step < 60; step++) { + int value = rng.nextInt(41) - 20; + String context = "seed=" + seed + " trial=" + trial + " ops=" + ops; + if (rng.nextInt(10) < 6) { + ops.add("insert " + value); + tree.insert(value); + model.add(value); + } else { + ops.add("delete " + value); + assertEquals(model.remove(value), tree.delete(value), context); + } + assertEquals(model.size(), tree.size(), context); + } + String context = "seed=" + seed + " trial=" + trial + " ops=" + ops; + assertEquals(new ArrayList<>(model), tree.inorder(), context); + assertTrue(tree.isValidBst(), context); + for (int q = -22; q <= 22; q++) { + assertEquals(Optional.ofNullable(model.floor(q)), tree.floor(q), context + " floor " + q); + assertEquals(Optional.ofNullable(model.ceiling(q)), tree.ceiling(q), context + " ceiling " + q); + assertEquals(model.contains(q), tree.search(q).isPresent(), context + " search " + q); + if (model.contains(q)) { + assertEquals(Optional.ofNullable(model.higher(q)), tree.successor(q), context + " successor " + q); + assertEquals(Optional.ofNullable(model.lower(q)), tree.predecessor(q), context + " predecessor " + q); + } + } + int rank = 1; + for (int value : model) { + assertEquals(Optional.of(value), tree.kthSmallest(rank++), context); + } + } + } + @Test void testFromSortedListEmpty() { BinarySearchTree tree = BinarySearchTree.fromSortedList(List.of()); From f2f5ddc16880f0823eaf7815f4e17dd2bd0be606 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 11/26] fix(python): repair diagram docstring escapes Several ASCII diagrams used a backslash followed by a space, an invalid escape that raises SyntaxWarning on Python 3.12+ and is scheduled to become an error. Others ended a line with a backslash, which joined the next line and garbled the diagram. Use raw docstrings where needed so the rendered text matches the source. Also correct the radix_sort docstring, which said it only handles non-negative integers. --- by-language/python/algorithms/sorting.py | 7 ++++--- by-language/python/data_structures/b_tree.py | 2 +- by-language/python/data_structures/heap.py | 2 +- by-language/python/data_structures/red_black_tree.py | 6 +++--- by-language/python/data_structures/trie.py | 4 ++-- by-language/python/data_structures/union_find.py | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/by-language/python/algorithms/sorting.py b/by-language/python/algorithms/sorting.py index aaee7eb..cdeeaf4 100644 --- a/by-language/python/algorithms/sorting.py +++ b/by-language/python/algorithms/sorting.py @@ -1,4 +1,4 @@ -""" +r""" Sorting Algorithms ╔════════════════════════════════════════════════════════════════════════════╗ @@ -410,10 +410,11 @@ def radix_sort(arr: List[int]) -> List[int]: Space Complexity: O(n + k) Stable: Yes - Works for non-negative integers. + Works for any integers. Negative values are sorted by magnitude + separately and placed, reversed, before the non-negative values. Args: - arr: List of non-negative integers to sort. + arr: List of integers to sort. Returns: New sorted list. diff --git a/by-language/python/data_structures/b_tree.py b/by-language/python/data_structures/b_tree.py index a1178b0..b19516d 100644 --- a/by-language/python/data_structures/b_tree.py +++ b/by-language/python/data_structures/b_tree.py @@ -1,4 +1,4 @@ -""" +r""" B-Tree Data Structure. ╔══════════════════════════════════════════════════════════════════════════════╗ diff --git a/by-language/python/data_structures/heap.py b/by-language/python/data_structures/heap.py index ac3e9c5..8ef82a1 100644 --- a/by-language/python/data_structures/heap.py +++ b/by-language/python/data_structures/heap.py @@ -1,4 +1,4 @@ -""" +r""" Binary Heap (Min-Heap and Max-Heap) ╔════════════════════════════════════════════════════════════════════════════╗ diff --git a/by-language/python/data_structures/red_black_tree.py b/by-language/python/data_structures/red_black_tree.py index bb80b8d..61706b0 100644 --- a/by-language/python/data_structures/red_black_tree.py +++ b/by-language/python/data_structures/red_black_tree.py @@ -1,4 +1,4 @@ -""" +r""" Red-Black Tree Data Structure. ╔══════════════════════════════════════════════════════════════════════════════╗ @@ -147,7 +147,7 @@ def __getitem__(self, key: K) -> V: # ========================================================================= def _rotate_left(self, x: RBNode[K, V]) -> None: - """ + r""" Left rotation around node x. x y @@ -179,7 +179,7 @@ def _rotate_left(self, x: RBNode[K, V]) -> None: x.parent = y def _rotate_right(self, y: RBNode[K, V]) -> None: - """ + r""" Right rotation around node y. y x diff --git a/by-language/python/data_structures/trie.py b/by-language/python/data_structures/trie.py index f6afa65..91c0ae2 100644 --- a/by-language/python/data_structures/trie.py +++ b/by-language/python/data_structures/trie.py @@ -1,4 +1,4 @@ -""" +r""" Trie (Prefix Tree) Implementation. A Trie is a tree-like data structure for storing strings that allows @@ -504,7 +504,7 @@ def _search_helper(self, word: str, index: int, node: TrieNode) -> bool: class CompressedTrie: - """ + r""" Compressed Trie (Radix Tree / Patricia Trie). A space-optimized trie where chains of single-child nodes are merged. diff --git a/by-language/python/data_structures/union_find.py b/by-language/python/data_structures/union_find.py index 9a9dc67..b4ce7cd 100644 --- a/by-language/python/data_structures/union_find.py +++ b/by-language/python/data_structures/union_find.py @@ -1,4 +1,4 @@ -""" +r""" Union-Find (Disjoint Set Union) Implementation. A data structure that tracks elements partitioned into disjoint (non-overlapping) sets. From 9cef64854f5a9d66837e4b6c07f142292ab01042 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 12/26] fix(python)!: keep binary search tree keys unique insert stored duplicates in the right subtree while is_valid_bst used strict bounds, so inserting the same value twice made the tree report itself invalid. Inserting a present value now returns the existing node and leaves size and shape unchanged, and the factory methods drop repeated values. BREAKING CHANGE: inserting a value that is already present no longer adds a node or increases len(). Callers that relied on storing duplicates should keep a count per value instead. --- .../data_structures/binary_search_tree.py | 14 +++++--- .../python/tests/test_binary_search_tree.py | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/by-language/python/data_structures/binary_search_tree.py b/by-language/python/data_structures/binary_search_tree.py index 938399a..3a498bd 100644 --- a/by-language/python/data_structures/binary_search_tree.py +++ b/by-language/python/data_structures/binary_search_tree.py @@ -1,4 +1,4 @@ -""" +r""" Binary Search Tree (BST) ╔════════════════════════════════════════════════════════════════════════════╗ @@ -138,7 +138,7 @@ class BinarySearchTree(Generic[T]): """ A binary search tree implementation. - Supports duplicate values (stored in right subtree). + Stores unique values; inserting a duplicate returns the existing node. Maintains parent pointers for efficient operations. """ @@ -191,7 +191,7 @@ def _height(self, node: Optional[TreeNode[T]]) -> int: def insert(self, value: T) -> TreeNode[T]: """ - Insert a value into the BST. + Insert a unique value into the BST, leaving duplicates unchanged. Time Complexity: O(log n) average, O(n) worst case @@ -199,7 +199,7 @@ def insert(self, value: T) -> TreeNode[T]: value: Value to insert. Returns: - The newly created node. + The newly created node, or the existing node for a duplicate. """ new_node = TreeNode(value) @@ -210,6 +210,8 @@ def insert(self, value: T) -> TreeNode[T]: current = self._root while True: + if value == current.value: + return current if value < current.value: if current.left is None: current.left = new_node @@ -672,7 +674,7 @@ def from_list(cls, values: List[T]) -> "BinarySearchTree[T]": @classmethod def from_sorted_list(cls, values: List[T]) -> "BinarySearchTree[T]": """ - Create a balanced BST from a sorted list. + Create a balanced BST from a sorted list, ignoring duplicates. Time Complexity: O(n) @@ -682,6 +684,8 @@ def from_sorted_list(cls, values: List[T]) -> "BinarySearchTree[T]": Returns: A balanced BST. """ + values = [value for i, value in enumerate(values) + if i == 0 or value != values[i - 1]] bst: BinarySearchTree[T] = cls() def build(left: int, right: int, parent: Optional[TreeNode[T]]) -> Optional[TreeNode[T]]: diff --git a/by-language/python/tests/test_binary_search_tree.py b/by-language/python/tests/test_binary_search_tree.py index 4533e98..34ab114 100644 --- a/by-language/python/tests/test_binary_search_tree.py +++ b/by-language/python/tests/test_binary_search_tree.py @@ -39,6 +39,38 @@ def test_insert_maintains_bst_property(self): assert bst.is_valid_bst() +@pytest.mark.parametrize('value', [50, 30, 70, 20, 40, 60, 80]) +def test_duplicate_insert_returns_existing_node_without_changing_shape(value): + bst = BinarySearchTree.from_list([50, 30, 70, 20, 40, 60, 80]) + existing = bst.search(value) + nodes = [bst.search(key) for key in bst] + shape = [(node, node.left, node.right, node.parent) for node in nodes] + assert bst.insert(value) is existing + assert len(bst) == 7 + assert bst.to_list() == [20, 30, 40, 50, 60, 70, 80] + assert bst.is_valid_bst() + assert [(node, node.left, node.right, node.parent) for node in nodes] == shape + + +@pytest.mark.parametrize('factory', [BinarySearchTree.from_list, BinarySearchTree.from_sorted_list]) +def test_factories_ignore_duplicates(factory): + bst = factory([1, 1, 2, 3, 3, 3]) + assert bst.to_list() == [1, 2, 3] + assert len(bst) == 3 + assert bst.is_valid_bst() + for key in bst: + node = bst.search(key) + for child in [node.left, node.right]: + if child is not None: + assert child.parent is node + + +def test_validity_check_still_rejects_duplicate_keys(): + bst = BinarySearchTree.from_list([5, 6]) + bst.search(6).value = 5 + assert not bst.is_valid_bst() + + class TestBSTSearch: """Test search operations.""" From b1806ca046de33ec9804b7a7d8b7d6839eda2ac5 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 13/26] fix(python): store explicit None values in AVL trees AVLTree.insert(key, None) stored the key instead of None, because None doubled as the "no value given" default. Use a private sentinel so an omitted value still defaults to the key while an explicit None is kept. Also annotate with typing.Any instead of the builtin any, and repair the diagram escapes in this module. --- .../python/data_structures/avl_tree.py | 33 ++++++++++--------- by-language/python/tests/test_avl_tree.py | 22 +++++++++++++ 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/by-language/python/data_structures/avl_tree.py b/by-language/python/data_structures/avl_tree.py index b430141..00caf10 100644 --- a/by-language/python/data_structures/avl_tree.py +++ b/by-language/python/data_structures/avl_tree.py @@ -1,4 +1,4 @@ -""" +r""" AVL Tree Implementation. A self-balancing binary search tree where the heights of left and right @@ -54,9 +54,10 @@ - When frequent insertions/deletions need guaranteed O(log n) """ -from typing import TypeVar, Generic, Optional, List, Iterator, Tuple +from typing import Any, TypeVar, Generic, Optional, List, Iterator, Tuple T = TypeVar('T') +_MISSING = object() class AVLNode(Generic[T]): @@ -64,10 +65,10 @@ class AVLNode(Generic[T]): __slots__ = ['key', 'value', 'height', 'left', 'right'] - def __init__(self, key: T, value: any = None) -> None: + def __init__(self, key: T, value: Any = _MISSING) -> None: """Initialize AVL node.""" self.key = key - self.value = value if value is not None else key + self.value = key if value is _MISSING else value self.height = 1 self.left: Optional['AVLNode[T]'] = None self.right: Optional['AVLNode[T]'] = None @@ -87,7 +88,7 @@ class AVLTree(Generic[T]): >>> tree.search(5) True >>> tree.inorder() - [1, 1, 2, 3, 4, 5, 6, 9] + [1, 2, 3, 4, 5, 6, 9] """ def __init__(self) -> None: @@ -110,7 +111,7 @@ def _update_height(self, node: AVLNode[T]) -> None: node.height = 1 + max(self._height(node.left), self._height(node.right)) def _rotate_right(self, z: AVLNode[T]) -> AVLNode[T]: - """ + r""" Right rotation for LL imbalance. z y @@ -131,7 +132,7 @@ def _rotate_right(self, z: AVLNode[T]) -> AVLNode[T]: return y def _rotate_left(self, z: AVLNode[T]) -> AVLNode[T]: - """ + r""" Left rotation for RR imbalance. z y @@ -174,20 +175,20 @@ def _rebalance(self, node: AVLNode[T]) -> AVLNode[T]: return node - def insert(self, key: T, value: any = None) -> None: + def insert(self, key: T, value: Any = _MISSING) -> None: """ Insert a key-value pair into the tree. Args: key: The key to insert - value: Optional value (defaults to key) + value: Value to store, including None (defaults to key if omitted) Time: O(log n) """ self._root = self._insert(self._root, key, value) self._size += 1 - def _insert(self, node: Optional[AVLNode[T]], key: T, value: any) -> AVLNode[T]: + def _insert(self, node: Optional[AVLNode[T]], key: T, value: Any) -> AVLNode[T]: """Recursive insert helper.""" if not node: return AVLNode(key, value) @@ -198,7 +199,7 @@ def _insert(self, node: Optional[AVLNode[T]], key: T, value: any) -> AVLNode[T]: node.right = self._insert(node.right, key, value) else: # Key exists, update value - node.value = value if value is not None else key + node.value = key if value is _MISSING else value self._size -= 1 # Compensate for increment in insert() return node @@ -273,7 +274,7 @@ def _search(self, node: Optional[AVLNode[T]], key: T) -> Optional[AVLNode[T]]: else: return self._search(node.right, key) - def get(self, key: T) -> Optional[any]: + def get(self, key: T) -> Optional[Any]: """Get value for key.""" node = self._search(self._root, key) return node.value if node else None @@ -403,11 +404,11 @@ def __init__(self) -> None: """Initialize empty map.""" self._tree = AVLTree() - def __setitem__(self, key: T, value: any) -> None: + def __setitem__(self, key: T, value: Any) -> None: """Set key-value pair.""" self._tree.insert(key, value) - def __getitem__(self, key: T) -> any: + def __getitem__(self, key: T) -> Any: """Get value for key.""" value = self._tree.get(key) if value is None and not self._tree.search(key): @@ -431,7 +432,7 @@ def __iter__(self) -> Iterator[T]: """Iterate over keys in sorted order.""" return iter(self._tree) - def get(self, key: T, default: any = None) -> any: + def get(self, key: T, default: Any = None) -> Any: """Get value with default.""" value = self._tree.get(key) if value is None and not self._tree.search(key): @@ -442,7 +443,7 @@ def keys(self) -> List[T]: """Return sorted keys.""" return self._tree.inorder() - def items(self) -> List[Tuple[T, any]]: + def items(self) -> List[Tuple[T, Any]]: """Return sorted (key, value) pairs.""" result = [] self._collect_items(self._tree._root, result) diff --git a/by-language/python/tests/test_avl_tree.py b/by-language/python/tests/test_avl_tree.py index ac574bf..48f40eb 100644 --- a/by-language/python/tests/test_avl_tree.py +++ b/by-language/python/tests/test_avl_tree.py @@ -7,6 +7,28 @@ from data_structures.avl_tree import AVLTree, AVLTreeMap +def test_map_preserves_none_on_insert_and_update(): + mapping = AVLTreeMap() + mapping['a'] = None + assert mapping['a'] is None + assert mapping.get('a', 'missing') is None + mapping['a'] = 10 + mapping['a'] = None + assert mapping.items() == [('a', None)] + assert len(mapping) == 1 + + +def test_omitted_tree_value_still_defaults_to_key(): + tree = AVLTree() + tree.insert(5) + assert tree.get(5) == 5 + tree.insert(5, None) + assert tree.get(5) is None + tree.insert(5) + assert tree.get(5) == 5 + assert len(tree) == 1 + + class TestAVLTreeBasics: """Test basic AVL tree operations.""" From 12ddb610102fceee218f1d3999350e8916947d9d Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 14/26] fix(python)!: unify LFU caches and keep minimum frequency current lru_cache.py carried a second, smaller LFUCache. Re-export the full implementation from lfu_cache.py so both import paths name one class. Deleting the last entry at the minimum frequency left min_frequency() stale, so the next eviction looked in an empty bucket. Recompute the minimum in that case, which makes that delete O(n) in the worst case. The TTL cache tests move from test_lru_cache.py to their own file. BREAKING CHANGE: data_structures.lru_cache.LFUCache is now the lfu_cache class, so put() returns the evicted (key, value) pair instead of None. --- .../python/data_structures/__init__.py | 3 +- .../python/data_structures/lfu_cache.py | 7 +- .../python/data_structures/lru_cache.py | 97 +------------------ by-language/python/tests/test_lfu_cache.py | 17 ++++ by-language/python/tests/test_lru_cache.py | 75 +++----------- by-language/python/tests/test_ttl_cache.py | 68 +++++++++++++ 6 files changed, 105 insertions(+), 162 deletions(-) create mode 100644 by-language/python/tests/test_ttl_cache.py diff --git a/by-language/python/data_structures/__init__.py b/by-language/python/data_structures/__init__.py index 11cb3a4..8e84d27 100644 --- a/by-language/python/data_structures/__init__.py +++ b/by-language/python/data_structures/__init__.py @@ -16,7 +16,8 @@ from .graph import Graph from .trie import Trie, TrieMap, WordDictionary, CompressedTrie from .union_find import UnionFind, UnionFindArray, WeightedUnionFind, PersistentUnionFind -from .lru_cache import LRUCache, LRUCacheSimple, LFUCache +from .lru_cache import LRUCache, LRUCacheSimple +from .lfu_cache import LFUCache from .ttl_cache import TTLCache from .avl_tree import AVLTree, AVLTreeMap from .segment_tree import SegmentTree, SegmentTreeLazy, FenwickTree, FenwickTree2D diff --git a/by-language/python/data_structures/lfu_cache.py b/by-language/python/data_structures/lfu_cache.py index 37151cd..a3e6e35 100644 --- a/by-language/python/data_structures/lfu_cache.py +++ b/by-language/python/data_structures/lfu_cache.py @@ -34,9 +34,10 @@ +-----------+------+-------+ | Get | O(1) | O(1) | | Put | O(1) | O(1) | -| Delete | O(1) | O(1) | +| Delete | O(n) | O(1) | +-----------+------+-------+ Space: O(capacity) for the cache +Delete is O(n) in the worst case to recompute the minimum frequency. LEETCODE PROBLEMS: - #460 LFU Cache @@ -242,7 +243,7 @@ def delete(self, key: K) -> bool: Returns: True if key was found and deleted - Time: O(1) + Time: O(n) worst case when the minimum frequency must be recomputed """ if key not in self._key_map: return False @@ -253,6 +254,8 @@ def delete(self, key: K) -> bool: del self._key_map[key] self._size -= 1 + if node.freq == self._min_freq and freq_list.is_empty(): + self._min_freq = min((n.freq for n in self._key_map.values()), default=0) return True diff --git a/by-language/python/data_structures/lru_cache.py b/by-language/python/data_structures/lru_cache.py index d7fb4c7..4785958 100644 --- a/by-language/python/data_structures/lru_cache.py +++ b/by-language/python/data_structures/lru_cache.py @@ -54,6 +54,8 @@ from typing import TypeVar, Generic, Dict, Optional, Callable, Iterator, Tuple, Any from collections import OrderedDict +from .lfu_cache import LFUCache # Backwards-compatible re-export. + K = TypeVar('K') V = TypeVar('V') @@ -382,98 +384,3 @@ def __len__(self) -> int: def __contains__(self, key: K) -> bool: """Check if key exists.""" return key in self._cache - - -class LFUCache(Generic[K, V]): - """ - LFU (Least Frequently Used) Cache implementation. - - Evicts the least frequently used item. If multiple items have - the same frequency, evicts the least recently used among them. - - LeetCode #460 - - Example: - >>> cache = LFUCache(2) - >>> cache.put(1, 'a') - >>> cache.put(2, 'b') - >>> cache.get(1) # freq[1] = 2 - 'a' - >>> cache.put(3, 'c') # Evicts key 2 (lower frequency) - >>> cache.get(2) - """ - - def __init__(self, capacity: int) -> None: - """Initialize LFU cache.""" - if capacity < 1: - raise ValueError("Capacity must be at least 1") - - self._capacity = capacity - self._cache: Dict[K, V] = {} - self._freq: Dict[K, int] = {} - self._freq_to_keys: Dict[int, OrderedDict[K, None]] = {} - self._min_freq = 0 - - def get(self, key: K) -> Optional[V]: - """Get value and increment frequency.""" - if key not in self._cache: - return None - - self._increment_freq(key) - return self._cache[key] - - def put(self, key: K, value: V) -> None: - """Insert or update key-value pair.""" - if key in self._cache: - self._cache[key] = value - self._increment_freq(key) - return - - # Evict if at capacity - if len(self._cache) >= self._capacity: - self._evict() - - # Insert new key - self._cache[key] = value - self._freq[key] = 1 - self._min_freq = 1 - - if 1 not in self._freq_to_keys: - self._freq_to_keys[1] = OrderedDict() - self._freq_to_keys[1][key] = None - - def _increment_freq(self, key: K) -> None: - """Increment frequency of key.""" - freq = self._freq[key] - self._freq[key] = freq + 1 - - # Remove from current frequency bucket - del self._freq_to_keys[freq][key] - - # Update min_freq if this was the last key at min_freq - if not self._freq_to_keys[freq] and freq == self._min_freq: - self._min_freq += 1 - - # Add to new frequency bucket - new_freq = freq + 1 - if new_freq not in self._freq_to_keys: - self._freq_to_keys[new_freq] = OrderedDict() - self._freq_to_keys[new_freq][key] = None - - def _evict(self) -> None: - """Evict least frequently used key.""" - # Get LFU keys and remove the LRU among them - lfu_keys = self._freq_to_keys[self._min_freq] - evict_key = next(iter(lfu_keys)) - - del lfu_keys[evict_key] - del self._cache[evict_key] - del self._freq[evict_key] - - def __len__(self) -> int: - """Return number of items.""" - return len(self._cache) - - def __contains__(self, key: K) -> bool: - """Check if key exists.""" - return key in self._cache diff --git a/by-language/python/tests/test_lfu_cache.py b/by-language/python/tests/test_lfu_cache.py index 5221305..4d0d62f 100644 --- a/by-language/python/tests/test_lfu_cache.py +++ b/by-language/python/tests/test_lfu_cache.py @@ -6,6 +6,23 @@ from data_structures.lfu_cache import LFUCache +def test_minimum_frequency_after_deleting_last_minimum(): + cache = LFUCache(2) + cache.put('frequent', 1) + cache.get('frequent') + cache.get('frequent') + cache.put('rare', 2) + assert cache.delete('rare') + assert cache.min_frequency() == 3 + cache.get('frequent') + assert cache.min_frequency() == 4 + cache.put('new', 3) + assert cache.put('replacement', 4) == ('new', 3) + assert cache.delete('replacement') + assert cache.delete('frequent') + assert cache.min_frequency() == 0 + + class TestLFUCacheBasics: """Test basic LFU cache operations.""" diff --git a/by-language/python/tests/test_lru_cache.py b/by-language/python/tests/test_lru_cache.py index 18d794b..5b27739 100644 --- a/by-language/python/tests/test_lru_cache.py +++ b/by-language/python/tests/test_lru_cache.py @@ -2,10 +2,8 @@ Tests for LRU Cache implementations. """ -import time import pytest from data_structures.lru_cache import LRUCache, LRUCacheSimple, LFUCache -from data_structures.ttl_cache import TTLCache def test_delitem_accepts_stored_none(): @@ -388,6 +386,17 @@ def test_contains(self): assert 'b' not in cache +def test_lfu_imports_share_the_canonical_class(): + from data_structures import LFUCache as PackageLFUCache + from data_structures.lfu_cache import LFUCache as CanonicalLFUCache + + assert LFUCache is CanonicalLFUCache + assert PackageLFUCache is CanonicalLFUCache + cache = LFUCache(1) + assert cache.put('a', None) is None + assert cache.put('b', 2) == ('a', None) + + class TestLFUCache: """Test LFU Cache implementation.""" @@ -444,68 +453,6 @@ def test_len_contains(self): assert 'b' not in cache -class TestTTLCache: - """Test TTL Cache implementation.""" - - def test_basic_operations(self): - """Test basic put and get.""" - cache = TTLCache(capacity=2, ttl=10.0) - cache.put('a', 1) - cache.put('b', 2) - - assert cache.get('a') == 1 - assert cache.get('b') == 2 - - def test_expiration(self): - """Test item expiration.""" - cache = TTLCache(capacity=2, ttl=0.1) - cache.put('a', 1) - - assert cache.get('a') == 1 - time.sleep(0.15) - assert cache.get('a') is None - - def test_custom_ttl(self): - """Test custom TTL per item.""" - cache = TTLCache(capacity=2, ttl=10.0) - cache.put('short', 1, ttl=0.1) - cache.put('long', 2, ttl=10.0) - - time.sleep(0.15) - assert cache.get('short') is None - assert cache.get('long') == 2 - - def test_capacity_eviction(self): - """Test capacity-based eviction.""" - cache = TTLCache(capacity=2, ttl=10.0) - cache.put('a', 1) - cache.put('b', 2) - cache.put('c', 3) # Evicts 'a' - - assert cache.get('a') is None - assert cache.get('b') == 2 - assert cache.get('c') == 3 - - def test_len_with_expiration(self): - """Test len accounts for expired items.""" - cache = TTLCache(capacity=3, ttl=0.1) - cache.put('a', 1) - cache.put('b', 2) - - assert len(cache) == 2 - time.sleep(0.15) - assert len(cache) == 0 - - def test_contains_with_expiration(self): - """Test contains checks expiration.""" - cache = TTLCache(capacity=2, ttl=0.1) - cache.put('a', 1) - - assert 'a' in cache - time.sleep(0.15) - assert 'a' not in cache - - class TestLRUCacheEdgeCases: """Test edge cases.""" diff --git a/by-language/python/tests/test_ttl_cache.py b/by-language/python/tests/test_ttl_cache.py new file mode 100644 index 0000000..8fbb628 --- /dev/null +++ b/by-language/python/tests/test_ttl_cache.py @@ -0,0 +1,68 @@ +"""Tests for TTL Cache implementation.""" + +import time +from data_structures.ttl_cache import TTLCache + + +class TestTTLCache: + """Test TTL Cache implementation.""" + + def test_basic_operations(self): + """Test basic put and get.""" + cache = TTLCache(capacity=2, ttl=10.0) + cache.put('a', 1) + cache.put('b', 2) + + assert cache.get('a') == 1 + assert cache.get('b') == 2 + + def test_expiration(self): + """Test item expiration.""" + cache = TTLCache(capacity=2, ttl=0.1) + cache.put('a', 1) + + assert cache.get('a') == 1 + time.sleep(0.15) + assert cache.get('a') is None + + def test_custom_ttl(self): + """Test custom TTL per item.""" + cache = TTLCache(capacity=2, ttl=10.0) + cache.put('short', 1, ttl=0.1) + cache.put('long', 2, ttl=10.0) + + time.sleep(0.15) + assert cache.get('short') is None + assert cache.get('long') == 2 + + def test_capacity_eviction(self): + """Test capacity-based eviction.""" + cache = TTLCache(capacity=2, ttl=10.0) + cache.put('a', 1) + cache.put('b', 2) + cache.put('c', 3) # Evicts 'a' + + assert cache.get('a') is None + assert cache.get('b') == 2 + assert cache.get('c') == 3 + + def test_len_with_expiration(self): + """Test len accounts for expired items.""" + cache = TTLCache(capacity=3, ttl=0.1) + cache.put('a', 1) + cache.put('b', 2) + + assert len(cache) == 2 + time.sleep(0.15) + assert len(cache) == 0 + + def test_contains_with_expiration(self): + """Test contains checks expiration.""" + cache = TTLCache(capacity=2, ttl=0.1) + cache.put('a', 1) + + assert 'a' in cache + time.sleep(0.15) + assert 'a' not in cache + + From 19a89c3e4c591b43483280812b00bd27294ca17c Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 15/26] test(python): add model tests and fail on warnings Add fixed-seed randomized tests that compare ordered maps, hash tables, heaps, caches, range trees, sorts, searches, string matching, DP, union-find, and trie against simple reference models. Move the B-tree tests out of test_red_black_tree.py into test_b_tree.py, add red-black tree and graph core coverage, and set filterwarnings to error so an invalid escape sequence fails the suite. --- by-language/python/pyproject.toml | 1 + by-language/python/tests/test_b_tree.py | 255 +++++++++++ by-language/python/tests/test_b_tree_none.py | 15 - by-language/python/tests/test_graph_core.py | 94 ++++ by-language/python/tests/test_randomized.py | 429 ++++++++++++++++++ .../python/tests/test_red_black_tree.py | 314 ++++--------- 6 files changed, 861 insertions(+), 247 deletions(-) create mode 100644 by-language/python/tests/test_b_tree.py delete mode 100644 by-language/python/tests/test_b_tree_none.py create mode 100644 by-language/python/tests/test_graph_core.py create mode 100644 by-language/python/tests/test_randomized.py diff --git a/by-language/python/pyproject.toml b/by-language/python/pyproject.toml index 43542ad..3c66e86 100644 --- a/by-language/python/pyproject.toml +++ b/by-language/python/pyproject.toml @@ -18,6 +18,7 @@ dev = [ [tool.pytest.ini_options] testpaths = ["tests"] +filterwarnings = ["error"] python_files = ["test_*.py"] python_functions = ["test_*"] addopts = "-v --tb=short" diff --git a/by-language/python/tests/test_b_tree.py b/by-language/python/tests/test_b_tree.py new file mode 100644 index 0000000..a9259de --- /dev/null +++ b/by-language/python/tests/test_b_tree.py @@ -0,0 +1,255 @@ +"""Tests for B-Tree implementation.""" + +import random +import pytest +from data_structures.b_tree import BTree + + +class TestBTreeBasics: + """Test B-Tree basic operations.""" + + def test_empty_tree(self): + """Test empty tree.""" + bt = BTree(t=2) + assert len(bt) == 0 + assert 10 not in bt + + def test_insert_single(self): + """Test single insertion.""" + bt = BTree(t=2) + bt.insert(10, "ten") + + assert len(bt) == 1 + assert 10 in bt + assert bt.search(10) == "ten" + + def test_insert_multiple(self): + """Test multiple insertions.""" + bt = BTree(t=2) + for i in range(10): + bt.insert(i, str(i)) + + assert len(bt) == 10 + for i in range(10): + assert bt.search(i) == str(i) + + def test_insert_update(self): + """Test updating existing key.""" + bt = BTree(t=2) + bt.insert(10, "ten") + bt.insert(10, "TEN") + + assert len(bt) == 1 + assert bt.search(10) == "TEN" + + def test_bracket_notation(self): + """Test bracket notation.""" + bt = BTree(t=2) + bt[10] = "ten" + bt[20] = "twenty" + + assert bt[10] == "ten" + assert bt[20] == "twenty" + + def test_getitem_keyerror(self): + """Test KeyError on missing key.""" + bt = BTree(t=2) + bt[10] = "ten" + + with pytest.raises(KeyError): + _ = bt[20] + + +class TestBTreeDeletion: + """Test B-Tree deletion.""" + + def test_delete_from_leaf(self): + """Test deleting from leaf.""" + bt = BTree(t=2) + for i in range(5): + bt.insert(i, str(i)) + + assert bt.delete(2) + assert 2 not in bt + assert len(bt) == 4 + + def test_delete_not_found(self): + """Test deleting non-existent key.""" + bt = BTree(t=2) + bt.insert(10, "ten") + + assert not bt.delete(20) + + def test_delete_all(self): + """Test deleting all keys.""" + bt = BTree(t=2) + keys = list(range(20)) + for k in keys: + bt.insert(k, str(k)) + + seed = 20260417 + random.Random(seed).shuffle(keys) + model = set(keys) + for iteration, k in enumerate(keys): + message = f"seed={seed}, iteration={iteration}, input={keys!r}" + assert bt.delete(k), message + model.remove(k) + assert bt.keys() == sorted(model), message + assert len(bt) == len(model), message + + def test_delitem(self): + """Test del notation.""" + bt = BTree(t=2) + bt[10] = "ten" + + del bt[10] + assert 10 not in bt + + +class TestBTreeTraversal: + """Test B-Tree traversal.""" + + def test_inorder(self): + """Test inorder returns sorted order.""" + bt = BTree(t=2) + values = [5, 3, 7, 1, 9, 4, 6, 8, 2] + for v in values: + bt.insert(v, str(v)) + + result = bt.inorder() + keys = [k for k, _ in result] + assert keys == sorted(values) + + def test_iter(self): + """Test iteration.""" + bt = BTree(t=2) + for i in [5, 3, 7, 1]: + bt.insert(i, str(i)) + + assert list(bt) == [1, 3, 5, 7] + + def test_keys_values(self): + """Test keys and values methods.""" + bt = BTree(t=2) + bt.insert(3, "three") + bt.insert(1, "one") + bt.insert(2, "two") + + assert bt.keys() == [1, 2, 3] + assert bt.values() == ["one", "two", "three"] + + +class TestBTreeMinMax: + """Test B-Tree min/max.""" + + def test_min(self): + """Test min.""" + bt = BTree(t=2) + for i in [5, 3, 7, 1]: + bt.insert(i, str(i)) + + assert bt.min() == (1, "1") + + def test_max(self): + """Test max.""" + bt = BTree(t=2) + for i in [5, 3, 7, 1]: + bt.insert(i, str(i)) + + assert bt.max() == (7, "7") + + def test_min_max_empty(self): + """Test min/max on empty tree.""" + bt = BTree(t=2) + assert bt.min() is None + assert bt.max() is None + + +class TestBTreeProperties: + """Test B-Tree properties.""" + + def test_different_degrees(self): + """Test with different minimum degrees.""" + for t in [2, 3, 5, 10]: + bt = BTree(t=t) + for i in range(100): + bt.insert(i, str(i)) + + assert len(bt) == 100 + for i in range(100): + assert i in bt + + def test_large_dataset(self): + """Test with large dataset.""" + bt = BTree(t=10) + values = list(range(1000)) + seed = 20260417 + random.Random(seed).shuffle(values) + + for v in values: + bt.insert(v, str(v)) + + message = f"seed={seed}, iteration={len(values) - 1}, input={values!r}" + assert len(bt) == 1000, message + assert bt.keys() == sorted(values), message + + def test_height(self): + """Test height is logarithmic.""" + bt = BTree(t=10) + for i in range(10000): + bt.insert(i, str(i)) + + # Height should be O(log_t(n)) + import math + max_height = math.ceil(math.log(10001, 10)) + assert bt.height() <= max_height + 1 + + def test_invalid_degree(self): + """Test invalid minimum degree.""" + with pytest.raises(ValueError): + BTree(t=1) + + +class TestBTreeClear: + """Test clear operation.""" + + def test_clear_btree(self): + """Test clearing B-Tree.""" + bt = BTree(t=2) + for i in range(10): + bt.insert(i, str(i)) + + bt.clear() + assert len(bt) == 0 + + +class TestRepr: + """Test string representations.""" + + def test_btree_repr(self): + """Test B-Tree repr.""" + bt = BTree(t=2) + bt.insert(10, "ten") + s = repr(bt) + assert "BTree" in s + + def test_btree_display(self): + """Test B-Tree display.""" + bt = BTree(t=2) + for i in range(5): + bt.insert(i, str(i)) + + display = bt.display() + assert len(display) > 0 + + +def test_none_value_has_independent_key_membership(): + tree = BTree() + tree.insert(1, None) + tree.insert(2, "two") + assert 1 in tree + assert tree[1] is None + assert tree.delete(1) + assert 1 not in tree + with pytest.raises(KeyError): + _ = tree[1] diff --git a/by-language/python/tests/test_b_tree_none.py b/by-language/python/tests/test_b_tree_none.py deleted file mode 100644 index c88c327..0000000 --- a/by-language/python/tests/test_b_tree_none.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest - -from data_structures.b_tree import BTree - - -def test_none_value_has_independent_key_membership(): - tree = BTree() - tree.insert(1, None) - tree.insert(2, "two") - assert 1 in tree - assert tree[1] is None - assert tree.delete(1) - assert 1 not in tree - with pytest.raises(KeyError): - _ = tree[1] diff --git a/by-language/python/tests/test_graph_core.py b/by-language/python/tests/test_graph_core.py new file mode 100644 index 0000000..7efc6ae --- /dev/null +++ b/by-language/python/tests/test_graph_core.py @@ -0,0 +1,94 @@ +"""Focused tests for the public GraphCore operations and edge cases.""" + +import pytest + +from data_structures.graph_core import GraphCore + + +@pytest.mark.parametrize('directed', [False, True]) +def test_missing_vertices_and_edges_do_not_mutate_graph(directed): + graph = GraphCore(directed=directed) + graph.add_vertex('isolated') + assert not graph.remove_vertex('missing') + assert not graph.remove_edge('missing', 'isolated') + assert not graph.remove_edge('isolated', 'missing') + assert not graph.has_edge('missing', 'isolated') + assert graph.get_weight('missing', 'isolated') is None + assert graph.get_weight('isolated', 'missing') is None + assert graph.neighbors('missing') == [] + assert graph.degree('missing') == 0 + assert graph.in_degree('missing') == 0 + assert list(graph) == ['isolated'] + assert len(graph) == 1 + assert graph.edge_count == 0 + assert graph.is_directed == directed + kind = 'Directed' if directed else 'Undirected' + assert repr(graph) == f'{kind}Graph(vertices=1, edges=0)' + + +@pytest.mark.parametrize('directed', [False, True]) +def test_in_degree_and_vertex_removal(directed): + graph = GraphCore(directed=directed) + graph.add_edge('a', 'b', 2) + graph.add_edge('c', 'b', 3) + graph.add_edge('b', 'b', 4) + graph.add_vertex('isolated') + assert graph.in_degree('b') == 3 + assert graph.degree('b') == (1 if directed else 3) + assert graph.edge_count == 3 + assert graph.remove_vertex('b') + assert set(graph.vertices) == {'a', 'c', 'isolated'} + assert graph.edge_count == 0 + assert all(graph.neighbors(v) == [] for v in graph) + assert not graph.remove_vertex('b') + + +@pytest.mark.parametrize('directed', [False, True]) +def test_copy_preserves_weights_loops_isolated_vertices_and_independence(directed): + graph = GraphCore(directed=directed) + graph.add_vertex('isolated') + graph.add_edge('a', 'b', 2.5) + graph.add_edge('b', 'b', 7) + if directed: + graph.add_edge('b', 'a', 3.5) + clone = graph.copy() + assert clone is not graph + assert clone.is_directed == directed + assert set(clone) == set(graph) + assert clone.edge_count == graph.edge_count + for u in graph: + for v in graph: + assert clone.get_weight(u, v) == graph.get_weight(u, v) + clone.add_edge('a', 'b', 99) + assert graph.get_weight('a', 'b') == 2.5 + assert clone.remove_edge('b', 'b') + assert not clone.remove_edge('b', 'b') + assert graph.has_edge('b', 'b') + clone.clear() + assert len(clone) == 0 + assert clone.edge_count == 0 + assert len(graph) == 3 + + +@pytest.mark.parametrize('directed', [False, True]) +def test_from_edge_list_and_matrix(directed): + graph = GraphCore.from_edge_list([('a', 'b'), ('b', 'c', 5), ('a', 'a', 2)], directed=directed) + assert graph.get_weight('a', 'b') == 1.0 + assert graph.get_weight('b', 'c') == 5 + assert graph.get_weight('a', 'a') == 2 + assert graph.edge_count == 3 + vertices, matrix = graph.to_adjacency_matrix() + assert vertices == ['a', 'b', 'c'] + inf = float('inf') + if directed: + assert matrix == [[2, 1, inf], [inf, 0, 5], [inf, inf, 0]] + else: + assert matrix == [[2, 1, inf], [1, 0, 5], [inf, 5, 0]] + matrix[0][1] = 99 + assert graph.get_weight('a', 'b') == 1.0 + + +def test_empty_conversions(): + graph = GraphCore.from_edge_list([]) + assert graph.to_adjacency_matrix() == ([], []) + assert list(graph.copy()) == [] diff --git a/by-language/python/tests/test_randomized.py b/by-language/python/tests/test_randomized.py new file mode 100644 index 0000000..2a22b97 --- /dev/null +++ b/by-language/python/tests/test_randomized.py @@ -0,0 +1,429 @@ +"""Small, reproducible model checks using only standard-library references.""" + +from bisect import bisect_left, bisect_right +from collections import OrderedDict, deque +from functools import lru_cache +import heapq +from itertools import combinations, product +import random + +import pytest + +from algorithms import dynamic_programming as dp, searching, sorting +from algorithms.string_algorithms import ( + kmp_search, naive_search, rabin_karp_search, z_algorithm_search, +) +from data_structures.avl_tree import AVLTreeMap +from data_structures.b_tree import BTree +from data_structures.binary_search_tree import BinarySearchTree +from data_structures.fenwick_tree import FenwickTree +from data_structures.hash_table import HashMap, HashTableChaining, HashTableOpenAddressing +from data_structures.heap import MaxHeap, MinHeap +from data_structures.lfu_cache import LFUCache +from data_structures.lru_cache import LRUCache +from data_structures.red_black_tree import RedBlackTree +from data_structures.segment_tree import SegmentTree, SegmentTreeLazy +from data_structures.skip_list import SkipList +from data_structures.trie import Trie +from data_structures.union_find import UnionFind, UnionFindArray + + +SEED = 20260417 + + +def context(iteration, **inputs): + return f"seed={SEED}, iteration={iteration}, input={inputs!r}" + + +@pytest.mark.parametrize('kind', ['red_black', 'b_tree_2', 'b_tree_3', 'skip', 'avl', 'bst']) +def test_ordered_maps(kind, monkeypatch): + rng = random.Random(SEED) + # Skip-list height generation must also be reproducible, without changing + # the process-wide random generator or leaking state into other tests. + monkeypatch.setattr('data_structures.skip_list.random', random.Random(SEED + 1)) + factories = { + 'red_black': RedBlackTree, + 'b_tree_2': lambda: BTree(t=2), + 'b_tree_3': lambda: BTree(t=3), + 'skip': SkipList, + 'avl': AVLTreeMap, + 'bst': BinarySearchTree, + } + tree = factories[kind]() + model = {} + for iteration in range(220): + key, value = rng.randrange(-25, 26), rng.choice([None, rng.randrange(-100, 101)]) + op = rng.choice(['put', 'put', 'delete', 'search']) + message = context(iteration, kind=kind, op=op, key=key, value=value, model=model) + if op == 'put': + if kind == 'bst': + old = tree.search(key) + node = tree.insert(key) + assert node.value == key, message + if old is not None: + assert node is old, message + model[key] = key + else: + tree[key] = value + model[key] = value + elif op == 'delete': + if kind == 'avl': + if key in model: + del tree[key] + else: + with pytest.raises(KeyError): + del tree[key] + else: + assert tree.delete(key) == (key in model), message + model.pop(key, None) + else: + assert (key in tree) == (key in model), message + keys = sorted(model) + assert list(tree) == keys, message + assert len(tree) == len(model), message + if kind == 'bst': + assert tree.is_valid_bst(), message + else: + assert list(tree.items()) == [(k, model[k]) for k in keys], message + for k in keys: + assert tree[k] == model[k], message + if kind == 'red_black': + assert tree.is_valid(), message + if hasattr(tree, 'floor'): + left, right = bisect_left(keys, key), bisect_right(keys, key) + assert tree.floor(key) == (keys[right - 1] if right else None), message + assert tree.ceiling(key) == (keys[left] if left < len(keys) else None), message + # Exercise deletion fixups after a nontrivial tree, not just isolated leaves. + remaining = list(model) + rng.shuffle(remaining) + for iteration, key in enumerate(remaining, start=220): + message = context(iteration, kind=kind, delete=key, model=model) + if kind == 'avl': + del tree[key] + else: + assert tree.delete(key), message + del model[key] + assert list(tree) == sorted(model), message + assert len(tree) == len(model), message + if kind == 'red_black': + assert tree.is_valid(), message + + +@pytest.mark.parametrize('factory', [HashMap, HashTableChaining, HashTableOpenAddressing]) +def test_hash_tables(factory): + rng = random.Random(SEED) + table, model = factory(capacity=4), {} + for iteration in range(300): + # Multiples of the initial capacity exercise collisions and tombstones. + key, value = rng.randrange(-30, 31) * 4, rng.choice([None, rng.randrange(-100, 101)]) + op = rng.choice(['put', 'put', 'remove', 'get']) + message = context(iteration, table=factory.__name__, op=op, key=key, value=value, model=model) + if op == 'put': + table[key] = value + model[key] = value + elif op == 'remove': + assert table.remove(key) == (key in model), message + model.pop(key, None) + else: + assert table.get(key, 'missing') == model.get(key, 'missing'), message + assert len(table) == len(model), message + assert dict(table.items()) == model, message + assert (key in table) == (key in model), message + + +@pytest.mark.parametrize('factory,sign', [(MinHeap, 1), (MaxHeap, -1)]) +def test_heaps(factory, sign): + rng = random.Random(SEED) + heap, model = factory(), [] + for iteration in range(300): + value = rng.randrange(-100, 101) + op = rng.choice(['push', 'push', 'pop', 'peek']) if model else 'push' + message = context(iteration, heap=factory.__name__, op=op, value=value, model=model) + if op == 'push': + heap.push(value) + heapq.heappush(model, sign * value) + elif op == 'pop': + assert heap.pop() == sign * heapq.heappop(model), message + else: + assert heap.peek() == sign * model[0], message + assert len(heap) == len(model), message + message = context(300, heap=factory.__name__, model=model) + assert [heap.pop() for _ in range(len(heap))] == [sign * heapq.heappop(model) for _ in range(len(model))], message + + +def test_lru_cache(): + rng = random.Random(SEED) + cache, model = LRUCache(4), OrderedDict() + for iteration in range(300): + key, value = rng.randrange(9), rng.choice([None, rng.randrange(100)]) + op = rng.choice(['put', 'get', 'delete', 'peek']) + message = context(iteration, op=op, key=key, value=value, model=list(model.items())) + if op == 'put': + evicted = None + if key in model: + del model[key] + elif len(model) == 4: + evicted = model.popitem(last=False) + model[key] = value + assert cache.put(key, value) == evicted, message + elif op == 'get': + assert cache.get(key) == model.get(key), message + if key in model: + model.move_to_end(key) + elif op == 'delete': + assert cache.delete(key) == model.pop(key, None), message + else: + assert cache.peek(key) == model.get(key), message + assert len(cache) == len(model), message + assert list(cache.items()) == list(reversed(model.items())), message + + +def test_lfu_cache(): + rng = random.Random(SEED) + cache, model = LFUCache(4), {} + for iteration in range(300): + key, value = rng.randrange(9), rng.choice([None, rng.randrange(100)]) + op = rng.choice(['put', 'put', 'get', 'delete', 'peek']) + message = context(iteration, op=op, key=key, value=value, model=model) + if op == 'put': + evicted = None + if key in model: + model[key] = (value, model[key][1] + 1, iteration) + else: + if len(model) == 4: + victim = min(model, key=lambda k: model[k][1:]) + evicted = (victim, model.pop(victim)[0]) + model[key] = (value, 1, iteration) + assert cache.put(key, value) == evicted, message + elif op == 'get': + assert cache.get(key) == (model[key][0] if key in model else None), message + if key in model: + old_value, frequency, _ = model[key] + model[key] = (old_value, frequency + 1, iteration) + elif op == 'delete': + assert cache.delete(key) == (key in model), message + model.pop(key, None) + else: + assert cache.peek(key) == (model[key][0] if key in model else None), message + assert len(cache) == len(model), message + assert cache.min_frequency() == min((v[1] for v in model.values()), default=0), message + for k in range(9): + assert (k in cache) == (k in model), message + assert cache.get_frequency(k) == (model[k][1] if k in model else -1), message + assert cache.peek(k) == (model[k][0] if k in model else None), message + + +@pytest.mark.parametrize('size', [1, 7, 20]) +def test_range_sum_structures(size): + rng = random.Random(SEED) + model = [rng.randrange(-20, 21) for _ in range(size)] + segment, lazy, fenwick = SegmentTree(model), SegmentTreeLazy(model), FenwickTree(model) + for iteration in range(150): + left, right = sorted([rng.randrange(size), rng.randrange(size)]) + value = rng.randrange(-20, 21) + op = rng.choice(['set', 'add', 'range_add', 'query']) + message = context(iteration, op=op, left=left, right=right, value=value, model=model) + if op == 'set': + lazy.range_update(left, left, value - model[left]) + segment.update(left, value) + fenwick.set(left, value) + model[left] = value + elif op in ('add', 'range_add'): + end = right if op == 'range_add' else left + lazy.range_update(left, end, value) + for i in range(left, end + 1): + model[i] += value + segment.update(i, model[i]) + fenwick.update(i, value) + expected = sum(model[left:right + 1]) + assert segment.query(left, right) == expected, message + assert lazy.query(left, right) == expected, message + assert fenwick.range_sum(left, right) == expected, message + assert fenwick.prefix_sum(right) == sum(model[:right + 1]), message + assert lazy.query(0, size - 1) == sum(model), message + + +SORTS = [sorting.bubble_sort, sorting.selection_sort, sorting.insertion_sort, + sorting.merge_sort, sorting.quick_sort, sorting.heap_sort, sorting.shell_sort, + sorting.counting_sort, sorting.radix_sort, sorting.bucket_sort] + + +@pytest.mark.parametrize('sort', SORTS, ids=lambda fn: fn.__name__) +def test_sorting(sort): + rng = random.Random(SEED) + cases = [[], [0], [-5, -5, 0, 3], list(range(-10, 11)), list(range(10, -11, -1))] + cases += [[rng.randrange(-50, 51) for _ in range(rng.randrange(35))] for _ in range(50)] + for iteration, values in enumerate(cases): + assert sort(values.copy()) == sorted(values), context(iteration, sort=sort.__name__, values=values) + + +def test_comparator_sort(): + rng = random.Random(SEED) + for iteration in range(50): + values = [rng.randrange(-50, 51) for _ in range(rng.randrange(35))] + message = context(iteration, values=values) + assert sorting.sort_with_comparator(values, lambda a, b: a - b) == sorted(values), message + assert sorting.sort_with_comparator(values, lambda a, b: b - a) == sorted(values, reverse=True), message + + +def test_binary_search_variants(): + rng = random.Random(SEED) + for iteration in range(150): + values = sorted(rng.randrange(-20, 21) for _ in range(rng.randrange(50))) + target = rng.randrange(-25, 26) + message = context(iteration, values=values, target=target) + left, right = bisect_left(values, target), bisect_right(values, target) + assert searching.lower_bound(values, target) == left, message + assert searching.upper_bound(values, target) == right, message + assert searching.find_first_occurrence(values, target) == (left if left != right else -1), message + assert searching.find_last_occurrence(values, target) == (right - 1 if left != right else -1), message + for search in [searching.binary_search, searching.binary_search_recursive]: + index = search(values, target) + assert (left <= index < right) if left != right else index == -1, message + + +@pytest.mark.parametrize('search', [kmp_search, rabin_karp_search, z_algorithm_search, naive_search], ids=lambda fn: fn.__name__) +def test_string_search(search): + rng = random.Random(SEED) + cases = [('', ''), ('abc', ''), ('', 'a'), ('aaaaa', 'aaa'), ('a$b$a$b', '$b')] + for _ in range(100): + text = ''.join(rng.choices('ab$c', k=rng.randrange(35))) + pattern = ''.join(rng.choices('ab$c', k=rng.randrange(8))) + cases.append((text, pattern)) + for iteration, (text, pattern) in enumerate(cases): + # Preserve the existing empty-pattern contracts: KMP returns [0] + # for nonempty text, while the other all-match APIs return []. + expected = [i for i in range(len(text) - len(pattern) + 1) + if text[i:i + len(pattern)] == pattern] if pattern else [] + if search is kmp_search and not pattern and text: + expected = [0] + assert search(text, pattern) == expected, context(iteration, search=search.__name__, text=text, pattern=pattern) + + +def brute_edit_distance(a, b): + @lru_cache(None) + def solve(i, j): + if i == len(a): + return len(b) - j + if j == len(b): + return len(a) - i + if a[i] == b[j]: + return solve(i + 1, j + 1) + return 1 + min(solve(i + 1, j), solve(i, j + 1), solve(i + 1, j + 1)) + return solve(0, 0) + + +def test_edit_distance(): + rng = random.Random(SEED) + for iteration in range(80): + a = ''.join(rng.choices('abc', k=rng.randrange(6))) + b = ''.join(rng.choices('abc', k=rng.randrange(6))) + expected = brute_edit_distance(a, b) + message = context(iteration, a=a, b=b) + assert dp.edit_distance(a, b) == expected, message + assert dp.edit_distance_optimized(a, b) == expected, message + + +def test_lis(): + rng = random.Random(SEED) + for iteration in range(60): + values = [rng.randrange(-4, 5) for _ in range(rng.randrange(9))] + expected = max((len(subseq) for size in range(len(values) + 1) + for subseq in combinations(values, size) + if all(a < b for a, b in zip(subseq, subseq[1:]))), default=0) + message = context(iteration, values=values) + assert dp.lis_length(values) == expected, message + assert dp.lis_binary_search(values) == expected, message + sequence = dp.lis_sequence(values) + assert len(sequence) == expected, message + assert all(a < b for a, b in zip(sequence, sequence[1:])), message + remaining = iter(values) + assert all(any(item == wanted for item in remaining) for wanted in sequence), message + + +def test_coin_change(): + rng = random.Random(SEED) + for iteration in range(60): + coins = sorted(rng.sample(range(1, 7), rng.randrange(1, 5))) + amount = rng.randrange(15) + # Enumerate counts for unordered combinations; BFS is an independent + # shortest-path reference for the minimum number of coins. + ways = sum(sum(c * n for c, n in zip(coins, counts)) == amount + for counts in product(*(range(amount // c + 1) for c in coins))) + queue, seen, minimum = deque([(0, 0)]), {0}, -1 + while queue: + total, count = queue.popleft() + if total == amount: + minimum = count + break + for coin in coins: + if total + coin <= amount and total + coin not in seen: + seen.add(total + coin) + queue.append((total + coin, count + 1)) + message = context(iteration, coins=coins, amount=amount) + assert dp.coin_change_min(coins, amount) == minimum, message + assert dp.coin_change_ways(coins, amount) == ways, message + + +def test_knapsack(): + rng = random.Random(SEED) + for iteration in range(70): + size, capacity = rng.randrange(8), rng.randrange(16) + weights = [rng.randrange(1, 8) for _ in range(size)] + values = [rng.randrange(15) for _ in range(size)] + expected = max(sum(v for i, v in enumerate(values) if mask & (1 << i)) + for mask in range(1 << size) + if sum(w for i, w in enumerate(weights) if mask & (1 << i)) <= capacity) + message = context(iteration, weights=weights, values=values, capacity=capacity) + assert dp.knapsack_01(weights, values, capacity) == expected, message + assert dp.knapsack_01_optimized(weights, values, capacity) == expected, message + + +@pytest.mark.parametrize('factory', [UnionFind, UnionFindArray]) +def test_union_find(factory): + rng = random.Random(SEED) + size = 16 + if factory is UnionFind: + uf = factory() + for i in range(size): + uf.make_set(i) + else: + uf = factory(size) + labels = list(range(size)) + for iteration in range(120): + a, b = rng.randrange(size), rng.randrange(size) + message = context(iteration, a=a, b=b, labels=labels) + assert uf.union(a, b) == (labels[a] != labels[b]), message + old, new = labels[b], labels[a] + labels = [new if label == old else label for label in labels] + assert uf.num_components == len(set(labels)), message + for x in range(size): + assert uf.set_size(x) == labels.count(labels[x]), message + for y in range(size): + assert uf.connected(x, y) == (labels[x] == labels[y]), message + + +def test_trie_prefix_counts(): + rng = random.Random(SEED) + trie, model = Trie(), set() + words = ['', 'a', 'ab', 'abc', 'b', 'ba', 'c'] + words += [''.join(rng.choices('abc', k=rng.randrange(1, 6))) for _ in range(40)] + for iteration in range(200): + word = rng.choice(words) + op = rng.choice(['insert', 'insert', 'delete', 'search']) + message = context(iteration, op=op, word=word, model=sorted(model)) + if op == 'insert': + trie.insert(word) + model.add(word) + elif op == 'delete': + assert trie.delete(word) == (word in model), message + model.discard(word) + else: + assert trie.search(word) == (word in model), message + assert len(trie) == len(model), message + assert set(trie) == model, message + for prefix in ['', 'a', 'ab', 'abc', 'b', word[:2], 'z']: + matches = {w for w in model if w.startswith(prefix)} + prefix_message = context(iteration, op=op, word=word, prefix=prefix, model=sorted(model)) + assert trie.count_words_with_prefix(prefix) == len(matches), prefix_message + assert set(trie.get_words_with_prefix(prefix)) == matches, prefix_message diff --git a/by-language/python/tests/test_red_black_tree.py b/by-language/python/tests/test_red_black_tree.py index f68c82b..5ca8169 100644 --- a/by-language/python/tests/test_red_black_tree.py +++ b/by-language/python/tests/test_red_black_tree.py @@ -1,11 +1,85 @@ """ -Tests for Red-Black Tree and B-Tree. +Tests for Red-Black Tree. """ import pytest import random -from data_structures.red_black_tree import RedBlackTree, Color -from data_structures.b_tree import BTree +from data_structures.red_black_tree import RedBlackTree, RBNode, Color + + +@pytest.mark.parametrize('values', [list(range(1, 7)), list(range(6, 0, -1))]) +def test_delete_with_red_sibling_and_mirrored_case(values): + tree = RedBlackTree() + model = {} + for key in values: + tree.insert(key, str(key)) + model[key] = str(key) + # Deleting the outer black leaf requires rotating its red sibling first. + for key in values: + assert tree.delete(key) + del model[key] + assert tree.inorder() == sorted(model.items()) + assert len(tree) == len(model) + assert tree.is_valid() + + +def test_black_height_empty_single_and_balanced(): + tree = RedBlackTree() + assert tree.black_height() == 0 + assert tree.height() == 0 + assert tree.is_valid() + tree.insert(2, 'two') + assert tree.black_height() == 1 + tree.insert(1, 'one') + tree.insert(3, 'three') + assert tree.black_height() == 1 + tree.insert(0, 'zero') + assert tree.black_height() == 2 + assert tree.is_valid() + + +def test_none_values_have_independent_membership(): + tree = RedBlackTree() + tree[1] = None + assert 1 in tree + assert tree[1] is None + del tree[1] + assert 1 not in tree + with pytest.raises(KeyError): + _ = tree[1] + + +def test_node_repr_reflects_color(): + node = RBNode(5, 'five') + assert repr(node) == 'RBNode(5:R)' + node.color = Color.BLACK + assert repr(node) == 'RBNode(5:B)' + + +def test_validity_rejects_red_root(): + tree = RedBlackTree() + tree.insert(1, 1) + tree._root.color = Color.RED + assert not tree.is_valid() + + +@pytest.mark.parametrize('side', ['left', 'right']) +def test_validity_rejects_red_parent_and_child(side): + tree = RedBlackTree() + tree.insert(10, 10) + child = RBNode(5 if side == 'left' else 15, None, parent=tree._root) + grandchild = RBNode(3 if side == 'left' else 17, None, parent=child) + setattr(tree._root, side, child) + setattr(child, side, grandchild) + assert not tree.is_valid() + + +def test_validity_rejects_unequal_black_paths(): + tree = RedBlackTree() + tree.insert(2, 2) + tree.insert(1, 1) + tree._root.left.color = Color.BLACK + assert not tree.is_valid() class TestRedBlackTreeBasics: @@ -124,12 +198,12 @@ def test_is_valid_after_random_inserts(self): """Tree should be valid after random inserts.""" rbt = RedBlackTree() values = list(range(100)) - random.shuffle(values) + seed = 20260417 + random.Random(seed).shuffle(values) - for v in values: + for iteration, v in enumerate(values): rbt.insert(v, str(v)) - - assert rbt.is_valid() + assert rbt.is_valid(), f"seed={seed}, iteration={iteration}, input={values!r}" def test_is_valid_after_deletes(self): """Tree should be valid after deletes.""" @@ -224,206 +298,7 @@ def test_min_max_empty(self): assert rbt.max() is None -class TestBTreeBasics: - """Test B-Tree basic operations.""" - - def test_empty_tree(self): - """Test empty tree.""" - bt = BTree(t=2) - assert len(bt) == 0 - assert 10 not in bt - - def test_insert_single(self): - """Test single insertion.""" - bt = BTree(t=2) - bt.insert(10, "ten") - - assert len(bt) == 1 - assert 10 in bt - assert bt.search(10) == "ten" - - def test_insert_multiple(self): - """Test multiple insertions.""" - bt = BTree(t=2) - for i in range(10): - bt.insert(i, str(i)) - - assert len(bt) == 10 - for i in range(10): - assert bt.search(i) == str(i) - - def test_insert_update(self): - """Test updating existing key.""" - bt = BTree(t=2) - bt.insert(10, "ten") - bt.insert(10, "TEN") - - assert len(bt) == 1 - assert bt.search(10) == "TEN" - - def test_bracket_notation(self): - """Test bracket notation.""" - bt = BTree(t=2) - bt[10] = "ten" - bt[20] = "twenty" - - assert bt[10] == "ten" - assert bt[20] == "twenty" - - def test_getitem_keyerror(self): - """Test KeyError on missing key.""" - bt = BTree(t=2) - bt[10] = "ten" - - with pytest.raises(KeyError): - _ = bt[20] - - -class TestBTreeDeletion: - """Test B-Tree deletion.""" - - def test_delete_from_leaf(self): - """Test deleting from leaf.""" - bt = BTree(t=2) - for i in range(5): - bt.insert(i, str(i)) - - assert bt.delete(2) - assert 2 not in bt - assert len(bt) == 4 - - def test_delete_not_found(self): - """Test deleting non-existent key.""" - bt = BTree(t=2) - bt.insert(10, "ten") - - assert not bt.delete(20) - - def test_delete_all(self): - """Test deleting all keys.""" - bt = BTree(t=2) - keys = list(range(20)) - for k in keys: - bt.insert(k, str(k)) - - random.shuffle(keys) - for k in keys: - bt.delete(k) - - assert len(bt) == 0 - - def test_delitem(self): - """Test del notation.""" - bt = BTree(t=2) - bt[10] = "ten" - - del bt[10] - assert 10 not in bt - - -class TestBTreeTraversal: - """Test B-Tree traversal.""" - - def test_inorder(self): - """Test inorder returns sorted order.""" - bt = BTree(t=2) - values = [5, 3, 7, 1, 9, 4, 6, 8, 2] - for v in values: - bt.insert(v, str(v)) - - result = bt.inorder() - keys = [k for k, _ in result] - assert keys == sorted(values) - - def test_iter(self): - """Test iteration.""" - bt = BTree(t=2) - for i in [5, 3, 7, 1]: - bt.insert(i, str(i)) - - assert list(bt) == [1, 3, 5, 7] - - def test_keys_values(self): - """Test keys and values methods.""" - bt = BTree(t=2) - bt.insert(3, "three") - bt.insert(1, "one") - bt.insert(2, "two") - - assert bt.keys() == [1, 2, 3] - assert bt.values() == ["one", "two", "three"] - - -class TestBTreeMinMax: - """Test B-Tree min/max.""" - - def test_min(self): - """Test min.""" - bt = BTree(t=2) - for i in [5, 3, 7, 1]: - bt.insert(i, str(i)) - - assert bt.min() == (1, "1") - - def test_max(self): - """Test max.""" - bt = BTree(t=2) - for i in [5, 3, 7, 1]: - bt.insert(i, str(i)) - - assert bt.max() == (7, "7") - - def test_min_max_empty(self): - """Test min/max on empty tree.""" - bt = BTree(t=2) - assert bt.min() is None - assert bt.max() is None - - -class TestBTreeProperties: - """Test B-Tree properties.""" - - def test_different_degrees(self): - """Test with different minimum degrees.""" - for t in [2, 3, 5, 10]: - bt = BTree(t=t) - for i in range(100): - bt.insert(i, str(i)) - - assert len(bt) == 100 - for i in range(100): - assert i in bt - - def test_large_dataset(self): - """Test with large dataset.""" - bt = BTree(t=10) - values = list(range(1000)) - random.shuffle(values) - - for v in values: - bt.insert(v, str(v)) - - assert len(bt) == 1000 - assert bt.keys() == sorted(values) - - def test_height(self): - """Test height is logarithmic.""" - bt = BTree(t=10) - for i in range(10000): - bt.insert(i, str(i)) - - # Height should be O(log_t(n)) - import math - max_height = math.ceil(math.log(10001, 10)) - assert bt.height() <= max_height + 1 - - def test_invalid_degree(self): - """Test invalid minimum degree.""" - with pytest.raises(ValueError): - BTree(t=1) - - -class TestBTreeClear: +class TestRedBlackTreeClear: """Test clear operation.""" def test_clear_rbt(self): @@ -435,15 +310,6 @@ def test_clear_rbt(self): rbt.clear() assert len(rbt) == 0 - def test_clear_btree(self): - """Test clearing B-Tree.""" - bt = BTree(t=2) - for i in range(10): - bt.insert(i, str(i)) - - bt.clear() - assert len(bt) == 0 - class TestRepr: """Test string representations.""" @@ -454,19 +320,3 @@ def test_rbt_repr(self): rbt.insert(10, "ten") s = repr(rbt) assert "RedBlackTree" in s - - def test_btree_repr(self): - """Test B-Tree repr.""" - bt = BTree(t=2) - bt.insert(10, "ten") - s = repr(bt) - assert "BTree" in s - - def test_btree_display(self): - """Test B-Tree display.""" - bt = BTree(t=2) - for i in range(5): - bt.insert(i, str(i)) - - display = bt.display() - assert len(display) > 0 From 5790f9fee18c817a211c3dd93d2a72c388330d7d Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:01 +0800 Subject: [PATCH 16/26] refactor(rust): resolve clippy and rustdoc warnings CI ran clippy with -W, so about 30 lints accumulated, and rustdoc read index expressions such as dist[v] as broken intra-doc links. Apply clippy's suggested rewrites without changing behaviour (the Bloom filter keeps its NaN handling) and put index notation in code spans so cargo doc passes with -D warnings. --- .../src/dynamic_programming/mod.rs | 10 +-- .../rust/dsa-algorithms/src/graph/mod.rs | 18 ++--- .../rust/dsa-algorithms/src/string/mod.rs | 14 ++-- .../src/advanced/skip_list.rs | 74 ++++++++----------- .../src/caches/lfu_cache.rs | 15 ++-- .../src/hashing/bloom_filter.rs | 10 ++- .../src/linear/singly_linked_list.rs | 19 ++--- .../dsa-data-structures/src/trees/avl_tree.rs | 3 +- .../src/trees/red_black_tree.rs | 4 +- .../dsa-data-structures/src/trees/trie.rs | 10 +-- 10 files changed, 77 insertions(+), 100 deletions(-) diff --git a/by-language/rust/dsa-algorithms/src/dynamic_programming/mod.rs b/by-language/rust/dsa-algorithms/src/dynamic_programming/mod.rs index 5932288..8cf2b4a 100644 --- a/by-language/rust/dsa-algorithms/src/dynamic_programming/mod.rs +++ b/by-language/rust/dsa-algorithms/src/dynamic_programming/mod.rs @@ -255,11 +255,11 @@ pub fn edit_distance(s1: &str, s2: &str) -> usize { let mut dp = vec![vec![0usize; n + 1]; m + 1]; // Base cases - for i in 0..=m { - dp[i][0] = i; + for (i, row) in dp.iter_mut().enumerate() { + row[0] = i; } - for j in 0..=n { - dp[0][j] = j; + for (j, cell) in dp[0].iter_mut().enumerate() { + *cell = j; } for i in 1..=m { @@ -474,7 +474,7 @@ pub fn max_subarray_with_indices(arr: &[i32]) -> (i32, usize, usize) { max_ending_here = num; temp_start = i; } else { - max_ending_here = max_ending_here + num; + max_ending_here += num; } if max_ending_here > max_so_far { diff --git a/by-language/rust/dsa-algorithms/src/graph/mod.rs b/by-language/rust/dsa-algorithms/src/graph/mod.rs index 91b42fc..ca50101 100644 --- a/by-language/rust/dsa-algorithms/src/graph/mod.rs +++ b/by-language/rust/dsa-algorithms/src/graph/mod.rs @@ -142,8 +142,8 @@ pub fn bfs(graph: &Graph, start: usize) -> Vec { /// BFS with distance tracking - Returns distances from start vertex. /// /// Returns (distances, parents) where: -/// - distances[v] = shortest number of edges from start to v (-1 if unreachable) -/// - parents[v] = predecessor of v in shortest path (-1 if start or unreachable) +/// - `distances[v]` = shortest number of edges from start to v (-1 if unreachable) +/// - `parents[v]` = predecessor of v in shortest path (-1 if start or unreachable) /// /// # Example /// @@ -271,8 +271,8 @@ pub fn dfs_iterative(graph: &Graph, start: usize) -> Vec { /// Dijkstra's Algorithm - Single-source shortest path for non-negative weights. /// /// Returns (distances, parents) where: -/// - distances[v] = shortest distance from start to v (i64::MAX if unreachable) -/// - parents[v] = predecessor of v in shortest path (-1 if start or unreachable) +/// - `distances[v]` = shortest distance from start to v (i64::MAX if unreachable) +/// - `parents[v]` = predecessor of v in shortest path (-1 if start or unreachable) /// /// # Complexity /// @@ -404,7 +404,7 @@ pub fn bellman_ford(graph: &Graph, start: usize) -> Option<(Vec, Vec)> /// Floyd-Warshall Algorithm - All-pairs shortest path. /// -/// Returns a matrix where result[i][j] = shortest distance from i to j. +/// Returns a matrix where `result[i][j]` = shortest distance from i to j. /// Uses i64::MAX for unreachable pairs. /// /// # Complexity @@ -599,8 +599,8 @@ pub fn kruskal(graph: &Graph) -> (i64, Vec<(usize, usize, i64)>) { // Collect all edges let mut edges: Vec<(i64, usize, usize)> = Vec::new(); - for u in 0..n { - for edge in &graph[u] { + for (u, neighbors) in graph.iter().enumerate() { + for edge in neighbors { if u < edge.to { // Avoid duplicates for undirected graphs edges.push((edge.weight, u, edge.to)); @@ -736,8 +736,8 @@ pub fn kosaraju_scc(graph: &Graph) -> Vec> { // Build reverse graph let mut reverse: Graph = vec![Vec::new(); n]; - for u in 0..n { - for edge in &graph[u] { + for (u, neighbors) in graph.iter().enumerate() { + for edge in neighbors { reverse[edge.to].push(Edge::new(u, edge.weight)); } } diff --git a/by-language/rust/dsa-algorithms/src/string/mod.rs b/by-language/rust/dsa-algorithms/src/string/mod.rs index ea90edc..54435c6 100644 --- a/by-language/rust/dsa-algorithms/src/string/mod.rs +++ b/by-language/rust/dsa-algorithms/src/string/mod.rs @@ -6,7 +6,7 @@ //! //! - [`kmp_search`] - Knuth-Morris-Pratt algorithm //! - [`rabin_karp`] - Rabin-Karp algorithm with rolling hash -//! - [`z_algorithm`] - Z-function based matching +//! - [`z_search`] - Z-function based matching //! //! ## String Processing //! @@ -23,8 +23,8 @@ use alloc::vec::Vec; /// Build the KMP failure function (prefix function). /// -/// For each position i, lps[i] is the length of the longest proper prefix -/// of pattern[0..=i] that is also a suffix. +/// For each position i, `lps[i]` is the length of the longest proper prefix +/// of `pattern[0..=i]` that is also a suffix. /// /// # Example /// @@ -190,7 +190,7 @@ pub fn rabin_karp(text: &str, pattern: &str) -> Vec { // Z Algorithm // ============================================================================ -/// Z-function - For each position i, z[i] is the length of the longest +/// Z-function - For each position i, `z[i]` is the length of the longest /// substring starting at i that matches a prefix of the string. /// /// # Complexity @@ -268,8 +268,8 @@ pub fn z_search(text: &str, pattern: &str) -> Vec { let z = z_function(&concat); // Find matches - for i in pattern_len + 1..z.len() { - if z[i] >= pattern_len { + for (i, &length) in z.iter().enumerate().skip(pattern_len + 1) { + if length >= pattern_len { result.push(i - pattern_len - 1); } } @@ -454,7 +454,7 @@ pub fn manacher(s: &str) -> (usize, usize) { } // Expand around center - while i >= p[i] + 1 && i + p[i] + 1 < n && t[i - p[i] - 1] == t[i + p[i] + 1] { + while i > p[i] && i + p[i] + 1 < n && t[i - p[i] - 1] == t[i + p[i] + 1] { p[i] += 1; } diff --git a/by-language/rust/dsa-data-structures/src/advanced/skip_list.rs b/by-language/rust/dsa-data-structures/src/advanced/skip_list.rs index fce3cda..3507e62 100644 --- a/by-language/rust/dsa-data-structures/src/advanced/skip_list.rs +++ b/by-language/rust/dsa-data-structures/src/advanced/skip_list.rs @@ -119,18 +119,14 @@ impl SkipList { // Find position for i in (0..=self.level).rev() { - loop { - if let Some(next_idx) = current_forward[i] { - match self.nodes[next_idx].value.as_ref().unwrap().cmp(&value) { - core::cmp::Ordering::Less => { - current = Some(next_idx); - current_forward = &self.nodes[next_idx].forward; - } - core::cmp::Ordering::Equal => return false, - core::cmp::Ordering::Greater => break, + while let Some(next_idx) = current_forward[i] { + match self.nodes[next_idx].value.as_ref().unwrap().cmp(&value) { + core::cmp::Ordering::Less => { + current = Some(next_idx); + current_forward = &self.nodes[next_idx].forward; } - } else { - break; + core::cmp::Ordering::Equal => return false, + core::cmp::Ordering::Greater => break, } } update[i] = current; @@ -139,8 +135,8 @@ impl SkipList { let new_level = self.random_level(); if new_level > self.level { - for i in (self.level + 1)..=new_level { - update[i] = None; // Head + for entry in update.iter_mut().take(new_level + 1).skip(self.level + 1) { + *entry = None; // Head } self.level = new_level; } @@ -149,8 +145,8 @@ impl SkipList { let new_idx = self.free_indices.pop().unwrap_or(self.nodes.len()); let mut new_forward = vec![None; new_level + 1]; - for i in 0..=new_level { - if let Some(prev_idx) = update[i] { + for (i, &previous) in update.iter().enumerate().take(new_level + 1) { + if let Some(prev_idx) = previous { new_forward[i] = self.nodes[prev_idx].forward[i]; } else { new_forward[i] = self.head_forward[i]; @@ -168,8 +164,8 @@ impl SkipList { } // Update forward pointers - for i in 0..=new_level { - if let Some(prev_idx) = update[i] { + for (i, &previous) in update.iter().enumerate().take(new_level + 1) { + if let Some(prev_idx) = previous { self.nodes[prev_idx].forward[i] = Some(new_idx); } else { self.head_forward[i] = Some(new_idx); @@ -196,17 +192,13 @@ impl SkipList { let mut current_forward = &self.head_forward; for i in (0..=self.level).rev() { - loop { - if let Some(next_idx) = current_forward[i] { - match self.nodes[next_idx].value.as_ref().unwrap().cmp(value) { - core::cmp::Ordering::Less => { - current_forward = &self.nodes[next_idx].forward; - } - core::cmp::Ordering::Equal => return true, - core::cmp::Ordering::Greater => break, + while let Some(next_idx) = current_forward[i] { + match self.nodes[next_idx].value.as_ref().unwrap().cmp(value) { + core::cmp::Ordering::Less => { + current_forward = &self.nodes[next_idx].forward; } - } else { - break; + core::cmp::Ordering::Equal => return true, + core::cmp::Ordering::Greater => break, } } } @@ -236,21 +228,17 @@ impl SkipList { let mut found_idx: Option = None; for i in (0..=self.level).rev() { - loop { - if let Some(next_idx) = current_forward[i] { - match self.nodes[next_idx].value.as_ref().unwrap().cmp(value) { - core::cmp::Ordering::Less => { - current = Some(next_idx); - current_forward = &self.nodes[next_idx].forward; - } - core::cmp::Ordering::Equal => { - found_idx = Some(next_idx); - break; - } - core::cmp::Ordering::Greater => break, + while let Some(next_idx) = current_forward[i] { + match self.nodes[next_idx].value.as_ref().unwrap().cmp(value) { + core::cmp::Ordering::Less => { + current = Some(next_idx); + current_forward = &self.nodes[next_idx].forward; } - } else { - break; + core::cmp::Ordering::Equal => { + found_idx = Some(next_idx); + break; + } + core::cmp::Ordering::Greater => break, } } update[i] = current; @@ -258,8 +246,8 @@ impl SkipList { if let Some(target_idx) = found_idx { // Update forward pointers - for i in 0..=self.level { - if let Some(prev_idx) = update[i] { + for (i, &previous) in update.iter().enumerate().take(self.level + 1) { + if let Some(prev_idx) = previous { if self.nodes[prev_idx].forward[i] == Some(target_idx) { let target_forward = if i < self.nodes[target_idx].forward.len() { self.nodes[target_idx].forward[i] diff --git a/by-language/rust/dsa-data-structures/src/caches/lfu_cache.rs b/by-language/rust/dsa-data-structures/src/caches/lfu_cache.rs index f822d1d..8037635 100644 --- a/by-language/rust/dsa-data-structures/src/caches/lfu_cache.rs +++ b/by-language/rust/dsa-data-structures/src/caches/lfu_cache.rs @@ -177,10 +177,7 @@ where // Insert new key with frequency 1 self.cache.insert(key.clone(), (value, 1)); - self.freq_to_keys - .entry(1) - .or_insert_with(Vec::new) - .push(key.clone()); + self.freq_to_keys.entry(1).or_default().push(key.clone()); let pos = self.freq_to_keys.get(&1).unwrap().len() - 1; self.key_to_pos.insert(key, pos); self.min_freq = 1; @@ -229,7 +226,7 @@ where // Add to new frequency list self.freq_to_keys .entry(new_freq) - .or_insert_with(Vec::new) + .or_default() .push(key.clone()); let pos = self.freq_to_keys.get(&new_freq).unwrap().len() - 1; self.key_to_pos.insert(key.clone(), pos); @@ -251,8 +248,8 @@ where if pos < keys.len() && &keys[pos] == key { keys.remove(pos); // Update positions for keys after this one - for i in pos..keys.len() { - self.key_to_pos.insert(keys[i].clone(), i); + for (i, key) in keys.iter().enumerate().skip(pos) { + self.key_to_pos.insert(key.clone(), i); } } } @@ -264,8 +261,8 @@ where if !keys.is_empty() { let evict_key = keys.remove(0); // Update positions - for i in 0..keys.len() { - self.key_to_pos.insert(keys[i].clone(), i); + for (i, key) in keys.iter().enumerate() { + self.key_to_pos.insert(key.clone(), i); } self.cache.remove(&evict_key); self.key_to_pos.remove(&evict_key); diff --git a/by-language/rust/dsa-data-structures/src/hashing/bloom_filter.rs b/by-language/rust/dsa-data-structures/src/hashing/bloom_filter.rs index 5206703..e6831c7 100644 --- a/by-language/rust/dsa-data-structures/src/hashing/bloom_filter.rs +++ b/by-language/rust/dsa-data-structures/src/hashing/bloom_filter.rs @@ -61,7 +61,11 @@ impl BloomFilter { /// ``` pub fn new(expected_items: usize, false_positive_rate: f64) -> Self { let expected_items = expected_items.max(1); - let fp_rate = false_positive_rate.max(0.0001).min(0.5); + let fp_rate = if false_positive_rate.is_nan() { + 0.0001 + } else { + false_positive_rate.clamp(0.0001, 0.5) + }; // Optimal number of bits: m = -n * ln(p) / (ln(2)^2) let num_bits = @@ -70,7 +74,7 @@ impl BloomFilter { // Optimal number of hash functions: k = (m/n) * ln(2) let num_hashes = ((num_bits as f64 / expected_items as f64) * 2.0_f64.ln()).ceil() as usize; - let num_hashes = num_hashes.max(1).min(16); + let num_hashes = num_hashes.clamp(1, 16); // Number of u64 words needed let num_words = (num_bits + 63) / 64; @@ -94,7 +98,7 @@ impl BloomFilter { /// ``` pub fn with_size(num_bits: usize, num_hashes: usize) -> Self { let num_bits = num_bits.max(64); - let num_hashes = num_hashes.max(1).min(16); + let num_hashes = num_hashes.clamp(1, 16); let num_words = (num_bits + 63) / 64; BloomFilter { diff --git a/by-language/rust/dsa-data-structures/src/linear/singly_linked_list.rs b/by-language/rust/dsa-data-structures/src/linear/singly_linked_list.rs index b36c4d2..5d1f86e 100644 --- a/by-language/rust/dsa-data-structures/src/linear/singly_linked_list.rs +++ b/by-language/rust/dsa-data-structures/src/linear/singly_linked_list.rs @@ -199,15 +199,14 @@ impl SinglyLinkedList { pub fn push_back(&mut self, data: T) { let new_node = Box::new(Node::new(data)); - if self.head.is_none() { - self.head = Some(new_node); - } else { + if let Some(mut current) = self.head.as_mut() { // Traverse to the end - let mut current = self.head.as_mut().unwrap(); while current.next.is_some() { current = current.next.as_mut().unwrap(); } current.next = Some(new_node); + } else { + self.head = Some(new_node); } self.len += 1; } @@ -253,9 +252,7 @@ impl SinglyLinkedList { /// assert_eq!(list.pop_back(), Some(20)); /// ``` pub fn pop_back(&mut self) -> Option { - if self.head.is_none() { - return None; - } + self.head.as_ref()?; // Single element case if self.head.as_ref().unwrap().next.is_none() { @@ -294,9 +291,7 @@ impl SinglyLinkedList { /// O(n) #[must_use] pub fn back(&self) -> Option<&T> { - if self.head.is_none() { - return None; - } + self.head.as_ref()?; let mut current = self.head.as_ref().unwrap(); while current.next.is_some() { @@ -524,9 +519,7 @@ impl SinglyLinkedList { /// ``` #[must_use] pub fn middle(&self) -> Option<&T> { - if self.head.is_none() { - return None; - } + self.head.as_ref()?; let mut slow = self.head.as_ref(); let mut fast = self.head.as_ref(); diff --git a/by-language/rust/dsa-data-structures/src/trees/avl_tree.rs b/by-language/rust/dsa-data-structures/src/trees/avl_tree.rs index 617bf97..553895d 100644 --- a/by-language/rust/dsa-data-structures/src/trees/avl_tree.rs +++ b/by-language/rust/dsa-data-structures/src/trees/avl_tree.rs @@ -483,8 +483,7 @@ impl AVLTree { None => true, Some(n) => { let balance = Self::balance_factor(n); - balance >= -1 - && balance <= 1 + (-1..=1).contains(&balance) && Self::check_balanced(&n.left) && Self::check_balanced(&n.right) } diff --git a/by-language/rust/dsa-data-structures/src/trees/red_black_tree.rs b/by-language/rust/dsa-data-structures/src/trees/red_black_tree.rs index 6dc18b0..c150dc3 100644 --- a/by-language/rust/dsa-data-structures/src/trees/red_black_tree.rs +++ b/by-language/rust/dsa-data-structures/src/trees/red_black_tree.rs @@ -201,7 +201,7 @@ impl RedBlackTree { // Parent is left child let uncle = self.nodes[gp_idx].right; - if uncle.map_or(false, |u| self.nodes[u].color == Color::Red) { + if uncle.is_some_and(|u| self.nodes[u].color == Color::Red) { // Case 1: Uncle is red self.nodes[p_idx].color = Color::Black; if let Some(u) = uncle { @@ -227,7 +227,7 @@ impl RedBlackTree { // Parent is right child (mirror cases) let uncle = self.nodes[gp_idx].left; - if uncle.map_or(false, |u| self.nodes[u].color == Color::Red) { + if uncle.is_some_and(|u| self.nodes[u].color == Color::Red) { // Case 1: Uncle is red self.nodes[p_idx].color = Color::Black; if let Some(u) = uncle { diff --git a/by-language/rust/dsa-data-structures/src/trees/trie.rs b/by-language/rust/dsa-data-structures/src/trees/trie.rs index 18a577c..fef5284 100644 --- a/by-language/rust/dsa-data-structures/src/trees/trie.rs +++ b/by-language/rust/dsa-data-structures/src/trees/trie.rs @@ -215,8 +215,7 @@ impl Trie { /// ``` #[must_use] pub fn search(&self, word: &str) -> bool { - self.find_node(word) - .map_or(false, |node| node.is_end_of_word) + self.find_node(word).is_some_and(|node| node.is_end_of_word) } /// Checks if any word in the trie starts with the given prefix. @@ -246,10 +245,7 @@ impl Trie { let mut current = &self.root; for ch in prefix.chars() { - match current.children.get(&ch) { - Some(node) => current = node, - None => return None, - } + current = current.children.get(&ch)?; } Some(current) @@ -739,7 +735,7 @@ mod tests { trie.insert("dad"); trie.insert("mad"); - assert!(trie.search_with_wildcard("pad") == false); + assert!(!trie.search_with_wildcard("pad")); assert!(trie.search_with_wildcard("bad")); assert!(trie.search_with_wildcard(".ad")); assert!(trie.search_with_wildcard("b..")); From 0d772c1ab93dde7fcebba0a1fb364335bf999dcd Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:02 +0800 Subject: [PATCH 17/26] fix(rust): bound counting sort ranges counting_sort allocated max + 1 counters, about 32 GB for u32::MAX, and counting_sort_i32 computed max - min + 1 in i32, which overflows for i32::MIN..i32::MAX. Compute ranges in a wider type, offset by the minimum, and fall back to sort_unstable when the range is more than 16 times the input length or the buffer cannot be allocated. --- .../rust/dsa-algorithms/src/sorting/mod.rs | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/by-language/rust/dsa-algorithms/src/sorting/mod.rs b/by-language/rust/dsa-algorithms/src/sorting/mod.rs index 925db71..aa59eea 100644 --- a/by-language/rust/dsa-algorithms/src/sorting/mod.rs +++ b/by-language/rust/dsa-algorithms/src/sorting/mod.rs @@ -342,6 +342,9 @@ fn heapify(arr: &mut [T], n: usize, i: usize) { /// /// Counts occurrences of each value and uses arithmetic to determine positions. /// +/// Ranges larger than 16 times the input length, or counting buffers that +/// cannot be allocated, fall back to `sort_unstable` (O(n log n) time). +/// /// # Complexity /// /// - Time: O(n + k) where k is the range of input @@ -366,25 +369,27 @@ pub fn counting_sort(arr: &mut [u32]) { return; } - // Find the maximum value - let max = match arr.iter().max() { - Some(&m) => m as usize, - None => return, + let min = *arr.iter().min().unwrap(); + let max = *arr.iter().max().unwrap(); + let range = u64::from(max) - u64::from(min) + 1; + let mut count = match counting_buffer(range, arr.len()) { + Some(count) => count, + None => { + arr.sort_unstable(); + return; + } }; - // Create counting array - let mut count = vec![0usize; max + 1]; - // Count occurrences for &val in arr.iter() { - count[val as usize] += 1; + count[(val - min) as usize] += 1; } // Reconstruct sorted array let mut idx = 0; - for (val, &cnt) in count.iter().enumerate() { + for (offset, &cnt) in count.iter().enumerate() { for _ in 0..cnt { - arr[idx] = val as u32; + arr[idx] = (u64::from(min) + offset as u64) as u32; idx += 1; } } @@ -392,6 +397,9 @@ pub fn counting_sort(arr: &mut [u32]) { /// Counting Sort for i32 - Handles negative numbers by shifting values. /// +/// Falls back to `sort_unstable` for ranges larger than 16 times the input +/// length or when the counting buffer cannot be allocated. +/// /// # Example /// /// ```rust @@ -408,17 +416,22 @@ pub fn counting_sort_i32(arr: &mut [i32]) { let min = *arr.iter().min().unwrap(); let max = *arr.iter().max().unwrap(); - let range = (max - min + 1) as usize; - - let mut count = vec![0usize; range]; + let range = (i64::from(max) - i64::from(min) + 1) as u64; + let mut count = match counting_buffer(range, arr.len()) { + Some(count) => count, + None => { + arr.sort_unstable(); + return; + } + }; for &val in arr.iter() { - count[(val - min) as usize] += 1; + count[(i64::from(val) - i64::from(min)) as usize] += 1; } let mut idx = 0; for (offset, &cnt) in count.iter().enumerate() { - let val = min + offset as i32; + let val = (i64::from(min) + offset as i64) as i32; for _ in 0..cnt { arr[idx] = val; idx += 1; @@ -426,6 +439,17 @@ pub fn counting_sort_i32(arr: &mut [i32]) { } } +fn counting_buffer(range: u64, len: usize) -> Option> { + let range = usize::try_from(range).ok()?; + if range > len.saturating_mul(16) { + return None; + } + let mut count = Vec::new(); + count.try_reserve_exact(range).ok()?; + count.resize(range, 0); + Some(count) +} + /// Radix Sort - Non-comparison integer sorting algorithm. /// /// Sorts numbers digit by digit, from least significant to most significant. @@ -514,6 +538,13 @@ pub fn is_sorted(arr: &[T]) -> bool { mod tests { use super::*; + #[test] + fn counting_buffer_limits() { + assert_eq!(counting_buffer(32, 2), Some(vec![0; 32])); + assert_eq!(counting_buffer(33, 2), None); + assert_eq!(counting_buffer(u64::MAX, usize::MAX), None); + } + mod bubble_sort_tests { use super::*; From dee3fe9ebca3faa049d490952f934cd53b1c7908 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:02 +0800 Subject: [PATCH 18/26] test(rust): add fixed-seed model tests Compare the sorts, searches, string matching, union-find, trees, skip list, and caches against standard-library reference models. Failures report the seed, iteration, and operation history. --- .../rust/dsa-algorithms/tests/models.rs | 155 +++++++++ .../rust/dsa-data-structures/tests/models.rs | 313 ++++++++++++++++++ 2 files changed, 468 insertions(+) create mode 100644 by-language/rust/dsa-algorithms/tests/models.rs create mode 100644 by-language/rust/dsa-data-structures/tests/models.rs diff --git a/by-language/rust/dsa-algorithms/tests/models.rs b/by-language/rust/dsa-algorithms/tests/models.rs new file mode 100644 index 0000000..975529f --- /dev/null +++ b/by-language/rust/dsa-algorithms/tests/models.rs @@ -0,0 +1,155 @@ +use dsa_algorithms::{searching::*, sorting::*, string::*}; +use rand::{rngs::StdRng, Rng, SeedableRng}; + +const SEED: u64 = 0xD5A_2026; + +#[test] +fn sorts_match_slice_sort() { + let mut rng = StdRng::seed_from_u64(SEED); + let sorts: &[fn(&mut [i32])] = &[ + bubble_sort, + selection_sort, + insertion_sort, + merge_sort, + quick_sort, + heap_sort, + counting_sort_i32, + ]; + for iteration in 0..200 { + let input: Vec = (0..rng.gen_range(0..65)) + .map(|_| { + if iteration % 2 == 0 { + rng.gen_range(-16..=16) + } else { + rng.gen() + } + }) + .collect(); + let mut expected = input.clone(); + expected.sort(); + for (sort_index, sort) in sorts.iter().enumerate() { + let mut actual = input.clone(); + sort(&mut actual); + assert_eq!( + actual, expected, + "seed={SEED} iteration={iteration} sort={sort_index} input={input:?}" + ); + } + let input: Vec = input + .iter() + .map(|&v| match iteration % 3 { + 0 => v as u32, + 1 => v.unsigned_abs() % 32, + _ => u32::MAX - v.unsigned_abs() % 32, + }) + .collect(); + let mut expected = input.clone(); + expected.sort(); + for sort in [counting_sort, radix_sort] { + let mut actual = input.clone(); + sort(&mut actual); + assert_eq!( + actual, expected, + "seed={SEED} iteration={iteration} input={input:?}" + ); + } + } +} + +#[test] +fn integer_sort_extremes() { + for input in [ + vec![i32::MAX, i32::MIN, 0, -1], + vec![i32::MIN + 1, i32::MIN], + vec![i32::MAX, i32::MAX - 1], + vec![-1000000, 1000000], + ] { + let mut expected = input.clone(); + expected.sort(); + let mut actual = input; + counting_sort_i32(&mut actual); + assert_eq!(actual, expected); + } + for input in [ + vec![u32::MAX, 0, 1, u32::MAX - 1], + vec![u32::MAX, u32::MAX - 1], + vec![1000000, 0], + vec![u32::MAX; 3], + ] { + let mut expected = input.clone(); + expected.sort(); + for sort in [counting_sort, radix_sort] { + let mut actual = input.clone(); + sort(&mut actual); + assert_eq!(actual, expected, "input={input:?}"); + } + } +} + +#[test] +fn binary_search_matches_partition_points() { + let mut rng = StdRng::seed_from_u64(SEED); + for iteration in 0..300 { + let mut input: Vec = (0..rng.gen_range(0..80)) + .map(|_| rng.gen_range(-20..=20)) + .collect(); + input.sort(); + for target in -21..=21 { + let lower = input.partition_point(|&x| x < target); + let upper = input.partition_point(|&x| x <= target); + let context = + format!("seed={SEED} iteration={iteration} input={input:?} target={target}"); + assert_eq!(lower_bound(&input, &target), lower, "{context}"); + assert_eq!(upper_bound(&input, &target), upper, "{context}"); + assert_eq!( + count_occurrences(&input, &target), + upper - lower, + "{context}" + ); + assert_eq!( + binary_search_first(&input, &target), + (lower < upper).then_some(lower), + "{context}" + ); + assert_eq!( + binary_search_last(&input, &target), + (lower < upper).then(|| upper - 1), + "{context}" + ); + let actual = binary_search(&input, &target); + assert_eq!(actual.is_some(), lower < upper, "{context}"); + if let Some(index) = actual { + assert!((lower..upper).contains(&index), "{context} index={index}"); + } + } + } +} + +#[test] +fn string_search_matches_naive_search() { + let mut rng = StdRng::seed_from_u64(SEED); + for iteration in 0..500 { + let text: String = (0..rng.gen_range(0..60)) + .map(|_| ['a', 'b', 'c', '\0'][rng.gen_range(0..4)]) + .collect(); + let pattern: String = (0..rng.gen_range(0..12)) + .map(|_| ['a', 'b', 'c', '\0'][rng.gen_range(0..4)]) + .collect(); + let expected: Vec = if pattern.is_empty() { + vec![] + } else { + text.as_bytes() + .windows(pattern.len()) + .enumerate() + .filter_map(|(i, window)| (window == pattern.as_bytes()).then_some(i)) + .collect() + }; + for search in [kmp_search, rabin_karp, z_search] { + assert_eq!( + search(&text, &pattern), + expected, + "seed={SEED} iteration={iteration} text={text:?} pattern={pattern:?}" + ); + } + } +} diff --git a/by-language/rust/dsa-data-structures/tests/models.rs b/by-language/rust/dsa-data-structures/tests/models.rs new file mode 100644 index 0000000..fc195d2 --- /dev/null +++ b/by-language/rust/dsa-data-structures/tests/models.rs @@ -0,0 +1,313 @@ +use dsa_data_structures::{ + advanced::SkipList, + caches::{LFUCache, LRUCache}, + graphs::UnionFind, + trees::{AVLTree, BTree, BinarySearchTree, RedBlackTree}, +}; +use rand::{rngs::StdRng, Rng, SeedableRng}; +use std::collections::{BTreeMap, BTreeSet}; + +const SEED: u64 = 0xD5A_2026; + +#[test] +fn ordered_sets_match_btree_set() { + let mut rng = StdRng::seed_from_u64(SEED); + let mut bst = BinarySearchTree::new(); + let mut avl = AVLTree::new(); + let mut skip = SkipList::new(); + let mut expected = BTreeSet::new(); + let mut operations = Vec::new(); + for iteration in 0..1000 { + let key = rng.gen_range(-40..=40); + let insert = rng.gen_bool(0.55); + operations.push((insert, key)); + let context = format!("seed={SEED} iteration={iteration} operations={operations:?}"); + if insert { + let inserted = expected.insert(key); + bst.insert(key); + avl.insert(key); + assert_eq!(skip.insert(key), inserted, "{context}"); + } else { + let removed = expected.remove(&key); + assert_eq!(bst.remove(&key), removed, "{context}"); + assert_eq!(avl.remove(&key), removed, "{context}"); + assert_eq!(skip.remove(&key), removed, "{context}"); + } + let sorted: Vec<_> = expected.iter().copied().collect(); + assert_eq!( + bst.inorder().copied().collect::>(), + sorted, + "{context}" + ); + assert_eq!( + avl.inorder().copied().collect::>(), + sorted, + "{context}" + ); + assert_eq!( + skip.iter().copied().collect::>(), + sorted, + "{context}" + ); + assert_eq!( + (bst.len(), avl.len(), skip.len()), + (expected.len(), expected.len(), expected.len()), + "{context}" + ); + assert!( + bst.is_valid() && avl.is_valid() && avl.is_balanced(), + "{context}" + ); + for key in -41..=41 { + let present = expected.contains(&key); + assert_eq!( + (bst.contains(&key), avl.contains(&key), skip.contains(&key)), + (present, present, present), + "{context} query={key}" + ); + } + } +} + +#[test] +fn insert_only_trees_match_models() { + let mut rng = StdRng::seed_from_u64(SEED); + for degree in 2..=5 { + let mut rb = RedBlackTree::new(); + let mut btree = BTree::new(degree); + let mut set = BTreeSet::new(); + let mut counts = BTreeMap::::new(); + let mut input = Vec::new(); + for iteration in 0..250 { + let key = rng.gen_range(-40..=40); + input.push(key); + let context = + format!("seed={SEED} iteration={iteration} degree={degree} input={input:?}"); + assert_eq!(rb.insert(key), set.insert(key), "{context}"); + btree.insert(key); + *counts.entry(key).or_default() += 1; + let expected: Vec<_> = counts + .iter() + .flat_map(|(&key, &count)| std::iter::repeat(key).take(count)) + .collect(); + assert_eq!( + btree.iter().copied().collect::>(), + expected, + "{context}" + ); + assert_eq!( + rb.iter().copied().collect::>(), + set.iter().copied().collect::>(), + "{context}" + ); + assert_eq!( + (btree.len(), rb.len()), + (input.len(), set.len()), + "{context}" + ); + for query in -41..=41 { + let present = set.contains(&query); + assert_eq!( + (rb.contains(&query), btree.contains(&query)), + (present, present), + "{context} query={query}" + ); + } + } + } +} + +#[test] +fn bst_duplicate_preserves_shape() { + let mut tree = BinarySearchTree::from_slice(&[5, 3, 7, 1, 4, 6, 9]); + let preorder = tree.preorder().copied().collect::>(); + let height = tree.height(); + for key in [5, 3, 7, 1, 4, 6, 9] { + tree.insert(key); + assert_eq!(tree.len(), 7); + assert_eq!(tree.height(), height); + assert_eq!(tree.preorder().copied().collect::>(), preorder); + } +} + +#[test] +fn caches_match_reference_models() { + let mut rng = StdRng::seed_from_u64(SEED); + for capacity in 1..=5 { + let mut lru = LRUCache::new(capacity); + let mut lfu = LFUCache::new(capacity); + // Value, access frequency, last access time. + let mut lru_model = BTreeMap::::new(); + let mut lfu_model = lru_model.clone(); + let mut operations = Vec::new(); + for iteration in 0..1000 { + let key = rng.gen_range(0..9); + let value = rng.gen_range(-100..=100); + let operation = rng.gen_range(0..5); + operations.push((operation, key, value)); + let context = format!( + "seed={SEED} iteration={iteration} capacity={capacity} operations={operations:?}" + ); + match operation { + 0 | 1 => { + let evicted = + reference_put(&mut lru_model, capacity, key, value, iteration, false); + reference_put(&mut lfu_model, capacity, key, value, iteration, true); + assert_eq!(lru.put(key, value), evicted, "{context}"); + lfu.put(key, value); + } + 2 | 3 => { + let expected_lru = reference_get(&mut lru_model, key, iteration); + let expected_lfu = reference_get(&mut lfu_model, key, iteration); + if operation == 2 { + assert_eq!(lru.get(&key).copied(), expected_lru, "{context}"); + assert_eq!(lfu.get(&key).copied(), expected_lfu, "{context}"); + } else { + assert_eq!( + lru.get_mut(&key).map(|v| { + *v += 1; + *v + }), + expected_lru.map(|v| v + 1), + "{context}" + ); + assert_eq!( + lfu.get_mut(&key).map(|v| { + *v += 1; + *v + }), + expected_lfu.map(|v| v + 1), + "{context}" + ); + if let Some(entry) = lru_model.get_mut(&key) { + entry.0 += 1; + } + if let Some(entry) = lfu_model.get_mut(&key) { + entry.0 += 1; + } + } + } + _ => { + assert_eq!( + lru.remove(&key), + lru_model.remove(&key).map(|entry| entry.0), + "{context}" + ); + assert_eq!( + lfu.remove(&key), + lfu_model.remove(&key).map(|entry| entry.0), + "{context}" + ); + } + } + assert_eq!( + (lru.len(), lfu.len()), + (lru_model.len(), lfu_model.len()), + "{context}" + ); + for query in 0..9 { + assert_eq!( + lru.peek(&query).copied(), + lru_model.get(&query).map(|entry| entry.0), + "{context} query={query}" + ); + assert_eq!( + lfu.contains(&query), + lfu_model.contains_key(&query), + "{context} query={query}" + ); + assert_eq!( + lfu.frequency(&query), + lfu_model.get(&query).map(|entry| entry.1), + "{context} query={query}" + ); + } + } + } +} + +fn reference_get( + model: &mut BTreeMap, + key: i32, + time: usize, +) -> Option { + model.get_mut(&key).map(|entry| { + entry.1 += 1; + entry.2 = time; + entry.0 + }) +} + +fn reference_put( + model: &mut BTreeMap, + capacity: usize, + key: i32, + value: i32, + time: usize, + lfu: bool, +) -> Option<(i32, i32)> { + if let Some(entry) = model.get_mut(&key) { + *entry = (value, entry.1 + 1, time); + return None; + } + let evicted = if model.len() == capacity { + let victim = *model + .iter() + .min_by_key(|(_, entry)| (if lfu { entry.1 } else { 0 }, entry.2)) + .unwrap() + .0; + Some((victim, model.remove(&victim).unwrap().0)) + } else { + None + }; + model.insert(key, (value, 1, time)); + evicted +} + +#[test] +fn union_find_matches_component_labels() { + let mut rng = StdRng::seed_from_u64(SEED); + for size in [1, 2, 8, 32] { + let mut uf = UnionFind::new(size); + let mut labels: Vec<_> = (0..size).collect(); + let mut input = Vec::new(); + for iteration in 0..250 { + let a = rng.gen_range(0..size); + let b = rng.gen_range(0..size); + input.push((a, b)); + let context = format!("seed={SEED} iteration={iteration} size={size} input={input:?}"); + assert_eq!(uf.union(a, b), labels[a] != labels[b], "{context}"); + let old = labels[b]; + let new = labels[a]; + for label in &mut labels { + if *label == old { + *label = new; + } + } + assert_eq!( + uf.count(), + labels.iter().collect::>().len(), + "{context}" + ); + for x in 0..size { + assert_eq!( + uf.set_size(x), + labels.iter().filter(|&&label| label == labels[x]).count(), + "{context} x={x}" + ); + for y in 0..size { + assert_eq!( + uf.connected(x, y), + labels[x] == labels[y], + "{context} x={x} y={y}" + ); + assert_eq!( + uf.find_immutable(x) == uf.find_immutable(y), + labels[x] == labels[y], + "{context} x={x} y={y}" + ); + } + } + } + } +} From 06ac6ca9d8e0aac641ec26aab90d428016c1f193 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:02 +0800 Subject: [PATCH 19/26] chore(rust): fix repository URL and TTL claim in crate docs The workspace pointed at github.com/zenco/dsa instead of github.com/notzenco/dsa, and the dsa-data-structures crate docs listed a TTL cache that the Rust implementation does not have. --- by-language/rust/Cargo.toml | 2 +- by-language/rust/dsa-data-structures/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/by-language/rust/Cargo.toml b/by-language/rust/Cargo.toml index dee5ba2..f41e93f 100644 --- a/by-language/rust/Cargo.toml +++ b/by-language/rust/Cargo.toml @@ -14,7 +14,7 @@ rust-version = "1.70" license = "MIT" authors = ["Zenco"] description = "Data Structures and Algorithms implementations in Rust" -repository = "https://github.com/zenco/dsa" +repository = "https://github.com/notzenco/dsa" keywords = ["data-structures", "algorithms", "dsa", "education"] categories = ["algorithms", "data-structures"] diff --git a/by-language/rust/dsa-data-structures/src/lib.rs b/by-language/rust/dsa-data-structures/src/lib.rs index e26c82a..4621f08 100644 --- a/by-language/rust/dsa-data-structures/src/lib.rs +++ b/by-language/rust/dsa-data-structures/src/lib.rs @@ -12,7 +12,7 @@ //! - `hashing` - Hash-based structures (hash table, bloom filter) //! - `heaps` - Heap data structures //! - `graphs` - Graph representations and algorithms -//! - `caches` - Cache implementations (LRU, LFU, TTL) +//! - `caches` - Cache implementations (LRU, LFU) //! - `advanced` - Advanced data structures (skip list, etc.) #![cfg_attr(not(feature = "std"), no_std)] From fb9d64628125363ba8775e2737a6a4f365b7c1b8 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:02 +0800 Subject: [PATCH 20/26] fix(typescript): accept every safe integer in counting and radix sort radixSort indexed its digit counts with negative digits, returned an all-negative array unsorted, and silently mis-sorted non-integers. Both sorts used Math.min and Math.max spreads, which throw on very large arrays. Radix sort now takes digits from each value's distance to the minimum, using BigInt so spans beyond 2^53 stay exact. Counting sort falls back to a comparison sort when the range is more than 16 times the input length. Both throw RangeError for values that are not safe integers. --- .../typescript/src/algorithms/sorting.ts | 80 ++++++++++++++---- .../tests/algorithms/sorting.test.ts | 83 +++++++++++++++++++ 2 files changed, 145 insertions(+), 18 deletions(-) diff --git a/by-language/typescript/src/algorithms/sorting.ts b/by-language/typescript/src/algorithms/sorting.ts index 03ebb7b..5268494 100644 --- a/by-language/typescript/src/algorithms/sorting.ts +++ b/by-language/typescript/src/algorithms/sorting.ts @@ -15,8 +15,8 @@ * Non-Comparison Sorts: * | Algorithm | Time | Space | Constraint | Stable | * |-----------------|------------|--------|------------|--------| - * | Counting Sort | O(n + k) | O(k) | Integers | Yes | - * | Radix Sort | O(d(n+k)) | O(n+k) | Integers | Yes | + * | Counting Sort | O(n + k) | O(k) | Safe ints | Yes | + * | Radix Sort | O(d(n+k)) | O(n+k) | Safe ints | Yes | * | Bucket Sort | O(n + k) | O(n) | Uniform | Yes | */ @@ -316,22 +316,51 @@ export function shellSort( // NON-COMPARISON SORTING ALGORITHMS // ============================================================================= +function assertSafeIntegers(arr: number[]): void { + for (let i = 0; i < arr.length; i++) { + if (!Number.isSafeInteger(arr[i])) { + throw new RangeError( + `expected a safe integer at index ${i}, received ${String(arr[i])}`, + ); + } + } +} + +function minMax(arr: number[]): [number, number] { + let minVal = arr[0]; + let maxVal = arr[0]; + for (let i = 1; i < arr.length; i++) { + const value = arr[i]; + if (value < minVal) minVal = value; + if (value > maxVal) maxVal = value; + } + return [minVal, maxVal]; +} + /** * Counting Sort - count occurrences, then reconstruct. * - * Time: O(n + k) where k is range of values - * Space: O(k) + * Time: O(n + k) where k is the range of values. When k > 16n, falls back + * to a comparison sort (O(n log n)). + * Space: O(k), or O(n) on the fallback path * Stable: Yes * - * Only works for integers. + * Accepts every safe integer, including negatives, by offsetting counts by + * the minimum. Throws RangeError if any value is not a safe integer. */ export function countingSort(arr: number[]): number[] { if (arr.length === 0) return []; + assertSafeIntegers(arr); - const minVal = Math.min(...arr); - const maxVal = Math.max(...arr); + const [minVal, maxVal] = minMax(arr); + const n = arr.length; const rangeSize = maxVal - minVal + 1; + // A wide domain would allocate a huge count array for little benefit. + if (rangeSize > 16 * n) { + return [...arr].sort((a, b) => a - b); + } + // Count occurrences const count = new Array(rangeSize).fill(0); for (const num of arr) { @@ -355,14 +384,23 @@ export function countingSort(arr: number[]): number[] { return result; } -function countingSortByDigit(arr: number[], exp: number): number[] { +function countingSortByDigit( + arr: number[], + exp: bigint, + minVal: bigint, +): number[] { const n = arr.length; const result = new Array(n); const count = new Array(10).fill(0); - for (const num of arr) { - const digit = Math.floor(num / exp) % 10; - count[digit]++; + const digitAt = (num: number): number => { + // (value - min) can exceed the safe-integer span, where Number + // subtraction is no longer exact. + return Number(((BigInt(num) - minVal) / exp) % 10n); + }; + + for (let i = 0; i < n; i++) { + count[digitAt(arr[i])]++; } for (let i = 1; i < 10; i++) { @@ -371,7 +409,7 @@ function countingSortByDigit(arr: number[], exp: number): number[] { for (let k = n - 1; k >= 0; k--) { const num = arr[k]; - const digit = Math.floor(num / exp) % 10; + const digit = digitAt(num); const idx = count[digit] - 1; result[idx] = num; count[digit]--; @@ -387,18 +425,24 @@ function countingSortByDigit(arr: number[], exp: number): number[] { * Space: O(n + k) * Stable: Yes * - * Works for non-negative integers. + * Accepts every safe integer, including negatives. Digits are taken from + * (value - min), so the keys passed to counting sort are non-negative. + * Throws RangeError if any value is not a safe integer. */ export function radixSort(arr: number[]): number[] { if (arr.length === 0) return []; + assertSafeIntegers(arr); + + const [minVal, maxVal] = minMax(arr); + const minBig = BigInt(minVal); + const maxShifted = BigInt(maxVal) - minBig; - const maxVal = Math.max(...arr); let result = [...arr]; - let exp = 1; + let exp = 1n; - while (Math.floor(maxVal / exp) > 0) { - result = countingSortByDigit(result, exp); - exp *= 10; + while (maxShifted / exp > 0n) { + result = countingSortByDigit(result, exp, minBig); + exp *= 10n; } return result; diff --git a/by-language/typescript/tests/algorithms/sorting.test.ts b/by-language/typescript/tests/algorithms/sorting.test.ts index deff3ed..a2c1b02 100644 --- a/by-language/typescript/tests/algorithms/sorting.test.ts +++ b/by-language/typescript/tests/algorithms/sorting.test.ts @@ -215,6 +215,46 @@ describe("countingSort", () => { const expected = Array.from({ length: 500 }, (_, i) => i + 1); expect(countingSort(input)).toEqual(expected); }); + + test("falls back to a comparison sort when the range exceeds 16n", () => { + const wide = [0, 1_000_000, -5, 1_000_000]; + expect(countingSort(wide)).toEqual([-5, 0, 1_000_000, 1_000_000]); + const copy = [...wide]; + countingSort(wide); + expect(wide).toEqual(copy); + + // range 33 > 16 * 2, so this takes the fallback path + expect(countingSort([0, 32])).toEqual([0, 32]); + // range 32 === 16 * 2, so this still uses the count array + expect(countingSort([0, 31])).toEqual([0, 31]); + }); + + test("safe integer extremes", () => { + const input = [ + Number.MAX_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, + 0, + -1, + 1, + ]; + expect(countingSort(input)).toEqual([...input].sort((a, b) => a - b)); + }); + + test("rejects values that are not safe integers", () => { + const bad = [ + NaN, + Infinity, + -Infinity, + 1.5, + -0.1, + Number.MAX_SAFE_INTEGER + 1, + Number.MIN_SAFE_INTEGER - 1, + ]; + for (const value of bad) { + expect(() => countingSort([value])).toThrow(RangeError); + expect(() => countingSort([0, value, -3])).toThrow(RangeError); + } + }); }); describe("radixSort", () => { @@ -266,6 +306,49 @@ describe("radixSort", () => { const expected = Array.from({ length: 500 }, (_, i) => i + 1); expect(radixSort(input)).toEqual(expected); }); + + test("with negative numbers", () => { + expect(radixSort([-3, 5, -1, 0, 4, -2, 3])).toEqual([ + -3, -2, -1, 0, 3, 4, 5, + ]); + }); + + test("all negative numbers", () => { + expect(radixSort([-10, -1, -20, -3, -1])).toEqual([-20, -10, -3, -1, -1]); + }); + + test("safe integer extremes", () => { + const input = [ + Number.MAX_SAFE_INTEGER, + Number.MIN_SAFE_INTEGER, + 0, + -1, + 1, + 42, + -42, + Number.MIN_SAFE_INTEGER + 2, + ]; + expect(radixSort(input)).toEqual([...input].sort((a, b) => a - b)); + const copy = [...input]; + radixSort(input); + expect(input).toEqual(copy); + }); + + test("rejects values that are not safe integers", () => { + const bad = [ + NaN, + Infinity, + -Infinity, + 1.5, + -0.1, + Number.MAX_SAFE_INTEGER + 1, + Number.MIN_SAFE_INTEGER - 1, + ]; + for (const value of bad) { + expect(() => radixSort([value])).toThrow(RangeError); + expect(() => radixSort([-2, value, 4])).toThrow(RangeError); + } + }); }); describe("bucketSort", () => { From 5e4d8de028c0f6ce3fb0e74a8673353ee8569ad1 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:02 +0800 Subject: [PATCH 21/26] fix(typescript)!: keep binary search tree keys unique insert sent duplicates right while isValid used strict bounds, so inserting the same value twice made the tree report itself invalid. Inserting a present value now returns the existing node and leaves the tree unchanged, and fromSortedArray keeps repeated values once. BREAKING CHANGE: inserting a value that is already present no longer adds a node or increases size. Callers that relied on storing duplicates should keep a count per value instead. --- .../src/data-structures/binary-search-tree.ts | 40 ++++++++++++------- .../binary-search-tree.test.ts | 37 +++++++++++++++-- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/by-language/typescript/src/data-structures/binary-search-tree.ts b/by-language/typescript/src/data-structures/binary-search-tree.ts index 6c7d3ff..9855c2d 100644 --- a/by-language/typescript/src/data-structures/binary-search-tree.ts +++ b/by-language/typescript/src/data-structures/binary-search-tree.ts @@ -5,6 +5,9 @@ * - All values in left subtree < node value * - All values in right subtree > node value * + * Keys are unique. Inserting a value already present returns that node + * and leaves the size and shape unchanged. + * * COMPLEXITY: * | Operation | Average | Worst | Space | * |--------------------|-----------|--------|-------| @@ -93,36 +96,39 @@ export class BinarySearchTree { } insert(value: T): TreeNode { - const newNode = new TreeNode(value); - if (this._root === null) { - this._root = newNode; + const node = new TreeNode(value); + this._root = node; this._size++; - return newNode; + return node; } let current: TreeNode = this._root; while (true) { const cmp = this._compare(value, current.value); + if (cmp === 0) { + return current; + } if (cmp < 0) { if (current.left === null) { - current.left = newNode; - newNode.parent = current; - break; + const node = new TreeNode(value); + current.left = node; + node.parent = current; + this._size++; + return node; } current = current.left; } else { if (current.right === null) { - current.right = newNode; - newNode.parent = current; - break; + const node = new TreeNode(value); + current.right = node; + node.parent = current; + this._size++; + return node; } current = current.right; } } - - this._size++; - return newNode; } search(value: T): TreeNode | null { @@ -411,8 +417,14 @@ export class BinarySearchTree { return null; } - static fromSortedArray(values: T[], compare?: Comparator): BinarySearchTree { + /** + * Build a balanced BST from a sorted array. Repeated values are kept once. + */ + static fromSortedArray(sortedValues: T[], compare?: Comparator): BinarySearchTree { const bst = new BinarySearchTree(compare); + const values = sortedValues.filter( + (value, i) => i === 0 || bst._compare(value, sortedValues[i - 1]) !== 0, + ); const build = ( left: number, diff --git a/by-language/typescript/tests/data-structures/binary-search-tree.test.ts b/by-language/typescript/tests/data-structures/binary-search-tree.test.ts index e86ab12..8e17422 100644 --- a/by-language/typescript/tests/data-structures/binary-search-tree.test.ts +++ b/by-language/typescript/tests/data-structures/binary-search-tree.test.ts @@ -40,6 +40,20 @@ describe("TreeNode", () => { expect(parent.isLeftChild).toBe(false); expect(parent.isRightChild).toBe(false); }); + + test("hasLeftChild and hasRightChild", () => { + const node = new TreeNode(5); + expect(node.hasLeftChild).toBe(false); + expect(node.hasRightChild).toBe(false); + + node.left = new TreeNode(3); + expect(node.hasLeftChild).toBe(true); + expect(node.hasRightChild).toBe(false); + + node.right = new TreeNode(7); + expect(node.hasLeftChild).toBe(true); + expect(node.hasRightChild).toBe(true); + }); }); describe("BinarySearchTree", () => { @@ -157,12 +171,20 @@ describe("BinarySearchTree", () => { expect(bst.height).toBe(4); // Skewed }); - test("handles duplicate values (stored in right subtree)", () => { + test("duplicate key returns the existing node", () => { const bst = new BinarySearchTree(); - bst.insert(10); - bst.insert(10); - bst.insert(10); + const first = bst.insert(10); + bst.insert(5); + bst.insert(15); + const again = bst.insert(10); + const third = bst.insert(10); + + expect(again).toBe(first); + expect(third).toBe(first); expect(bst.size).toBe(3); + expect(bst.isValid()).toBe(true); + expect(bst.inOrder()).toEqual([5, 10, 15]); + expect(bst.preOrder()).toEqual([10, 5, 15]); }); }); @@ -453,6 +475,13 @@ describe("BinarySearchTree", () => { expect(bst.size).toBe(1); expect(bst.root).toBe(42); }); + + test("keeps repeated values once", () => { + const bst = BinarySearchTree.fromSortedArray([1, 1, 2, 3, 3, 3]); + expect(bst.size).toBe(3); + expect(bst.inOrder()).toEqual([1, 2, 3]); + expect(bst.isValid()).toBe(true); + }); }); describe("lowestCommonAncestor", () => { From 57a84f2076b183899e588a066e3c72c1645d94b2 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:02 +0800 Subject: [PATCH 22/26] build(typescript): add typecheck script and pin type packages Add typescript as a dev dependency and a typecheck script that runs tsc --noEmit, so CI can enforce the strict tsconfig. Pin @types/bun to the locked 1.3.x line instead of latest. --- by-language/typescript/bun.lock | 45 ++++++++++++++++++++++++++++- by-language/typescript/package.json | 6 ++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/by-language/typescript/bun.lock b/by-language/typescript/bun.lock index c7bddff..f2e4c5a 100644 --- a/by-language/typescript/bun.lock +++ b/by-language/typescript/bun.lock @@ -5,7 +5,8 @@ "": { "name": "dsa-typescript", "devDependencies": { - "@types/bun": "latest", + "@types/bun": "^1.3.10", + "typescript": "^7.0.2", }, }, }, @@ -14,8 +15,50 @@ "@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="], + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], } } diff --git a/by-language/typescript/package.json b/by-language/typescript/package.json index b696e70..858c3a4 100644 --- a/by-language/typescript/package.json +++ b/by-language/typescript/package.json @@ -3,9 +3,11 @@ "version": "1.0.0", "type": "module", "scripts": { - "test": "bun test" + "test": "bun test", + "typecheck": "tsc --noEmit -p tsconfig.json" }, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "^1.3.10", + "typescript": "^7.0.2" } } From 4a7ec1e892db6c78e1352a39be416130fb7121af Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:02 +0800 Subject: [PATCH 23/26] test(typescript): add model tests and close coverage gaps Add a seeded PRNG and randomized tests that compare sorts, searches, string matching, trees, hash tables, heaps, caches, range trees, union-find, and trie against reference models. Cover the remaining B-tree deletion and heap branches. --- .../tests/data-structures/b-tree.test.ts | 12 + .../tests/data-structures/heap.test.ts | 22 + .../typescript/tests/randomized/model.test.ts | 616 ++++++++++++++++++ .../typescript/tests/randomized/prng.ts | 15 + 4 files changed, 665 insertions(+) create mode 100644 by-language/typescript/tests/randomized/model.test.ts create mode 100644 by-language/typescript/tests/randomized/prng.ts diff --git a/by-language/typescript/tests/data-structures/b-tree.test.ts b/by-language/typescript/tests/data-structures/b-tree.test.ts index fa7f7b2..0cbcf44 100644 --- a/by-language/typescript/tests/data-structures/b-tree.test.ts +++ b/by-language/typescript/tests/data-structures/b-tree.test.ts @@ -247,6 +247,18 @@ describe("BTree", () => { expect(after).toEqual(before.filter((v) => v !== 15)); }); + test("delete walks a non-leaf predecessor", () => { + const tree = new BTree(2); + for (let i = 10; i >= 1; i--) { + tree.insert(i); + } + + expect(tree.delete(7)).toBe(true); + expect(tree.search(7)).toBe(false); + expect(tree.size).toBe(9); + expect(tree.traverse()).toEqual([1, 2, 3, 4, 5, 6, 8, 9, 10]); + }); + test("delete internal node (predecessor replacement)", () => { const tree = new BTree(2); for (let i = 1; i <= 10; i++) { diff --git a/by-language/typescript/tests/data-structures/heap.test.ts b/by-language/typescript/tests/data-structures/heap.test.ts index 9010e56..2555417 100644 --- a/by-language/typescript/tests/data-structures/heap.test.ts +++ b/by-language/typescript/tests/data-structures/heap.test.ts @@ -241,6 +241,28 @@ describe("MaxHeap", () => { expect(heap.size).toBe(3); // original not modified }); }); + + describe("toArray", () => { + test("returns the heap elements", () => { + const heap = new MaxHeap(); + heap.push(1); + heap.push(5); + heap.push(3); + expect(heap.toArray().sort((a, b) => a - b)).toEqual([1, 3, 5]); + expect(heap.size).toBe(3); + }); + }); + + describe("clear", () => { + test("removes every element", () => { + const heap = new MaxHeap(); + heap.push(1); + heap.push(4); + heap.clear(); + expect(heap.size).toBe(0); + expect(heap.isEmpty).toBe(true); + }); + }); }); describe("PriorityQueue", () => { diff --git a/by-language/typescript/tests/randomized/model.test.ts b/by-language/typescript/tests/randomized/model.test.ts new file mode 100644 index 0000000..9743708 --- /dev/null +++ b/by-language/typescript/tests/randomized/model.test.ts @@ -0,0 +1,616 @@ +import { describe, expect, test } from "bun:test"; +import { + binarySearch, + exponentialSearch, + interpolationSearch, + jumpSearch, + lowerBound, + ternarySearch, + upperBound, +} from "../../src/algorithms/searching"; +import { + bubbleSort, + bucketSort, + countingSort, + heapSort, + insertionSort, + mergeSort, + quickSort, + radixSort, + selectionSort, + shellSort, +} from "../../src/algorithms/sorting"; +import { + buildSuffixArray, + kmpSearch, + naiveSearch, + rabinKarp, + zAlgorithm, +} from "../../src/algorithms/string-algorithms"; +import { AVLTree } from "../../src/data-structures/avl-tree"; +import { BTree } from "../../src/data-structures/b-tree"; +import { BinarySearchTree } from "../../src/data-structures/binary-search-tree"; +import { FenwickTree } from "../../src/data-structures/fenwick-tree"; +import { + HashTableChaining, + HashTableOpenAddressing, +} from "../../src/data-structures/hash-table"; +import { MaxHeap, MinHeap } from "../../src/data-structures/heap"; +import { LFUCache } from "../../src/data-structures/lfu-cache"; +import { LRUCache } from "../../src/data-structures/lru-cache"; +import { RedBlackTree } from "../../src/data-structures/red-black-tree"; +import { + SegmentTree, + SegmentTreeLazy, +} from "../../src/data-structures/segment-tree"; +import { SkipList } from "../../src/data-structures/skip-list"; +import { Trie } from "../../src/data-structures/trie"; +import { UnionFind } from "../../src/data-structures/union-find"; +import { mulberry32, randInt } from "./prng"; + +const SEED = 0x5a17d5a; + +type Rng = () => number; + +function expectEqual(actual: unknown, expected: unknown, detail: string): void { + const left = JSON.stringify(actual); + const right = JSON.stringify(expected); + if (left !== right) { + throw new Error(`${detail}\n actual: ${left}\n expected: ${right}`); + } +} + +function numericSorted(values: number[]): number[] { + return [...values].sort((a, b) => a - b); +} + +function sumRange(data: number[], lo: number, hi: number): number { + let total = 0; + for (let i = lo; i <= hi; i++) total += data[i]; + return total; +} + +function bruteSearch(text: string, pattern: string): number[] { + if (pattern.length === 0 || text.length === 0 || pattern.length > text.length) { + return []; + } + const hits: number[] = []; + for (let i = 0; i <= text.length - pattern.length; i++) { + let match = true; + for (let j = 0; j < pattern.length; j++) { + if (text[i + j] !== pattern[j]) { + match = false; + break; + } + } + if (match) hits.push(i); + } + return hits; +} + +function naiveSuffixArray(text: string): number[] { + const order = Array.from({ length: text.length }, (_, i) => i); + order.sort((a, b) => { + const left = text.slice(a); + const right = text.slice(b); + if (left < right) return -1; + if (left > right) return 1; + return 0; + }); + return order; +} + +function linearIndex(arr: number[], target: number): number { + for (let i = 0; i < arr.length; i++) { + if (arr[i] === target) return i; + } + return -1; +} + +function linearLower(arr: number[], target: number): number { + for (let i = 0; i < arr.length; i++) { + if (arr[i] >= target) return i; + } + return arr.length; +} + +function linearUpper(arr: number[], target: number): number { + for (let i = 0; i < arr.length; i++) { + if (arr[i] > target) return i; + } + return arr.length; +} + +function expectHit( + arr: number[], + target: number, + index: number, + detail: string, +): void { + const found = linearIndex(arr, target); + if (found === -1) { + expectEqual(index, -1, detail); + return; + } + if (index < 0 || index >= arr.length || arr[index] !== target) { + throw new Error( + `${detail}\n index ${index} is not a match for ${target} in ${JSON.stringify(arr)}`, + ); + } +} + +function randomArray(next: Rng, length: number, lo: number, hi: number): number[] { + const values: number[] = []; + for (let i = 0; i < length; i++) values.push(randInt(next, lo, hi)); + return values; +} + +function randomSafe(next: Rng): number { + const sign = next() < 0.5 ? -1 : 1; + const hi = Math.floor(next() * 0x200000); + const lo = Math.floor(next() * 0x100000000); + return sign * (hi * 0x100000000 + lo); +} + +function randomWord(next: Rng, alphabet: string, maxLen: number): string { + const length = randInt(next, 0, maxLen); + let word = ""; + for (let i = 0; i < length; i++) { + word += alphabet[randInt(next, 0, alphabet.length - 1)]; + } + return word; +} + +describe("randomized models", () => { + test("sorts match numeric sort", () => { + const next = mulberry32(SEED); + const sorts = [ + bubbleSort, + selectionSort, + insertionSort, + mergeSort, + quickSort, + heapSort, + shellSort, + countingSort, + radixSort, + bucketSort, + ]; + + for (let i = 0; i < 24; i++) { + const input = randomArray(next, randInt(next, 0, 36), -40, 40); + const expected = numericSorted(input); + for (const sort of sorts) { + expectEqual(sort(input), expected, `seed=${SEED} iteration=${i} sort input=${JSON.stringify(input)}`); + const copy = [...input]; + sort(input); + expectEqual(input, copy, `seed=${SEED} iteration=${i} mutated input`); + } + } + + for (let i = 0; i < 8; i++) { + const input = [randomSafe(next), randomSafe(next), randomSafe(next), randomSafe(next)]; + if (next() < 0.5) input.push(0, -1, 1); + const expected = numericSorted(input); + expectEqual( + countingSort(input), + expected, + `seed=${SEED} iteration=${i} counting wide ${JSON.stringify(input)}`, + ); + expectEqual( + radixSort(input), + expected, + `seed=${SEED} iteration=${i} radix wide ${JSON.stringify(input)}`, + ); + } + }); + + test("searches match a linear scan", () => { + const next = mulberry32(SEED + 1); + for (let i = 0; i < 20; i++) { + const arr = numericSorted(randomArray(next, randInt(next, 0, 40), -25, 25)); + for (let t = 0; t < 6; t++) { + const target = next() < 0.7 && arr.length > 0 + ? arr[randInt(next, 0, arr.length - 1)] + : randInt(next, -30, 30); + const detail = `seed=${SEED} iteration=${i} target=${target} arr=${JSON.stringify(arr)}`; + expectEqual(lowerBound(arr, target), linearLower(arr, target), `lowerBound ${detail}`); + expectEqual(upperBound(arr, target), linearUpper(arr, target), `upperBound ${detail}`); + expectEqual(jumpSearch(arr, target), linearIndex(arr, target), `jumpSearch ${detail}`); + expectHit(arr, target, binarySearch(arr, target), `binarySearch ${detail}`); + expectHit(arr, target, interpolationSearch(arr, target), `interpolationSearch ${detail}`); + expectHit(arr, target, exponentialSearch(arr, target), `exponentialSearch ${detail}`); + expectHit(arr, target, ternarySearch(arr, target), `ternarySearch ${detail}`); + } + } + }); + + test("string search and suffix array match brute force", () => { + const next = mulberry32(SEED + 2); + const alphabet = "abca"; + for (let i = 0; i < 20; i++) { + const text = randomWord(next, alphabet, 28); + const pattern = randomWord(next, "abc", 6); + const expected = bruteSearch(text, pattern); + const detail = `seed=${SEED} iteration=${i} text=${JSON.stringify(text)} pattern=${JSON.stringify(pattern)}`; + expectEqual(naiveSearch(text, pattern), expected, `naive ${detail}`); + expectEqual(kmpSearch(text, pattern), expected, `kmp ${detail}`); + expectEqual(rabinKarp(text, pattern), expected, `rabin ${detail}`); + expectEqual(zAlgorithm(text, pattern), expected, `z ${detail}`); + } + + for (let i = 0; i < 12; i++) { + const text = randomWord(next, alphabet, 18); + expectEqual( + buildSuffixArray(text), + naiveSuffixArray(text), + `seed=${SEED} iteration=${i} suffix ${JSON.stringify(text)}`, + ); + } + }); + + test("ordered trees match a sorted key set", () => { + const next = mulberry32(SEED + 3); + for (let i = 0; i < 16; i++) { + const bst = new BinarySearchTree(); + const avl = new AVLTree(); + const rb = new RedBlackTree(); + const btree = new BTree(randInt(next, 2, 4)); + const skip = new SkipList(); + const keys = new Set(); + + const skipRandom = mulberry32(SEED + 300 + i); + const originalRandom = Math.random; + Math.random = skipRandom; + const ops = randInt(next, 20, 40); + try { + for (let step = 0; step < ops; step++) { + const key = randInt(next, 0, 24); + const detail = `seed=${SEED} iteration=${i} step=${step} key=${key}`; + if (next() < 0.65) { + const before = bst.size; + const node = bst.insert(key); + avl.insert(key); + rb.insert(key); + btree.insert(key); + skip.insert(key); + if (keys.has(key)) { + if (bst.search(key) !== node) { + throw new Error(`bst duplicate did not return the existing node ${detail}`); + } + expectEqual(bst.size, before, `bst duplicate size ${detail}`); + } + keys.add(key); + expect(node.value).toBe(key); + } else { + const removed = keys.delete(key); + expectEqual(bst.delete(key), removed, `bst delete ${detail}`); + expectEqual(avl.delete(key), removed, `avl delete ${detail}`); + expectEqual(rb.delete(key), removed, `rb delete ${detail}`); + expectEqual(btree.delete(key), removed, `btree delete ${detail}`); + expectEqual(skip.delete(key), removed, `skip delete ${detail}`); + } + + const sorted = [...keys].sort((a, b) => a - b); + expectEqual(bst.inOrder(), sorted, `bst inorder ${detail}`); + expectEqual(bst.size, sorted.length, `bst size ${detail}`); + expectEqual(bst.isValid(), true, `bst valid ${detail}`); + expectEqual(avl.inOrder(), sorted, `avl inorder ${detail}`); + expectEqual(avl.size, sorted.length, `avl size ${detail}`); + expectEqual(avl.isBalanced(), true, `avl balanced ${detail}`); + expectEqual(rb.inOrder(), sorted, `rb inorder ${detail}`); + expectEqual(rb.size, sorted.length, `rb size ${detail}`); + expectEqual(rb.isValid(), true, `rb valid ${detail}`); + expectEqual(btree.traverse(), sorted, `btree ${detail}`); + expectEqual(btree.size, sorted.length, `btree size ${detail}`); + expectEqual(skip.toArray(), sorted, `skip ${detail}`); + expectEqual(skip.size, sorted.length, `skip size ${detail}`); + expectEqual(bst.contains(key), keys.has(key), `bst contains ${detail}`); + expectEqual(avl.search(key), keys.has(key), `avl search ${detail}`); + expectEqual(rb.contains(key), keys.has(key), `rb contains ${detail}`); + expectEqual(btree.search(key), keys.has(key), `btree search ${detail}`); + expectEqual(skip.search(key), keys.has(key), `skip search ${detail}`); + + const min = sorted.length === 0 ? null : sorted[0]; + const max = sorted.length === 0 ? null : sorted[sorted.length - 1]; + expectEqual(bst.min(), min, `bst min ${detail}`); + expectEqual(bst.max(), max, `bst max ${detail}`); + expectEqual(btree.min(), min, `btree min ${detail}`); + expectEqual(btree.max(), max, `btree max ${detail}`); + + const probe = randInt(next, -1, 26); + let floor: number | null = null; + let ceiling: number | null = null; + for (const value of sorted) { + if (value <= probe) floor = value; + if (ceiling === null && value >= probe) ceiling = value; + } + expectEqual(bst.floor(probe), floor, `bst floor ${detail}`); + expectEqual(bst.ceiling(probe), ceiling, `bst ceiling ${detail}`); + expectEqual(skip.floor(probe), floor, `skip floor ${detail}`); + expectEqual(skip.ceiling(probe), ceiling, `skip ceiling ${detail}`); + const lo = randInt(next, -1, 12); + const hi = randInt(next, lo, 26); + expectEqual( + bst.rangeQuery(lo, hi), + sorted.filter((value) => value >= lo && value <= hi), + `bst range ${detail}`, + ); + expectEqual(skip.range(lo, hi), sorted.filter((value) => value >= lo && value <= hi), `skip range ${detail}`); + const k = randInt(next, 0, sorted.length + 1); + const kth = k < 1 || k > sorted.length ? null : sorted[k - 1]; + expectEqual(bst.kthSmallest(k), kth, `bst kth ${detail}`); + } + } finally { + Math.random = originalRandom; + } + } + }); + + test("hash tables match Map", () => { + const next = mulberry32(SEED + 4); + for (let i = 0; i < 12; i++) { + const chain = new HashTableChaining(2); + const open = new HashTableOpenAddressing(2); + const model = new Map(); + for (let step = 0; step < 40; step++) { + const key = randInt(next, -15, 20); + const detail = `seed=${SEED} iteration=${i} step=${step} key=${key} map=${JSON.stringify([...model])}`; + const roll = next(); + if (roll < 0.6) { + const value = randInt(next, -50, 50); + chain.put(key, value); + open.put(key, value); + model.set(key, value); + } else if (roll < 0.85) { + chain.delete(key); + open.delete(key); + model.delete(key); + } + expectEqual(chain.get(key), model.get(key), `chain get ${detail}`); + expectEqual(open.get(key), model.get(key), `open get ${detail}`); + expectEqual(chain.has(key), model.has(key), `chain has ${detail}`); + expectEqual(open.has(key), model.has(key), `open has ${detail}`); + expectEqual(chain.size, model.size, `chain size ${detail}`); + expectEqual(open.size, model.size, `open size ${detail}`); + for (const [storedKey, storedValue] of model) { + expectEqual(chain.get(storedKey), storedValue, `chain scan ${detail}`); + expectEqual(open.get(storedKey), storedValue, `open scan ${detail}`); + } + } + } + }); + + test("heaps match sort", () => { + const next = mulberry32(SEED + 5); + for (let i = 0; i < 12; i++) { + const values = randomArray(next, randInt(next, 0, 30), -40, 40); + const min = new MinHeap(); + const max = new MaxHeap(); + for (const value of values) { + min.push(value); + max.push(value); + } + const ascending = numericSorted(values); + const descending = [...ascending].reverse(); + const detail = `seed=${SEED} iteration=${i} values=${JSON.stringify(values)}`; + expectEqual(min.toSortedArray(), ascending, `min sorted ${detail}`); + expectEqual(max.toSortedArray(), descending, `max sorted ${detail}`); + expectEqual([...min.toArray()].sort((a, b) => a - b), ascending, `min bag ${detail}`); + expectEqual([...max.toArray()].sort((a, b) => a - b), ascending, `max bag ${detail}`); + const minPop: number[] = []; + const maxPop: number[] = []; + while (!min.isEmpty) minPop.push(min.pop()); + while (!max.isEmpty) maxPop.push(max.pop()); + expectEqual(minPop, ascending, `min pop ${detail}`); + expectEqual(maxPop, descending, `max pop ${detail}`); + } + }); + + test("LRU and LFU match reference caches", () => { + const next = mulberry32(SEED + 6); + for (let i = 0; i < 10; i++) { + const capacity = randInt(next, 1, 4); + const lru = new LRUCache(capacity); + const lfu = new LFUCache(capacity); + const lruOrder: number[] = []; + const lruValues = new Map(); + const lfuEntries = new Map(); + let stamp = 0; + + const lruKeys = (): number[] => [...lruOrder].reverse(); + const evictLfu = (): void => { + let victim: number | null = null; + for (const [key, entry] of lfuEntries) { + if (victim === null) { + victim = key; + continue; + } + const best = lfuEntries.get(victim)!; + if (entry.freq < best.freq || (entry.freq === best.freq && entry.stamp < best.stamp)) { + victim = key; + } + } + if (victim !== null) lfuEntries.delete(victim); + }; + + for (let step = 0; step < 36; step++) { + const key = randInt(next, 0, 8); + const detail = `seed=${SEED} iteration=${i} step=${step} cap=${capacity} key=${key}`; + const roll = next(); + if (roll < 0.55) { + const value = randInt(next, 0, 20); + lru.put(key, value); + const existing = lruOrder.indexOf(key); + if (existing >= 0) { + lruOrder.splice(existing, 1); + } else if (lruOrder.length >= capacity) { + const evicted = lruOrder.shift(); + if (evicted !== undefined) lruValues.delete(evicted); + } + lruOrder.push(key); + lruValues.set(key, value); + + const hit = lfuEntries.get(key); + if (hit) { + hit.value = value; + hit.freq++; + hit.stamp = ++stamp; + } else { + if (lfuEntries.size >= capacity) evictLfu(); + lfuEntries.set(key, { value, freq: 1, stamp: ++stamp }); + } + lfu.put(key, value); + } else if (roll < 0.85) { + const fromLru = lruValues.has(key) ? lruValues.get(key) : undefined; + if (fromLru !== undefined) { + const existing = lruOrder.indexOf(key); + lruOrder.splice(existing, 1); + lruOrder.push(key); + } + expectEqual(lru.get(key), fromLru, `lru get ${detail}`); + + const hit = lfuEntries.get(key); + if (hit) { + hit.freq++; + hit.stamp = ++stamp; + } + expectEqual(lfu.get(key), hit ? hit.value : undefined, `lfu get ${detail}`); + } else { + const had = lruValues.delete(key); + const existing = lruOrder.indexOf(key); + if (existing >= 0) lruOrder.splice(existing, 1); + expectEqual(lru.delete(key), had, `lru delete ${detail}`); + expectEqual(lfu.delete(key), lfuEntries.delete(key), `lfu delete ${detail}`); + } + expectEqual(lru.keys(), lruKeys(), `lru keys ${detail}`); + expectEqual(lru.size, lruValues.size, `lru size ${detail}`); + expectEqual(lfu.size, lfuEntries.size, `lfu size ${detail}`); + expectEqual(lfu.getFrequency(key), lfuEntries.get(key)?.freq ?? -1, `lfu freq ${detail}`); + } + } + }); + + test("segment trees and fenwick match naive sums", () => { + const next = mulberry32(SEED + 7); + for (let i = 0; i < 10; i++) { + const initial = randomArray(next, randInt(next, 1, 18), -10, 10); + const point = [...initial]; + const tree = new SegmentTree(initial); + const fenwickData = [...initial]; + const fenwick = FenwickTree.fromArray(initial); + const lazyData = [...initial]; + const lazy = new SegmentTreeLazy(initial); + const detailBase = `seed=${SEED} iteration=${i}`; + + for (let step = 0; step < 24; step++) { + const lo = randInt(next, 0, point.length - 1); + const hi = randInt(next, lo, point.length - 1); + if (next() < 0.5) { + const index = randInt(next, 0, point.length - 1); + const value = randInt(next, -10, 10); + point[index] = value; + tree.update(index, value); + fenwickData[index] += value; + fenwick.update(index, value); + lazy.pointUpdate(index, value); + lazyData[index] += value; + } else { + const delta = randInt(next, -5, 5); + lazy.rangeUpdate(lo, hi, delta); + for (let j = lo; j <= hi; j++) lazyData[j] += delta; + } + const detail = `${detailBase} step=${step} lo=${lo} hi=${hi}`; + expectEqual(tree.query(lo, hi), sumRange(point, lo, hi), `segment ${detail}`); + expectEqual(fenwick.rangeSum(lo, hi), sumRange(fenwickData, lo, hi), `fenwick ${detail}`); + expectEqual(fenwick.prefixSum(hi), sumRange(fenwickData, 0, hi), `fenwick prefix ${detail}`); + expectEqual(fenwick.pointQuery(lo), fenwickData[lo], `fenwick point ${detail}`); + expectEqual(lazy.query(lo, hi), sumRange(lazyData, lo, hi), `lazy ${detail}`); + } + } + }); + + test("union-find matches a parent-pointer model", () => { + const next = mulberry32(SEED + 8); + for (let i = 0; i < 10; i++) { + const n = randInt(next, 2, 18); + const uf = new UnionFind(); + const parent = new Map(); + const find = (x: number): number => { + let root = x; + while (parent.get(root) !== root) root = parent.get(root)!; + return root; + }; + const componentSize = (x: number): number => { + const root = find(x); + let count = 0; + for (const value of parent.keys()) { + if (find(value) === root) count++; + } + return count; + }; + + for (let x = 0; x < n; x++) { + expectEqual(uf.makeSet(x), !parent.has(x), `seed=${SEED} iteration=${i} make ${x}`); + parent.set(x, x); + expectEqual(uf.makeSet(x), false, `seed=${SEED} iteration=${i} remake ${x}`); + } + + for (let step = 0; step < 24; step++) { + const a = randInt(next, 0, n - 1); + const b = randInt(next, 0, n - 1); + const detail = `seed=${SEED} iteration=${i} step=${step} a=${a} b=${b}`; + const merged = find(a) !== find(b); + if (merged) parent.set(find(b), find(a)); + expectEqual(uf.union(a, b), merged, `union ${detail}`); + expectEqual(uf.connected(a, b), find(a) === find(b), `connected ${detail}`); + expectEqual(uf.componentSize(a), componentSize(a), `size ${detail}`); + expectEqual(uf.numComponents, new Set([...parent.keys()].map(find)).size, `components ${detail}`); + } + } + }); + + test("trie matches a set of words", () => { + const next = mulberry32(SEED + 9); + for (let i = 0; i < 10; i++) { + const trie = new Trie(); + const words = new Set(); + for (let step = 0; step < 28; step++) { + const word = randomWord(next, "abc", 4); + const detail = `seed=${SEED} iteration=${i} step=${step} word=${JSON.stringify(word)}`; + const roll = next(); + if (roll < 0.7) { + trie.insert(word); + words.add(word); + } else { + const removed = words.delete(word); + expectEqual(trie.delete(word), removed, `delete ${detail}`); + } + const stored = [...words].sort(); + expectEqual([...trie.getAllWords()].sort(), stored, `words ${detail}`); + expectEqual(trie.size, words.size, `size ${detail}`); + expectEqual(trie.search(word), words.has(word), `search ${detail}`); + const prefix = word.slice(0, randInt(next, 0, word.length)); + const prefixed = [...words].filter((value) => value.startsWith(prefix)); + expectEqual( + trie.startsWith(prefix), + prefix.length === 0 || prefixed.length > 0, + `startsWith ${detail} prefix=${JSON.stringify(prefix)}`, + ); + expectEqual(trie.countWordsWithPrefix(prefix), prefixed.length, `count ${detail}`); + const limit = randInt(next, 1, 4); + const completed = [...prefixed].sort((a, b) => { + if (a.length !== b.length) return a.length - b.length; + if (a < b) return -1; + if (a > b) return 1; + return 0; + }); + expectEqual( + trie.autocomplete(prefix, limit), + completed.slice(0, limit), + `autocomplete ${detail} prefix=${JSON.stringify(prefix)} limit=${limit}`, + ); + } + } + }); +}); diff --git a/by-language/typescript/tests/randomized/prng.ts b/by-language/typescript/tests/randomized/prng.ts new file mode 100644 index 0000000..b2ca75e --- /dev/null +++ b/by-language/typescript/tests/randomized/prng.ts @@ -0,0 +1,15 @@ +/** Mulberry32. `next()` returns a float in [0, 1). */ +export function mulberry32(seed: number): () => number { + let state = seed >>> 0; + return () => { + state = (state + 0x6d2b79f5) >>> 0; + let t = Math.imul(state ^ (state >>> 15), 1 | state); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +/** Inclusive integer in [lo, hi]. */ +export function randInt(next: () => number, lo: number, hi: number): number { + return lo + Math.floor(next() * (hi - lo + 1)); +} From b8529f1e38a155737122b701c9bd6f7554bc363b Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:02 +0800 Subject: [PATCH 24/26] ci: enforce lints, sanitizers, typecheck, and minimum versions CI did not enforce the checks REMEDIATION.md relies on. - C: add an AddressSanitizer and UBSan run that aborts on any report. - Rust: deny clippy warnings, check no_std and rustdoc, and check the library crates on the declared rust-version, 1.70. - Go: check gofmt and go vet, and test on 1.21 and current stable. - TypeScript: install from the frozen lockfile and type-check. - Java: use the committed Gradle wrapper instead of the runner's gradle. - Python: add 3.13 and 3.14 and fail on invalid escape sequences. --- .github/workflows/test.yml | 73 ++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8b1467b..c10bc6e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v4 @@ -26,6 +26,11 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-cov + - name: Check for invalid escape sequences + run: | + cd by-language/python + python -W error::SyntaxWarning -m compileall -q -f data_structures algorithms tests + - name: Run Python tests run: | cd by-language/python @@ -47,6 +52,13 @@ jobs: cd by-language/c make test + - name: Run C tests with AddressSanitizer and UBSan + run: | + cd by-language/c + make -B test \ + CFLAGS='-Wall -Wextra -std=c11 -g -O1 -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer -I.' \ + LDFLAGS='-fsanitize=address,undefined' + rust-tests: runs-on: ubuntu-latest @@ -66,18 +78,44 @@ jobs: - name: Run clippy run: | cd by-language/rust - cargo clippy --all-targets -- -W clippy::all + cargo clippy --workspace --all-targets --all-features -- -D warnings - name: Run tests run: | cd by-language/rust - cargo test --all-features + cargo test --workspace --all-features + + - name: Check no_std build + run: | + cd by-language/rust + cargo check --workspace --no-default-features + + - name: Check documentation + env: + RUSTDOCFLAGS: -D warnings + run: | + cd by-language/rust + cargo doc --workspace --no-deps + + rust-msrv: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Rust 1.70 (declared rust-version) + uses: dtolnay/rust-toolchain@1.70 + + - name: Check library crates + run: | + cd by-language/rust + cargo check -p dsa-core -p dsa-data-structures -p dsa-algorithms --lib go-tests: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.21', '1.22'] + go-version: ['1.21', 'stable'] steps: - uses: actions/checkout@v4 @@ -87,10 +125,26 @@ jobs: with: go-version: ${{ matrix.go-version }} + - name: Check formatting + if: matrix.go-version == 'stable' + run: | + cd by-language/go + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "Files need gofmt:" + echo "$unformatted" + exit 1 + fi + + - name: Run go vet + run: | + cd by-language/go + go vet ./... + - name: Run Go tests run: | cd by-language/go - go test ./... + go test -race ./... typescript-tests: runs-on: ubuntu-latest @@ -104,7 +158,12 @@ jobs: - name: Install dependencies run: | cd by-language/typescript - bun install + bun install --frozen-lockfile + + - name: Type-check + run: | + cd by-language/typescript + bun run typecheck - name: Run TypeScript tests run: | @@ -132,4 +191,4 @@ jobs: - name: Run Java tests run: | cd by-language/java - gradle test + ./gradlew test From 5a3324db4151e4c2ab051b1b55ed924e00c44e42 Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:02 +0800 Subject: [PATCH 25/26] chore(hooks): make hooks executable and check every language The hooks were committed without the executable bit, so the README's ./.githooks/install.sh failed. The commit-msg pattern rejected the ! breaking-change marker. Pre-commit ignored clippy failures; it now blocks on them and checks gofmt. Pre-push also runs Go, TypeScript, and Java when their toolchains are installed. --- .githooks/commit-msg | 6 ++++-- .githooks/install.sh | 0 .githooks/pre-commit | 20 ++++++++++++++++++-- .githooks/pre-push | 35 ++++++++++++++++++++++++++++++++++- 4 files changed, 56 insertions(+), 5 deletions(-) mode change 100644 => 100755 .githooks/commit-msg mode change 100644 => 100755 .githooks/install.sh mode change 100644 => 100755 .githooks/pre-commit mode change 100644 => 100755 .githooks/pre-push diff --git a/.githooks/commit-msg b/.githooks/commit-msg old mode 100644 new mode 100755 index 42c561c..3319956 --- a/.githooks/commit-msg +++ b/.githooks/commit-msg @@ -13,13 +13,14 @@ NC='\033[0m' # Valid prefixes (conventional commits) valid_prefixes="feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert" -# Pattern: prefix(optional-scope): description -pattern="^($valid_prefixes)(\(.+\))?: .+" +# Pattern: prefix(optional-scope)(optional !): description +pattern="^($valid_prefixes)(\([^)]+\))?!?: .+" if ! echo "$commit_msg" | head -1 | grep -qE "$pattern"; then echo -e "${RED}Invalid commit message format!${NC}" echo "" echo "Commit message must follow: (): " + echo "Add ! before the colon for a breaking change: ()!: " echo "" echo -e "${YELLOW}Valid prefixes:${NC}" echo " feat: New feature" @@ -39,6 +40,7 @@ if ! echo "$commit_msg" | head -1 | grep -qE "$pattern"; then echo " fix(rust): resolve memory leak in linked list" echo " docs: update README with examples" echo " test(python): add edge case tests for sorting" + echo " feat(c)!: require path capacity in shortest_path_reconstruct" echo "" echo -e "Your message: ${RED}$commit_msg${NC}" exit 1 diff --git a/.githooks/install.sh b/.githooks/install.sh old mode 100644 new mode 100755 diff --git a/.githooks/pre-commit b/.githooks/pre-commit old mode 100644 new mode 100755 index c7d8833..1849bd0 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -25,10 +25,26 @@ if command -v cargo &> /dev/null; then fi echo -n "Running Rust clippy... " - if (cd by-language/rust && cargo clippy --all-targets -- -D warnings) &> /dev/null 2>&1; then + if (cd by-language/rust && cargo clippy --workspace --all-targets --all-features -- -D warnings) &> /dev/null; then echo -e "${GREEN}OK${NC}" else - echo -e "${RED}WARNINGS${NC} (continuing)" + echo -e "${RED}FAILED${NC}" + echo "Run 'cd by-language/rust && cargo clippy --workspace --all-targets --all-features -- -D warnings' for details" + FAILED=1 + fi +fi + +# Go checks +if command -v gofmt &> /dev/null; then + echo -n "Checking Go formatting... " + unformatted=$(cd by-language/go && gofmt -l .) + if [ -z "$unformatted" ]; then + echo -e "${GREEN}OK${NC}" + else + echo -e "${RED}FAILED${NC}" + echo "$unformatted" + echo "Run 'cd by-language/go && gofmt -w .' to fix" + FAILED=1 fi fi diff --git a/.githooks/pre-push b/.githooks/pre-push old mode 100644 new mode 100755 index 6dd2e8c..c91a3c5 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -16,7 +16,7 @@ FAILED=0 # Rust tests if command -v cargo &> /dev/null; then echo -e "\n${YELLOW}Running Rust tests...${NC}" - if (cd by-language/rust && cargo test --all-features); then + if (cd by-language/rust && cargo test --workspace --all-features); then echo -e "${GREEN}Rust tests passed!${NC}" else echo -e "${RED}Rust tests failed!${NC}" @@ -46,6 +46,39 @@ if command -v make &> /dev/null && [ -f "by-language/c/Makefile" ]; then fi fi +# Go tests +if command -v go &> /dev/null; then + echo -e "\n${YELLOW}Running Go tests...${NC}" + if (cd by-language/go && go vet ./... && go test ./...); then + echo -e "${GREEN}Go tests passed!${NC}" + else + echo -e "${RED}Go tests failed!${NC}" + FAILED=1 + fi +fi + +# TypeScript tests +if command -v bun &> /dev/null; then + echo -e "\n${YELLOW}Running TypeScript tests...${NC}" + if (cd by-language/typescript && bun install --frozen-lockfile && bun run typecheck && bun test); then + echo -e "${GREEN}TypeScript tests passed!${NC}" + else + echo -e "${RED}TypeScript tests failed!${NC}" + FAILED=1 + fi +fi + +# Java tests (macOS ships a java stub without a runtime, so probe it) +if java -version &> /dev/null; then + echo -e "\n${YELLOW}Running Java tests...${NC}" + if (cd by-language/java && ./gradlew test); then + echo -e "${GREEN}Java tests passed!${NC}" + else + echo -e "${RED}Java tests failed!${NC}" + FAILED=1 + fi +fi + if [ $FAILED -eq 1 ]; then echo -e "\n${RED}Pre-push tests failed. Push aborted.${NC}" exit 1 From c2c02a20d9fa34f745013d5acdacfa00560e073f Mon Sep 17 00:00:00 2001 From: Zenco <64717583+notzenco@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:34:02 +0800 Subject: [PATCH 26/26] docs: align README, topic pages, and remediation notes The README feature table claimed Rust has a min/max stack, circular queue, adjacency matrix, and Tarjan's SCC, and that TypeScript has two-pointer helpers; none of these exist. List Kosaraju's SCC and the Go-only modules, and say which languages carry diagrams and LeetCode mappings. The Rust README drops types it does not have and marks the trees without removal. Topic pages now link all six languages. REMEDIATION.md records the new sort, BST, LFU, B-tree, and Go search contracts and the full verification commands. --- README.md | 43 +++++++++++++++++++------- REMEDIATION.md | 42 ++++++++++++++++++------- by-language/rust/README.md | 31 ++++++++++++------- by-topic/arrays/README.md | 6 ++-- by-topic/dynamic-programming/README.md | 6 ++-- by-topic/graphs/README.md | 10 +++--- by-topic/hash-tables/README.md | 14 ++++----- by-topic/heaps/README.md | 6 ++-- by-topic/linked-lists/README.md | 8 ++--- by-topic/queues/README.md | 8 ++--- by-topic/searching/README.md | 6 ++-- by-topic/sorting/README.md | 15 ++++++--- by-topic/stacks/README.md | 6 ++-- by-topic/strings/README.md | 8 ++--- by-topic/trees/README.md | 18 +++++------ 15 files changed, 140 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 572e219..a2b7aae 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,14 @@ A comprehensive collection of data structures and algorithms implemented in **C**, **Go**, **Java**, **Python**, **Rust**, and **TypeScript**. -Each implementation includes: -- Detailed ASCII visual diagrams -- Time & space complexity analysis -- LeetCode problem mappings -- Comprehensive unit tests +Every language has: +- Unit tests for each data structure and algorithm +- Seeded randomized tests that compare implementations against simple reference models +- Time & space complexity notes + +Most C, Python, and Rust sources also include ASCII diagrams and LeetCode problem +mappings in their header comments. The Go, Java, and TypeScript sources use shorter +doc comments. ## Language Requirements @@ -58,8 +61,8 @@ dsa/ | Stack | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Queue | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Deque | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Circular Queue | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Min/Max Stack | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Circular Queue | ✓ | ✓ | ✓ | ✓ | - | ✓ | +| Min/Max Stack | ✓ | ✓ | ✓ | ✓ | - | ✓ | | Monotonic Queue | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ### Trees @@ -89,7 +92,7 @@ dsa/ | Structure | C | Go | Java | Python | Rust | TypeScript | |-----------|:-:|:--:|:----:|:------:|:----:|:----------:| | Adjacency List | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Adjacency Matrix | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Adjacency Matrix | ✓ | ✓ | ✓ | ✓ | - | ✓ | | Union-Find | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ### Caches @@ -122,7 +125,7 @@ dsa/ | Algorithm | C | Go | Java | Python | Rust | TypeScript | |-----------|:-:|:--:|:----:|:------:|:----:|:----------:| | Binary Search | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Two Pointers | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Two Pointers | ✓ | ✓ | ✓ | ✓ | ✓ | - | | Sliding Window | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ### Graph @@ -136,7 +139,8 @@ dsa/ | Prim's MST | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Kruskal's MST | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Topological Sort | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Tarjan's SCC | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Tarjan's SCC | ✓ | ✓ | ✓ | ✓ | - | ✓ | +| Kosaraju's SCC | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ### Dynamic Programming | Problem | C | Go | Java | Python | Rust | TypeScript | @@ -155,6 +159,16 @@ dsa/ | Rabin-Karp | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Z-Algorithm | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +### Go-only modules +The Go implementation also has algorithm modules that the other languages do not +have yet: + +| Module | Contents | +|--------|----------| +| Backtracking | Permutations, subsets, combinations, combination sum, N-Queens, Sudoku, word search, generate parentheses | +| Greedy | Activity selection, interval problems, fractional knapsack, job sequencing, Huffman encoding, jump game, gas station, task scheduler | +| Math | GCD/LCM, primality and sieve, prime factors, fast power, Euler's totient, modular inverse, binomial and Catalan numbers, bit tricks | + ## Quick Start ### C @@ -192,6 +206,7 @@ cargo test ```bash cd by-language/typescript bun install +bun run typecheck bun test ``` @@ -205,13 +220,17 @@ APIs should update their arguments before using the revised implementations. ### Git Hooks (Optional) ```bash -# Install pre-commit and pre-push hooks +# Install pre-commit, pre-push, and commit-msg hooks ./.githooks/install.sh ``` +The pre-commit hook checks Rust formatting and Clippy, Go formatting, and Python +lint when `ruff` or `black` is installed. The pre-push hook runs the suite for +each language whose toolchain is installed. + ## Implementation Template -Each implementation follows a consistent format: +The C, Python, and Rust header comments follow this format: ``` /** diff --git a/REMEDIATION.md b/REMEDIATION.md index d20e74d..bf55b6e 100644 --- a/REMEDIATION.md +++ b/REMEDIATION.md @@ -16,7 +16,15 @@ possible input or deployment has been audited. | Python segment tree | Point updates outside `[0, len)` raise `IndexError`. | | Java adjacency matrix | Positive infinity is reserved for a missing edge and is rejected as an edge weight. | | Trie insertion | Reinserting an existing word does not increase unique size or prefix counts. | -| Python maps and trees | A stored `None` value is distinct from an absent key for membership and deletion. | +| Python maps and trees | A stored `None` value is distinct from an absent key for membership and deletion. `AVLTree.insert(key, None)` stores `None`; omitting the value still stores the key. | +| Python LFU cache | `data_structures.lru_cache.LFUCache` is now a re-export of `data_structures.lfu_cache.LFUCache`, so `put()` returns the evicted `(key, value)` pair instead of `None`. `min_frequency()` stays correct after deleting the last entry at the minimum frequency; that delete is O(n) in the worst case. | +| Binary search trees | Keys are unique in every language. Inserting a value that is already present leaves the size and shape unchanged: Python, Java, TypeScript, and Go return the existing node, Rust returns `false`, and C (a key-to-value map) replaces the stored value. Python, Java, and TypeScript previously stored duplicates, which made their own validity checks fail. The sorted-input builders (Python `from_sorted_list`, Java `fromSortedList`, TypeScript `fromSortedArray`, Go `BSTFromSortedSlice`) keep repeated values once. | +| Radix sort | C, Go, Java, and TypeScript accept negative values and the full range of their integer type; digits come from each value's distance to the minimum. Python already handled negatives, and Rust takes `u32`. Before this change, negative input overflowed a stack buffer in C, panicked in Go, threw in Java, and returned wrong output in TypeScript. | +| Counting sort | C, Go, Java, Rust, and TypeScript accept the full range of their integer type and fall back to a comparison sort when the value range is more than 16 times the input length, instead of allocating the whole range. Java previously threw on negative values, and Go and Rust could overflow while computing the range. Python's version allocates the whole range, as its docstring states. | +| Go B-tree | Inserting a key that already lives in an internal node updates its value. Previously the duplicate check looked at the wrong slot, so the key was inserted a second time. | +| Go interpolation search | `InterpolationSearch` accepts the full `int` range and repeated endpoint values. The probe is computed from unsigned distances instead of a `float64` ratio that could overflow or divide by zero. | +| Go string search | `KMPSearch`, `RabinKarpSearch`, and `ZAlgorithmSearch` all return no matches for an empty pattern. `RabinKarpSearch` previously returned `[0]` and `ZAlgorithmSearch` returned every index except the last. | +| TypeScript sorts | `countingSort` and `radixSort` throw `RangeError` for values that are not safe integers instead of returning a wrongly ordered array. | ## Regression areas @@ -29,23 +37,35 @@ possible input or deployment has been audited. palindrome sentinels. - Data structures: lazy segment-tree logical length, skip-list live length, Fenwick index validation, LFU frequency buckets, and panic-safe LRU clear. +- Sorting: negative values, all-negative input, and integer-type extremes for + every sort, including the counting-sort comparison fallback. +- Randomized model tests in every language compare sorts, searches, string + matching, ordered trees, caches, and range trees against simple reference + implementations. Each uses a fixed seed and reports the seed, iteration, + and input on failure. Go also has native fuzz targets whose seed corpus runs + with `go test`. ## Verification From each language directory, run the project suite: ```bash -(cd by-language/c && make -B test CFLAGS='-Wall -Wextra -std=c11 -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer -I.' LDFLAGS='-fsanitize=address,undefined') -(cd by-language/go && go test ./... && go vet ./...) +(cd by-language/c && make -B test CFLAGS='-Wall -Wextra -std=c11 -g -O1 -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer -I.' LDFLAGS='-fsanitize=address,undefined') +(cd by-language/go && test -z "$(gofmt -l .)" && go vet ./... && go test -race ./...) (cd by-language/java && ./gradlew test) -(cd by-language/python && python3 -m pytest -q -o addopts='') -(cd by-language/rust && cargo test --offline && cargo fmt --all -- --check && cargo check --offline --workspace --no-default-features) -(cd by-language/typescript && bun test) +(cd by-language/python && python3 -W error::SyntaxWarning -m compileall -q -f data_structures algorithms tests && python3 -m pytest -q -o addopts='') +(cd by-language/rust && cargo test --offline && cargo fmt --all -- --check && cargo clippy --offline --workspace --all-targets --all-features -- -D warnings && cargo check --offline --workspace --no-default-features && RUSTDOCFLAGS='-D warnings' cargo doc --offline --workspace --no-deps) +(cd by-language/typescript && bun install --frozen-lockfile && bun run typecheck && bun test) ``` -The TypeScript project also has a strict `tsconfig.json`; run a local TypeScript -compiler with `--noEmit -p by-language/typescript/tsconfig.json` when one is -available. It requires the project's `bun-types` type declarations. +CI runs the same checks, plus the Rust library crates on the declared minimum +version (`cargo +1.70.0 check -p dsa-core -p dsa-data-structures -p dsa-algorithms --lib`) +and Go on both 1.21 and the current stable release. -The C sanitizer command rebuilds objects with sanitizer flags. Run `make -B` -again when switching back to ordinary compiler or linker flags. +The Python suite treats warnings as errors (`filterwarnings = ["error"]` in +`pyproject.toml`), so an invalid escape sequence in a docstring fails it. + +The C Makefile tracks header dependencies, so editing a header rebuilds the +objects that include it. Changing `CFLAGS` or `LDFLAGS` is not tracked: the +sanitizer command uses `make -B`, and switching back to ordinary flags needs +`make -B` again. diff --git a/by-language/rust/README.md b/by-language/rust/README.md index f196d76..fd4f9ea 100644 --- a/by-language/rust/README.md +++ b/by-language/rust/README.md @@ -17,7 +17,7 @@ rust/ └── dsa-algorithms/ # All algorithm implementations ``` -## Data Structures (24) +## Data Structures ### Linear Structures - `DynamicArray` - Resizable array @@ -29,10 +29,10 @@ rust/ - `MonotonicQueue` - Sliding window min/max ### Trees -- `BinarySearchTree` - Basic BST +- `BinarySearchTree` - Basic BST with unique keys - `AVLTree` - Self-balancing tree -- `RedBlackTree` - Color-balanced tree -- `BTree` - Disk-optimized tree +- `RedBlackTree` - Color-balanced tree (no removal) +- `BTree` - Disk-optimized tree (keeps duplicate keys; no removal) - `Trie` - Prefix tree - `SegmentTree` - Range queries - `FenwickTree` - Binary indexed tree @@ -45,8 +45,7 @@ rust/ - `BinaryHeap` - Min/max heap ### Graphs -- `AdjacencyList` - Sparse graph representation -- `AdjacencyMatrix` - Dense graph representation +- `Graph` - Adjacency list (`Vec>` in `dsa_algorithms::graph`) - `UnionFind` - Disjoint set ### Caches @@ -61,7 +60,8 @@ rust/ ### Sorting - Bubble, Selection, Insertion (comparison) - Merge, Quick, Heap (efficient) -- Counting, Radix (non-comparison) +- Counting, Radix (non-comparison); counting sort falls back to + `sort_unstable` when the value range is more than 16 times the input length ### Searching - Binary Search (and variants) @@ -81,7 +81,6 @@ rust/ - LCS, LIS - Edit Distance - Coin Change -- Matrix Chain Multiplication - Word Break ### String Algorithms @@ -136,8 +135,17 @@ cargo test --doc # Check all crates without the std feature cargo check --offline --workspace --no-default-features + +# Lints and documentation, as run in CI +cargo clippy --workspace --all-targets --all-features -- -D warnings +RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps ``` +The `tests/models.rs` suites in `dsa-algorithms` and `dsa-data-structures` +compare each implementation against standard-library reference models with a +fixed seed. CI also checks the library crates on the declared minimum Rust +version, 1.70. + ## Path reconstruction `dsa_algorithms::graph::reconstruct_path` takes the predecessor array, the @@ -153,12 +161,13 @@ the earlier two-argument form must pass the source explicitly. | LinkedList | O(1) | O(1) | O(n) | O(n) | | HashTable | O(1)* | O(1)* | O(1)* | O(n) | | BST | O(log n)* | O(log n)* | O(log n)* | O(n) | -| AVL/RB Tree | O(log n) | O(log n) | O(log n) | O(n) | -| B-Tree | O(log n) | O(log n) | O(log n) | O(n) | +| AVL Tree | O(log n) | O(log n) | O(log n) | O(n) | +| Red-Black Tree | O(log n) | - | O(log n) | O(n) | +| B-Tree | O(log n) | - | O(log n) | O(n) | | Heap | O(log n) | O(log n) | O(n) | O(n) | | BloomFilter | O(k) | - | O(k) | O(m) | -*Amortized or average case +*Amortized or average case; - means the operation is not implemented ## License diff --git a/by-topic/arrays/README.md b/by-topic/arrays/README.md index 22d5c7c..23c446b 100644 --- a/by-topic/arrays/README.md +++ b/by-topic/arrays/README.md @@ -4,9 +4,9 @@ Arrays are contiguous blocks of memory that store elements of the same type, pro ## Implementations -| Structure | Python | C | -|-----------|--------|---| -| Dynamic Array | [dynamic_array.py](../../by-language/python/data_structures/dynamic_array.py) | [dynamic_array.c](../../by-language/c/data-structures/dynamic_array.c) | +| Structure | Python | C | Go | Java | Rust | TypeScript | +|-----------|--------|---|---|------|------|------------| +| Dynamic Array | [dynamic_array.py](../../by-language/python/data_structures/dynamic_array.py) | [dynamic_array.c](../../by-language/c/data-structures/dynamic_array.c) | [dynamic_array.go](../../by-language/go/datastructures/dynamic_array.go) | [DynamicArray.java](../../by-language/java/src/main/java/dsa/datastructures/DynamicArray.java) | [dynamic_array.rs](../../by-language/rust/dsa-data-structures/src/linear/dynamic_array.rs) | [dynamic-array.ts](../../by-language/typescript/src/data-structures/dynamic-array.ts) | ## Complexity diff --git a/by-topic/dynamic-programming/README.md b/by-topic/dynamic-programming/README.md index 8d725e2..a6b5e5f 100644 --- a/by-topic/dynamic-programming/README.md +++ b/by-topic/dynamic-programming/README.md @@ -4,9 +4,9 @@ Dynamic Programming (DP) solves complex problems by breaking them into overlappi ## Implementations -| Algorithm | Python | C | -|-----------|--------|---| -| DP Algorithms | [dynamic_programming.py](../../by-language/python/algorithms/dynamic_programming.py) | [dynamic_programming.c](../../by-language/c/algorithms/dynamic_programming.c) | +| Algorithm | Python | C | Go | Java | Rust | TypeScript | +|-----------|--------|---|---|------|------|------------| +| DP Algorithms | [dynamic_programming.py](../../by-language/python/algorithms/dynamic_programming.py) | [dynamic_programming.c](../../by-language/c/algorithms/dynamic_programming.c) | [dynamic_programming.go](../../by-language/go/algorithms/dynamic_programming.go) | [DynamicProgramming.java](../../by-language/java/src/main/java/dsa/algorithms/DynamicProgramming.java) | [mod.rs](../../by-language/rust/dsa-algorithms/src/dynamic_programming/mod.rs) | [dynamic-programming.ts](../../by-language/typescript/src/algorithms/dynamic-programming.ts) | ## Complexity Summary diff --git a/by-topic/graphs/README.md b/by-topic/graphs/README.md index e8a4142..97b6d10 100644 --- a/by-topic/graphs/README.md +++ b/by-topic/graphs/README.md @@ -4,11 +4,11 @@ Graphs are data structures consisting of vertices (nodes) connected by edges, us ## Implementations -| Structure/Algorithm | Python | C | -|---------------------|--------|---| -| Adjacency List | [graph.py](../../by-language/python/data_structures/graph.py) | [graph_core.c](../../by-language/c/data-structures/graph_core.c) | -| Adjacency Matrix | [adjacency_matrix.py](../../by-language/python/data_structures/adjacency_matrix.py) | [adjacency_matrix.c](../../by-language/c/data-structures/adjacency_matrix.c) | -| Union-Find | [union_find.py](../../by-language/python/data_structures/union_find.py) | [union_find.c](../../by-language/c/data-structures/union_find.c) | +| Structure/Algorithm | Python | C | Go | Java | Rust | TypeScript | +|---------------------|--------|---|---|------|------|------------| +| Adjacency List | [graph.py](../../by-language/python/data_structures/graph.py) | [graph_core.c](../../by-language/c/data-structures/graph_core.c) | [graph_core.go](../../by-language/go/datastructures/graph_core.go) | [GraphCore.java](../../by-language/java/src/main/java/dsa/datastructures/GraphCore.java) | [mod.rs](../../by-language/rust/dsa-algorithms/src/graph/mod.rs) | [graph-core.ts](../../by-language/typescript/src/data-structures/graph-core.ts) | +| Adjacency Matrix | [adjacency_matrix.py](../../by-language/python/data_structures/adjacency_matrix.py) | [adjacency_matrix.c](../../by-language/c/data-structures/adjacency_matrix.c) | [adjacency_matrix.go](../../by-language/go/datastructures/adjacency_matrix.go) | [AdjacencyMatrix.java](../../by-language/java/src/main/java/dsa/datastructures/AdjacencyMatrix.java) | - | [adjacency-matrix.ts](../../by-language/typescript/src/data-structures/adjacency-matrix.ts) | +| Union-Find | [union_find.py](../../by-language/python/data_structures/union_find.py) | [union_find.c](../../by-language/c/data-structures/union_find.c) | [union_find.go](../../by-language/go/datastructures/union_find.go) | [UnionFind.java](../../by-language/java/src/main/java/dsa/datastructures/UnionFind.java) | [union_find.rs](../../by-language/rust/dsa-data-structures/src/graphs/union_find.rs) | [union-find.ts](../../by-language/typescript/src/data-structures/union-find.ts) | ## Complexity diff --git a/by-topic/hash-tables/README.md b/by-topic/hash-tables/README.md index 06557df..4baaee0 100644 --- a/by-topic/hash-tables/README.md +++ b/by-topic/hash-tables/README.md @@ -4,13 +4,13 @@ Hash tables provide O(1) average-case lookup, insertion, and deletion by mapping ## Implementations -| Structure | Python | C | -|-----------|--------|---| -| Hash Table | [hash_table.py](../../by-language/python/data_structures/hash_table.py) | [hash_table.c](../../by-language/c/data-structures/hash_table.c) | -| Bloom Filter | [bloom_filter.py](../../by-language/python/data_structures/bloom_filter.py) | [bloom_filter.c](../../by-language/c/data-structures/bloom_filter.c) | -| LRU Cache | [lru_cache.py](../../by-language/python/data_structures/lru_cache.py) | [lru_cache.c](../../by-language/c/data-structures/lru_cache.c) | -| LFU Cache | [lfu_cache.py](../../by-language/python/data_structures/lfu_cache.py) | [lfu_cache.c](../../by-language/c/data-structures/lfu_cache.c) | -| TTL Cache | [lru_cache.py](../../by-language/python/data_structures/lru_cache.py) | [ttl_cache.c](../../by-language/c/data-structures/ttl_cache.c) | +| Structure | Python | C | Go | Java | Rust | TypeScript | +|-----------|--------|---|---|------|------|------------| +| Hash Table | [hash_table.py](../../by-language/python/data_structures/hash_table.py) | [hash_table.c](../../by-language/c/data-structures/hash_table.c) | [hash_table.go](../../by-language/go/datastructures/hash_table.go) | [HashTable.java](../../by-language/java/src/main/java/dsa/datastructures/HashTable.java) | [hash_table.rs](../../by-language/rust/dsa-data-structures/src/hashing/hash_table.rs) | [hash-table.ts](../../by-language/typescript/src/data-structures/hash-table.ts) | +| Bloom Filter | [bloom_filter.py](../../by-language/python/data_structures/bloom_filter.py) | [bloom_filter.c](../../by-language/c/data-structures/bloom_filter.c) | [bloom_filter.go](../../by-language/go/datastructures/bloom_filter.go) | [BloomFilter.java](../../by-language/java/src/main/java/dsa/datastructures/BloomFilter.java) | [bloom_filter.rs](../../by-language/rust/dsa-data-structures/src/hashing/bloom_filter.rs) | [bloom-filter.ts](../../by-language/typescript/src/data-structures/bloom-filter.ts) | +| LRU Cache | [lru_cache.py](../../by-language/python/data_structures/lru_cache.py) | [lru_cache.c](../../by-language/c/data-structures/lru_cache.c) | [lru_cache.go](../../by-language/go/datastructures/lru_cache.go) | [LruCache.java](../../by-language/java/src/main/java/dsa/datastructures/LruCache.java) | [lru_cache.rs](../../by-language/rust/dsa-data-structures/src/caches/lru_cache.rs) | [lru-cache.ts](../../by-language/typescript/src/data-structures/lru-cache.ts) | +| LFU Cache | [lfu_cache.py](../../by-language/python/data_structures/lfu_cache.py) | [lfu_cache.c](../../by-language/c/data-structures/lfu_cache.c) | [lfu_cache.go](../../by-language/go/datastructures/lfu_cache.go) | [LfuCache.java](../../by-language/java/src/main/java/dsa/datastructures/LfuCache.java) | [lfu_cache.rs](../../by-language/rust/dsa-data-structures/src/caches/lfu_cache.rs) | [lfu-cache.ts](../../by-language/typescript/src/data-structures/lfu-cache.ts) | +| TTL Cache | [ttl_cache.py](../../by-language/python/data_structures/ttl_cache.py) | [ttl_cache.c](../../by-language/c/data-structures/ttl_cache.c) | [ttl_cache.go](../../by-language/go/datastructures/ttl_cache.go) | [TtlCache.java](../../by-language/java/src/main/java/dsa/datastructures/TtlCache.java) | - | [ttl-cache.ts](../../by-language/typescript/src/data-structures/ttl-cache.ts) | ## Complexity diff --git a/by-topic/heaps/README.md b/by-topic/heaps/README.md index 7001d0a..94867ea 100644 --- a/by-topic/heaps/README.md +++ b/by-topic/heaps/README.md @@ -4,9 +4,9 @@ A heap is a complete binary tree that satisfies the heap property: in a max-heap ## Implementations -| Structure | Python | C | -|-----------|--------|---| -| Binary Heap | [heap.py](../../by-language/python/data_structures/heap.py) | [heap.c](../../by-language/c/data-structures/heap.c) | +| Structure | Python | C | Go | Java | Rust | TypeScript | +|-----------|--------|---|---|------|------|------------| +| Binary Heap | [heap.py](../../by-language/python/data_structures/heap.py) | [heap.c](../../by-language/c/data-structures/heap.c) | [heap.go](../../by-language/go/datastructures/heap.go) | [Heap.java](../../by-language/java/src/main/java/dsa/datastructures/Heap.java) | [binary_heap.rs](../../by-language/rust/dsa-data-structures/src/heaps/binary_heap.rs) | [heap.ts](../../by-language/typescript/src/data-structures/heap.ts) | ## Complexity diff --git a/by-topic/linked-lists/README.md b/by-topic/linked-lists/README.md index c475772..291a6dc 100644 --- a/by-topic/linked-lists/README.md +++ b/by-topic/linked-lists/README.md @@ -4,10 +4,10 @@ Linked lists are linear data structures where elements are stored in nodes, with ## Implementations -| Structure | Python | C | -|-----------|--------|---| -| Singly Linked List | [singly_linked_list.py](../../by-language/python/data_structures/singly_linked_list.py) | [singly_linked_list.c](../../by-language/c/data-structures/singly_linked_list.c) | -| Doubly Linked List | [doubly_linked_list.py](../../by-language/python/data_structures/doubly_linked_list.py) | [doubly_linked_list.c](../../by-language/c/data-structures/doubly_linked_list.c) | +| Structure | Python | C | Go | Java | Rust | TypeScript | +|-----------|--------|---|---|------|------|------------| +| Singly Linked List | [singly_linked_list.py](../../by-language/python/data_structures/singly_linked_list.py) | [singly_linked_list.c](../../by-language/c/data-structures/singly_linked_list.c) | [singly_linked_list.go](../../by-language/go/datastructures/singly_linked_list.go) | [SinglyLinkedList.java](../../by-language/java/src/main/java/dsa/datastructures/SinglyLinkedList.java) | [singly_linked_list.rs](../../by-language/rust/dsa-data-structures/src/linear/singly_linked_list.rs) | [singly-linked-list.ts](../../by-language/typescript/src/data-structures/singly-linked-list.ts) | +| Doubly Linked List | [doubly_linked_list.py](../../by-language/python/data_structures/doubly_linked_list.py) | [doubly_linked_list.c](../../by-language/c/data-structures/doubly_linked_list.c) | [doubly_linked_list.go](../../by-language/go/datastructures/doubly_linked_list.go) | [DoublyLinkedList.java](../../by-language/java/src/main/java/dsa/datastructures/DoublyLinkedList.java) | [doubly_linked_list.rs](../../by-language/rust/dsa-data-structures/src/linear/doubly_linked_list.rs) | [doubly-linked-list.ts](../../by-language/typescript/src/data-structures/doubly-linked-list.ts) | ## Complexity diff --git a/by-topic/queues/README.md b/by-topic/queues/README.md index c9537c8..22566a2 100644 --- a/by-topic/queues/README.md +++ b/by-topic/queues/README.md @@ -4,10 +4,10 @@ A queue is a First-In-First-Out (FIFO) data structure where elements are added a ## Implementations -| Structure | Python | C | -|-----------|--------|---| -| Queue | [queue.py](../../by-language/python/data_structures/queue.py) | [queue.c](../../by-language/c/data-structures/queue.c) | -| Monotonic Queue | [queue.py](../../by-language/python/data_structures/queue.py) | [monotonic_queue.c](../../by-language/c/data-structures/monotonic_queue.c) | +| Structure | Python | C | Go | Java | Rust | TypeScript | +|-----------|--------|---|---|------|------|------------| +| Queue | [queue.py](../../by-language/python/data_structures/queue.py) | [queue.c](../../by-language/c/data-structures/queue.c) | [queue.go](../../by-language/go/datastructures/queue.go) | [Queue.java](../../by-language/java/src/main/java/dsa/datastructures/Queue.java) | [queue.rs](../../by-language/rust/dsa-data-structures/src/linear/queue.rs) | [queue.ts](../../by-language/typescript/src/data-structures/queue.ts) | +| Monotonic Queue | [queue.py](../../by-language/python/data_structures/queue.py) | [monotonic_queue.c](../../by-language/c/data-structures/monotonic_queue.c) | [queue.go](../../by-language/go/datastructures/queue.go) | [MonotonicQueue.java](../../by-language/java/src/main/java/dsa/datastructures/MonotonicQueue.java) | [monotonic_queue.rs](../../by-language/rust/dsa-data-structures/src/linear/monotonic_queue.rs) | [monotonic-queue.ts](../../by-language/typescript/src/data-structures/monotonic-queue.ts) | ## Complexity diff --git a/by-topic/searching/README.md b/by-topic/searching/README.md index 8b0558d..fe9517d 100644 --- a/by-topic/searching/README.md +++ b/by-topic/searching/README.md @@ -4,9 +4,9 @@ Searching algorithms find elements or determine their absence in data structures ## Implementations -| Algorithm | Python | C | -|-----------|--------|---| -| All Searching Algorithms | [searching.py](../../by-language/python/algorithms/searching.py) | [searching.c](../../by-language/c/algorithms/searching.c) | +| Algorithm | Python | C | Go | Java | Rust | TypeScript | +|-----------|--------|---|---|------|------|------------| +| All Searching Algorithms | [searching.py](../../by-language/python/algorithms/searching.py) | [searching.c](../../by-language/c/algorithms/searching.c) | [searching.go](../../by-language/go/algorithms/searching.go) | [Searching.java](../../by-language/java/src/main/java/dsa/algorithms/Searching.java) | [mod.rs](../../by-language/rust/dsa-algorithms/src/searching/mod.rs) | [searching.ts](../../by-language/typescript/src/algorithms/searching.ts) | ## Complexity Comparison diff --git a/by-topic/sorting/README.md b/by-topic/sorting/README.md index 5d681fe..92dc045 100644 --- a/by-topic/sorting/README.md +++ b/by-topic/sorting/README.md @@ -4,9 +4,9 @@ Sorting algorithms arrange elements in a specific order (ascending or descending ## Implementations -| Algorithm | Python | C | -|-----------|--------|---| -| All Sorting Algorithms | [sorting.py](../../by-language/python/algorithms/sorting.py) | [sorting.c](../../by-language/c/algorithms/sorting.c) | +| Algorithm | Python | C | Go | Java | Rust | TypeScript | +|-----------|--------|---|---|------|------|------------| +| All Sorting Algorithms | [sorting.py](../../by-language/python/algorithms/sorting.py) | [sorting.c](../../by-language/c/algorithms/sorting.c) | [sorting.go](../../by-language/go/algorithms/sorting.go) | [Sorting.java](../../by-language/java/src/main/java/dsa/algorithms/Sorting.java) | [mod.rs](../../by-language/rust/dsa-algorithms/src/sorting/mod.rs) | [sorting.ts](../../by-language/typescript/src/algorithms/sorting.ts) | ## Complexity Comparison @@ -80,8 +80,10 @@ Build max-heap, repeatedly extract maximum. Count occurrences, calculate positions, place elements. - **Best for**: Small integer range, many duplicates - **Requirement**: Know range of values -- **C implementation**: Falls back to comparison sorting when the range is - too large for practical count storage; this case takes O(n log n). +- **Wide ranges**: The C, Go, Java, Rust, and TypeScript implementations fall + back to comparison sorting when the range is more than 16 times the input + length; this case takes O(n log n). The Python version allocates the full + range. #### Radix Sort Sort by each digit, from least to most significant. @@ -89,6 +91,9 @@ Sort by each digit, from least to most significant. - **Variants**: LSD (least significant digit first), MSD - **Integer implementations**: Stop after the highest required digit so the exponent does not overflow after the final pass. +- **Negative values**: The C, Go, Java, and TypeScript implementations take + digits from each value's distance to the minimum, so negative values sort + correctly. Python sorts negatives separately; Rust accepts only `u32`. #### Bucket Sort Distribute into buckets, sort each bucket, concatenate. diff --git a/by-topic/stacks/README.md b/by-topic/stacks/README.md index c97ed7c..76eaadb 100644 --- a/by-topic/stacks/README.md +++ b/by-topic/stacks/README.md @@ -4,9 +4,9 @@ A stack is a Last-In-First-Out (LIFO) data structure where elements are added an ## Implementations -| Structure | Python | C | -|-----------|--------|---| -| Stack | [stack.py](../../by-language/python/data_structures/stack.py) | [stack.c](../../by-language/c/data-structures/stack.c) | +| Structure | Python | C | Go | Java | Rust | TypeScript | +|-----------|--------|---|---|------|------|------------| +| Stack | [stack.py](../../by-language/python/data_structures/stack.py) | [stack.c](../../by-language/c/data-structures/stack.c) | [stack.go](../../by-language/go/datastructures/stack.go) | [Stack.java](../../by-language/java/src/main/java/dsa/datastructures/Stack.java) | [stack.rs](../../by-language/rust/dsa-data-structures/src/linear/stack.rs) | [stack.ts](../../by-language/typescript/src/data-structures/stack.ts) | ## Complexity diff --git a/by-topic/strings/README.md b/by-topic/strings/README.md index 4a1d2dd..1432cfc 100644 --- a/by-topic/strings/README.md +++ b/by-topic/strings/README.md @@ -4,10 +4,10 @@ String algorithms handle text processing, pattern matching, and string manipulat ## Implementations -| Algorithm | Python | C | -|-----------|--------|---| -| All String Algorithms | [string_algorithms.py](../../by-language/python/algorithms/string_algorithms.py) | [string_algorithms.c](../../by-language/c/algorithms/string_algorithms.c) | -| Trie | [trie.py](../../by-language/python/data_structures/trie.py) | [trie.c](../../by-language/c/data-structures/trie.c) | +| Algorithm | Python | C | Go | Java | Rust | TypeScript | +|-----------|--------|---|---|------|------|------------| +| All String Algorithms | [string_algorithms.py](../../by-language/python/algorithms/string_algorithms.py) | [string_algorithms.c](../../by-language/c/algorithms/string_algorithms.c) | [string_algorithms.go](../../by-language/go/algorithms/string_algorithms.go) | [StringAlgorithms.java](../../by-language/java/src/main/java/dsa/algorithms/StringAlgorithms.java) | [mod.rs](../../by-language/rust/dsa-algorithms/src/string/mod.rs) | [string-algorithms.ts](../../by-language/typescript/src/algorithms/string-algorithms.ts) | +| Trie | [trie.py](../../by-language/python/data_structures/trie.py) | [trie.c](../../by-language/c/data-structures/trie.c) | [trie.go](../../by-language/go/datastructures/trie.go) | [Trie.java](../../by-language/java/src/main/java/dsa/datastructures/Trie.java) | [trie.rs](../../by-language/rust/dsa-data-structures/src/trees/trie.rs) | [trie.ts](../../by-language/typescript/src/data-structures/trie.ts) | ## Complexity Comparison diff --git a/by-topic/trees/README.md b/by-topic/trees/README.md index abacaff..bc4fd3d 100644 --- a/by-topic/trees/README.md +++ b/by-topic/trees/README.md @@ -4,15 +4,15 @@ Trees are hierarchical data structures consisting of nodes connected by edges, w ## Implementations -| Structure | Python | C | -|-----------|--------|---| -| Binary Search Tree | [binary_search_tree.py](../../by-language/python/data_structures/binary_search_tree.py) | [binary_search_tree.c](../../by-language/c/data-structures/binary_search_tree.c) | -| AVL Tree | [avl_tree.py](../../by-language/python/data_structures/avl_tree.py) | [avl_tree.c](../../by-language/c/data-structures/avl_tree.c) | -| Red-Black Tree | [red_black_tree.py](../../by-language/python/data_structures/red_black_tree.py) | [red_black_tree.c](../../by-language/c/data-structures/red_black_tree.c) | -| B-Tree | [b_tree.py](../../by-language/python/data_structures/b_tree.py) | [b_tree.c](../../by-language/c/data-structures/b_tree.c) | -| Trie | [trie.py](../../by-language/python/data_structures/trie.py) | [trie.c](../../by-language/c/data-structures/trie.c) | -| Segment Tree | [segment_tree.py](../../by-language/python/data_structures/segment_tree.py) | [segment_tree.c](../../by-language/c/data-structures/segment_tree.c) | -| Fenwick Tree | [fenwick_tree.py](../../by-language/python/data_structures/fenwick_tree.py) | [fenwick_tree.c](../../by-language/c/data-structures/fenwick_tree.c) | +| Structure | Python | C | Go | Java | Rust | TypeScript | +|-----------|--------|---|---|------|------|------------| +| Binary Search Tree | [binary_search_tree.py](../../by-language/python/data_structures/binary_search_tree.py) | [binary_search_tree.c](../../by-language/c/data-structures/binary_search_tree.c) | [binary_search_tree.go](../../by-language/go/datastructures/binary_search_tree.go) | [BinarySearchTree.java](../../by-language/java/src/main/java/dsa/datastructures/BinarySearchTree.java) | [binary_search_tree.rs](../../by-language/rust/dsa-data-structures/src/trees/binary_search_tree.rs) | [binary-search-tree.ts](../../by-language/typescript/src/data-structures/binary-search-tree.ts) | +| AVL Tree | [avl_tree.py](../../by-language/python/data_structures/avl_tree.py) | [avl_tree.c](../../by-language/c/data-structures/avl_tree.c) | [avl_tree.go](../../by-language/go/datastructures/avl_tree.go) | [AvlTree.java](../../by-language/java/src/main/java/dsa/datastructures/AvlTree.java) | [avl_tree.rs](../../by-language/rust/dsa-data-structures/src/trees/avl_tree.rs) | [avl-tree.ts](../../by-language/typescript/src/data-structures/avl-tree.ts) | +| Red-Black Tree | [red_black_tree.py](../../by-language/python/data_structures/red_black_tree.py) | [red_black_tree.c](../../by-language/c/data-structures/red_black_tree.c) | [red_black_tree.go](../../by-language/go/datastructures/red_black_tree.go) | [RedBlackTree.java](../../by-language/java/src/main/java/dsa/datastructures/RedBlackTree.java) | [red_black_tree.rs](../../by-language/rust/dsa-data-structures/src/trees/red_black_tree.rs) | [red-black-tree.ts](../../by-language/typescript/src/data-structures/red-black-tree.ts) | +| B-Tree | [b_tree.py](../../by-language/python/data_structures/b_tree.py) | [b_tree.c](../../by-language/c/data-structures/b_tree.c) | [b_tree.go](../../by-language/go/datastructures/b_tree.go) | [BTree.java](../../by-language/java/src/main/java/dsa/datastructures/BTree.java) | [b_tree.rs](../../by-language/rust/dsa-data-structures/src/trees/b_tree.rs) | [b-tree.ts](../../by-language/typescript/src/data-structures/b-tree.ts) | +| Trie | [trie.py](../../by-language/python/data_structures/trie.py) | [trie.c](../../by-language/c/data-structures/trie.c) | [trie.go](../../by-language/go/datastructures/trie.go) | [Trie.java](../../by-language/java/src/main/java/dsa/datastructures/Trie.java) | [trie.rs](../../by-language/rust/dsa-data-structures/src/trees/trie.rs) | [trie.ts](../../by-language/typescript/src/data-structures/trie.ts) | +| Segment Tree | [segment_tree.py](../../by-language/python/data_structures/segment_tree.py) | [segment_tree.c](../../by-language/c/data-structures/segment_tree.c) | [segment_tree.go](../../by-language/go/datastructures/segment_tree.go) | [SegmentTree.java](../../by-language/java/src/main/java/dsa/datastructures/SegmentTree.java) | [segment_tree.rs](../../by-language/rust/dsa-data-structures/src/trees/segment_tree.rs) | [segment-tree.ts](../../by-language/typescript/src/data-structures/segment-tree.ts) | +| Fenwick Tree | [fenwick_tree.py](../../by-language/python/data_structures/fenwick_tree.py) | [fenwick_tree.c](../../by-language/c/data-structures/fenwick_tree.c) | [fenwick_tree.go](../../by-language/go/datastructures/fenwick_tree.go) | [FenwickTree.java](../../by-language/java/src/main/java/dsa/datastructures/FenwickTree.java) | [fenwick_tree.rs](../../by-language/rust/dsa-data-structures/src/trees/fenwick_tree.rs) | [fenwick-tree.ts](../../by-language/typescript/src/data-structures/fenwick-tree.ts) | ## Complexity