@zakkster/lite-logn 0.1.0 → 0.4.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/CHANGELOG.md +146 -1
- package/LogN.d.ts +107 -0
- package/LogN.js +1031 -13
- package/README.md +150 -19
- package/llms.txt +91 -10
- package/package.json +2 -2
package/LogN.js
CHANGED
|
@@ -4,17 +4,27 @@
|
|
|
4
4
|
* problem AND proves its logarithm is real (the O(log n) Witness -- see
|
|
5
5
|
* test/witness.mjs).
|
|
6
6
|
*
|
|
7
|
-
* v0.1.0
|
|
7
|
+
* v0.1.0 shipped the FIRST member: BinaryHeap, an INDEXED binary heap -- an
|
|
8
8
|
* addressable priority queue (a min|max binary heap over three parallel typed
|
|
9
9
|
* arrays plus a reverse-index map that makes changeKey / remove O(log n) by
|
|
10
|
-
* caller-supplied entity id).
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* caller-supplied entity id). v0.2.0 adds the SECOND member: Fenwick (BIT), a
|
|
11
|
+
* flat-array structure whose point-update AND prefix-sum are BOTH O(log n) via
|
|
12
|
+
* the lowest-set-bit walk (`i & -i`). v0.3.0 adds the THIRD member: SegmentTree,
|
|
13
|
+
* a flat `Float64Array(2n)` (leaves at n..2n-1) whose range-query AND point-update
|
|
14
|
+
* are BOTH O(log n) via iterative bottom-up walks, with the associative fold
|
|
15
|
+
* (min / max / sum / gcd) chosen ONCE at construction. v0.4.0 adds the FOURTH
|
|
16
|
+
* member: SkipList, a pointer-free ordered map (get / set / delete / successor /
|
|
17
|
+
* predecessor / rangeIter) whose links are slot INDICES in flat `Uint32Array`
|
|
18
|
+
* columns over a private free-list (NodePool), giving EXPECTED O(log n) with zero
|
|
19
|
+
* per-op allocation and a deterministic instance-local PRNG. Members land
|
|
20
|
+
* append-only, leaving this header and the `VERSION` const the only prior lines
|
|
21
|
+
* that ever change. Roster: BinaryHeap (v0.1.0, array-embedded O(log n) push / pop
|
|
22
|
+
* min|max heap), Fenwick / BIT (v0.2.0, O(log n) point-update AND prefix-sum via
|
|
23
|
+
* the `i & -i` walk), SegmentTree (v0.3.0, O(log n) associative range-query +
|
|
24
|
+
* point-update over a flat 2n array, fold chosen at construction), and SkipList
|
|
25
|
+
* (v0.4.0, pointer-free expected-O(log n) ordered map over a private free-list
|
|
26
|
+
* node pool). Members are independent (no shared mutable module state), so a
|
|
27
|
+
* bundler that imports one drops the others (`sideEffects: false`).
|
|
18
28
|
*
|
|
19
29
|
* The family delta: lite-o1 proves a FLAT ops/ms line on a log-x axis (the
|
|
20
30
|
* constant, slope ~ 0); lite-logn proves a STRAIGHT line on that same axis (one
|
|
@@ -29,13 +39,13 @@
|
|
|
29
39
|
*/
|
|
30
40
|
|
|
31
41
|
/** Package version. One of the three version sites (package.json / VERSION / llms.txt). */
|
|
32
|
-
export const VERSION = '0.
|
|
42
|
+
export const VERSION = '0.4.0';
|
|
33
43
|
|
|
34
44
|
// --- members land here, append-only, one tree-shakeable class each -----------
|
|
35
45
|
// BinaryHeap (v0.1.0 session) -- indexed O(log n) min|max heap (BELOW)
|
|
36
|
-
// Fenwick (v0.2.0)
|
|
37
|
-
// SegmentTree (v0.3.0)
|
|
38
|
-
// SkipList (v0.4.0)
|
|
46
|
+
// Fenwick (v0.2.0 session) -- O(log n) point-update + prefix-sum (BELOW)
|
|
47
|
+
// SegmentTree (v0.3.0 session) -- O(log n) associative range-query + point-update (BELOW)
|
|
48
|
+
// SkipList (v0.4.0 session) -- pointer-free expected-O(log n) ordered map (BELOW)
|
|
39
49
|
|
|
40
50
|
/** Max heap capacity: slot indices 0..cap-1 must fit the Int32Array _pos map. */
|
|
41
51
|
const BH_MAX_CAPACITY = 0x7FFFFFFF; // 2^31 - 1
|
|
@@ -392,3 +402,1011 @@ export class BinaryHeap {
|
|
|
392
402
|
'[lite-logn] BinaryHeap.changeKey: id ' + id + ' is not in the heap');
|
|
393
403
|
}
|
|
394
404
|
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Max Fenwick length: internal 1-based indices `k` run in [1, length] and the
|
|
408
|
+
* whole trick is the two's-complement lowest-set-bit `k & -k`. JavaScript
|
|
409
|
+
* bitwise operators coerce to a SIGNED 32-bit integer, so `k & -k` is only the
|
|
410
|
+
* lowest set bit while `k` fits a positive int32 -- i.e. `k <= 2^31 - 1`. Since
|
|
411
|
+
* the largest `k` the walk ever reaches equals `length`, `length` itself must
|
|
412
|
+
* stay in that range. Same bound as BinaryHeap's capacity, for the same reason:
|
|
413
|
+
* the index arithmetic, not the byte count, is the hard ceiling.
|
|
414
|
+
*/
|
|
415
|
+
const FENWICK_MAX = 0x7FFFFFFF; // 2^31 - 1
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* A Fenwick tree (Binary Indexed Tree): BOTH point-update AND prefix-sum in
|
|
419
|
+
* O(log n) over a SINGLE flat `Float64Array`, using nothing but the lowest-set-
|
|
420
|
+
* bit walk (`i & -i`). The member whose Big-O is most delightfully non-obvious
|
|
421
|
+
* -- "how can update AND query both be logarithmic on a plain array?" is exactly
|
|
422
|
+
* the claim the witness answers, with TWO straight log lines (one per op). No
|
|
423
|
+
* nodes, no pointers; nothing is allocated per op after construction.
|
|
424
|
+
*
|
|
425
|
+
* Index base: PUBLIC indices are 0-based in `[0, length)`. Internally the tree is
|
|
426
|
+
* 1-based -- each public `i` maps to `i + 1` in the backing `_t` Float64Array, so
|
|
427
|
+
* `_t[0]` is the unused identity sentinel and is NEVER read as data (null is not
|
|
428
|
+
* zero: slot 0 does not mean "the element at 0" -- it means "no cell"). Element
|
|
429
|
+
* `i` lives, spread across a logarithmic set of cells, in `_t[1 .. length]`.
|
|
430
|
+
*
|
|
431
|
+
* The two walks are the whole structure:
|
|
432
|
+
* - `update(i, delta)` climbs: from `k = i + 1`, repeatedly `k += k & -k` (add
|
|
433
|
+
* the lowest set bit) until `k > length`, touching ONE cell per level.
|
|
434
|
+
* - `prefix(i)` descends: from `k = i + 1`, repeatedly `k -= k & -k` (strip the
|
|
435
|
+
* lowest set bit) until `k == 0`, summing ONE cell per level.
|
|
436
|
+
* Both take at most `log2(length)` steps -- the logarithm the witness proves.
|
|
437
|
+
*
|
|
438
|
+
* Value type: `Float64Array`; deltas and values may be ANY finite number,
|
|
439
|
+
* including negatives. NaN / +-Infinity / non-number fail closed (typeof-guarded
|
|
440
|
+
* BEFORE coercion) rather than corrupt a running sum. Honesty note on precision:
|
|
441
|
+
* sums are IEEE-754 double addition, so a very large corpus of very different
|
|
442
|
+
* magnitudes accumulates the usual floating-point rounding error -- the bound is
|
|
443
|
+
* exact in step count, not in the last ULP of the sum. For exact integer sums,
|
|
444
|
+
* keep values within the 2^53 safe-integer range.
|
|
445
|
+
*
|
|
446
|
+
* Fixed capacity: `length` is frozen at construction; there is no grow. Every
|
|
447
|
+
* out-of-range index and every non-finite value is a hard `[lite-logn]` throw.
|
|
448
|
+
*/
|
|
449
|
+
export class Fenwick {
|
|
450
|
+
/**
|
|
451
|
+
* @param {number} length exact element count; integer in [1, 2^31-1].
|
|
452
|
+
*/
|
|
453
|
+
constructor(length) {
|
|
454
|
+
// typeof guard BEFORE coercion (Number.isInteger is Symbol/BigInt-safe).
|
|
455
|
+
if (typeof length !== 'number' || !Number.isInteger(length) ||
|
|
456
|
+
length < 1 || length > FENWICK_MAX) {
|
|
457
|
+
throw new RangeError(
|
|
458
|
+
'[lite-logn] Fenwick length must be an integer in [1, 2^31-1], got ' +
|
|
459
|
+
String(length));
|
|
460
|
+
}
|
|
461
|
+
this._n = length; // element count (fixed)
|
|
462
|
+
this._t = new Float64Array(length + 1); // 1-based; _t[0] is the unused sentinel
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Element count this tree was sized for. O(1). */
|
|
466
|
+
get length() { return this._n; }
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Add `delta` to the element at 0-based index `i`. O(log n): climb from
|
|
470
|
+
* `k = i + 1` by the lowest set bit, one `_t` touch per level. Fails closed:
|
|
471
|
+
* a non-number / non-finite delta (typeof-guarded first) or an out-of-range
|
|
472
|
+
* index each throw `[lite-logn]` as a no-op.
|
|
473
|
+
* @param {number} i integer in [0, length)
|
|
474
|
+
* @param {number} delta a finite number (may be negative)
|
|
475
|
+
* @returns {this}
|
|
476
|
+
*/
|
|
477
|
+
update(i, delta) {
|
|
478
|
+
if (typeof delta !== 'number' || !Number.isFinite(delta)) return this._badDelta(delta);
|
|
479
|
+
if (typeof i !== 'number' || !Number.isInteger(i) || i < 0 || i >= this._n) {
|
|
480
|
+
return this._badIndex(i);
|
|
481
|
+
}
|
|
482
|
+
const t = this._t, n = this._n;
|
|
483
|
+
for (let k = i + 1; k <= n; k += k & -k) t[k] += delta; // <= log2(n) steps
|
|
484
|
+
return this;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Sum of elements in `[0, i]` INCLUSIVE. O(log n): descend from `k = i + 1`
|
|
489
|
+
* by the lowest set bit, one `_t` read per level. `prefix(-1) === 0` is the
|
|
490
|
+
* clean base case (the empty prefix). An out-of-range index throws; the valid
|
|
491
|
+
* domain is `[-1, length)`.
|
|
492
|
+
* @param {number} i integer in [-1, length)
|
|
493
|
+
* @returns {number}
|
|
494
|
+
*/
|
|
495
|
+
prefix(i) {
|
|
496
|
+
if (typeof i !== 'number' || !Number.isInteger(i) || i < -1 || i >= this._n) {
|
|
497
|
+
return this._badPrefixIndex(i);
|
|
498
|
+
}
|
|
499
|
+
const t = this._t;
|
|
500
|
+
let s = 0;
|
|
501
|
+
for (let k = i + 1; k > 0; k -= k & -k) s += t[k]; // <= log2(n) steps
|
|
502
|
+
return s;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Sum of elements in `[lo, hi]` INCLUSIVE on both ends = `prefix(hi) -
|
|
507
|
+
* prefix(lo - 1)`, inlined as two lowest-set-bit walks (no intermediate
|
|
508
|
+
* object, no double validation). O(log n). Fails closed: out-of-range `lo` or
|
|
509
|
+
* `hi`, or `lo > hi`, each throw `[lite-logn]`.
|
|
510
|
+
* @param {number} lo integer in [0, length)
|
|
511
|
+
* @param {number} hi integer in [lo, length)
|
|
512
|
+
* @returns {number}
|
|
513
|
+
*/
|
|
514
|
+
rangeSum(lo, hi) {
|
|
515
|
+
if (typeof lo !== 'number' || !Number.isInteger(lo) || lo < 0 || lo >= this._n) {
|
|
516
|
+
return this._badRange(lo, hi);
|
|
517
|
+
}
|
|
518
|
+
if (typeof hi !== 'number' || !Number.isInteger(hi) || hi < 0 || hi >= this._n) {
|
|
519
|
+
return this._badRange(lo, hi);
|
|
520
|
+
}
|
|
521
|
+
if (lo > hi) return this._badRange(lo, hi);
|
|
522
|
+
const t = this._t;
|
|
523
|
+
let s = 0;
|
|
524
|
+
for (let k = hi + 1; k > 0; k -= k & -k) s += t[k]; // prefix(hi)
|
|
525
|
+
for (let k = lo; k > 0; k -= k & -k) s -= t[k]; // - prefix(lo-1)
|
|
526
|
+
return s;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* The single element at 0-based index `i` = `prefix(i) - prefix(i - 1)`,
|
|
531
|
+
* inlined as two lowest-set-bit walks. O(log n), zero allocation. An
|
|
532
|
+
* out-of-range index throws `[lite-logn]`.
|
|
533
|
+
* @param {number} i integer in [0, length)
|
|
534
|
+
* @returns {number}
|
|
535
|
+
*/
|
|
536
|
+
at(i) {
|
|
537
|
+
if (typeof i !== 'number' || !Number.isInteger(i) || i < 0 || i >= this._n) {
|
|
538
|
+
return this._badIndex(i);
|
|
539
|
+
}
|
|
540
|
+
const t = this._t;
|
|
541
|
+
let s = 0;
|
|
542
|
+
for (let k = i + 1; k > 0; k -= k & -k) s += t[k]; // prefix(i)
|
|
543
|
+
for (let k = i; k > 0; k -= k & -k) s -= t[k]; // - prefix(i-1)
|
|
544
|
+
return s;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Set the element at 0-based index `i` to `value` (absolute), via
|
|
549
|
+
* `update(i, value - at(i))`, inlined so the read and the climb share one
|
|
550
|
+
* validation and allocate nothing. O(log n). Fails closed: a non-finite
|
|
551
|
+
* value (typeof-guarded first) or out-of-range index throws `[lite-logn]`.
|
|
552
|
+
* @param {number} i integer in [0, length)
|
|
553
|
+
* @param {number} value a finite number
|
|
554
|
+
* @returns {this}
|
|
555
|
+
*/
|
|
556
|
+
set(i, value) {
|
|
557
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return this._badValue(value);
|
|
558
|
+
if (typeof i !== 'number' || !Number.isInteger(i) || i < 0 || i >= this._n) {
|
|
559
|
+
return this._badIndex(i);
|
|
560
|
+
}
|
|
561
|
+
const t = this._t, n = this._n;
|
|
562
|
+
let cur = 0; // = at(i)
|
|
563
|
+
for (let k = i + 1; k > 0; k -= k & -k) cur += t[k];
|
|
564
|
+
for (let k = i; k > 0; k -= k & -k) cur -= t[k];
|
|
565
|
+
const delta = value - cur;
|
|
566
|
+
for (let k = i + 1; k <= n; k += k & -k) t[k] += delta; // update(i, delta)
|
|
567
|
+
return this;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** Zero every element in place, keeping the fixed capacity. O(n) cold path. */
|
|
571
|
+
clear() {
|
|
572
|
+
this._t.fill(0);
|
|
573
|
+
return this;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Visit every element as `(value, index, fenwick)` for index in `[0, length)`,
|
|
578
|
+
* in ascending index order. O(n log n) COLD scan (each element is an `at`
|
|
579
|
+
* walk); allocation-free in the loop body (pass a hoisted callback).
|
|
580
|
+
* @param {(value:number, index:number, fenwick:Fenwick)=>void} fn
|
|
581
|
+
*/
|
|
582
|
+
forEach(fn) {
|
|
583
|
+
const t = this._t, n = this._n;
|
|
584
|
+
for (let i = 0; i < n; i++) {
|
|
585
|
+
let s = 0;
|
|
586
|
+
for (let k = i + 1; k > 0; k -= k & -k) s += t[k];
|
|
587
|
+
for (let k = i; k > 0; k -= k & -k) s -= t[k];
|
|
588
|
+
fn(s, i, this);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* O(n) LINEAR bulk build from `values` -- the SECOND teachable trick. Load
|
|
594
|
+
* each value into its own cell, then in ONE forward pass let each cell add
|
|
595
|
+
* itself to its parent (`_t[j] += _t[i]` where `j = i + (i & -i)`). This is
|
|
596
|
+
* O(n), NOT n incremental O(log n) updates. COLD path; fails closed on a
|
|
597
|
+
* non-array-like `values` or any non-finite entry before the tree is usable.
|
|
598
|
+
* @param {ArrayLike<number>} values finite numbers; length in [1, 2^31-1]
|
|
599
|
+
* @returns {Fenwick}
|
|
600
|
+
*/
|
|
601
|
+
static build(values) {
|
|
602
|
+
if (values == null || typeof values.length !== 'number') {
|
|
603
|
+
throw new TypeError('[lite-logn] Fenwick.build needs an array-like of finite numbers');
|
|
604
|
+
}
|
|
605
|
+
const length = values.length;
|
|
606
|
+
const f = new Fenwick(length); // validates length in [1, 2^31-1]
|
|
607
|
+
const t = f._t;
|
|
608
|
+
for (let i = 0; i < length; i++) {
|
|
609
|
+
const v = values[i];
|
|
610
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
611
|
+
throw new TypeError(
|
|
612
|
+
'[lite-logn] Fenwick.build value must be a finite number, got ' + String(v));
|
|
613
|
+
}
|
|
614
|
+
t[i + 1] = v; // seed each cell with its own value
|
|
615
|
+
}
|
|
616
|
+
// Linear propagation: each 1-based cell i pushes its running sum to its
|
|
617
|
+
// parent j = i + (i & -i). One pass, O(n) -- the non-obvious build trick.
|
|
618
|
+
for (let i = 1; i <= length; i++) {
|
|
619
|
+
const j = i + (i & -i);
|
|
620
|
+
if (j <= length) t[j] += t[i];
|
|
621
|
+
}
|
|
622
|
+
return f;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ---- cold path only: throw builders (string concat off the hot body) ---
|
|
626
|
+
|
|
627
|
+
/** @private */
|
|
628
|
+
_badIndex(i) {
|
|
629
|
+
throw new RangeError(
|
|
630
|
+
'[lite-logn] Fenwick index must be an integer in [0, ' + this._n + '), got ' +
|
|
631
|
+
String(i));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** @private */
|
|
635
|
+
_badPrefixIndex(i) {
|
|
636
|
+
throw new RangeError(
|
|
637
|
+
'[lite-logn] Fenwick prefix index must be an integer in [-1, ' + this._n + '), got ' +
|
|
638
|
+
String(i));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** @private */
|
|
642
|
+
_badRange(lo, hi) {
|
|
643
|
+
throw new RangeError(
|
|
644
|
+
'[lite-logn] Fenwick rangeSum needs integers 0 <= lo <= hi < ' + this._n +
|
|
645
|
+
', got lo=' + String(lo) + ' hi=' + String(hi));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** @private */
|
|
649
|
+
_badDelta(delta) {
|
|
650
|
+
throw new TypeError(
|
|
651
|
+
'[lite-logn] Fenwick delta must be a finite number, got ' + String(delta));
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** @private */
|
|
655
|
+
_badValue(value) {
|
|
656
|
+
throw new TypeError(
|
|
657
|
+
'[lite-logn] Fenwick value must be a finite number, got ' + String(value));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Max SegmentTree length: the whole structure lives in ONE `Float64Array(2 *
|
|
663
|
+
* length)` with leaves at indices `n .. 2n-1` and internal node `p`'s children
|
|
664
|
+
* at `2p` / `2p+1`. Both the leaf index (`n + i`, up to `2n - 1`) and the child
|
|
665
|
+
* index (`p << 1`, up to `2n - 2`) are computed with the signed-int32 `<<` / `+`
|
|
666
|
+
* operators, so the largest index the walks ever reach -- `2n - 1` -- must stay a
|
|
667
|
+
* POSITIVE int32 (`<= 2^31 - 1`). That caps `length` at `2^30 - 1`: HALF of
|
|
668
|
+
* BinaryHeap's / Fenwick's ceiling, because SegmentTree's backing array is 2n
|
|
669
|
+
* wide (a node per leaf plus a node per internal cell) where theirs are n wide.
|
|
670
|
+
* The index arithmetic, not the byte count, is the hard ceiling.
|
|
671
|
+
*/
|
|
672
|
+
const SEGTREE_MAX = 0x3FFFFFFF; // 2^30 - 1 (so 2n stays a positive int32)
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Euclidean GCD over nonnegative integers-in-doubles. Off the value door the
|
|
676
|
+
* inputs are already validated nonnegative finite integers (the `gcd` kind
|
|
677
|
+
* constrains its domain -- see SegmentTree's value door), and the identity 0
|
|
678
|
+
* makes this associative + commutative: `gcd(0, x) === x`, `gcd(x, 0) === x`.
|
|
679
|
+
* A plain module function (monomorphic, allocation-free) -- it is the arithmetic
|
|
680
|
+
* of the fold, NOT the fold dispatch (which is the ctor-cached `_k` inline
|
|
681
|
+
* switch). `%` is exact for integers within the 2^53 safe range.
|
|
682
|
+
* @param {number} a nonnegative finite integer
|
|
683
|
+
* @param {number} b nonnegative finite integer
|
|
684
|
+
* @returns {number} gcd(a, b), with gcd(0, 0) === 0
|
|
685
|
+
*/
|
|
686
|
+
function segGcd(a, b) {
|
|
687
|
+
while (b !== 0) { const r = a % b; a = b; b = r; }
|
|
688
|
+
return a;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* A SEGMENT TREE: an associative range-query AND a point-update, BOTH O(log n),
|
|
693
|
+
* over a SINGLE flat `Float64Array(2n)` -- no nodes, no pointers, no recursion on
|
|
694
|
+
* the hot path. The complement to Fenwick: Fenwick's `rangeSum` works only
|
|
695
|
+
* because subtraction inverts addition, so it is a SUM machine; SegmentTree folds
|
|
696
|
+
* ANY associative + commutative operation over a range -- min / max / sum / gcd --
|
|
697
|
+
* because it stores a fold of each subtree at its internal node rather than a
|
|
698
|
+
* prefix. The fold is chosen ONCE at construction and cached as a small-int `_k`
|
|
699
|
+
* combined by an INLINE switch in the hot body (no function ref, no closure, no
|
|
700
|
+
* megamorphic call site).
|
|
701
|
+
*
|
|
702
|
+
* Layout (the iterative "2n" trick):
|
|
703
|
+
* - `_t` is a `Float64Array(2 * length)`; `_t[0]` is UNUSED (null is not zero:
|
|
704
|
+
* index 0 is never read as data).
|
|
705
|
+
* - Leaves are `_t[n + i]` for public index `i` in `[0, n)`.
|
|
706
|
+
* - Internal node `p` (in `[1, n)`) holds the fold of its subtree; its children
|
|
707
|
+
* are `_t[2p]` and `_t[2p + 1]`, so `_t[1]` is the fold of the whole array.
|
|
708
|
+
*
|
|
709
|
+
* The two hot walks:
|
|
710
|
+
* - `update(i, value)` sets leaf `_t[n + i] = value`, then climbs to the root
|
|
711
|
+
* recomputing each ancestor `_t[p] = fold(_t[2p], _t[2p+1])` -- one write per
|
|
712
|
+
* level, `<= log2(n)` levels.
|
|
713
|
+
* - `query(lo, hi)` walks the two boundary indices UP the tree
|
|
714
|
+
* (`l = n + lo`, `r = n + hi + 1`), folding in each node that lies fully
|
|
715
|
+
* inside `[lo, hi]` as the boundaries ascend -- `<= 2 * log2(n)` folds.
|
|
716
|
+
*
|
|
717
|
+
* RISK (recorded in decisions/0005-segtree.md, D-05): the iterative 2n layout is
|
|
718
|
+
* ORDER-AGNOSTIC -- `query` mixes left- and right-boundary contributions into one
|
|
719
|
+
* accumulator, so it is correct ONLY because min / max / sum / gcd are all
|
|
720
|
+
* COMMUTATIVE as well as associative. A future NON-commutative fold (matrix
|
|
721
|
+
* product, string concat) must NOT reuse this layout; it belongs on a pow2
|
|
722
|
+
* layout with separate left/right accumulators combined in order.
|
|
723
|
+
*
|
|
724
|
+
* Identity (the trap): the fold's identity fills query accumulators and cleared /
|
|
725
|
+
* fresh leaves -- sum -> 0, min -> +Infinity, max -> -Infinity, gcd -> 0. Identity
|
|
726
|
+
* is a legal RESULT (a cleared min tree queries to +Infinity) but NEVER a legal
|
|
727
|
+
* INPUT: the value door still rejects user NaN / +-Infinity (and, for the `gcd`
|
|
728
|
+
* kind, any negative or non-integer value), typeof-guarded BEFORE coercion. Fixed
|
|
729
|
+
* capacity: `length` is frozen at construction; every out-of-range index and
|
|
730
|
+
* every non-finite (or out-of-domain gcd) value is a hard `[lite-logn]` throw.
|
|
731
|
+
*/
|
|
732
|
+
export class SegmentTree {
|
|
733
|
+
/**
|
|
734
|
+
* @param {number} length exact element count; integer in [1, 2^30-1].
|
|
735
|
+
* @param {'min'|'max'|'sum'|'gcd'} kind the frozen associative fold.
|
|
736
|
+
*/
|
|
737
|
+
constructor(length, kind) {
|
|
738
|
+
// typeof guard BEFORE coercion (Number.isInteger is Symbol/BigInt-safe).
|
|
739
|
+
if (typeof length !== 'number' || !Number.isInteger(length) ||
|
|
740
|
+
length < 1 || length > SEGTREE_MAX) {
|
|
741
|
+
throw new RangeError(
|
|
742
|
+
'[lite-logn] SegmentTree length must be an integer in [1, 2^30-1], got ' +
|
|
743
|
+
String(length));
|
|
744
|
+
}
|
|
745
|
+
const k = kind === 'min' ? 0 : kind === 'max' ? 1 : kind === 'sum' ? 2 :
|
|
746
|
+
kind === 'gcd' ? 3 : -1;
|
|
747
|
+
if (k === -1) {
|
|
748
|
+
throw new RangeError(
|
|
749
|
+
'[lite-logn] SegmentTree kind must be "min", "max", "sum" or "gcd", got ' +
|
|
750
|
+
String(kind));
|
|
751
|
+
}
|
|
752
|
+
this._n = length; // element count (fixed)
|
|
753
|
+
this._k = k; // ctor-frozen fold: 0 min 1 max 2 sum 3 gcd
|
|
754
|
+
this._idv = k === 0 ? Infinity : k === 1 ? -Infinity : 0; // fold identity
|
|
755
|
+
this._t = new Float64Array(2 * length); // _t[0] unused; leaves at n..2n-1
|
|
756
|
+
if (this._idv !== 0) this._t.fill(this._idv); // sum/gcd identity is 0 already
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/** Element count this tree was sized for. O(1). */
|
|
760
|
+
get length() { return this._n; }
|
|
761
|
+
|
|
762
|
+
/** The frozen associative fold, 'min' | 'max' | 'sum' | 'gcd'. O(1). */
|
|
763
|
+
get kind() {
|
|
764
|
+
const k = this._k;
|
|
765
|
+
return k === 0 ? 'min' : k === 1 ? 'max' : k === 2 ? 'sum' : 'gcd';
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* The folded value over `[lo, hi]` INCLUSIVE on both ends. O(log n): walk the
|
|
770
|
+
* two boundaries up the tree, folding each node that lies fully inside the
|
|
771
|
+
* range into a single accumulator (started at the fold identity). Fails closed:
|
|
772
|
+
* out-of-range `lo` or `hi`, or `lo > hi`, each throw `[lite-logn]` (matching
|
|
773
|
+
* Fenwick.rangeSum). A one-element range `lo == hi` returns that leaf's value.
|
|
774
|
+
* @param {number} lo integer in [0, length)
|
|
775
|
+
* @param {number} hi integer in [lo, length)
|
|
776
|
+
* @returns {number} the fold over `[lo, hi]` (always folds at least one leaf)
|
|
777
|
+
*/
|
|
778
|
+
query(lo, hi) {
|
|
779
|
+
const n = this._n;
|
|
780
|
+
if (typeof lo !== 'number' || !Number.isInteger(lo) || lo < 0 || lo >= n) {
|
|
781
|
+
return this._badRange(lo, hi);
|
|
782
|
+
}
|
|
783
|
+
if (typeof hi !== 'number' || !Number.isInteger(hi) || hi < 0 || hi >= n) {
|
|
784
|
+
return this._badRange(lo, hi);
|
|
785
|
+
}
|
|
786
|
+
if (lo > hi) return this._badRange(lo, hi);
|
|
787
|
+
const t = this._t, k = this._k;
|
|
788
|
+
let res = this._idv;
|
|
789
|
+
// Order-agnostic fold (correct because the fold is commutative -- D-05).
|
|
790
|
+
for (let l = n + lo, r = n + hi + 1; l < r; l >>= 1, r >>= 1) {
|
|
791
|
+
if (l & 1) {
|
|
792
|
+
const v = t[l++];
|
|
793
|
+
res = k === 0 ? (v < res ? v : res) : k === 1 ? (v > res ? v : res) :
|
|
794
|
+
k === 2 ? res + v : segGcd(res, v);
|
|
795
|
+
}
|
|
796
|
+
if (r & 1) {
|
|
797
|
+
const v = t[--r];
|
|
798
|
+
res = k === 0 ? (v < res ? v : res) : k === 1 ? (v > res ? v : res) :
|
|
799
|
+
k === 2 ? res + v : segGcd(res, v);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
return res;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Set the element at 0-based leaf `i` to `value` (ABSOLUTE), then fix every
|
|
807
|
+
* ancestor by recomputing its fold. O(log n): one leaf write plus one write
|
|
808
|
+
* per level up to the root. Fails closed: a non-finite value (typeof-guarded
|
|
809
|
+
* first), a gcd-kind value that is negative or non-integer, or an out-of-range
|
|
810
|
+
* index each throw `[lite-logn]` as a no-op.
|
|
811
|
+
* @param {number} i integer in [0, length)
|
|
812
|
+
* @param {number} value a finite number (nonnegative integer for the gcd kind)
|
|
813
|
+
* @returns {this}
|
|
814
|
+
*/
|
|
815
|
+
update(i, value) {
|
|
816
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return this._badValue(value);
|
|
817
|
+
if (this._k === 3 && (!Number.isInteger(value) || value < 0)) return this._badGcdValue(value);
|
|
818
|
+
if (typeof i !== 'number' || !Number.isInteger(i) || i < 0 || i >= this._n) {
|
|
819
|
+
return this._badIndex(i);
|
|
820
|
+
}
|
|
821
|
+
const t = this._t, k = this._k;
|
|
822
|
+
let p = this._n + i;
|
|
823
|
+
t[p] = value;
|
|
824
|
+
for (p >>= 1; p >= 1; p >>= 1) {
|
|
825
|
+
const c = p << 1; // left child; right is c + 1
|
|
826
|
+
const a = t[c], b = t[c + 1];
|
|
827
|
+
t[p] = k === 0 ? (a < b ? a : b) : k === 1 ? (a > b ? a : b) :
|
|
828
|
+
k === 2 ? a + b : segGcd(a, b);
|
|
829
|
+
}
|
|
830
|
+
return this;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* The single element at 0-based leaf `i` (the stored leaf value). O(1). An
|
|
835
|
+
* out-of-range index throws `[lite-logn]`.
|
|
836
|
+
* @param {number} i integer in [0, length)
|
|
837
|
+
* @returns {number}
|
|
838
|
+
*/
|
|
839
|
+
at(i) {
|
|
840
|
+
if (typeof i !== 'number' || !Number.isInteger(i) || i < 0 || i >= this._n) {
|
|
841
|
+
return this._badIndex(i);
|
|
842
|
+
}
|
|
843
|
+
return this._t[this._n + i];
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Reset every element to the fold identity in place, keeping the fixed
|
|
848
|
+
* capacity. O(n) cold path. Because `fold(identity, identity) === identity`,
|
|
849
|
+
* filling the WHOLE backing array (leaves and internal nodes alike) with the
|
|
850
|
+
* identity leaves a fully-consistent tree -- a query returns the identity.
|
|
851
|
+
* @returns {this}
|
|
852
|
+
*/
|
|
853
|
+
clear() {
|
|
854
|
+
this._t.fill(this._idv);
|
|
855
|
+
return this;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Visit every element as `(value, index, tree)` for index in `[0, length)`, in
|
|
860
|
+
* ASCENDING leaf order. O(n) COLD scan, allocation-free in the loop body (pass
|
|
861
|
+
* a hoisted callback).
|
|
862
|
+
* @param {(value:number, index:number, tree:SegmentTree)=>void} fn
|
|
863
|
+
*/
|
|
864
|
+
forEach(fn) {
|
|
865
|
+
const t = this._t, n = this._n;
|
|
866
|
+
for (let i = 0; i < n; i++) fn(t[n + i], i, this);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* O(n) bottom-up bulk build from `values` -- NOT n individual O(log n) updates.
|
|
871
|
+
* Seed each leaf `_t[n + i] = values[i]`, then fold every internal node once,
|
|
872
|
+
* deepest-first (`p` from `n - 1` down to `1`): `_t[p] = fold(_t[2p],
|
|
873
|
+
* _t[2p+1])`. COLD path; fails closed on a non-array-like `values`, any
|
|
874
|
+
* non-finite entry, or (gcd kind) any negative / non-integer entry before the
|
|
875
|
+
* tree is usable. `length` and `kind` are validated by the delegated ctor.
|
|
876
|
+
* @param {ArrayLike<number>} values finite numbers; length in [1, 2^30-1]
|
|
877
|
+
* @param {'min'|'max'|'sum'|'gcd'} kind the frozen associative fold
|
|
878
|
+
* @returns {SegmentTree}
|
|
879
|
+
*/
|
|
880
|
+
static build(values, kind) {
|
|
881
|
+
if (values == null || typeof values.length !== 'number') {
|
|
882
|
+
throw new TypeError('[lite-logn] SegmentTree.build needs an array-like of finite numbers');
|
|
883
|
+
}
|
|
884
|
+
const length = values.length;
|
|
885
|
+
const st = new SegmentTree(length, kind); // validates length in [1, 2^30-1] + kind
|
|
886
|
+
const t = st._t, n = st._n, k = st._k;
|
|
887
|
+
const gcdKind = k === 3;
|
|
888
|
+
for (let i = 0; i < length; i++) {
|
|
889
|
+
const v = values[i];
|
|
890
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
891
|
+
throw new TypeError(
|
|
892
|
+
'[lite-logn] SegmentTree.build value must be a finite number, got ' + String(v));
|
|
893
|
+
}
|
|
894
|
+
if (gcdKind && (!Number.isInteger(v) || v < 0)) {
|
|
895
|
+
throw new RangeError(
|
|
896
|
+
'[lite-logn] SegmentTree.build gcd value must be a nonnegative integer, got ' +
|
|
897
|
+
String(v));
|
|
898
|
+
}
|
|
899
|
+
t[n + i] = v; // seed the leaf
|
|
900
|
+
}
|
|
901
|
+
// Fold every internal node once, deepest-first -- O(n), not n * O(log n).
|
|
902
|
+
for (let p = n - 1; p >= 1; p--) {
|
|
903
|
+
const c = p << 1;
|
|
904
|
+
const a = t[c], b = t[c + 1];
|
|
905
|
+
t[p] = k === 0 ? (a < b ? a : b) : k === 1 ? (a > b ? a : b) :
|
|
906
|
+
k === 2 ? a + b : segGcd(a, b);
|
|
907
|
+
}
|
|
908
|
+
return st;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ---- cold path only: throw builders (string concat off the hot body) ---
|
|
912
|
+
|
|
913
|
+
/** @private */
|
|
914
|
+
_badIndex(i) {
|
|
915
|
+
throw new RangeError(
|
|
916
|
+
'[lite-logn] SegmentTree index must be an integer in [0, ' + this._n + '), got ' +
|
|
917
|
+
String(i));
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** @private */
|
|
921
|
+
_badRange(lo, hi) {
|
|
922
|
+
throw new RangeError(
|
|
923
|
+
'[lite-logn] SegmentTree query needs integers 0 <= lo <= hi < ' + this._n +
|
|
924
|
+
', got lo=' + String(lo) + ' hi=' + String(hi));
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** @private */
|
|
928
|
+
_badValue(value) {
|
|
929
|
+
throw new TypeError(
|
|
930
|
+
'[lite-logn] SegmentTree value must be a finite number, got ' + String(value));
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/** @private */
|
|
934
|
+
_badGcdValue(value) {
|
|
935
|
+
throw new RangeError(
|
|
936
|
+
'[lite-logn] SegmentTree gcd value must be a nonnegative integer, got ' + String(value));
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Advance a 32-bit Numerical-Recipes LCG one step: `s' = (s*1664525 + 1013904223)
|
|
942
|
+
* mod 2^32`, kept as a SIGNED int32. The repo's single PRNG -- deterministic,
|
|
943
|
+
* instance-local, no Math.random, no module state, no new generator. Two integer
|
|
944
|
+
* disciplines keep it zero-alloc:
|
|
945
|
+
* - `Math.imul` does the multiply as a 32-bit integer op (the low 32 bits of the
|
|
946
|
+
* product), so no large intermediate double is ever formed (`s * 1664525` would
|
|
947
|
+
* reach ~7.1e15); and
|
|
948
|
+
* - the result is folded with `| 0` (a SIGNED int32), NOT `>>> 0`: a `>>> 0`
|
|
949
|
+
* Uint32 exceeds the Smi range (2^31) about half the time and would box as a
|
|
950
|
+
* transient HeapNumber every step (the perf-gate scavenge counter catches it),
|
|
951
|
+
* whereas an `| 0` signed int32 always stays an unboxed Smi.
|
|
952
|
+
* The 32-BIT WORD is identical either way, so the sequence is unchanged: `Math.imul`
|
|
953
|
+
* gives the same low 32 bits as the plain multiply, and mod-2^32 addition is
|
|
954
|
+
* sign-agnostic. SkipList draws a node level from the HIGH bits via `Math.clz32`,
|
|
955
|
+
* which does ToUint32 internally -- so the signed int32 and its Uint32 twin yield
|
|
956
|
+
* the IDENTICAL level. The LOW bits of any power-of-two-modulus LCG are periodic
|
|
957
|
+
* (here the lowest bit strictly alternates, since a is odd and c is odd), so
|
|
958
|
+
* counting halvings from the low end would be non-random -- the high bits are the
|
|
959
|
+
* well-mixed ones.
|
|
960
|
+
* @param {number} s current state, a signed 32-bit integer
|
|
961
|
+
* @returns {number} the next state, a signed 32-bit integer (same 32-bit word)
|
|
962
|
+
*/
|
|
963
|
+
function _lcgNext(s) {
|
|
964
|
+
return (Math.imul(s, 1664525) + 1013904223) | 0;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/** SkipList tower-height ceiling: at most this many parallel `_next` columns. */
|
|
968
|
+
const SL_MAXLEVEL = 32;
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Column count actually allocated for a `capacity`-slot SkipList: `ceil(log2 cap)`
|
|
972
|
+
* plus one column of headroom for the geometric tail, clamped to SL_MAXLEVEL.
|
|
973
|
+
* Fixed at construction so `_next` NEVER reallocates -- the memory risk (a full
|
|
974
|
+
* cap * 32 column set) is avoided by sizing to the capacity's real need, and level
|
|
975
|
+
* generation clamps to it, so there is no lazy grow to threaten the 0-B/op gate
|
|
976
|
+
* (decisions/0006-skiplist.md). The clamp only ever bites the extreme geometric
|
|
977
|
+
* tail (probability ~ 2^-log2(cap)), which cannot change ordering or membership --
|
|
978
|
+
* a node merely stops gaining express lanes above the ceiling.
|
|
979
|
+
* @param {number} capacity data-slot count
|
|
980
|
+
* @returns {number} column count in [1, SL_MAXLEVEL]
|
|
981
|
+
*/
|
|
982
|
+
function _levelCap(capacity) {
|
|
983
|
+
let l = 1;
|
|
984
|
+
while ((1 << l) < capacity && l < SL_MAXLEVEL) l++;
|
|
985
|
+
l += 1; // one column of headroom above ceil(log2 cap)
|
|
986
|
+
return l > SL_MAXLEVEL ? SL_MAXLEVEL : l;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** SkipList default PRNG seed when the caller does not supply one. */
|
|
990
|
+
const SL_DEFAULT_SEED = 0x9E3779B9;
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* A private, pointer-free slot allocator: a free-list (a LIFO free-stack over a
|
|
994
|
+
* `Uint32Array`) that hands out a slot INDEX in [1, capacity] rather than a heap
|
|
995
|
+
* object, so nothing is collected per insert. `NIL = 0`; slot 0 is RESERVED (the
|
|
996
|
+
* SkipList head sentinel) and is never allocatable. `alloc()` returns 0 when the
|
|
997
|
+
* pool is exhausted so the caller can fail closed. The conservation invariant
|
|
998
|
+
* `activeSlots + freeListLength === capacity` holds after every operation.
|
|
999
|
+
*
|
|
1000
|
+
* D-01 bind (decisions/0006-skiplist.md): this is DESIGN-PARITY with
|
|
1001
|
+
* `@zakkster/lite-o1`'s private pools (FreqO1 / BucketQueue / TimerWheel) and its
|
|
1002
|
+
* deferred `SlotPool` -- the identical free-list contract (allocate an index,
|
|
1003
|
+
* `NIL = 0`, slot 0 reserved) and the same conservation invariant -- NOT shared
|
|
1004
|
+
* code. A runtime dependency on lite-o1 was REJECTED: the suite's zero-runtime-deps
|
|
1005
|
+
* law forbids it, and lite-o1's `SlotPool` was never made public. Shaped so a later
|
|
1006
|
+
* pointer member (Treap) can reuse it, without over-engineering it now.
|
|
1007
|
+
*/
|
|
1008
|
+
class NodePool {
|
|
1009
|
+
/** @param {number} capacity allocatable data-slot count (excludes slot 0). */
|
|
1010
|
+
constructor(capacity) {
|
|
1011
|
+
this._cap = capacity;
|
|
1012
|
+
this._free = new Uint32Array(capacity); // free-stack of slot indices
|
|
1013
|
+
this._freeLen = capacity;
|
|
1014
|
+
for (let i = 0; i < capacity; i++) this._free[i] = capacity - i; // top = slot 1
|
|
1015
|
+
this._active = 0;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/** Allocatable data-slot count (excludes the reserved slot 0). O(1). */
|
|
1019
|
+
get capacity() { return this._cap; }
|
|
1020
|
+
/** Slots currently handed out and not yet freed. O(1). */
|
|
1021
|
+
get activeSlots() { return this._active; }
|
|
1022
|
+
/** Slots currently on the free-stack. O(1). */
|
|
1023
|
+
get freeListLength() { return this._freeLen; }
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* Hand out a free slot INDEX in [1, capacity], or 0 (NIL) if exhausted. O(1),
|
|
1027
|
+
* zero allocation.
|
|
1028
|
+
* @returns {number}
|
|
1029
|
+
*/
|
|
1030
|
+
alloc() {
|
|
1031
|
+
const n = this._freeLen;
|
|
1032
|
+
if (n === 0) return 0; // NIL: exhausted -> caller fails closed
|
|
1033
|
+
const slot = this._free[n - 1];
|
|
1034
|
+
this._freeLen = n - 1;
|
|
1035
|
+
this._active++;
|
|
1036
|
+
return slot;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Return a slot INDEX to the free-stack. O(1), zero allocation. The caller owns
|
|
1041
|
+
* correctness: a slot must be live and freed at most once (the SkipList only
|
|
1042
|
+
* frees a node it just unlinked).
|
|
1043
|
+
* @param {number} slot a slot previously returned by alloc()
|
|
1044
|
+
*/
|
|
1045
|
+
free(slot) {
|
|
1046
|
+
const n = this._freeLen;
|
|
1047
|
+
this._free[n] = slot;
|
|
1048
|
+
this._freeLen = n + 1;
|
|
1049
|
+
this._active--;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/** Reset to all-free (O(capacity) cold path), restoring the conservation invariant. */
|
|
1053
|
+
clear() {
|
|
1054
|
+
const cap = this._cap, free = this._free;
|
|
1055
|
+
for (let i = 0; i < cap; i++) free[i] = cap - i;
|
|
1056
|
+
this._freeLen = cap;
|
|
1057
|
+
this._active = 0;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Max SkipList capacity: `0x03FFFFFF` (2^26 - 1). Every node is addressed by a slot
|
|
1063
|
+
* INDEX stored in `Uint32Array` link columns, so an index must fit an unsigned
|
|
1064
|
+
* 32-bit word; `NIL = 0` reserves slot 0 as the head sentinel, so live slots run
|
|
1065
|
+
* [1, capacity]. The backing `_next` is a SINGLE flat `Uint32Array` of
|
|
1066
|
+
* `columns * (capacity + 1)` cells, stride-indexed `lvl*(capacity + 1) + slot`; the
|
|
1067
|
+
* 2^26 ceiling keeps `columns * (capacity + 1)` an addressable typed-array length
|
|
1068
|
+
* (at most ~27 columns for the largest capacity) and every stride offset an exact
|
|
1069
|
+
* integer. The index arithmetic (Uint32 slot indices + `NIL = 0` + the MAXLEVEL
|
|
1070
|
+
* column width), not the byte count, is the hard ceiling -- the same "the
|
|
1071
|
+
* arithmetic caps it" reasoning as the array-embedded members, one column-set wider.
|
|
1072
|
+
*/
|
|
1073
|
+
const SL_MAX_CAPACITY = 0x03FFFFFF; // 2^26 - 1
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* A SKIP LIST: a pointer-free ordered map (key -> value) whose get / set / delete /
|
|
1077
|
+
* successor / predecessor are EXPECTED O(log n) via a probabilistic tower of
|
|
1078
|
+
* forward links -- the family's first randomized member and its first pointer-based
|
|
1079
|
+
* one. Where BinaryHeap / Fenwick / SegmentTree embed a FIXED-shape tree in index
|
|
1080
|
+
* arithmetic, a skip list's shape is random, so it needs real per-node links; the
|
|
1081
|
+
* trick that keeps it zero-GC is storing those links as slot INDICES in flat
|
|
1082
|
+
* `Uint32Array` columns over a private free-list (NodePool), never as heap objects.
|
|
1083
|
+
*
|
|
1084
|
+
* Storage (allocated once, sized to capacity):
|
|
1085
|
+
* - `_key` / `_val` `Float64Array(capacity + 1)` -- key and value at each slot.
|
|
1086
|
+
* - `_next` a SINGLE flat `Uint32Array(columns * (capacity + 1))`, stride-indexed
|
|
1087
|
+
* `lvl*(capacity + 1) + slot`: the forward link of `slot` at level `lvl`, or
|
|
1088
|
+
* `NIL = 0` for end-of-list. Slot 0 is the HEAD sentinel (its links are the
|
|
1089
|
+
* first node at each level); no real node's link ever points AT the head, so a
|
|
1090
|
+
* link value of 0 unambiguously means NIL.
|
|
1091
|
+
* - `_pool` NodePool -- the free-list handing out slot indices [1, capacity].
|
|
1092
|
+
* - `_update` `Uint32Array(columns)` -- reused predecessor scratch for the ONE
|
|
1093
|
+
* structural descent (`_find`); preallocated so set / delete allocate nothing.
|
|
1094
|
+
*
|
|
1095
|
+
* Level generation is one LCG step (the repo's NR generator) whose HIGH bits pick a
|
|
1096
|
+
* geometric height: `level = 1 + clz32(word)`, clamped to the allocated column
|
|
1097
|
+
* count (the low bits of a power-of-two LCG are periodic -- see `_lcgNext`). The
|
|
1098
|
+
* seed is instance-local, so a fixed seed replays an IDENTICAL structure and a
|
|
1099
|
+
* different seed diverges -- deterministic, never Math.random.
|
|
1100
|
+
*
|
|
1101
|
+
* Honesty (randomized member): a hot op is EXPECTED O(log n), not worst-case. An
|
|
1102
|
+
* unlucky seed can build a tall thin tower and spike a single op; the witness prints
|
|
1103
|
+
* that MAX single-op alongside the fitted line so the expectation never masquerades
|
|
1104
|
+
* as a worst-case guarantee (decisions/0006-skiplist.md).
|
|
1105
|
+
*
|
|
1106
|
+
* Keys are FINITE numbers (typeof-guarded BEFORE coercion -- Symbol / BigInt / NaN /
|
|
1107
|
+
* +-Infinity fail closed with a `[lite-logn]` throw); values are Float64 (zero-GC).
|
|
1108
|
+
* `set` on an EXISTING key updates its value in place (no new node). An empty or
|
|
1109
|
+
* missing query returns `undefined` (never throws). Fixed capacity: a full pool
|
|
1110
|
+
* throws, never silently drops. `rangeIter` is a VERSION-STAMPED iterator -- any
|
|
1111
|
+
* structural mutation mid-iteration throws `[lite-logn]` rather than yield garbage.
|
|
1112
|
+
*/
|
|
1113
|
+
export class SkipList {
|
|
1114
|
+
/**
|
|
1115
|
+
* @param {number} capacity exact max live entries; integer in [1, 2^26-1].
|
|
1116
|
+
* @param {number} [seed] PRNG seed; unsigned 32-bit integer (default fixed).
|
|
1117
|
+
*/
|
|
1118
|
+
constructor(capacity, seed) {
|
|
1119
|
+
// typeof guard BEFORE coercion (Number.isInteger is Symbol/BigInt-safe).
|
|
1120
|
+
if (typeof capacity !== 'number' || !Number.isInteger(capacity) ||
|
|
1121
|
+
capacity < 1 || capacity > SL_MAX_CAPACITY) {
|
|
1122
|
+
throw new RangeError(
|
|
1123
|
+
'[lite-logn] SkipList capacity must be an integer in [1, 2^26-1], got ' +
|
|
1124
|
+
String(capacity));
|
|
1125
|
+
}
|
|
1126
|
+
let s;
|
|
1127
|
+
if (seed === undefined) {
|
|
1128
|
+
s = SL_DEFAULT_SEED;
|
|
1129
|
+
} else if (typeof seed !== 'number' || !Number.isInteger(seed) ||
|
|
1130
|
+
seed < 0 || seed > 0xFFFFFFFF) {
|
|
1131
|
+
throw new RangeError(
|
|
1132
|
+
'[lite-logn] SkipList seed must be an unsigned 32-bit integer, got ' +
|
|
1133
|
+
String(seed));
|
|
1134
|
+
} else {
|
|
1135
|
+
s = seed >>> 0;
|
|
1136
|
+
}
|
|
1137
|
+
s = s | 0; // store the LCG state as a SIGNED int32 (an unboxed Smi) -- see _lcgNext
|
|
1138
|
+
const cols = _levelCap(capacity);
|
|
1139
|
+
this._cap = capacity; // max live entries
|
|
1140
|
+
this._stride = capacity + 1; // slots 0..capacity (0 = head)
|
|
1141
|
+
this._maxLevel = cols; // allocated column count
|
|
1142
|
+
this._key = new Float64Array(capacity + 1); // key at each slot
|
|
1143
|
+
this._val = new Float64Array(capacity + 1); // value at each slot
|
|
1144
|
+
this._next = new Uint32Array(cols * (capacity + 1)); // links; all NIL (0)
|
|
1145
|
+
this._update = new Uint32Array(cols); // reused predecessor scratch
|
|
1146
|
+
this._pool = new NodePool(capacity); // free-list over slots [1, capacity]
|
|
1147
|
+
this._level = 1; // current live tower height
|
|
1148
|
+
this._size = 0; // live entries
|
|
1149
|
+
this._version = 0; // iterator invalidation stamp
|
|
1150
|
+
this._seed0 = s; // initial seed (clear resets to it)
|
|
1151
|
+
this._seed = s; // live LCG state
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/** Live entry count. O(1). */
|
|
1155
|
+
get size() { return this._size; }
|
|
1156
|
+
|
|
1157
|
+
/** The fixed capacity this list was sized for. O(1). */
|
|
1158
|
+
get capacity() { return this._cap; }
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* The value stored under `key`, or `undefined` if absent (never throws on a
|
|
1162
|
+
* missing / empty query). EXPECTED O(log n): a top-down descent that at each
|
|
1163
|
+
* level advances while the next key is strictly less than `key`. Fails closed:
|
|
1164
|
+
* a non-number / non-finite key (typeof-guarded first) throws `[lite-logn]`.
|
|
1165
|
+
* @param {number} key a finite number
|
|
1166
|
+
* @returns {number|undefined}
|
|
1167
|
+
*/
|
|
1168
|
+
get(key) {
|
|
1169
|
+
if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
|
|
1170
|
+
const next = this._next, K = this._key, stride = this._stride;
|
|
1171
|
+
let slot = 0; // head
|
|
1172
|
+
for (let lvl = this._level - 1; lvl >= 0; lvl--) {
|
|
1173
|
+
const base = lvl * stride;
|
|
1174
|
+
let nx = next[base + slot];
|
|
1175
|
+
while (nx !== 0 && K[nx] < key) { slot = nx; nx = next[base + slot]; }
|
|
1176
|
+
}
|
|
1177
|
+
const cand = next[slot]; // level-0 next (base 0)
|
|
1178
|
+
return (cand !== 0 && K[cand] === key) ? this._val[cand] : undefined;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Insert `key -> value`, or UPDATE the value in place if `key` already exists
|
|
1183
|
+
* (no new node). EXPECTED O(log n). Fails closed: a non-finite key or value
|
|
1184
|
+
* (typeof-guarded first), or a full pool, each throw `[lite-logn]` as a no-op.
|
|
1185
|
+
* @param {number} key a finite number
|
|
1186
|
+
* @param {number} value a finite number
|
|
1187
|
+
* @returns {this}
|
|
1188
|
+
*/
|
|
1189
|
+
set(key, value) {
|
|
1190
|
+
if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
|
|
1191
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return this._badValue(value);
|
|
1192
|
+
const cand = this._find(key); // fills _update with per-level predecessors
|
|
1193
|
+
const K = this._key;
|
|
1194
|
+
if (cand !== 0 && K[cand] === key) { // existing key: update value in place
|
|
1195
|
+
this._val[cand] = value;
|
|
1196
|
+
this._version = (this._version + 1) | 0;
|
|
1197
|
+
return this;
|
|
1198
|
+
}
|
|
1199
|
+
const slot = this._pool.alloc();
|
|
1200
|
+
if (slot === 0) return this._full();
|
|
1201
|
+
// One LCG step; HIGH bits pick a geometric height, clamped to the columns.
|
|
1202
|
+
const word = this._seed = _lcgNext(this._seed);
|
|
1203
|
+
let nl = 1 + Math.clz32(word);
|
|
1204
|
+
if (nl > this._maxLevel) nl = this._maxLevel;
|
|
1205
|
+
const upd = this._update, next = this._next, stride = this._stride;
|
|
1206
|
+
if (nl > this._level) {
|
|
1207
|
+
for (let lvl = this._level; lvl < nl; lvl++) upd[lvl] = 0; // head is predecessor
|
|
1208
|
+
this._level = nl;
|
|
1209
|
+
}
|
|
1210
|
+
K[slot] = key;
|
|
1211
|
+
this._val[slot] = value;
|
|
1212
|
+
for (let lvl = 0; lvl < nl; lvl++) {
|
|
1213
|
+
const base = lvl * stride;
|
|
1214
|
+
const p = upd[lvl];
|
|
1215
|
+
next[base + slot] = next[base + p]; // splice slot after predecessor p
|
|
1216
|
+
next[base + p] = slot;
|
|
1217
|
+
}
|
|
1218
|
+
this._size++;
|
|
1219
|
+
this._version = (this._version + 1) | 0;
|
|
1220
|
+
return this;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Remove `key`. EXPECTED O(log n). Idempotent: returns `false` if `key` is
|
|
1225
|
+
* absent (no throw), `true` if it was present and removed. Fails closed on a
|
|
1226
|
+
* non-finite key (typeof-guarded first) with a `[lite-logn]` throw.
|
|
1227
|
+
* @param {number} key a finite number
|
|
1228
|
+
* @returns {boolean}
|
|
1229
|
+
*/
|
|
1230
|
+
delete(key) {
|
|
1231
|
+
if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
|
|
1232
|
+
const cand = this._find(key); // fills _update
|
|
1233
|
+
const K = this._key;
|
|
1234
|
+
if (cand === 0 || K[cand] !== key) return false; // absent (no throw)
|
|
1235
|
+
const next = this._next, stride = this._stride, upd = this._update;
|
|
1236
|
+
for (let lvl = 0; lvl < this._level; lvl++) {
|
|
1237
|
+
const base = lvl * stride;
|
|
1238
|
+
const p = upd[lvl];
|
|
1239
|
+
if (next[base + p] === cand) next[base + p] = next[base + cand];
|
|
1240
|
+
}
|
|
1241
|
+
// Shrink the live height while the top levels are empty (head link == NIL).
|
|
1242
|
+
let lv = this._level;
|
|
1243
|
+
while (lv > 1 && next[(lv - 1) * stride] === 0) lv--;
|
|
1244
|
+
this._level = lv;
|
|
1245
|
+
this._pool.free(cand);
|
|
1246
|
+
this._size--;
|
|
1247
|
+
this._version = (this._version + 1) | 0;
|
|
1248
|
+
return true;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* The smallest key STRICTLY greater than `key`, or `undefined` if none. EXPECTED
|
|
1253
|
+
* O(log n). `key` itself need not be present. Fails closed on a non-finite key.
|
|
1254
|
+
* @param {number} key a finite number
|
|
1255
|
+
* @returns {number|undefined}
|
|
1256
|
+
*/
|
|
1257
|
+
successor(key) {
|
|
1258
|
+
if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
|
|
1259
|
+
const next = this._next, K = this._key, stride = this._stride;
|
|
1260
|
+
let slot = 0;
|
|
1261
|
+
for (let lvl = this._level - 1; lvl >= 0; lvl--) {
|
|
1262
|
+
const base = lvl * stride;
|
|
1263
|
+
let nx = next[base + slot];
|
|
1264
|
+
while (nx !== 0 && K[nx] <= key) { slot = nx; nx = next[base + slot]; }
|
|
1265
|
+
}
|
|
1266
|
+
const cand = next[slot];
|
|
1267
|
+
return cand !== 0 ? K[cand] : undefined;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* The largest key STRICTLY less than `key`, or `undefined` if none. EXPECTED
|
|
1272
|
+
* O(log n). `key` itself need not be present. Fails closed on a non-finite key.
|
|
1273
|
+
* @param {number} key a finite number
|
|
1274
|
+
* @returns {number|undefined}
|
|
1275
|
+
*/
|
|
1276
|
+
predecessor(key) {
|
|
1277
|
+
if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
|
|
1278
|
+
const next = this._next, K = this._key, stride = this._stride;
|
|
1279
|
+
let slot = 0;
|
|
1280
|
+
for (let lvl = this._level - 1; lvl >= 0; lvl--) {
|
|
1281
|
+
const base = lvl * stride;
|
|
1282
|
+
let nx = next[base + slot];
|
|
1283
|
+
while (nx !== 0 && K[nx] < key) { slot = nx; nx = next[base + slot]; }
|
|
1284
|
+
}
|
|
1285
|
+
return slot !== 0 ? K[slot] : undefined; // slot = largest key < key, or head
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* A VERSION-STAMPED iterator over the keys in `[lo, hi]` INCLUSIVE, in ascending
|
|
1290
|
+
* order. Bounds may be any number INCLUDING +-Infinity (an unbounded end);
|
|
1291
|
+
* `NaN` (unordered) fails closed, as does `lo > hi`. The generator captures the
|
|
1292
|
+
* list's version and throws `[lite-logn]` if any STRUCTURAL mutation (set of a
|
|
1293
|
+
* new key, delete, clear -- or any value update) happens mid-iteration, rather
|
|
1294
|
+
* than yield stale / recycled data. The one documented per-protocol allocator
|
|
1295
|
+
* (a {value, done} per step); the loop body itself allocates nothing.
|
|
1296
|
+
* @param {number} lo lower bound (inclusive); may be -Infinity
|
|
1297
|
+
* @param {number} hi upper bound (inclusive); may be +Infinity
|
|
1298
|
+
* @returns {IterableIterator<number>} the keys in [lo, hi], ascending
|
|
1299
|
+
*/
|
|
1300
|
+
rangeIter(lo, hi) {
|
|
1301
|
+
if (typeof lo !== 'number' || Number.isNaN(lo)) return this._badBound(lo);
|
|
1302
|
+
if (typeof hi !== 'number' || Number.isNaN(hi)) return this._badBound(hi);
|
|
1303
|
+
if (lo > hi) return this._badRange(lo, hi);
|
|
1304
|
+
return this._rangeGen(lo, hi);
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/** @private version-stamped range generator (see rangeIter). */
|
|
1308
|
+
*_rangeGen(lo, hi) {
|
|
1309
|
+
const ver = this._version;
|
|
1310
|
+
const next = this._next, K = this._key, stride = this._stride;
|
|
1311
|
+
let slot = 0;
|
|
1312
|
+
for (let lvl = this._level - 1; lvl >= 0; lvl--) {
|
|
1313
|
+
const base = lvl * stride;
|
|
1314
|
+
let nx = next[base + slot];
|
|
1315
|
+
while (nx !== 0 && K[nx] < lo) { slot = nx; nx = next[base + slot]; }
|
|
1316
|
+
}
|
|
1317
|
+
slot = next[slot]; // first slot with key >= lo
|
|
1318
|
+
while (slot !== 0 && K[slot] <= hi) {
|
|
1319
|
+
if (this._version !== ver) {
|
|
1320
|
+
throw new Error('[lite-logn] SkipList mutated during iteration');
|
|
1321
|
+
}
|
|
1322
|
+
yield K[slot];
|
|
1323
|
+
slot = next[slot]; // level-0 next (base 0)
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* Visit every live `(key, value)` pair in ASCENDING key order. O(n) cold scan,
|
|
1329
|
+
* allocation-free in the loop body (pass a hoisted callback). Unlike rangeIter
|
|
1330
|
+
* this is NOT version-stamped -- mutating from within the callback is the
|
|
1331
|
+
* caller's responsibility (matching the other members' forEach).
|
|
1332
|
+
* @param {(key:number, value:number, list:SkipList)=>void} fn
|
|
1333
|
+
*/
|
|
1334
|
+
forEach(fn) {
|
|
1335
|
+
const next = this._next, K = this._key, V = this._val;
|
|
1336
|
+
let slot = next[0]; // level-0 first (base 0, head)
|
|
1337
|
+
while (slot !== 0) {
|
|
1338
|
+
fn(K[slot], V[slot], this);
|
|
1339
|
+
slot = next[slot];
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Empty the list, keeping the fixed capacity. O(capacity) cold path: returns
|
|
1345
|
+
* every node to the pool, points the head's links at NIL, resets the live
|
|
1346
|
+
* height, and restores the PRNG to its initial seed (a cleared list replays a
|
|
1347
|
+
* fresh one). @returns {this}
|
|
1348
|
+
*/
|
|
1349
|
+
clear() {
|
|
1350
|
+
this._pool.clear();
|
|
1351
|
+
const next = this._next, stride = this._stride, cols = this._maxLevel;
|
|
1352
|
+
for (let lvl = 0; lvl < cols; lvl++) next[lvl * stride] = 0; // head links -> NIL
|
|
1353
|
+
this._level = 1;
|
|
1354
|
+
this._size = 0;
|
|
1355
|
+
this._seed = this._seed0;
|
|
1356
|
+
this._version = (this._version + 1) | 0;
|
|
1357
|
+
return this;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// ---- private structural descent (hot body) -----------------------------
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* The ONE structural descent (set / delete): walk top-down, at each level
|
|
1364
|
+
* advancing while the next key is strictly less than `key`, recording the
|
|
1365
|
+
* predecessor per level in the reused `_update` scratch. Returns the level-0
|
|
1366
|
+
* candidate (first slot with key >= `key`, or NIL). Zero allocation.
|
|
1367
|
+
* @private
|
|
1368
|
+
*/
|
|
1369
|
+
_find(key) {
|
|
1370
|
+
const next = this._next, K = this._key, stride = this._stride, upd = this._update;
|
|
1371
|
+
let slot = 0; // head
|
|
1372
|
+
for (let lvl = this._level - 1; lvl >= 0; lvl--) {
|
|
1373
|
+
const base = lvl * stride;
|
|
1374
|
+
let nx = next[base + slot];
|
|
1375
|
+
while (nx !== 0 && K[nx] < key) { slot = nx; nx = next[base + slot]; }
|
|
1376
|
+
upd[lvl] = slot;
|
|
1377
|
+
}
|
|
1378
|
+
return next[slot]; // level-0 next (base 0)
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// ---- cold path only: throw builders (string concat off the hot body) ----
|
|
1382
|
+
|
|
1383
|
+
/** @private */
|
|
1384
|
+
_badKey(key) {
|
|
1385
|
+
throw new TypeError(
|
|
1386
|
+
'[lite-logn] SkipList key must be a finite number, got ' + String(key));
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/** @private */
|
|
1390
|
+
_badValue(value) {
|
|
1391
|
+
throw new TypeError(
|
|
1392
|
+
'[lite-logn] SkipList value must be a finite number, got ' + String(value));
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/** @private */
|
|
1396
|
+
_badBound(b) {
|
|
1397
|
+
throw new TypeError(
|
|
1398
|
+
'[lite-logn] SkipList rangeIter bound must be a number (not NaN), got ' + String(b));
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/** @private */
|
|
1402
|
+
_badRange(lo, hi) {
|
|
1403
|
+
throw new RangeError(
|
|
1404
|
+
'[lite-logn] SkipList rangeIter needs lo <= hi, got lo=' + String(lo) +
|
|
1405
|
+
' hi=' + String(hi));
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/** @private */
|
|
1409
|
+
_full() {
|
|
1410
|
+
throw new RangeError('[lite-logn] SkipList full (capacity ' + this._cap + ')');
|
|
1411
|
+
}
|
|
1412
|
+
}
|