d-ary-heap 2.2.0 → 2.5.0
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.
- package/README.md +100 -6
- package/dist/index.cjs +231 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +324 -6
- package/dist/index.d.ts +324 -6
- package/dist/index.js +225 -8
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/PriorityQueue.ts +128 -6
- package/src/comparators.ts +1 -1
- package/src/index.ts +18 -1
- package/src/instrumentation.ts +339 -0
package/src/PriorityQueue.ts
CHANGED
|
@@ -6,14 +6,18 @@
|
|
|
6
6
|
* - Min-heap or max-heap behavior via comparator functions
|
|
7
7
|
* - O(1) item lookup using Map for efficient priority updates
|
|
8
8
|
* - O(1) access to highest-priority item
|
|
9
|
-
* - O(log_d n) insert and
|
|
10
|
-
* - O(d · log_d n) pop and
|
|
9
|
+
* - O(log_d n) insert and increasePriority operations
|
|
10
|
+
* - O(d · log_d n) pop and decreasePriority operations
|
|
11
|
+
* - O((d+1) · log_d n) updatePriority (bidirectional)
|
|
12
|
+
* - Optional instrumentation hooks for performance analysis
|
|
11
13
|
*
|
|
12
|
-
* @version 2.
|
|
14
|
+
* @version 2.4.0
|
|
13
15
|
* @license Apache-2.0
|
|
14
16
|
* @copyright 2023-2025 Eric Jacopin
|
|
15
17
|
*/
|
|
16
18
|
|
|
19
|
+
import type { OperationType } from './instrumentation';
|
|
20
|
+
|
|
17
21
|
/** Type alias for position indices (cross-language consistency) */
|
|
18
22
|
export type Position = number;
|
|
19
23
|
|
|
@@ -41,6 +45,37 @@ export interface PriorityQueueOptions<T, K> {
|
|
|
41
45
|
keyExtractor: KeyExtractor<T, K>;
|
|
42
46
|
/** Initial capacity hint for pre-allocation */
|
|
43
47
|
initialCapacity?: number;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Optional hook called before each heap operation.
|
|
51
|
+
*
|
|
52
|
+
* This enables opt-in instrumentation for performance analysis without
|
|
53
|
+
* adding overhead when not used. Pair with an instrumented comparator
|
|
54
|
+
* to track comparison counts per operation type.
|
|
55
|
+
*
|
|
56
|
+
* @param type - The operation about to be performed
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const cmp = instrumentComparator(minBy((v) => v.priority));
|
|
61
|
+
* const pq = new PriorityQueue({
|
|
62
|
+
* comparator: cmp,
|
|
63
|
+
* keyExtractor: (v) => v.id,
|
|
64
|
+
* onBeforeOperation: (op) => cmp.startOperation(op),
|
|
65
|
+
* onAfterOperation: () => cmp.endOperation(),
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @see instrumentComparator from './instrumentation'
|
|
70
|
+
*/
|
|
71
|
+
onBeforeOperation?: (type: OperationType) => void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Optional hook called after each heap operation completes.
|
|
75
|
+
*
|
|
76
|
+
* @see onBeforeOperation for usage example
|
|
77
|
+
*/
|
|
78
|
+
onAfterOperation?: () => void;
|
|
44
79
|
}
|
|
45
80
|
|
|
46
81
|
/**
|
|
@@ -61,6 +96,7 @@ export interface PriorityQueueOptions<T, K> {
|
|
|
61
96
|
* - pop(): O(d · log_d n)
|
|
62
97
|
* - increasePriority(): O(log_d n)
|
|
63
98
|
* - decreasePriority(): O(d · log_d n)
|
|
99
|
+
* - updatePriority(): O((d+1) · log_d n)
|
|
64
100
|
* - contains(): O(1)
|
|
65
101
|
* - len(), isEmpty(), d(): O(1)
|
|
66
102
|
*
|
|
@@ -83,6 +119,12 @@ export class PriorityQueue<T, K = string | number> {
|
|
|
83
119
|
/** Key extractor for identity-based lookup */
|
|
84
120
|
private readonly keyExtractor: KeyExtractor<T, K>;
|
|
85
121
|
|
|
122
|
+
/** Optional hook called before operations (for instrumentation) */
|
|
123
|
+
private readonly onBeforeOperation: ((type: OperationType) => void) | undefined;
|
|
124
|
+
|
|
125
|
+
/** Optional hook called after operations (for instrumentation) */
|
|
126
|
+
private readonly onAfterOperation: (() => void) | undefined;
|
|
127
|
+
|
|
86
128
|
/**
|
|
87
129
|
* Create a new d-ary heap priority queue.
|
|
88
130
|
*
|
|
@@ -108,6 +150,8 @@ export class PriorityQueue<T, K = string | number> {
|
|
|
108
150
|
this.depth = d;
|
|
109
151
|
this.comparator = options.comparator;
|
|
110
152
|
this.keyExtractor = options.keyExtractor;
|
|
153
|
+
this.onBeforeOperation = options.onBeforeOperation;
|
|
154
|
+
this.onAfterOperation = options.onAfterOperation;
|
|
111
155
|
|
|
112
156
|
// Pre-allocate if capacity hint provided
|
|
113
157
|
const capacity = options.initialCapacity ?? 0;
|
|
@@ -255,17 +299,22 @@ export class PriorityQueue<T, K = string | number> {
|
|
|
255
299
|
* to update existing items.
|
|
256
300
|
*/
|
|
257
301
|
insert(item: T): void {
|
|
302
|
+
this.onBeforeOperation?.('insert');
|
|
303
|
+
|
|
258
304
|
const index = this.container.length;
|
|
259
305
|
this.container.push(item);
|
|
260
306
|
|
|
261
307
|
// Fast path: first item doesn't need sift-up
|
|
262
308
|
if (index === 0) {
|
|
263
309
|
this.positions.set(this.keyExtractor(item), 0);
|
|
310
|
+
this.onAfterOperation?.();
|
|
264
311
|
return;
|
|
265
312
|
}
|
|
266
313
|
|
|
267
314
|
this.positions.set(this.keyExtractor(item), index);
|
|
268
315
|
this.moveUp(index);
|
|
316
|
+
|
|
317
|
+
this.onAfterOperation?.();
|
|
269
318
|
}
|
|
270
319
|
|
|
271
320
|
/**
|
|
@@ -337,15 +386,20 @@ export class PriorityQueue<T, K = string | number> {
|
|
|
337
386
|
* This method only moves items upward for performance.
|
|
338
387
|
*/
|
|
339
388
|
increasePriority(updatedItem: T): void {
|
|
389
|
+
this.onBeforeOperation?.('increasePriority');
|
|
390
|
+
|
|
340
391
|
const key = this.keyExtractor(updatedItem);
|
|
341
392
|
const index = this.positions.get(key);
|
|
342
393
|
|
|
343
394
|
if (index === undefined) {
|
|
395
|
+
this.onAfterOperation?.();
|
|
344
396
|
throw new Error('Item not found in priority queue');
|
|
345
397
|
}
|
|
346
398
|
|
|
347
399
|
this.container[index] = updatedItem;
|
|
348
400
|
this.moveUp(index);
|
|
401
|
+
|
|
402
|
+
this.onAfterOperation?.();
|
|
349
403
|
}
|
|
350
404
|
|
|
351
405
|
/** Alias for increasePriority() - snake_case for cross-language consistency */
|
|
@@ -375,6 +429,30 @@ export class PriorityQueue<T, K = string | number> {
|
|
|
375
429
|
this.increasePriorityByIndex(index);
|
|
376
430
|
}
|
|
377
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Decrease the priority of the item at the given index.
|
|
434
|
+
* Time complexity: O(d · log_d n)
|
|
435
|
+
*
|
|
436
|
+
* @param index - Index of the item in the heap array
|
|
437
|
+
* @throws Error if index is out of bounds
|
|
438
|
+
*
|
|
439
|
+
* @remarks
|
|
440
|
+
* This is a lower-level method. Prefer decreasePriority() with the item itself.
|
|
441
|
+
* The item at the given index should already have its priority value updated
|
|
442
|
+
* in the container before calling this method.
|
|
443
|
+
*/
|
|
444
|
+
decreasePriorityByIndex(index: number): void {
|
|
445
|
+
if (index < 0 || index >= this.container.length) {
|
|
446
|
+
throw new Error('Index out of bounds');
|
|
447
|
+
}
|
|
448
|
+
this.moveDown(index);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Alias for decreasePriorityByIndex() - snake_case for cross-language consistency */
|
|
452
|
+
decrease_priority_by_index(index: number): void {
|
|
453
|
+
this.decreasePriorityByIndex(index);
|
|
454
|
+
}
|
|
455
|
+
|
|
378
456
|
/**
|
|
379
457
|
* Decrease the priority of an existing item (move toward leaves).
|
|
380
458
|
* Time complexity: O(d · log_d n)
|
|
@@ -385,20 +463,23 @@ export class PriorityQueue<T, K = string | number> {
|
|
|
385
463
|
* @remarks
|
|
386
464
|
* For min-heap: increasing the priority value decreases importance.
|
|
387
465
|
* For max-heap: decreasing the priority value decreases importance.
|
|
388
|
-
* This method
|
|
466
|
+
* This method only moves items downward. Use updatePriority() if direction is unknown.
|
|
389
467
|
*/
|
|
390
468
|
decreasePriority(updatedItem: T): void {
|
|
469
|
+
this.onBeforeOperation?.('decreasePriority');
|
|
470
|
+
|
|
391
471
|
const key = this.keyExtractor(updatedItem);
|
|
392
472
|
const index = this.positions.get(key);
|
|
393
473
|
|
|
394
474
|
if (index === undefined) {
|
|
475
|
+
this.onAfterOperation?.();
|
|
395
476
|
throw new Error('Item not found in priority queue');
|
|
396
477
|
}
|
|
397
478
|
|
|
398
479
|
this.container[index] = updatedItem;
|
|
399
|
-
// Check both directions since we don't know if priority actually decreased
|
|
400
|
-
this.moveUp(index);
|
|
401
480
|
this.moveDown(index);
|
|
481
|
+
|
|
482
|
+
this.onAfterOperation?.();
|
|
402
483
|
}
|
|
403
484
|
|
|
404
485
|
/** Alias for decreasePriority() - snake_case for cross-language consistency */
|
|
@@ -406,6 +487,42 @@ export class PriorityQueue<T, K = string | number> {
|
|
|
406
487
|
this.decreasePriority(updatedItem);
|
|
407
488
|
}
|
|
408
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Update the priority of an existing item when direction is unknown.
|
|
492
|
+
* Time complexity: O((d+1) · log_d n) - checks both directions
|
|
493
|
+
*
|
|
494
|
+
* @param updatedItem - Item with same identity but updated priority
|
|
495
|
+
* @throws Error if item not found
|
|
496
|
+
*
|
|
497
|
+
* @remarks
|
|
498
|
+
* Use this method when you don't know whether the priority increased or decreased.
|
|
499
|
+
* If you know the direction, prefer increasePriority() or decreasePriority() for
|
|
500
|
+
* better performance (log_d n vs (d+1) · log_d n comparisons).
|
|
501
|
+
*/
|
|
502
|
+
updatePriority(updatedItem: T): void {
|
|
503
|
+
this.onBeforeOperation?.('updatePriority');
|
|
504
|
+
|
|
505
|
+
const key = this.keyExtractor(updatedItem);
|
|
506
|
+
const index = this.positions.get(key);
|
|
507
|
+
|
|
508
|
+
if (index === undefined) {
|
|
509
|
+
this.onAfterOperation?.();
|
|
510
|
+
throw new Error('Item not found in priority queue');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
this.container[index] = updatedItem;
|
|
514
|
+
// Check both directions since we don't know which way priority changed
|
|
515
|
+
this.moveUp(index);
|
|
516
|
+
this.moveDown(index);
|
|
517
|
+
|
|
518
|
+
this.onAfterOperation?.();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Alias for updatePriority() - snake_case for cross-language consistency */
|
|
522
|
+
update_priority(updatedItem: T): void {
|
|
523
|
+
this.updatePriority(updatedItem);
|
|
524
|
+
}
|
|
525
|
+
|
|
409
526
|
/**
|
|
410
527
|
* Remove and return the highest-priority item.
|
|
411
528
|
* Time complexity: O(d · log_d n)
|
|
@@ -413,10 +530,13 @@ export class PriorityQueue<T, K = string | number> {
|
|
|
413
530
|
* @returns The removed item, or undefined if empty
|
|
414
531
|
*/
|
|
415
532
|
pop(): T | undefined {
|
|
533
|
+
this.onBeforeOperation?.('pop');
|
|
534
|
+
|
|
416
535
|
const container = this.container;
|
|
417
536
|
const n = container.length;
|
|
418
537
|
|
|
419
538
|
if (n === 0) {
|
|
539
|
+
this.onAfterOperation?.();
|
|
420
540
|
return undefined;
|
|
421
541
|
}
|
|
422
542
|
|
|
@@ -426,6 +546,7 @@ export class PriorityQueue<T, K = string | number> {
|
|
|
426
546
|
|
|
427
547
|
if (n === 1) {
|
|
428
548
|
container.length = 0;
|
|
549
|
+
this.onAfterOperation?.();
|
|
429
550
|
return top;
|
|
430
551
|
}
|
|
431
552
|
|
|
@@ -437,6 +558,7 @@ export class PriorityQueue<T, K = string | number> {
|
|
|
437
558
|
|
|
438
559
|
this.moveDown(0);
|
|
439
560
|
|
|
561
|
+
this.onAfterOperation?.();
|
|
440
562
|
return top;
|
|
441
563
|
}
|
|
442
564
|
|
package/src/comparators.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @packageDocumentation
|
|
7
7
|
* @module d-ary-heap
|
|
8
|
-
* @version 2.
|
|
8
|
+
* @version 2.4.0
|
|
9
9
|
* @license Apache-2.0
|
|
10
10
|
* @copyright 2023-2025 Eric Jacopin
|
|
11
11
|
*/
|
|
@@ -28,3 +28,20 @@ export {
|
|
|
28
28
|
reverse,
|
|
29
29
|
chain,
|
|
30
30
|
} from './comparators';
|
|
31
|
+
|
|
32
|
+
// Instrumentation utilities for performance analysis (opt-in, zero-cost when disabled)
|
|
33
|
+
export {
|
|
34
|
+
createComparisonStats,
|
|
35
|
+
instrumentComparator,
|
|
36
|
+
theoreticalInsertComparisons,
|
|
37
|
+
theoreticalPopComparisons,
|
|
38
|
+
theoreticalIncreasePriorityComparisons,
|
|
39
|
+
theoreticalDecreasePriorityComparisons,
|
|
40
|
+
theoreticalUpdatePriorityComparisons,
|
|
41
|
+
} from './instrumentation';
|
|
42
|
+
|
|
43
|
+
export type {
|
|
44
|
+
OperationType,
|
|
45
|
+
ComparisonStats,
|
|
46
|
+
InstrumentedComparator,
|
|
47
|
+
} from './instrumentation';
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instrumentation utilities for d-ary heap performance analysis.
|
|
3
|
+
*
|
|
4
|
+
* This module provides opt-in instrumentation to count comparisons performed
|
|
5
|
+
* during heap operations. It is designed for:
|
|
6
|
+
*
|
|
7
|
+
* - **Educational purposes**: Understanding the theoretical vs actual cost of heap operations
|
|
8
|
+
* - **Benchmarking**: Measuring real comparison counts across different arities
|
|
9
|
+
* - **Visualization**: Powering interactive demos that show heap behavior
|
|
10
|
+
*
|
|
11
|
+
* ## Design Philosophy: Zero-Cost When Disabled
|
|
12
|
+
*
|
|
13
|
+
* Instrumentation follows these principles:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Opt-in only**: No overhead when not using instrumentation
|
|
16
|
+
* 2. **Non-breaking**: Existing code continues to work unchanged
|
|
17
|
+
* 3. **Per-operation tracking**: Distinguish insert/pop/decreasePriority comparisons
|
|
18
|
+
*
|
|
19
|
+
* ## Cross-Language Consistency
|
|
20
|
+
*
|
|
21
|
+
* Currently, instrumentation is implemented in TypeScript only. The table below
|
|
22
|
+
* shows the idiomatic zero-cost approach for each language, planned for v2.5.0:
|
|
23
|
+
*
|
|
24
|
+
* | Language | Mechanism | Overhead When Disabled | Status |
|
|
25
|
+
* |------------|----------------------------------|------------------------|--------|
|
|
26
|
+
* | TypeScript | Optional hooks + instrumented comparator | Zero (JIT optimization) | ✅ Implemented |
|
|
27
|
+
* | Go | Nil stats pointer | ~1 cycle (nil check) | Planned v2.5.0 |
|
|
28
|
+
* | Rust | Generic over StatsCollector trait | Zero (monomorphization) | Planned v2.5.0 |
|
|
29
|
+
* | C++ | Template policy class | Zero (inlining) | Planned v2.5.0 |
|
|
30
|
+
* | Zig | Comptime bool parameter | Zero (branch elimination) | Planned v2.5.0 |
|
|
31
|
+
*
|
|
32
|
+
* ## Usage Example
|
|
33
|
+
*
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import { PriorityQueue, minBy, instrumentComparator } from 'd-ary-heap';
|
|
36
|
+
*
|
|
37
|
+
* // 1. Wrap your comparator with instrumentation
|
|
38
|
+
* const comparator = instrumentComparator(minBy((v: Vertex) => v.distance));
|
|
39
|
+
*
|
|
40
|
+
* // 2. Create priority queue with operation hooks
|
|
41
|
+
* const pq = new PriorityQueue({
|
|
42
|
+
* d: 4,
|
|
43
|
+
* comparator,
|
|
44
|
+
* keyExtractor: (v) => v.id,
|
|
45
|
+
* onBeforeOperation: (op) => comparator.startOperation(op),
|
|
46
|
+
* onAfterOperation: () => comparator.endOperation(),
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* // 3. Use normally - comparisons are tracked automatically
|
|
50
|
+
* pq.insert({ id: 'A', distance: 0 });
|
|
51
|
+
* pq.insert({ id: 'B', distance: 5 });
|
|
52
|
+
* pq.pop();
|
|
53
|
+
*
|
|
54
|
+
* // 4. Access statistics
|
|
55
|
+
* console.log(comparator.stats);
|
|
56
|
+
* // { insert: 1, pop: 2, decreasePriority: 0, increasePriority: 0, updatePriority: 0, total: 3 }
|
|
57
|
+
*
|
|
58
|
+
* // 5. Reset for next measurement
|
|
59
|
+
* comparator.stats.reset();
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* ## Theoretical Complexity Reference
|
|
63
|
+
*
|
|
64
|
+
* For a d-ary heap with n elements:
|
|
65
|
+
*
|
|
66
|
+
* | Operation | Comparisons (worst case) |
|
|
67
|
+
* |------------------|-------------------------------|
|
|
68
|
+
* | insert | ⌊log_d(n)⌋ |
|
|
69
|
+
* | pop | d × ⌊log_d(n)⌋ |
|
|
70
|
+
* | increasePriority | ⌊log_d(n)⌋ (moveUp only) |
|
|
71
|
+
* | decreasePriority | d × ⌊log_d(n)⌋ (moveDown only)|
|
|
72
|
+
* | updatePriority | (d+1) × ⌊log_d(n)⌋ (both) |
|
|
73
|
+
*
|
|
74
|
+
* The demo visualization compares actual counts against these theoretical bounds.
|
|
75
|
+
*
|
|
76
|
+
* @module instrumentation
|
|
77
|
+
* @version 2.4.0
|
|
78
|
+
* @license Apache-2.0
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
import type { Comparator } from './PriorityQueue';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Operation types that can be tracked.
|
|
85
|
+
*
|
|
86
|
+
* Note: `updatePriority` is tracked separately for when the caller doesn't know
|
|
87
|
+
* whether priority increased or decreased (checks both directions).
|
|
88
|
+
*/
|
|
89
|
+
export type OperationType = 'insert' | 'pop' | 'decreasePriority' | 'increasePriority' | 'updatePriority';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Statistics tracking comparison counts per operation type.
|
|
93
|
+
*
|
|
94
|
+
* All counts start at zero and accumulate until `reset()` is called.
|
|
95
|
+
*/
|
|
96
|
+
export interface ComparisonStats {
|
|
97
|
+
/** Comparisons during insert operations (moveUp) */
|
|
98
|
+
insert: number;
|
|
99
|
+
|
|
100
|
+
/** Comparisons during pop operations (moveDown + bestChildPosition) */
|
|
101
|
+
pop: number;
|
|
102
|
+
|
|
103
|
+
/** Comparisons during decreasePriority operations (moveDown only) */
|
|
104
|
+
decreasePriority: number;
|
|
105
|
+
|
|
106
|
+
/** Comparisons during increasePriority operations (moveUp only) */
|
|
107
|
+
increasePriority: number;
|
|
108
|
+
|
|
109
|
+
/** Comparisons during updatePriority operations (moveUp + moveDown) */
|
|
110
|
+
updatePriority: number;
|
|
111
|
+
|
|
112
|
+
/** Total comparisons across all operation types */
|
|
113
|
+
readonly total: number;
|
|
114
|
+
|
|
115
|
+
/** Reset all counters to zero */
|
|
116
|
+
reset(): void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* An instrumented comparator that tracks comparison counts.
|
|
121
|
+
*
|
|
122
|
+
* This extends a regular comparator with:
|
|
123
|
+
* - `stats`: Current comparison counts
|
|
124
|
+
* - `startOperation(type)`: Begin tracking for an operation
|
|
125
|
+
* - `endOperation()`: Stop tracking current operation
|
|
126
|
+
*
|
|
127
|
+
* The comparator itself remains a valid `Comparator<T>` and can be used
|
|
128
|
+
* anywhere a regular comparator is expected.
|
|
129
|
+
*/
|
|
130
|
+
export interface InstrumentedComparator<T> extends Comparator<T> {
|
|
131
|
+
/** Current comparison statistics */
|
|
132
|
+
readonly stats: ComparisonStats;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Signal the start of a heap operation.
|
|
136
|
+
* Comparisons will be attributed to this operation type until `endOperation()`.
|
|
137
|
+
*
|
|
138
|
+
* @param type - The operation type being started
|
|
139
|
+
*/
|
|
140
|
+
startOperation(type: OperationType): void;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Signal the end of the current heap operation.
|
|
144
|
+
* Subsequent comparisons will not be counted until the next `startOperation()`.
|
|
145
|
+
*/
|
|
146
|
+
endOperation(): void;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create comparison statistics tracker.
|
|
151
|
+
*
|
|
152
|
+
* @returns Fresh stats object with all counts at zero
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```typescript
|
|
156
|
+
* const stats = createComparisonStats();
|
|
157
|
+
* stats.insert = 5;
|
|
158
|
+
* stats.pop = 10;
|
|
159
|
+
* console.log(stats.total); // 15
|
|
160
|
+
* stats.reset();
|
|
161
|
+
* console.log(stats.total); // 0
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export function createComparisonStats(): ComparisonStats {
|
|
165
|
+
const stats: ComparisonStats = {
|
|
166
|
+
insert: 0,
|
|
167
|
+
pop: 0,
|
|
168
|
+
decreasePriority: 0,
|
|
169
|
+
increasePriority: 0,
|
|
170
|
+
updatePriority: 0,
|
|
171
|
+
|
|
172
|
+
get total(): number {
|
|
173
|
+
return this.insert + this.pop + this.decreasePriority + this.increasePriority + this.updatePriority;
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
reset(): void {
|
|
177
|
+
this.insert = 0;
|
|
178
|
+
this.pop = 0;
|
|
179
|
+
this.decreasePriority = 0;
|
|
180
|
+
this.increasePriority = 0;
|
|
181
|
+
this.updatePriority = 0;
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return stats;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Wrap a comparator with instrumentation to track comparison counts.
|
|
190
|
+
*
|
|
191
|
+
* The returned comparator:
|
|
192
|
+
* - Behaves identically to the original for comparison purposes
|
|
193
|
+
* - Tracks how many times it's called, attributed to operation types
|
|
194
|
+
* - Has zero overhead when `startOperation()` hasn't been called
|
|
195
|
+
*
|
|
196
|
+
* ## How It Works
|
|
197
|
+
*
|
|
198
|
+
* 1. Call `startOperation('insert')` before `pq.insert()`
|
|
199
|
+
* 2. The comparator increments `stats.insert` for each comparison
|
|
200
|
+
* 3. Call `endOperation()` after the operation completes
|
|
201
|
+
* 4. Repeat for other operations
|
|
202
|
+
*
|
|
203
|
+
* The `PriorityQueue` class supports `onBeforeOperation` and `onAfterOperation`
|
|
204
|
+
* hooks to automate this.
|
|
205
|
+
*
|
|
206
|
+
* ## Performance Note
|
|
207
|
+
*
|
|
208
|
+
* When `currentOperation` is null (between operations), the instrumented
|
|
209
|
+
* comparator performs only a single null check before calling the original.
|
|
210
|
+
* Modern JavaScript engines optimize this extremely well.
|
|
211
|
+
*
|
|
212
|
+
* @param comparator - The original comparator to instrument
|
|
213
|
+
* @returns An instrumented comparator with stats tracking
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* import { minBy, instrumentComparator } from 'd-ary-heap';
|
|
218
|
+
*
|
|
219
|
+
* const cmp = instrumentComparator(minBy<number, number>(x => x));
|
|
220
|
+
*
|
|
221
|
+
* // Manual usage (without hooks)
|
|
222
|
+
* cmp.startOperation('insert');
|
|
223
|
+
* console.log(cmp(5, 3)); // false, and stats.insert++
|
|
224
|
+
* console.log(cmp(3, 5)); // true, and stats.insert++
|
|
225
|
+
* cmp.endOperation();
|
|
226
|
+
*
|
|
227
|
+
* console.log(cmp.stats.insert); // 2
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
export function instrumentComparator<T>(comparator: Comparator<T>): InstrumentedComparator<T> {
|
|
231
|
+
const stats = createComparisonStats();
|
|
232
|
+
let currentOperation: OperationType | null = null;
|
|
233
|
+
|
|
234
|
+
// Create the instrumented function
|
|
235
|
+
const instrumented = ((a: T, b: T): boolean => {
|
|
236
|
+
// Only count when actively tracking an operation
|
|
237
|
+
if (currentOperation !== null) {
|
|
238
|
+
stats[currentOperation]++;
|
|
239
|
+
}
|
|
240
|
+
return comparator(a, b);
|
|
241
|
+
}) as InstrumentedComparator<T>;
|
|
242
|
+
|
|
243
|
+
// Attach stats (read-only from outside)
|
|
244
|
+
Object.defineProperty(instrumented, 'stats', {
|
|
245
|
+
value: stats,
|
|
246
|
+
writable: false,
|
|
247
|
+
enumerable: true,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Attach operation control methods
|
|
251
|
+
instrumented.startOperation = (type: OperationType): void => {
|
|
252
|
+
currentOperation = type;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
instrumented.endOperation = (): void => {
|
|
256
|
+
currentOperation = null;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return instrumented;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Calculate theoretical comparison count for an insert operation.
|
|
264
|
+
*
|
|
265
|
+
* Insert performs at most ⌊log_d(n)⌋ comparisons (one per level during moveUp).
|
|
266
|
+
*
|
|
267
|
+
* @param n - Number of elements in heap AFTER insert
|
|
268
|
+
* @param d - Heap arity
|
|
269
|
+
* @returns Theoretical worst-case comparison count
|
|
270
|
+
*/
|
|
271
|
+
export function theoreticalInsertComparisons(n: number, d: number): number {
|
|
272
|
+
if (n <= 1) return 0;
|
|
273
|
+
return Math.floor(Math.log(n) / Math.log(d));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Calculate theoretical comparison count for a pop operation.
|
|
278
|
+
*
|
|
279
|
+
* Pop performs at most d × ⌊log_d(n)⌋ comparisons:
|
|
280
|
+
* - At each level, find best among d children (d-1 comparisons)
|
|
281
|
+
* - Compare best child with current (1 comparison)
|
|
282
|
+
* - Total: d comparisons per level × ⌊log_d(n)⌋ levels
|
|
283
|
+
*
|
|
284
|
+
* @param n - Number of elements in heap BEFORE pop
|
|
285
|
+
* @param d - Heap arity
|
|
286
|
+
* @returns Theoretical worst-case comparison count
|
|
287
|
+
*/
|
|
288
|
+
export function theoreticalPopComparisons(n: number, d: number): number {
|
|
289
|
+
if (n <= 1) return 0;
|
|
290
|
+
const height = Math.floor(Math.log(n) / Math.log(d));
|
|
291
|
+
return d * height;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Calculate theoretical comparison count for an increasePriority operation.
|
|
296
|
+
*
|
|
297
|
+
* IncreasePriority performs only moveUp (item became more important).
|
|
298
|
+
* Worst case: ⌊log_d(n)⌋ comparisons (one per level).
|
|
299
|
+
*
|
|
300
|
+
* @param n - Number of elements in heap
|
|
301
|
+
* @param d - Heap arity
|
|
302
|
+
* @returns Theoretical worst-case comparison count
|
|
303
|
+
*/
|
|
304
|
+
export function theoreticalIncreasePriorityComparisons(n: number, d: number): number {
|
|
305
|
+
if (n <= 1) return 0;
|
|
306
|
+
return Math.floor(Math.log(n) / Math.log(d));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Calculate theoretical comparison count for a decreasePriority operation.
|
|
311
|
+
*
|
|
312
|
+
* DecreasePriority performs only moveDown (item became less important).
|
|
313
|
+
* Worst case: d × ⌊log_d(n)⌋ comparisons.
|
|
314
|
+
*
|
|
315
|
+
* @param n - Number of elements in heap
|
|
316
|
+
* @param d - Heap arity
|
|
317
|
+
* @returns Theoretical worst-case comparison count
|
|
318
|
+
*/
|
|
319
|
+
export function theoreticalDecreasePriorityComparisons(n: number, d: number): number {
|
|
320
|
+
if (n <= 1) return 0;
|
|
321
|
+
const height = Math.floor(Math.log(n) / Math.log(d));
|
|
322
|
+
return d * height;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Calculate theoretical comparison count for an updatePriority operation.
|
|
327
|
+
*
|
|
328
|
+
* UpdatePriority performs both moveUp and moveDown (direction unknown).
|
|
329
|
+
* Worst case: (d + 1) × ⌊log_d(n)⌋ comparisons.
|
|
330
|
+
*
|
|
331
|
+
* @param n - Number of elements in heap
|
|
332
|
+
* @param d - Heap arity
|
|
333
|
+
* @returns Theoretical worst-case comparison count
|
|
334
|
+
*/
|
|
335
|
+
export function theoreticalUpdatePriorityComparisons(n: number, d: number): number {
|
|
336
|
+
if (n <= 1) return 0;
|
|
337
|
+
const height = Math.floor(Math.log(n) / Math.log(d));
|
|
338
|
+
return (d + 1) * height;
|
|
339
|
+
}
|