From f1abb268b26bfbd94dc02d3fc0f8b3c6197c69dd Mon Sep 17 00:00:00 2001 From: Keon Kim Date: Thu, 12 Mar 2026 11:09:25 -0400 Subject: [PATCH 1/3] Add data structure tests, fix README coin_change example - Create tests/test_data_structures.py with 62 tests covering red-black tree, AVL tree, trie, union-find, segment tree, hash table, separate chaining hash table, stack, and queue (#2727) - Fix README Quick Start coin_change example to use correct function name `count` matching the actual export (#2738) Closes #2727, closes #2738 Co-Authored-By: Claude Opus 4.6 --- README.md | 9 +- tests/test_data_structures.py | 407 ++++++++++++++++++++++++++++++++++ 2 files changed, 412 insertions(+), 4 deletions(-) create mode 100644 tests/test_data_structures.py diff --git a/README.md b/README.md index f1408d7e0..3e0739363 100644 --- a/README.md +++ b/README.md @@ -52,11 +52,11 @@ print(dijkstra(graph, "s", "t")) **Dynamic programming — coin change:** ```python -from algorithms.dynamic_programming import coin_change +from algorithms.dynamic_programming import count -# Minimum coins to make amount 29 using denominations [1, 5, 10, 25] -print(coin_change([1, 5, 10, 25], 29)) -# 7 (25 + 1 + 1 + 1 + 1) +# Number of ways to make amount 10 using denominations [2, 5, 3, 6] +print(count([2, 5, 3, 6], 10)) +# 5 ``` **Backtracking — generate permutations:** @@ -172,6 +172,7 @@ All core data structures live in [`algorithms/data_structures/`](algorithms/data | Stack | `stack.py` | `ArrayStack`, `LinkedListStack` | | Trie | `trie.py` | `Trie` | | Union-Find | `union_find.py` | `Union` | +| vEB Tree | `veb_tree.py` | `VEBTree` | ## Algorithms diff --git a/tests/test_data_structures.py b/tests/test_data_structures.py new file mode 100644 index 000000000..cbcfe6c03 --- /dev/null +++ b/tests/test_data_structures.py @@ -0,0 +1,407 @@ +"""Tests for data structures in algorithms/data_structures.""" + +import unittest + +from algorithms.data_structures.red_black_tree import RBNode, RBTree +from algorithms.data_structures.avl_tree import AvlTree +from algorithms.data_structures.trie import Trie +from algorithms.data_structures.union_find import Union +from algorithms.data_structures.segment_tree import SegmentTree +from algorithms.data_structures.hash_table import HashTable, ResizableHashTable +from algorithms.data_structures.separate_chaining_hash_table import SeparateChainingHashTable +from algorithms.data_structures.stack import ArrayStack, LinkedListStack +from algorithms.data_structures.queue import ArrayQueue, LinkedListQueue + + +class TestRBTree(unittest.TestCase): + def _make_tree(self, values): + tree = RBTree() + for v in values: + tree.insert(RBNode(v, 1)) + return tree + + def test_insert_single(self): + tree = self._make_tree([5]) + result = tree.inorder() + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["val"], 5) + + def test_insert_multiple_sorted(self): + values = [11, 2, 14, 1, 7, 15, 5, 8, 4] + tree = self._make_tree(values) + result = tree.inorder() + vals = [r["val"] for r in result] + self.assertEqual(vals, sorted(values)) + + def test_root_is_black(self): + tree = self._make_tree([10, 5, 15]) + self.assertEqual(tree.root.color, 0) + + def test_empty_tree(self): + tree = RBTree() + self.assertIsNone(tree.root) + self.assertEqual(tree.inorder(), []) + + def test_insert_duplicates_order(self): + tree = self._make_tree([3, 1, 2]) + result = tree.inorder() + vals = [r["val"] for r in result] + self.assertEqual(vals, [1, 2, 3]) + + +class TestAvlTree(unittest.TestCase): + def test_insert_single(self): + tree = AvlTree() + tree.insert(10) + self.assertIsNotNone(tree.node) + self.assertEqual(tree.node.val, 10) + + def test_insert_multiple_root_exists(self): + tree = AvlTree() + for v in [5, 3, 7, 1, 4]: + tree.insert(v) + self.assertIsNotNone(tree.node) + + def test_balanced_after_insert(self): + tree = AvlTree() + for v in [1, 2, 3, 4, 5]: + tree.insert(v) + # Tree should remain balanced; height should be <= log2(5)+1 ~ 3 + self.assertLessEqual(tree.height, 3) + + def test_empty_tree(self): + tree = AvlTree() + self.assertIsNone(tree.node) + self.assertEqual(tree.in_order_traverse(), []) + + def test_insert_balance_factor(self): + tree = AvlTree() + for v in [5, 4, 3, 2, 1]: + tree.insert(v) + # After balancing, the balance factor should be within [-1, 1] + self.assertIn(tree.balance, [-1, 0, 1]) + + +class TestTrie(unittest.TestCase): + def test_insert_and_search(self): + trie = Trie() + trie.insert("apple") + self.assertTrue(trie.search("apple")) + + def test_search_missing(self): + trie = Trie() + trie.insert("apple") + self.assertFalse(trie.search("app")) + + def test_starts_with(self): + trie = Trie() + trie.insert("apple") + self.assertTrue(trie.starts_with("app")) + self.assertFalse(trie.starts_with("apl")) + + def test_empty_trie(self): + trie = Trie() + self.assertFalse(trie.search("anything")) + self.assertFalse(trie.starts_with("a")) + + def test_multiple_words(self): + trie = Trie() + for w in ["cat", "car", "card", "care"]: + trie.insert(w) + self.assertTrue(trie.search("card")) + self.assertFalse(trie.search("ca")) + self.assertTrue(trie.starts_with("ca")) + + def test_insert_single_char(self): + trie = Trie() + trie.insert("a") + self.assertTrue(trie.search("a")) + self.assertFalse(trie.search("b")) + + +class TestUnionFind(unittest.TestCase): + def test_add_and_root(self): + uf = Union() + uf.add(1) + self.assertEqual(uf.root(1), 1) + + def test_unite_connects(self): + uf = Union() + uf.add(1) + uf.add(2) + uf.unite(1, 2) + self.assertEqual(uf.root(1), uf.root(2)) + + def test_not_connected(self): + uf = Union() + uf.add(1) + uf.add(2) + self.assertNotEqual(uf.root(1), uf.root(2)) + + def test_count_decrements_on_unite(self): + uf = Union() + uf.add(1) + uf.add(2) + uf.add(3) + self.assertEqual(uf.count, 3) + uf.unite(1, 2) + self.assertEqual(uf.count, 2) + + def test_unite_same_element(self): + uf = Union() + uf.add(1) + uf.unite(1, 1) + self.assertEqual(uf.count, 1) + + def test_transitive_connectivity(self): + uf = Union() + for x in [1, 2, 3]: + uf.add(x) + uf.unite(1, 2) + uf.unite(2, 3) + self.assertEqual(uf.root(1), uf.root(3)) + + +class TestSegmentTree(unittest.TestCase): + def test_max_query(self): + tree = SegmentTree([2, 4, 5, 3, 4], max) + self.assertEqual(tree.query(2, 4), 5) + + def test_sum_query(self): + tree = SegmentTree([1, 2, 3, 4, 5], lambda a, b: a + b) + self.assertEqual(tree.query(0, 4), 15) + + def test_single_element_query(self): + tree = SegmentTree([7, 2, 9], max) + self.assertEqual(tree.query(0, 0), 7) + self.assertEqual(tree.query(2, 2), 9) + + def test_full_range_max(self): + arr = [3, 1, 4, 1, 5, 9, 2, 6] + tree = SegmentTree(arr, max) + self.assertEqual(tree.query(0, len(arr) - 1), 9) + + +class TestHashTable(unittest.TestCase): + def test_put_and_get(self): + ht = HashTable() + ht.put(1, "one") + self.assertEqual(ht.get(1), "one") + + def test_get_missing(self): + ht = HashTable() + self.assertIsNone(ht.get(99)) + + def test_delete(self): + ht = HashTable() + ht.put(1, "one") + ht.del_(1) + self.assertIsNone(ht.get(1)) + + def test_update_existing(self): + ht = HashTable() + ht.put(1, "one") + ht.put(1, "ONE") + self.assertEqual(ht.get(1), "ONE") + + def test_len(self): + ht = HashTable() + ht.put(1, "a") + ht.put(2, "b") + self.assertEqual(len(ht), 2) + + def test_bracket_operators(self): + ht = HashTable() + ht[5] = "five" + self.assertEqual(ht[5], "five") + del ht[5] + self.assertIsNone(ht[5]) + + +class TestResizableHashTable(unittest.TestCase): + def test_put_and_get(self): + ht = ResizableHashTable() + ht.put(1, "a") + self.assertEqual(ht.get(1), "a") + + def test_resizes_automatically(self): + ht = ResizableHashTable() + for i in range(20): + ht.put(i, str(i)) + for i in range(20): + self.assertEqual(ht.get(i), str(i)) + + +class TestSeparateChainingHashTable(unittest.TestCase): + def test_put_and_get(self): + table = SeparateChainingHashTable() + table.put("hello", "world") + self.assertEqual(table.get("hello"), "world") + + def test_get_missing(self): + table = SeparateChainingHashTable() + self.assertIsNone(table.get("missing")) + + def test_delete(self): + table = SeparateChainingHashTable() + table.put("key", "value") + table.del_("key") + self.assertIsNone(table.get("key")) + + def test_len(self): + table = SeparateChainingHashTable() + table.put("a", 1) + table.put("b", 2) + self.assertEqual(len(table), 2) + + def test_collision_handling(self): + # Force collision by using small table + table = SeparateChainingHashTable(size=1) + table.put("x", 10) + table.put("y", 20) + self.assertEqual(table.get("x"), 10) + self.assertEqual(table.get("y"), 20) + + def test_bracket_operators(self): + table = SeparateChainingHashTable() + table["k"] = "v" + self.assertEqual(table["k"], "v") + del table["k"] + self.assertIsNone(table["k"]) + + +class TestArrayStack(unittest.TestCase): + def test_push_and_pop(self): + s = ArrayStack() + s.push(1) + self.assertEqual(s.pop(), 1) + + def test_peek(self): + s = ArrayStack() + s.push(42) + self.assertEqual(s.peek(), 42) + self.assertEqual(len(s), 1) # peek doesn't remove + + def test_is_empty(self): + s = ArrayStack() + self.assertTrue(s.is_empty()) + s.push(1) + self.assertFalse(s.is_empty()) + + def test_pop_empty_raises(self): + s = ArrayStack() + with self.assertRaises(IndexError): + s.pop() + + def test_peek_empty_raises(self): + s = ArrayStack() + with self.assertRaises(IndexError): + s.peek() + + def test_lifo_order(self): + s = ArrayStack() + for v in [1, 2, 3]: + s.push(v) + self.assertEqual(s.pop(), 3) + self.assertEqual(s.pop(), 2) + self.assertEqual(s.pop(), 1) + + +class TestLinkedListStack(unittest.TestCase): + def test_push_and_pop(self): + s = LinkedListStack() + s.push(10) + self.assertEqual(s.pop(), 10) + + def test_is_empty(self): + s = LinkedListStack() + self.assertTrue(s.is_empty()) + s.push(5) + self.assertFalse(s.is_empty()) + + def test_peek(self): + s = LinkedListStack() + s.push(7) + self.assertEqual(s.peek(), 7) + + def test_pop_empty_raises(self): + s = LinkedListStack() + with self.assertRaises(IndexError): + s.pop() + + def test_lifo_order(self): + s = LinkedListStack() + for v in [10, 20, 30]: + s.push(v) + self.assertEqual([s.pop(), s.pop(), s.pop()], [30, 20, 10]) + + +class TestArrayQueue(unittest.TestCase): + def test_enqueue_and_dequeue(self): + q = ArrayQueue() + q.enqueue(1) + self.assertEqual(q.dequeue(), 1) + + def test_is_empty(self): + q = ArrayQueue() + self.assertTrue(q.is_empty()) + q.enqueue(5) + self.assertFalse(q.is_empty()) + + def test_peek(self): + q = ArrayQueue() + q.enqueue(99) + self.assertEqual(q.peek(), 99) + self.assertEqual(len(q), 1) + + def test_dequeue_empty_raises(self): + q = ArrayQueue() + with self.assertRaises(IndexError): + q.dequeue() + + def test_fifo_order(self): + q = ArrayQueue() + for v in [1, 2, 3]: + q.enqueue(v) + self.assertEqual(q.dequeue(), 1) + self.assertEqual(q.dequeue(), 2) + self.assertEqual(q.dequeue(), 3) + + +class TestLinkedListQueue(unittest.TestCase): + def test_enqueue_and_dequeue(self): + q = LinkedListQueue() + q.enqueue("a") + self.assertEqual(q.dequeue(), "a") + + def test_is_empty(self): + q = LinkedListQueue() + self.assertTrue(q.is_empty()) + q.enqueue("x") + self.assertFalse(q.is_empty()) + + def test_peek(self): + q = LinkedListQueue() + q.enqueue(42) + self.assertEqual(q.peek(), 42) + + def test_dequeue_empty_raises(self): + q = LinkedListQueue() + with self.assertRaises(IndexError): + q.dequeue() + + def test_fifo_order(self): + q = LinkedListQueue() + for v in ["x", "y", "z"]: + q.enqueue(v) + self.assertEqual([q.dequeue(), q.dequeue(), q.dequeue()], ["x", "y", "z"]) + + def test_single_element(self): + q = LinkedListQueue() + q.enqueue(1) + self.assertEqual(q.dequeue(), 1) + self.assertTrue(q.is_empty()) + + +if __name__ == "__main__": + unittest.main() From 11ed784ebb8f1246753d7c12eb18ea39379c98aa Mon Sep 17 00:00:00 2001 From: Keon Kim Date: Thu, 12 Mar 2026 11:21:13 -0400 Subject: [PATCH 2/3] Fix ruff I001: sort imports in test_data_structures.py Co-Authored-By: Claude Opus 4.6 --- tests/test_data_structures.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_data_structures.py b/tests/test_data_structures.py index cbcfe6c03..42963abd1 100644 --- a/tests/test_data_structures.py +++ b/tests/test_data_structures.py @@ -2,15 +2,17 @@ import unittest -from algorithms.data_structures.red_black_tree import RBNode, RBTree from algorithms.data_structures.avl_tree import AvlTree -from algorithms.data_structures.trie import Trie -from algorithms.data_structures.union_find import Union -from algorithms.data_structures.segment_tree import SegmentTree from algorithms.data_structures.hash_table import HashTable, ResizableHashTable -from algorithms.data_structures.separate_chaining_hash_table import SeparateChainingHashTable -from algorithms.data_structures.stack import ArrayStack, LinkedListStack from algorithms.data_structures.queue import ArrayQueue, LinkedListQueue +from algorithms.data_structures.red_black_tree import RBNode, RBTree +from algorithms.data_structures.segment_tree import SegmentTree +from algorithms.data_structures.separate_chaining_hash_table import ( + SeparateChainingHashTable, +) +from algorithms.data_structures.stack import ArrayStack, LinkedListStack +from algorithms.data_structures.trie import Trie +from algorithms.data_structures.union_find import Union class TestRBTree(unittest.TestCase): From 9d9d822875f938d921032f220f7721ce653e0e20 Mon Sep 17 00:00:00 2001 From: Keon Kim Date: Fri, 13 Mar 2026 16:54:28 -0400 Subject: [PATCH 3/3] Fix AVL in_order_traverse crash and remove duplicate tests - Fix avl_tree.py: .key -> .val to match TreeNode dataclass - Add test_in_order_traverse_populated to catch this regression - Remove duplicate stack/queue tests already covered in test_stack.py and test_queue.py Co-Authored-By: Claude Opus 4.6 --- algorithms/data_structures/avl_tree.py | 2 +- tests/test_data_structures.py | 141 ++----------------------- 2 files changed, 7 insertions(+), 136 deletions(-) diff --git a/algorithms/data_structures/avl_tree.py b/algorithms/data_structures/avl_tree.py index e2a4b6325..7be8cbe0e 100644 --- a/algorithms/data_structures/avl_tree.py +++ b/algorithms/data_structures/avl_tree.py @@ -121,6 +121,6 @@ def in_order_traverse(self): return result result.extend(self.node.left.in_order_traverse()) - result.append(self.node.key) + result.append(self.node.val) result.extend(self.node.right.in_order_traverse()) return result diff --git a/tests/test_data_structures.py b/tests/test_data_structures.py index 42963abd1..c415ba010 100644 --- a/tests/test_data_structures.py +++ b/tests/test_data_structures.py @@ -4,13 +4,11 @@ from algorithms.data_structures.avl_tree import AvlTree from algorithms.data_structures.hash_table import HashTable, ResizableHashTable -from algorithms.data_structures.queue import ArrayQueue, LinkedListQueue from algorithms.data_structures.red_black_tree import RBNode, RBTree from algorithms.data_structures.segment_tree import SegmentTree from algorithms.data_structures.separate_chaining_hash_table import ( SeparateChainingHashTable, ) -from algorithms.data_structures.stack import ArrayStack, LinkedListStack from algorithms.data_structures.trie import Trie from algorithms.data_structures.union_find import Union @@ -76,6 +74,12 @@ def test_empty_tree(self): self.assertIsNone(tree.node) self.assertEqual(tree.in_order_traverse(), []) + def test_in_order_traverse_populated(self): + tree = AvlTree() + for v in [5, 3, 7, 1, 4]: + tree.insert(v) + self.assertEqual(tree.in_order_traverse(), [1, 3, 4, 5, 7]) + def test_insert_balance_factor(self): tree = AvlTree() for v in [5, 4, 3, 2, 1]: @@ -272,138 +276,5 @@ def test_bracket_operators(self): self.assertIsNone(table["k"]) -class TestArrayStack(unittest.TestCase): - def test_push_and_pop(self): - s = ArrayStack() - s.push(1) - self.assertEqual(s.pop(), 1) - - def test_peek(self): - s = ArrayStack() - s.push(42) - self.assertEqual(s.peek(), 42) - self.assertEqual(len(s), 1) # peek doesn't remove - - def test_is_empty(self): - s = ArrayStack() - self.assertTrue(s.is_empty()) - s.push(1) - self.assertFalse(s.is_empty()) - - def test_pop_empty_raises(self): - s = ArrayStack() - with self.assertRaises(IndexError): - s.pop() - - def test_peek_empty_raises(self): - s = ArrayStack() - with self.assertRaises(IndexError): - s.peek() - - def test_lifo_order(self): - s = ArrayStack() - for v in [1, 2, 3]: - s.push(v) - self.assertEqual(s.pop(), 3) - self.assertEqual(s.pop(), 2) - self.assertEqual(s.pop(), 1) - - -class TestLinkedListStack(unittest.TestCase): - def test_push_and_pop(self): - s = LinkedListStack() - s.push(10) - self.assertEqual(s.pop(), 10) - - def test_is_empty(self): - s = LinkedListStack() - self.assertTrue(s.is_empty()) - s.push(5) - self.assertFalse(s.is_empty()) - - def test_peek(self): - s = LinkedListStack() - s.push(7) - self.assertEqual(s.peek(), 7) - - def test_pop_empty_raises(self): - s = LinkedListStack() - with self.assertRaises(IndexError): - s.pop() - - def test_lifo_order(self): - s = LinkedListStack() - for v in [10, 20, 30]: - s.push(v) - self.assertEqual([s.pop(), s.pop(), s.pop()], [30, 20, 10]) - - -class TestArrayQueue(unittest.TestCase): - def test_enqueue_and_dequeue(self): - q = ArrayQueue() - q.enqueue(1) - self.assertEqual(q.dequeue(), 1) - - def test_is_empty(self): - q = ArrayQueue() - self.assertTrue(q.is_empty()) - q.enqueue(5) - self.assertFalse(q.is_empty()) - - def test_peek(self): - q = ArrayQueue() - q.enqueue(99) - self.assertEqual(q.peek(), 99) - self.assertEqual(len(q), 1) - - def test_dequeue_empty_raises(self): - q = ArrayQueue() - with self.assertRaises(IndexError): - q.dequeue() - - def test_fifo_order(self): - q = ArrayQueue() - for v in [1, 2, 3]: - q.enqueue(v) - self.assertEqual(q.dequeue(), 1) - self.assertEqual(q.dequeue(), 2) - self.assertEqual(q.dequeue(), 3) - - -class TestLinkedListQueue(unittest.TestCase): - def test_enqueue_and_dequeue(self): - q = LinkedListQueue() - q.enqueue("a") - self.assertEqual(q.dequeue(), "a") - - def test_is_empty(self): - q = LinkedListQueue() - self.assertTrue(q.is_empty()) - q.enqueue("x") - self.assertFalse(q.is_empty()) - - def test_peek(self): - q = LinkedListQueue() - q.enqueue(42) - self.assertEqual(q.peek(), 42) - - def test_dequeue_empty_raises(self): - q = LinkedListQueue() - with self.assertRaises(IndexError): - q.dequeue() - - def test_fifo_order(self): - q = LinkedListQueue() - for v in ["x", "y", "z"]: - q.enqueue(v) - self.assertEqual([q.dequeue(), q.dequeue(), q.dequeue()], ["x", "y", "z"]) - - def test_single_element(self): - q = LinkedListQueue() - q.enqueue(1) - self.assertEqual(q.dequeue(), 1) - self.assertTrue(q.is_empty()) - - if __name__ == "__main__": unittest.main()