pkstruct 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. pkstruct/__init__.py +167 -0
  2. pkstruct/graphs/__init__.py +127 -0
  3. pkstruct/graphs/connectivity.py +157 -0
  4. pkstruct/graphs/directed.py +95 -0
  5. pkstruct/graphs/exceptions.py +63 -0
  6. pkstruct/graphs/graph.py +262 -0
  7. pkstruct/graphs/mst.py +118 -0
  8. pkstruct/graphs/scc.py +138 -0
  9. pkstruct/graphs/shortest_path.py +250 -0
  10. pkstruct/graphs/topo_sort.py +108 -0
  11. pkstruct/graphs/traversal.py +175 -0
  12. pkstruct/graphs/visualization.py +90 -0
  13. pkstruct/graphs/weighted.py +37 -0
  14. pkstruct/linear/__init__.py +95 -0
  15. pkstruct/linear/deques/__init__.py +33 -0
  16. pkstruct/linear/deques/deque.py +194 -0
  17. pkstruct/linear/deques/linked_deque.py +198 -0
  18. pkstruct/linear/exceptions.py +26 -0
  19. pkstruct/linear/linked_lists/__init__.py +5 -0
  20. pkstruct/linear/linked_lists/_base.py +608 -0
  21. pkstruct/linear/linked_lists/circular_linked_list.py +230 -0
  22. pkstruct/linear/linked_lists/doubly_linked_list.py +151 -0
  23. pkstruct/linear/linked_lists/nodes.py +68 -0
  24. pkstruct/linear/linked_lists/singly_linked_list.py +136 -0
  25. pkstruct/linear/queues/__init__.py +44 -0
  26. pkstruct/linear/queues/circular_queue.py +258 -0
  27. pkstruct/linear/queues/linked_queue.py +186 -0
  28. pkstruct/linear/queues/priority_queue.py +202 -0
  29. pkstruct/linear/queues/queue.py +174 -0
  30. pkstruct/linear/stacks/__init__.py +38 -0
  31. pkstruct/linear/stacks/array_stack.py +165 -0
  32. pkstruct/linear/stacks/linked_stack.py +168 -0
  33. pkstruct/linear/stacks/stack.py +158 -0
  34. pkstruct/linear/utils/__init__.py +18 -0
  35. pkstruct/linear/utils/benchmark.py +255 -0
  36. pkstruct/linear/utils/debug_tools.py +239 -0
  37. pkstruct/linear/utils/helpers.py +143 -0
  38. pkstruct/linear/utils/iterators.py +148 -0
  39. pkstruct/linear/visualization/__init__.py +0 -0
  40. pkstruct/linear/visualization/ascii_visualizer.py +114 -0
  41. pkstruct/linear/visualization/linked_list_visualizer.py +126 -0
  42. pkstruct/shared/__init__.py +67 -0
  43. pkstruct/shared/benchmarking/__init__.py +78 -0
  44. pkstruct/shared/debugging/__init__.py +69 -0
  45. pkstruct/shared/exceptions/__init__.py +59 -0
  46. pkstruct/shared/serializers/__init__.py +65 -0
  47. pkstruct/shared/threading/__init__.py +43 -0
  48. pkstruct/shared/validators/__init__.py +98 -0
  49. pkstruct/shared/visualization/__init__.py +21 -0
  50. pkstruct/trees/__init__.py +92 -0
  51. pkstruct/trees/avl.py +321 -0
  52. pkstruct/trees/balancing.py +253 -0
  53. pkstruct/trees/bplus.py +425 -0
  54. pkstruct/trees/bst.py +948 -0
  55. pkstruct/trees/btree.py +504 -0
  56. pkstruct/trees/exceptions.py +96 -0
  57. pkstruct/trees/fenwick_tree.py +312 -0
  58. pkstruct/trees/interval_tree.py +541 -0
  59. pkstruct/trees/node.py +356 -0
  60. pkstruct/trees/red_black.py +710 -0
  61. pkstruct/trees/segment_tree.py +398 -0
  62. pkstruct/trees/traversal.py +456 -0
  63. pkstruct/trees/tree_helpers.py +366 -0
  64. pkstruct/trees/utils/__init__.py +15 -0
  65. pkstruct/trees/utils/complexity_helpers.py +231 -0
  66. pkstruct/trees/visualization/__init__.py +0 -0
  67. pkstruct/trees/visualization/ascii_renderer.py +220 -0
  68. pkstruct/trees/visualization/tree_printer.py +129 -0
  69. pkstruct-0.1.0.dist-info/METADATA +482 -0
  70. pkstruct-0.1.0.dist-info/RECORD +72 -0
  71. pkstruct-0.1.0.dist-info/WHEEL +4 -0
  72. pkstruct-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,366 @@
