d-ary-heap 2.1.1

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.
@@ -0,0 +1,599 @@
1
+ /**
2
+ * d-ary Heap Priority Queue - TypeScript Implementation
3
+ *
4
+ * A generic d-ary heap (d-heap) priority queue with:
5
+ * - Configurable arity (d): number of children per node
6
+ * - Min-heap or max-heap behavior via comparator functions
7
+ * - O(1) item lookup using Map for efficient priority updates
8
+ * - O(1) access to highest-priority item
9
+ * - O(log_d n) insert and priority increase operations
10
+ * - O(d · log_d n) pop and priority decrease operations
11
+ *
12
+ * @version 2.0.0
13
+ * @license Apache-2.0
14
+ * @copyright 2023-2025 Eric Jacopin
15
+ */
16
+
17
+ /** Type alias for position indices (cross-language consistency) */
18
+ export type Position = number;
19
+
20
+ /**
21
+ * Comparator function type for priority comparison.
22
+ * Returns true if `a` has higher priority than `b`.
23
+ */
24
+ export type Comparator<T> = (a: T, b: T) => boolean;
25
+
26
+ /**
27
+ * Key extractor function type for identity-based lookup.
28
+ * Must return a value that can be used as a Map key (string or number recommended).
29
+ */
30
+ export type KeyExtractor<T, K> = (item: T) => K;
31
+
32
+ /**
33
+ * Configuration options for PriorityQueue construction.
34
+ */
35
+ export interface PriorityQueueOptions<T, K> {
36
+ /** Number of children per node (arity). Must be >= 1. Default: 2 */
37
+ d?: number;
38
+ /** Comparator function. Returns true if first arg has higher priority. */
39
+ comparator: Comparator<T>;
40
+ /** Key extractor for identity-based lookup. Required for decrease/increase priority. */
41
+ keyExtractor: KeyExtractor<T, K>;
42
+ /** Initial capacity hint for pre-allocation */
43
+ initialCapacity?: number;
44
+ }
45
+
46
+ /**
47
+ * Generic d-ary heap priority queue with O(1) lookup.
48
+ *
49
+ * A d-ary heap is a tree structure where:
50
+ * - Each node has at most d children
51
+ * - The root contains the highest-priority item
52
+ * - Each parent has higher priority than all its children
53
+ * - The tree is complete (filled left-to-right, level by level)
54
+ *
55
+ * This implementation uses an array-based representation with O(1) item lookup
56
+ * via a Map that tracks each item's position in the heap.
57
+ *
58
+ * ## Time Complexities
59
+ * - front(), peek(): O(1)
60
+ * - insert(): O(log_d n)
61
+ * - pop(): O(d · log_d n)
62
+ * - increasePriority(): O(log_d n)
63
+ * - decreasePriority(): O(d · log_d n)
64
+ * - contains(): O(1)
65
+ * - len(), isEmpty(), d(): O(1)
66
+ *
67
+ * @typeParam T - Item type stored in the queue
68
+ * @typeParam K - Key type for identity lookup (typically string or number)
69
+ */
70
+ export class PriorityQueue<T, K = string | number> {
71
+ /** Array-based heap storage (complete tree representation) */
72
+ private container: T[];
73
+
74
+ /** Maps each item's key to its position in the container for O(1) lookup */
75
+ private positions: Map<K, Position>;
76
+
77
+ /** Number of children per node (arity of the heap) */
78
+ private depth: number;
79
+
80
+ /** Comparator determining heap order (min vs max) */
81
+ private readonly comparator: Comparator<T>;
82
+
83
+ /** Key extractor for identity-based lookup */
84
+ private readonly keyExtractor: KeyExtractor<T, K>;
85
+
86
+ /**
87
+ * Create a new d-ary heap priority queue.
88
+ *
89
+ * @param options - Configuration options
90
+ * @throws Error if d < 1
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * // Min-heap by cost
95
+ * const pq = new PriorityQueue<Item, number>({
96
+ * d: 4,
97
+ * comparator: (a, b) => a.cost < b.cost,
98
+ * keyExtractor: (item) => item.id
99
+ * });
100
+ * ```
101
+ */
102
+ constructor(options: PriorityQueueOptions<T, K>) {
103
+ const d = options.d ?? 2;
104
+ if (d < 1) {
105
+ throw new Error('Heap arity (d) must be >= 1');
106
+ }
107
+
108
+ this.depth = d;
109
+ this.comparator = options.comparator;
110
+ this.keyExtractor = options.keyExtractor;
111
+
112
+ // Pre-allocate if capacity hint provided
113
+ const capacity = options.initialCapacity ?? 0;
114
+ this.container = capacity > 0 ? new Array<T>(capacity) : [];
115
+ if (capacity > 0) this.container.length = 0; // Reset length but keep capacity
116
+ this.positions = new Map<K, Position>();
117
+ }
118
+
119
+ /**
120
+ * Create a new priority queue with an initial item already inserted.
121
+ * Equivalent to Rust's `with_first()` constructor.
122
+ *
123
+ * @param options - Configuration options
124
+ * @param firstItem - First item to insert
125
+ * @returns New PriorityQueue with the item already inserted
126
+ */
127
+ static withFirst<T, K>(
128
+ options: PriorityQueueOptions<T, K>,
129
+ firstItem: T
130
+ ): PriorityQueue<T, K> {
131
+ const pq = new PriorityQueue<T, K>(options);
132
+ pq.insert(firstItem);
133
+ return pq;
134
+ }
135
+
136
+ // ===========================================================================
137
+ // Public API - Query Operations
138
+ // ===========================================================================
139
+
140
+ /**
141
+ * Get the number of items in the heap.
142
+ * Time complexity: O(1)
143
+ */
144
+ len(): number {
145
+ return this.container.length;
146
+ }
147
+
148
+ /** Alias for len() - backward compatibility */
149
+ get size(): number {
150
+ return this.container.length;
151
+ }
152
+
153
+ /**
154
+ * Check if the heap is empty.
155
+ * Time complexity: O(1)
156
+ */
157
+ isEmpty(): boolean {
158
+ return this.container.length === 0;
159
+ }
160
+
161
+ /** Alias for isEmpty() - snake_case for cross-language consistency */
162
+ is_empty(): boolean {
163
+ return this.isEmpty();
164
+ }
165
+
166
+ /**
167
+ * Get the arity (number of children per node) of the heap.
168
+ * Time complexity: O(1)
169
+ */
170
+ d(): number {
171
+ return this.depth;
172
+ }
173
+
174
+ /**
175
+ * Check if an item with the given key exists in the heap.
176
+ * Time complexity: O(1)
177
+ *
178
+ * @param item - Item to check (uses keyExtractor for identity)
179
+ */
180
+ contains(item: T): boolean {
181
+ return this.positions.has(this.keyExtractor(item));
182
+ }
183
+
184
+ /**
185
+ * Check if an item with the given key exists in the heap.
186
+ * Time complexity: O(1)
187
+ *
188
+ * @param key - Key to check directly
189
+ */
190
+ containsKey(key: K): boolean {
191
+ return this.positions.has(key);
192
+ }
193
+
194
+ /**
195
+ * Get the current position (index) of an item in the heap.
196
+ * Time complexity: O(1)
197
+ *
198
+ * @param item - Item to find (uses keyExtractor for identity)
199
+ * @returns Position index, or undefined if not found
200
+ */
201
+ getPosition(item: T): Position | undefined {
202
+ return this.positions.get(this.keyExtractor(item));
203
+ }
204
+
205
+ /**
206
+ * Get the current position (index) of an item by its key.
207
+ * Time complexity: O(1)
208
+ *
209
+ * @param key - Key to find
210
+ * @returns Position index, or undefined if not found
211
+ */
212
+ getPositionByKey(key: K): Position | undefined {
213
+ return this.positions.get(key);
214
+ }
215
+
216
+ /**
217
+ * Get the highest-priority item without removing it.
218
+ * Time complexity: O(1)
219
+ *
220
+ * @returns The highest-priority item
221
+ * @throws Error if heap is empty
222
+ */
223
+ front(): T {
224
+ const item = this.container[0];
225
+ if (item === undefined) {
226
+ throw new Error('front() called on empty priority queue');
227
+ }
228
+ return item;
229
+ }
230
+
231
+ /**
232
+ * Get the highest-priority item without removing it.
233
+ * Safe alternative to front().
234
+ * Time complexity: O(1)
235
+ *
236
+ * @returns The highest-priority item, or undefined if empty
237
+ */
238
+ peek(): T | undefined {
239
+ return this.container.length > 0 ? this.container[0] : undefined;
240
+ }
241
+
242
+ // ===========================================================================
243
+ // Public API - Modification Operations
244
+ // ===========================================================================
245
+
246
+ /**
247
+ * Insert a new item into the heap.
248
+ * Time complexity: O(log_d n)
249
+ *
250
+ * @param item - Item to insert
251
+ *
252
+ * @remarks
253
+ * If an item with the same key already exists, behavior is undefined.
254
+ * Use contains() to check first, or use increasePriority()/decreasePriority()
255
+ * to update existing items.
256
+ */
257
+ insert(item: T): void {
258
+ const index = this.container.length;
259
+ this.container.push(item);
260
+
261
+ // Fast path: first item doesn't need sift-up
262
+ if (index === 0) {
263
+ this.positions.set(this.keyExtractor(item), 0);
264
+ return;
265
+ }
266
+
267
+ this.positions.set(this.keyExtractor(item), index);
268
+ this.moveUp(index);
269
+ }
270
+
271
+ /**
272
+ * Insert multiple items into the heap.
273
+ * Uses heapify algorithm which is O(n) for bulk insertion vs O(n log n) for individual inserts.
274
+ * Time complexity: O(n) where n is total items after insertion
275
+ *
276
+ * @param items - Array of items to insert
277
+ *
278
+ * @remarks
279
+ * More efficient than calling insert() repeatedly when adding many items at once.
280
+ * If any item has a key that already exists, behavior is undefined.
281
+ */
282
+ insertMany(items: T[]): void {
283
+ if (items.length === 0) return;
284
+
285
+ const keyExtractor = this.keyExtractor;
286
+ const container = this.container;
287
+ const positions = this.positions;
288
+ const startIndex = container.length;
289
+
290
+ // Add all items to container and positions map
291
+ for (let i = 0; i < items.length; i++) {
292
+ const item = items[i]!;
293
+ container.push(item);
294
+ positions.set(keyExtractor(item), startIndex + i);
295
+ }
296
+
297
+ // If this was an empty heap, use heapify (O(n)) instead of n insertions (O(n log n))
298
+ if (startIndex === 0 && items.length > 1) {
299
+ this.heapify();
300
+ } else {
301
+ // Otherwise, sift up each new item
302
+ for (let i = startIndex; i < container.length; i++) {
303
+ this.moveUp(i);
304
+ }
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Build heap property from unordered array.
310
+ * Uses Floyd's algorithm - O(n) time complexity.
311
+ * Called internally by insertMany when starting from empty heap.
312
+ */
313
+ private heapify(): void {
314
+ const n = this.container.length;
315
+ if (n <= 1) return;
316
+
317
+ const d = this.depth;
318
+ // Start from last non-leaf node and sift down each
319
+ // Last non-leaf is parent of last element: floor((n-2)/d)
320
+ const lastNonLeaf = ((n - 2) / d) | 0;
321
+
322
+ for (let i = lastNonLeaf; i >= 0; i--) {
323
+ this.moveDown(i);
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Increase the priority of an existing item (move toward root).
329
+ * Time complexity: O(log_d n)
330
+ *
331
+ * @param updatedItem - Item with same identity but updated priority
332
+ * @throws Error if item not found
333
+ *
334
+ * @remarks
335
+ * For min-heap: decreasing the priority value increases importance.
336
+ * For max-heap: increasing the priority value increases importance.
337
+ * This method only moves items upward for performance.
338
+ */
339
+ increasePriority(updatedItem: T): void {
340
+ const key = this.keyExtractor(updatedItem);
341
+ const index = this.positions.get(key);
342
+
343
+ if (index === undefined) {
344
+ throw new Error('Item not found in priority queue');
345
+ }
346
+
347
+ this.container[index] = updatedItem;
348
+ this.moveUp(index);
349
+ }
350
+
351
+ /** Alias for increasePriority() - snake_case for cross-language consistency */
352
+ increase_priority(updatedItem: T): void {
353
+ this.increasePriority(updatedItem);
354
+ }
355
+
356
+ /**
357
+ * Increase the priority of the item at the given index.
358
+ * Time complexity: O(log_d n)
359
+ *
360
+ * @param index - Index of the item in the heap array
361
+ * @throws Error if index is out of bounds
362
+ *
363
+ * @remarks
364
+ * This is a lower-level method. Prefer increasePriority() with the item itself.
365
+ */
366
+ increasePriorityByIndex(index: number): void {
367
+ if (index < 0 || index >= this.container.length) {
368
+ throw new Error('Index out of bounds');
369
+ }
370
+ this.moveUp(index);
371
+ }
372
+
373
+ /** Alias for increasePriorityByIndex() - snake_case for cross-language consistency */
374
+ increase_priority_by_index(index: number): void {
375
+ this.increasePriorityByIndex(index);
376
+ }
377
+
378
+ /**
379
+ * Decrease the priority of an existing item (move toward leaves).
380
+ * Time complexity: O(d · log_d n)
381
+ *
382
+ * @param updatedItem - Item with same identity but updated priority
383
+ * @throws Error if item not found
384
+ *
385
+ * @remarks
386
+ * For min-heap: increasing the priority value decreases importance.
387
+ * For max-heap: decreasing the priority value decreases importance.
388
+ * This method checks both directions for robustness.
389
+ */
390
+ decreasePriority(updatedItem: T): void {
391
+ const key = this.keyExtractor(updatedItem);
392
+ const index = this.positions.get(key);
393
+
394
+ if (index === undefined) {
395
+ throw new Error('Item not found in priority queue');
396
+ }
397
+
398
+ this.container[index] = updatedItem;
399
+ // Check both directions since we don't know if priority actually decreased
400
+ this.moveUp(index);
401
+ this.moveDown(index);
402
+ }
403
+
404
+ /** Alias for decreasePriority() - snake_case for cross-language consistency */
405
+ decrease_priority(updatedItem: T): void {
406
+ this.decreasePriority(updatedItem);
407
+ }
408
+
409
+ /**
410
+ * Remove and return the highest-priority item.
411
+ * Time complexity: O(d · log_d n)
412
+ *
413
+ * @returns The removed item, or undefined if empty
414
+ */
415
+ pop(): T | undefined {
416
+ const container = this.container;
417
+ const n = container.length;
418
+
419
+ if (n === 0) {
420
+ return undefined;
421
+ }
422
+
423
+ const keyExtractor = this.keyExtractor;
424
+ const top = container[0]!;
425
+ this.positions.delete(keyExtractor(top));
426
+
427
+ if (n === 1) {
428
+ container.length = 0;
429
+ return top;
430
+ }
431
+
432
+ // Move last item to root and sift down
433
+ const lastItem = container[n - 1]!;
434
+ container[0] = lastItem;
435
+ this.positions.set(keyExtractor(lastItem), 0);
436
+ container.length = n - 1;
437
+
438
+ this.moveDown(0);
439
+
440
+ return top;
441
+ }
442
+
443
+ /**
444
+ * Remove and return multiple highest-priority items.
445
+ * More efficient than calling pop() repeatedly.
446
+ * Time complexity: O(count · d · log_d n)
447
+ *
448
+ * @param count - Number of items to remove
449
+ * @returns Array of removed items in priority order
450
+ */
451
+ popMany(count: number): T[] {
452
+ const result: T[] = [];
453
+ const actualCount = count < this.container.length ? count : this.container.length;
454
+
455
+ for (let i = 0; i < actualCount; i++) {
456
+ const item = this.pop();
457
+ if (item !== undefined) {
458
+ result.push(item);
459
+ }
460
+ }
461
+
462
+ return result;
463
+ }
464
+
465
+ /**
466
+ * Clear all items from the heap, optionally changing the arity.
467
+ * Time complexity: O(1) (references cleared, GC handles memory)
468
+ *
469
+ * @param newD - Optional new arity value (must be >= 1 if provided)
470
+ * @throws Error if newD < 1
471
+ */
472
+ clear(newD?: number): void {
473
+ this.container.length = 0;
474
+ this.positions.clear();
475
+
476
+ if (newD !== undefined) {
477
+ if (newD < 1) {
478
+ throw new Error('Heap arity (d) must be >= 1');
479
+ }
480
+ this.depth = newD;
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Get a string representation of the heap contents.
486
+ * Time complexity: O(n)
487
+ *
488
+ * @returns Formatted string showing all items in heap order
489
+ */
490
+ toString(): string {
491
+ return '{' + this.container.map(String).join(', ') + '}';
492
+ }
493
+
494
+ /** Alias for toString() - snake_case for cross-language consistency */
495
+ to_string(): string {
496
+ return this.toString();
497
+ }
498
+
499
+ /**
500
+ * Get all items in heap order (for debugging/iteration).
501
+ * Time complexity: O(n) - creates a copy
502
+ *
503
+ * @returns Copy of internal array
504
+ */
505
+ toArray(): T[] {
506
+ return [...this.container];
507
+ }
508
+
509
+ /**
510
+ * Iterate over items in heap order (not priority order).
511
+ */
512
+ *[Symbol.iterator](): Iterator<T> {
513
+ for (const item of this.container) {
514
+ yield item;
515
+ }
516
+ }
517
+
518
+ // ===========================================================================
519
+ // Private Methods - Heap Operations
520
+ // ===========================================================================
521
+
522
+ /**
523
+ * Swap two items in the heap and update their position mappings.
524
+ * V8 optimizes simple swap patterns well.
525
+ */
526
+ private swap(i: number, j: number): void {
527
+ const container = this.container;
528
+ const temp = container[i]!;
529
+ container[i] = container[j]!;
530
+ container[j] = temp;
531
+
532
+ // Update positions
533
+ this.positions.set(this.keyExtractor(container[i]!), i);
534
+ this.positions.set(this.keyExtractor(container[j]!), j);
535
+ }
536
+
537
+ /**
538
+ * Find the child with highest priority among all children of node i.
539
+ */
540
+ private bestChildPosition(i: number): number {
541
+ const d = this.depth;
542
+ const container = this.container;
543
+ const n = container.length;
544
+ const left = i * d + 1;
545
+
546
+ if (left >= n) return left;
547
+
548
+ let best = left;
549
+ const right = Math.min((i + 1) * d, n - 1);
550
+
551
+ for (let j = left + 1; j <= right; j++) {
552
+ if (this.comparator(container[j]!, container[best]!)) {
553
+ best = j;
554
+ }
555
+ }
556
+
557
+ return best;
558
+ }
559
+
560
+ /**
561
+ * Move an item upward in the heap to restore heap property.
562
+ * Uses simple swap pattern which V8 optimizes well.
563
+ */
564
+ private moveUp(i: number): void {
565
+ const d = this.depth;
566
+ const container = this.container;
567
+
568
+ while (i > 0) {
569
+ const p = Math.floor((i - 1) / d);
570
+ if (this.comparator(container[i]!, container[p]!)) {
571
+ this.swap(i, p);
572
+ i = p;
573
+ } else {
574
+ break;
575
+ }
576
+ }
577
+ }
578
+
579
+ /**
580
+ * Move an item downward in the heap to restore heap property.
581
+ */
582
+ private moveDown(i: number): void {
583
+ const d = this.depth;
584
+ const container = this.container;
585
+
586
+ while (true) {
587
+ const firstChild = i * d + 1;
588
+ if (firstChild >= container.length) break;
589
+
590
+ const best = this.bestChildPosition(i);
591
+ if (this.comparator(container[best]!, container[i]!)) {
592
+ this.swap(i, best);
593
+ i = best;
594
+ } else {
595
+ break;
596
+ }
597
+ }
598
+ }
599
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Pre-built comparator factories for common use cases.
3
+ *
4
+ * @module comparators
5
+ * @version 2.0.0
6
+ * @license Apache-2.0
7
+ */
8
+
9
+ import type { Comparator } from './PriorityQueue';
10
+
11
+ /**
12
+ * Create a min-heap comparator using a key extractor.
13
+ * Lower key values have higher priority (appear closer to root).
14
+ *
15
+ * @param keyFn - Function to extract the comparable key from an item
16
+ * @returns Comparator function for min-heap behavior
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const minByCost = minBy<Item, number>(item => item.cost);
21
+ * ```
22
+ */
23
+ export function minBy<T, K>(keyFn: (item: T) => K): Comparator<T> {
24
+ return (a: T, b: T) => keyFn(a) < keyFn(b);
25
+ }
26
+
27
+ /**
28
+ * Create a max-heap comparator using a key extractor.
29
+ * Higher key values have higher priority (appear closer to root).
30
+ *
31
+ * @param keyFn - Function to extract the comparable key from an item
32
+ * @returns Comparator function for max-heap behavior
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const maxByCost = maxBy<Item, number>(item => item.cost);
37
+ * ```
38
+ */
39
+ export function maxBy<T, K>(keyFn: (item: T) => K): Comparator<T> {
40
+ return (a: T, b: T) => keyFn(a) > keyFn(b);
41
+ }
42
+
43
+ /**
44
+ * Min-heap comparator for primitive number values.
45
+ * Lower numbers have higher priority.
46
+ */
47
+ export const minNumber: Comparator<number> = (a, b) => a < b;
48
+
49
+ /**
50
+ * Max-heap comparator for primitive number values.
51
+ * Higher numbers have higher priority.
52
+ */
53
+ export const maxNumber: Comparator<number> = (a, b) => a > b;
54
+
55
+ /**
56
+ * Min-heap comparator for primitive string values.
57
+ * Lexicographically smaller strings have higher priority.
58
+ */
59
+ export const minString: Comparator<string> = (a, b) => a < b;
60
+
61
+ /**
62
+ * Max-heap comparator for primitive string values.
63
+ * Lexicographically larger strings have higher priority.
64
+ */
65
+ export const maxString: Comparator<string> = (a, b) => a > b;
66
+
67
+ /**
68
+ * Create a comparator that reverses another comparator.
69
+ *
70
+ * @param cmp - Original comparator to reverse
71
+ * @returns Reversed comparator
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * const maxByCost = reverse(minBy<Item, number>(item => item.cost));
76
+ * ```
77
+ */
78
+ export function reverse<T>(cmp: Comparator<T>): Comparator<T> {
79
+ return (a: T, b: T) => cmp(b, a);
80
+ }
81
+
82
+ /**
83
+ * Create a comparator that compares by multiple keys in order.
84
+ * Falls back to subsequent comparators when items are equal.
85
+ *
86
+ * @param comparators - Array of comparators to apply in order
87
+ * @returns Combined comparator
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * // Sort by priority first, then by timestamp
92
+ * const cmp = chain(
93
+ * minBy<Task, number>(t => t.priority),
94
+ * minBy<Task, number>(t => t.timestamp)
95
+ * );
96
+ * ```
97
+ */
98
+ export function chain<T>(...comparators: Comparator<T>[]): Comparator<T> {
99
+ return (a: T, b: T) => {
100
+ for (const cmp of comparators) {
101
+ if (cmp(a, b)) return true;
102
+ if (cmp(b, a)) return false;
103
+ }
104
+ return false;
105
+ };
106
+ }