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.
- pkstruct/__init__.py +167 -0
- pkstruct/graphs/__init__.py +127 -0
- pkstruct/graphs/connectivity.py +157 -0
- pkstruct/graphs/directed.py +95 -0
- pkstruct/graphs/exceptions.py +63 -0
- pkstruct/graphs/graph.py +262 -0
- pkstruct/graphs/mst.py +118 -0
- pkstruct/graphs/scc.py +138 -0
- pkstruct/graphs/shortest_path.py +250 -0
- pkstruct/graphs/topo_sort.py +108 -0
- pkstruct/graphs/traversal.py +175 -0
- pkstruct/graphs/visualization.py +90 -0
- pkstruct/graphs/weighted.py +37 -0
- pkstruct/linear/__init__.py +95 -0
- pkstruct/linear/deques/__init__.py +33 -0
- pkstruct/linear/deques/deque.py +194 -0
- pkstruct/linear/deques/linked_deque.py +198 -0
- pkstruct/linear/exceptions.py +26 -0
- pkstruct/linear/linked_lists/__init__.py +5 -0
- pkstruct/linear/linked_lists/_base.py +608 -0
- pkstruct/linear/linked_lists/circular_linked_list.py +230 -0
- pkstruct/linear/linked_lists/doubly_linked_list.py +151 -0
- pkstruct/linear/linked_lists/nodes.py +68 -0
- pkstruct/linear/linked_lists/singly_linked_list.py +136 -0
- pkstruct/linear/queues/__init__.py +44 -0
- pkstruct/linear/queues/circular_queue.py +258 -0
- pkstruct/linear/queues/linked_queue.py +186 -0
- pkstruct/linear/queues/priority_queue.py +202 -0
- pkstruct/linear/queues/queue.py +174 -0
- pkstruct/linear/stacks/__init__.py +38 -0
- pkstruct/linear/stacks/array_stack.py +165 -0
- pkstruct/linear/stacks/linked_stack.py +168 -0
- pkstruct/linear/stacks/stack.py +158 -0
- pkstruct/linear/utils/__init__.py +18 -0
- pkstruct/linear/utils/benchmark.py +255 -0
- pkstruct/linear/utils/debug_tools.py +239 -0
- pkstruct/linear/utils/helpers.py +143 -0
- pkstruct/linear/utils/iterators.py +148 -0
- pkstruct/linear/visualization/__init__.py +0 -0
- pkstruct/linear/visualization/ascii_visualizer.py +114 -0
- pkstruct/linear/visualization/linked_list_visualizer.py +126 -0
- pkstruct/shared/__init__.py +67 -0
- pkstruct/shared/benchmarking/__init__.py +78 -0
- pkstruct/shared/debugging/__init__.py +69 -0
- pkstruct/shared/exceptions/__init__.py +59 -0
- pkstruct/shared/serializers/__init__.py +65 -0
- pkstruct/shared/threading/__init__.py +43 -0
- pkstruct/shared/validators/__init__.py +98 -0
- pkstruct/shared/visualization/__init__.py +21 -0
- pkstruct/trees/__init__.py +92 -0
- pkstruct/trees/avl.py +321 -0
- pkstruct/trees/balancing.py +253 -0
- pkstruct/trees/bplus.py +425 -0
- pkstruct/trees/bst.py +948 -0
- pkstruct/trees/btree.py +504 -0
- pkstruct/trees/exceptions.py +96 -0
- pkstruct/trees/fenwick_tree.py +312 -0
- pkstruct/trees/interval_tree.py +541 -0
- pkstruct/trees/node.py +356 -0
- pkstruct/trees/red_black.py +710 -0
- pkstruct/trees/segment_tree.py +398 -0
- pkstruct/trees/traversal.py +456 -0
- pkstruct/trees/tree_helpers.py +366 -0
- pkstruct/trees/utils/__init__.py +15 -0
- pkstruct/trees/utils/complexity_helpers.py +231 -0
- pkstruct/trees/visualization/__init__.py +0 -0
- pkstruct/trees/visualization/ascii_renderer.py +220 -0
- pkstruct/trees/visualization/tree_printer.py +129 -0
- pkstruct-0.1.0.dist-info/METADATA +482 -0
- pkstruct-0.1.0.dist-info/RECORD +72 -0
- pkstruct-0.1.0.dist-info/WHEEL +4 -0
- 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
|