1
+ """
2
+ Shared tree helper utilities for pkstruct.trees.
3
+
4
+ This module provides pure utility functions that are genuinely reusable across
5
+ multiple tree types (BST, AVL, Red-Black, B-Tree, Segment, Interval, etc.).
6
+
7
+ Design contract:
8
+ - Functions operate on duck-typed node objects (Protocol-style).
9
+ - No import from other pkstruct.trees sub-modules to prevent circular deps.
10
+ - No balancing or traversal logic (those live in balancing.py / traversal.py).
11
+ - All functions are stateless (no global state).
12
+
13
+ Reusable helpers:
14
+ calculate_height(node)
15
+ calculate_size(node)
16
+ is_leaf(node)
17
+ is_internal(node)
18
+ clone_subtree(node, node_factory)
19
+ tree_depth(node) alias for calculate_height
20
+ count_nodes(node) alias for calculate_size
21
+ count_leaves(node)
22
+ validate_bst_order(node, key_fn, min_val, max_val)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import math
28
+ from collections.abc import Callable
29
+ from typing import Any, Protocol, TypeVar
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Structural protocol
33
+ # ---------------------------------------------------------------------------
34
+
35
+ class _HasChildren(Protocol):
36
+ """Minimal structural protocol for a binary tree node."""
37
+ left: Any | None
38
+ right: Any | None
39
+
40
+
41
+ class _HasKey(_HasChildren, Protocol):
42
+ """Node with a comparable key (for BST validation)."""
43
+ key: Any
44
+
45
+
46
+ N = TypeVar("N")
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Height / depth
51
+ # ---------------------------------------------------------------------------
52
+
53
+ def calculate_height(node: Any | None) -> int:
54
+ """
55
+ Compute the height of the subtree rooted at node.
56
+
57
+ Height of a single-node tree is 1; height of None is 0.
58
+
59
+ Complexity: O(n) — visits every node.
60
+
61
+ Args:
62
+ node: Root of a subtree. Must expose `.left` and `.right` attributes,
63
+ or be None.
64
+
65
+ Returns:
66
+ Integer height of the subtree.
67
+
68
+ Example:
69
+ >>> calculate_height(None)
70
+ 0
71
+ >>> calculate_height(leaf_node)
72
+ 1
73
+ """
74
+ if node is None:
75
+ return 0
76
+ return 1 + max(calculate_height(node.left), calculate_height(node.right))
77
+
78
+
79
+ def tree_depth(node: Any | None) -> int:
80
+ """
81
+ Alias for calculate_height().
82
+
83
+ Returns the depth (height) of the subtree rooted at node.
84
+
85
+ Complexity: O(n)
86
+ """
87
+ return calculate_height(node)
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Size / count
92
+ # ---------------------------------------------------------------------------
93
+
94
+ def calculate_size(node: Any | None) -> int:
95
+ """
96
+ Count all nodes in the subtree rooted at node.
97
+
98
+ Complexity: O(n)
99
+
100
+ Args:
101
+ node: Root of a subtree (exposes `.left` and `.right`), or None.
102
+
103
+ Returns:
104
+ Total number of nodes in the subtree.
105
+ """
106
+ if node is None:
107
+ return 0
108
+ return 1 + calculate_size(node.left) + calculate_size(node.right)
109
+
110
+
111
+ def count_nodes(node: Any | None) -> int:
112
+ """
113
+ Alias for calculate_size().
114
+
115
+ Count all nodes in the subtree.
116
+
117
+ Complexity: O(n)
118
+ """
119
+ return calculate_size(node)
120
+
121
+
122
+ def count_leaves(node: Any | None) -> int:
123
+ """
124
+ Count leaf nodes (nodes with no children) in the subtree.
125
+
126
+ Complexity: O(n)
127
+
128
+ Args:
129
+ node: Root of a subtree (exposes `.left` and `.right`), or None.
130
+
131
+ Returns:
132
+ Number of leaf nodes.
133
+ """
134
+ if node is None:
135
+ return 0
136
+ if node.left is None and node.right is None:
137
+ return 1
138
+ return count_leaves(node.left) + count_leaves(node.right)
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Leaf / internal classification
143
+ # ---------------------------------------------------------------------------
144
+
145
+ def is_leaf(node: Any) -> bool:
146
+ """
147
+ Return True if node has no children.
148
+
149
+ Works for any node type that exposes `.left` and `.right` attributes.
150
+
151
+ Complexity: O(1)
152
+
153
+ Args:
154
+ node: A tree node (not None).
155
+
156
+ Returns:
157
+ True if both children are None.
158
+
159
+ Raises:
160
+ AttributeError: If node does not expose `.left` / `.right`.
161
+ """
162
+ return node.left is None and node.right is None
163
+
164
+
165
+ def is_internal(node: Any) -> bool:
166
+ """
167
+ Return True if node has at least one child.
168
+
169
+ Complement of is_leaf().
170
+
171
+ Complexity: O(1)
172
+
173
+ Args:
174
+ node: A tree node (not None).
175
+
176
+ Returns:
177
+ True if node has at least one child.
178
+ """
179
+ return not is_leaf(node)
180
+
181
+
182
+ # ---------------------------------------------------------------------------
183
+ # Clone
184
+ # ---------------------------------------------------------------------------
185
+
186
+ def clone_subtree(
187
+ node: N | None,
188
+ node_factory: Callable[[N], N],
189
+ ) -> N | None:
190
+ """
191
+ Deep-clone a subtree using a caller-supplied node factory.
192
+
193
+ The factory receives the original node and must return a *new* node whose
194
+ key/value fields are copied but whose `.left`/`.right` are unset (they will
195
+ be wired by this function).
196
+
197
+ This design is node-type agnostic: each tree module supplies its own
198
+ factory that knows what fields to copy.
199
+
200
+ Complexity: O(n)
201
+
202
+ Args:
203
+ node: Root of the subtree to clone (or None).
204
+ node_factory: ``f(original) -> new_node`` — creates a shallow copy of
205
+ a single node (fields only, children ignored).
206
+
207
+ Returns:
208
+ Root of the cloned subtree, or None if node is None.
209
+
210
+ Example:
211
+ >>> def bst_factory(n):
212
+ ... clone = BSTNode(n.key, n.value)
213
+ ... return clone
214
+ >>> new_root = clone_subtree(tree.root, bst_factory)
215
+ """
216
+ if node is None:
217
+ return None
218
+ new_node: N = node_factory(node)
219
+ new_node.left = clone_subtree(node.left, node_factory) # type: ignore[attr-defined]
220
+ new_node.right = clone_subtree(node.right, node_factory) # type: ignore[attr-defined]
221
+ return new_node
222
+
223
+
224
+ # ---------------------------------------------------------------------------
225
+ # BST order validation
226
+ # ---------------------------------------------------------------------------
227
+
228
+ def validate_bst_order(
229
+ node: Any | None,
230
+ key_fn: Callable[[Any], Any] = lambda n: n.key,
231
+ min_val: Any = -math.inf,
232
+ max_val: Any = math.inf,
233
+ ) -> bool:
234
+ """
235
+ Recursively verify BST ordering invariant for a binary subtree.
236
+
237
+ Each node's key must satisfy min_val < key < max_val (strict).
238
+
239
+ Compatible with any node type that exposes `.left`, `.right`, and a
240
+ key attribute/property accessible via key_fn.
241
+
242
+ Complexity: O(n)
243
+
244
+ Args:
245
+ node: Root of subtree to validate (or None).
246
+ key_fn: Callable that extracts a comparable key from a node.
247
+ Defaults to ``lambda n: n.key``.
248
+ min_val: Exclusive lower bound for all keys in this subtree.
249
+ max_val: Exclusive upper bound for all keys in this subtree.
250
+
251
+ Returns:
252
+ True if the BST ordering invariant holds throughout.
253
+
254
+ Raises:
255
+ ValueError: If an ordering violation is detected.
256
+ """
257
+ if node is None:
258
+ return True
259
+ key = key_fn(node)
260
+ if key <= min_val:
261
+ raise ValueError(
262
+ f"BST order violation: key {key!r} is not > lower bound {min_val!r}."
263
+ )
264
+ if key >= max_val:
265
+ raise ValueError(
266
+ f"BST order violation: key {key!r} is not < upper bound {max_val!r}."
267
+ )
268
+ validate_bst_order(node.left, key_fn, min_val, key)
269
+ validate_bst_order(node.right, key_fn, key, max_val)
270
+ return True
271
+
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # Path / level utilities
275
+ # ---------------------------------------------------------------------------
276
+
277
+ def path_to_node(
278
+ root: Any | None,
279
+ key_fn: Callable[[Any], Any],
280
+ target_key: Any,
281
+ ) -> list[Any] | None:
282
+ """
283
+ Return the path (list of nodes) from root to the node with target_key.
284
+
285
+ Uses BST ordering (key_fn) for O(log n) traversal in a balanced BST, but
286
+ falls back correctly for any binary tree if keys are unique.
287
+
288
+ Complexity: O(log n) for balanced BST, O(n) worst-case.
289
+
290
+ Args:
291
+ root: Root of the tree.
292
+ key_fn: Extracts a comparable key from a node.
293
+ target_key: The key to search for.
294
+
295
+ Returns:
296
+ List of nodes from root to target (inclusive), or None if not found.
297
+ """
298
+ path: list[Any] = []
299
+ node = root
300
+ while node is not None:
301
+ path.append(node)
302
+ k = key_fn(node)
303
+ if target_key == k:
304
+ return path
305
+ elif target_key < k:
306
+ node = node.left
307
+ else:
308
+ node = node.right
309
+ return None
310
+
311
+
312
+ def level_of_node(
313
+ root: Any | None,
314
+ key_fn: Callable[[Any], Any],
315
+ target_key: Any,
316
+ ) -> int | None:
317
+ """
318
+ Return the 0-based level (depth) of the node with target_key.
319
+
320
+ Root is at level 0.
321
+
322
+ Complexity: O(log n) for balanced BST, O(n) worst-case.
323
+
324
+ Args:
325
+ root: Root of the tree.
326
+ key_fn: Extracts a comparable key from a node.
327
+ target_key: The key to locate.
328
+
329
+ Returns:
330
+ Integer level, or None if key is not found.
331
+ """
332
+ path = path_to_node(root, key_fn, target_key)
333
+ return len(path) - 1 if path is not None else None
334
+
335
+
336
+ # ---------------------------------------------------------------------------
337
+ # Width utilities
338
+ # ---------------------------------------------------------------------------
339
+
340
+ def max_width(root: Any | None) -> int:
341
+ """
342
+ Return the maximum number of nodes on any single level (BFS width).
343
+
344
+ Complexity: O(n)
345
+
346
+ Args:
347
+ root: Root of the tree.
348
+
349
+ Returns:
350
+ Maximum level width, or 0 for an empty tree.
351
+ """
352
+ if root is None:
353
+ return 0
354
+ from collections import deque
355
+ queue: deque = deque([root])
356
+ max_w = 0
357
+ while queue:
358
+ width = len(queue)
359
+ max_w = max(max_w, width)
360
+ for _ in range(width):
361
+ node = queue.popleft()
362
+ if node.left is not None:
363
+ queue.append(node.left)
364
+ if node.right is not None:
365
+ queue.append(node.right)
366
+ return max_w
@@ -0,0 +1,15 @@
1
+ from .complexity_helpers import (
2
+ benchmark_delete,
3
+ benchmark_insert,
4
+ benchmark_search,
5
+ compare_operations,
6
+ measure_execution_time,
7
+ )
8
+
9
+ __all__ = [
10
+ "measure_execution_time",
11
+ "benchmark_insert",
12
+ "benchmark_search",
13
+ "benchmark_delete",
14
+ "compare_operations",
15
+ ]
@@ -0,0 +1,231 @@
1
+ """
2
+ pkstruct.trees.utils.complexity_helpers
3
+ ========================================
4
+ Benchmarking utilities for pkstruct tree classes.
5
+
6
+ All functions use only the standard library. They operate on tree instances
7
+ duck-typed through a minimal public API (insert, search, delete, size, etc.)
8
+ so they work with any tree class supported by ``pkstruct.trees``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import time
14
+ from collections.abc import Callable
15
+ from typing import Any
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Helpers
19
+ # ---------------------------------------------------------------------------
20
+
21
+
22
+ def _timeit(fn: Callable, *args: Any, **kwargs: Any) -> float:
23
+ """Run *fn* once and return elapsed seconds."""
24
+ start = time.perf_counter()
25
+ fn(*args, **kwargs)
26
+ return time.perf_counter() - start
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Public API
31
+ # ---------------------------------------------------------------------------
32
+
33
+
34
+ def measure_execution_time(
35
+ fn: Callable,
36
+ *args: Any,
37
+ iterations: int = 1,
38
+ **kwargs: Any,
39
+ ) -> float:
40
+ """
41
+ Measure the average execution time of *fn* over *iterations* runs.
42
+
43
+ Parameters
44
+ ----------
45
+ fn:
46
+ Function to time.
47
+ *args:
48
+ Positional arguments forwarded to *fn*.
49
+ iterations:
50
+ Number of times to repeat the measurement. Defaults to 1.
51
+ **kwargs:
52
+ Keyword arguments forwarded to *fn*.
53
+
54
+ Returns
55
+ -------
56
+ float
57
+ Average execution time in seconds.
58
+ """
59
+ total: float = 0.0
60
+ for _ in range(iterations):
61
+ total += _timeit(fn, *args, **kwargs)
62
+ return total / iterations
63
+
64
+
65
+ def benchmark_insert(
66
+ tree_class: type,
67
+ n: int = 1000,
68
+ value_factory: Callable[[int], Any] = lambda i: i,
69
+ ) -> float:
70
+ """
71
+ Benchmark insertion of *n* elements into a tree.
72
+
73
+ Parameters
74
+ ----------
75
+ tree_class:
76
+ A pkstruct tree class (e.g. ``BinarySearchTree``, ``AVLTree``).
77
+ n:
78
+ Number of elements to insert. Defaults to 1000.
79
+ value_factory:
80
+ Callable ``f(i) -> value`` that generates the i-th key (0-indexed).
81
+
82
+ Returns
83
+ -------
84
+ float
85
+ Total time in seconds for all *n* insertions.
86
+ """
87
+ tree = tree_class()
88
+ start = time.perf_counter()
89
+ for i in range(n):
90
+ tree.insert(value_factory(i))
91
+ return time.perf_counter() - start
92
+
93
+
94
+ def benchmark_search(
95
+ tree_class: type,
96
+ n: int = 1000,
97
+ value_factory: Callable[[int], Any] = lambda i: i,
98
+ ) -> tuple[float, float]:
99
+ """
100
+ Benchmark search (found + not-found) in a tree of size *n*.
101
+
102
+ Parameters
103
+ ----------
104
+ tree_class:
105
+ A pkstruct tree class.
106
+ n:
107
+ Number of elements to pre-populate and search.
108
+ value_factory:
109
+ Callable ``f(i) -> key``.
110
+
111
+ Returns
112
+ -------
113
+ tuple[float, float]
114
+ ``(found_time, not_found_time)`` in seconds.
115
+
116
+ - ``found_time``: time to search for each of the *n* keys.
117
+ - ``not_found_time``: time to search for *n* keys that are
118
+ guaranteed absent.
119
+ """
120
+ tree = tree_class()
121
+ keys = [value_factory(i) for i in range(n)]
122
+ for k in keys:
123
+ tree.insert(k)
124
+
125
+ # Found searches
126
+ start = time.perf_counter()
127
+ for k in keys:
128
+ tree.search(k)
129
+ found_time = time.perf_counter() - start
130
+
131
+ # Not-found searches
132
+ not_found_keys = [value_factory(i + n) for i in range(n)]
133
+ start = time.perf_counter()
134
+ for k in not_found_keys:
135
+ tree.search(k)
136
+ not_found_time = time.perf_counter() - start
137
+
138
+ return found_time, not_found_time
139
+
140
+
141
+ def benchmark_delete(
142
+ tree_class: type,
143
+ n: int = 1000,
144
+ value_factory: Callable[[int], Any] = lambda i: i,
145
+ ) -> float:
146
+ """
147
+ Benchmark deletion of *n* elements from a tree.
148
+
149
+ Parameters
150
+ ----------
151
+ tree_class:
152
+ A pkstruct tree class.
153
+ n:
154
+ Number of elements to insert and then delete.
155
+ value_factory:
156
+ Callable ``f(i) -> key``.
157
+
158
+ Returns
159
+ -------
160
+ float
161
+ Total time in seconds for all *n* deletions.
162
+ """
163
+ tree = tree_class()
164
+ keys = [value_factory(i) for i in range(n)]
165
+ for k in keys:
166
+ tree.insert(k)
167
+
168
+ start = time.perf_counter()
169
+ for k in keys:
170
+ tree.delete(k)
171
+ return time.perf_counter() - start
172
+
173
+
174
+ def compare_operations(
175
+ tree_classes: list[type],
176
+ sizes: list[int] | None = None,
177
+ ) -> dict[str, Any]:
178
+ """
179
+ Compare insert / search / delete performance across multiple tree classes.
180
+
181
+ Parameters
182
+ ----------
183
+ tree_classes:
184
+ List of tree classes to benchmark.
185
+ sizes:
186
+ List of element counts to test. Defaults to ``[100, 1000, 10000]``.
187
+
188
+ Returns
189
+ -------
190
+ dict
191
+ Nested dictionary::
192
+
193
+ {
194
+ class_name: {
195
+ size: {
196
+ "insert": float,
197
+ "search_found": float,
198
+ "search_not_found": float,
199
+ "delete": float,
200
+ },
201
+ ...
202
+ },
203
+ ...
204
+ }
205
+
206
+ Times are in seconds.
207
+ """
208
+ if sizes is None:
209
+ sizes = [100, 1000, 10000]
210
+
211
+ results: dict[str, Any] = {}
212
+
213
+ for cls in tree_classes:
214
+ name = cls.__name__
215
+ class_results: dict[str, Any] = {}
216
+
217
+ for n in sizes:
218
+ insert_time = benchmark_insert(cls, n=n)
219
+ search_found, search_not_found = benchmark_search(cls, n=n)
220
+ delete_time = benchmark_delete(cls, n=n)
221
+
222
+ class_results[str(n)] = {
223
+ "insert": insert_time,
224
+ "search_found": search_found,
225
+ "search_not_found": search_not_found,
226
+ "delete": delete_time,
227
+ }
228
+
229
+ results[name] = class_results
230
+
231
+ return results
File without changes