@zakkster/lite-logn 0.4.0 → 0.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/CHANGELOG.md CHANGED
@@ -6,6 +6,74 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0] - 2026-09-20
10
+
11
+ ### Added
12
+
13
+ - **Treap** -- the fifth member and the family's balanced BST: a randomized,
14
+ self-balancing binary search tree that is ALSO an order-statistic tree (an AUGMENTED
15
+ ordered map key -> value). A BST order on `_key` x a MAX-HEAP order on a per-node
16
+ random priority `_prio` gives EXPECTED O(log n) height, and a subtree-size column
17
+ `_size` (maintained in the SAME pass as every link rewrite) adds O(log n) order
18
+ statistics + set surgery. Surface: `get` / `has` / `set` (updates the value in place
19
+ on an existing key) / `delete` (idempotent) / `rank(x)` (count of keys STRICTLY less
20
+ than x) / `select(k)` (the k-th smallest key, 0-based) / `successor` (strictly
21
+ greater) / `predecessor` (strictly less) / `rangeIter(lo, hi)` (a VERSION-STAMPED
22
+ iterator over `[lo, hi]` inclusive, ascending; `+-Infinity` bounds allowed,
23
+ structural OR value mutation mid-iteration throws) / `forEach` / `clear` / `split`,
24
+ the static `Treap.merge(a, b)`, and `size` / `capacity` getters. Keys and values are
25
+ finite numbers (typeof-guarded before coercion -- Symbol / BigInt / NaN / +-Infinity
26
+ fail closed with a `[lite-logn]` throw).
27
+ - **Pointer-free, zero-GC, second bind of the shared NodePool.** Nodes are slot
28
+ INDICES in six flat columns (`_key` / `_value` Float64; `_left` / `_right` / `_prio`
29
+ / `_size` Uint32, `NIL = 0`) over the SAME private free-list (`NodePool`) SkipList
30
+ ships -- design-parity, not a fork or a runtime dep (decisions/0007-treap.md). The
31
+ conservation invariant `activeSlots + freeListLength === capacity` holds after every
32
+ op. `TR_MAX_CAPACITY = 0x7FFFFFFF` (2^31 - 1: slot indices + subtree counts fit a
33
+ `Uint32`). Rotations rewrite one child link pair + two `_size` cells; `set` / `delete`
34
+ / `split` / `merge` recurse over slot indices on the native CALL STACK (not the GC
35
+ heap), so every hot op is 0 B/op.
36
+ - **Priority via the repo LCG.** One instance-local Numerical-Recipes LCG draw per
37
+ inserted node; a fixed seed replays an identical structure, ties break by key, so the
38
+ tree shape is a deterministic function of the (key, priority) set.
39
+ - **split / merge are O(log n) EXPECTED (arena-sharing).** `split(key)` returns
40
+ `[left (keys < key), right (keys >= key)]` by rewiring in place, so the two treaps
41
+ SHARE the source's backing arena and the source is CONSUMED (left empty);
42
+ `Treap.merge(a, b)` requires `a` / `b` to share an arena (all keys of a < all of b)
43
+ and consumes both. Fails closed on non-Treap inputs, cross-arena treaps, or an
44
+ overlapping key range.
45
+ - **EXPECTED, not worst-case.** A hot op is EXPECTED O(log n) (the randomized
46
+ priority heap); the MAX single insert (rotation chain) is DISCLOSED by the witness,
47
+ never gated -- the same honesty contract as SkipList.
48
+ - **Witness: one more log line.** `test/witness.mjs` gains `Treap.get` (a BST descent)
49
+ against a linear-scan O(n) foil. Measured on this machine (shared, FROZEN R^2 floor
50
+ 0.958, the four prior members' bands UNTOUCHED): `get` R^2 ~ 0.988, slope ~ 4.03
51
+ ns/level, in its OWN band `[2.55, 5.95]` = median-of-15 fit-runs (median 4.25) x
52
+ `[0.6, 1.4]`, centered on the median (ADR-0004). A treap descent touches one node per
53
+ level, so its per-level slope is lower than SkipList.get's (~8.78) -- expected, which
54
+ is why only the R^2 floor is shared. The linear-scan foil MISSES the floor
55
+ (R^2 ~ 0.79). All EIGHT gated op-rows are ON-LINE; MAX single insert disclosed
56
+ (~54 us on the cold shuffled build trace).
57
+ - **Types + docs.** `LogN.d.ts` gains the `Treap` ambient block; `llms.txt` gains the
58
+ Treap roster entry + full export surface; `decisions/0007-treap.md` records D-06/D-07
59
+ (the balanced-BST pick, the augmentation, the NodePool reuse, the arena-sharing
60
+ split/merge, and the recursion-depth disclosure).
61
+
62
+ ### Verified
63
+
64
+ - Torture: 0 B/op on every Treap hot lane (get / set / delete / rank / select /
65
+ successor / forEach / rangeIter) + the mixed steady-state churn; `gc major = 0`;
66
+ leak `size 0/0`; the private-pool conservation invariant after every soak cycle; a
67
+ 32 B/op control lane proving the instrument has teeth. Prior four members still green.
68
+ - `test/perf/PerfGate.test.mjs`: four Treap scenarios (get / set / delete / rank-select-
69
+ successor mix) at 0 scavenges, backing buffers fixed (the `grows` counter reads 0).
70
+ - `test/Treap.test.mjs`: a >= 1e5 mixed-op differential fuzz vs a Map + sorted-array
71
+ oracle (0 divergences), the three treap invariants checked throughout (BST order,
72
+ heap order, subtree-size correctness), rank/select/split/merge correctness, the
73
+ fail-closed doors ([lite-logn] tag pinned), determinism, and conservation.
74
+ - `LogN.js`: the prior four classes are BYTE-IDENTICAL; only the `VERSION` const and
75
+ the appended `Treap` section changed.
76
+
9
77
  ## [0.4.0] - 2026-09-17
10
78
 
11
79
  ### Added
package/LogN.d.ts CHANGED
@@ -171,3 +171,58 @@ export class SkipList {
171
171
  /** Empty the list, keeping capacity (resets the PRNG to its initial seed). */
172
172
  clear(): this;
173
173
  }
174
+
175
+ /**
176
+ * A treap: a randomized, self-balancing BST that is also an order-statistic tree (an
177
+ * AUGMENTED ordered map key -> value). BST order on keys x max-heap order on a per-node
178
+ * random priority gives EXPECTED O(log n) height; a subtree-size column adds O(log n)
179
+ * rank / select / split / merge. Nodes are slot INDICES in flat typed-array columns
180
+ * over a private free-list (no heap object per op). Keys and values are finite numbers
181
+ * (typeof-guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail closed). set
182
+ * on an existing key updates the value in place. EXPECTED, not worst-case (an unlucky
183
+ * priority draw can spike one op; the MAX single insert is disclosed, not gated). Fixed
184
+ * capacity: a full pool throws. Every hot op allocates zero bytes.
185
+ */
186
+ export class Treap {
187
+ /** @param capacity exact max live entries; integer in [1, 2^31-1].
188
+ * @param seed PRNG seed; unsigned 32-bit integer (default fixed). */
189
+ constructor(capacity: number, seed?: number);
190
+
191
+ /** Live entry count. */
192
+ readonly size: number;
193
+ /** The fixed capacity this treap was sized for. */
194
+ readonly capacity: number;
195
+
196
+ /** The value under key, or undefined if absent (no throw). Non-finite key throws. */
197
+ get(key: number): number | undefined;
198
+ /** True iff key is currently stored. Non-finite key throws. */
199
+ has(key: number): boolean;
200
+ /** Insert key -> value, or update the value in place if key exists. Non-finite
201
+ * key/value throws; a full pool throws. */
202
+ set(key: number, value: number): this;
203
+ /** Remove key; true if it was present, false if absent (idempotent). Non-finite key throws. */
204
+ delete(key: number): boolean;
205
+ /** Count of stored keys strictly less than x (its rank), in [0, size]. Non-finite x throws. */
206
+ rank(x: number): number;
207
+ /** The k-th smallest key (0-based), or undefined if k is out of [0, size). Non-integer k throws. */
208
+ select(k: number): number | undefined;
209
+ /** The smallest key strictly greater than key, or undefined. Non-finite key throws. */
210
+ successor(key: number): number | undefined;
211
+ /** The largest key strictly less than key, or undefined. Non-finite key throws. */
212
+ predecessor(key: number): number | undefined;
213
+ /** A version-stamped iterator over keys in [lo, hi] inclusive, ascending. Bounds
214
+ * may be +-Infinity (unbounded ends); NaN or lo > hi throws; mutation during
215
+ * iteration throws. */
216
+ rangeIter(lo: number, hi: number): IterableIterator<number>;
217
+ /** Visit every (key, value) pair in ascending key order. */
218
+ forEach(fn: (key: number, value: number, treap: Treap) => void): void;
219
+ /** Empty the treap, keeping capacity (resets the PRNG to its initial seed). */
220
+ clear(): this;
221
+
222
+ /** Split at key into [left (keys < key), right (keys >= key)]; CONSUMES this and
223
+ * returns two treaps SHARING this treap's backing arena. Non-finite key throws. */
224
+ split(key: number): [Treap, Treap];
225
+ /** Merge two arena-sharing treaps where every key of a < every key of b, CONSUMING
226
+ * both. Non-Treap inputs, different arenas, or an overlapping range throw. */
227
+ static merge(a: Treap, b: Treap): Treap;
228
+ }
package/LogN.js CHANGED
@@ -39,7 +39,7 @@
39
39
  */
40
40
 
41
41
  /** Package version. One of the three version sites (package.json / VERSION / llms.txt). */
42
- export const VERSION = '0.4.0';
42
+ export const VERSION = '0.5.0';
43
43
 
44
44
  // --- members land here, append-only, one tree-shakeable class each -----------
45
45
  // BinaryHeap (v0.1.0 session) -- indexed O(log n) min|max heap (BELOW)
@@ -1410,3 +1410,576 @@ export class SkipList {
1410
1410
  throw new RangeError('[lite-logn] SkipList full (capacity ' + this._cap + ')');
1411
1411
  }
1412
1412
  }
1413
+
1414
+ // Treap (v0.5.0 session) -- an AUGMENTED randomized-balanced ordered map (BELOW).
1415
+
1416
+ /** Treap default PRNG seed when the caller does not supply one. */
1417
+ const TR_DEFAULT_SEED = 0x9E3779B9;
1418
+
1419
+ /**
1420
+ * Max Treap capacity: `0x7FFFFFFF` (2^31 - 1). Every node is addressed by a slot
1421
+ * INDEX stored in `Uint32Array` link columns (`_left` / `_right`), and each subtree
1422
+ * count lives in a `Uint32Array` (`_size`); an index and a count must both fit an
1423
+ * unsigned 32-bit word. `NIL = 0` reserves slot 0 as the empty-subtree sentinel, so
1424
+ * live slots run [1, capacity]. The index / count arithmetic (Uint32 slot indices +
1425
+ * `NIL = 0` + Uint32 subtree sizes), not the byte count, is the hard ceiling -- the
1426
+ * same "the arithmetic caps it" reasoning as the array-embedded members.
1427
+ */
1428
+ const TR_MAX_CAPACITY = 0x7FFFFFFF; // 2^31 - 1
1429
+
1430
+ /**
1431
+ * A TREAP: a randomized, self-balancing BINARY SEARCH TREE that is ALSO an order-
1432
+ * statistic tree (an AUGMENTED ordered map key -> value). It is the family's second
1433
+ * randomized member and its second pointer-based one; where SkipList threads a
1434
+ * probabilistic tower of forward links, a treap keeps a single BST whose SHAPE is
1435
+ * randomized by a per-node priority, giving EXPECTED O(log n) height. The trick that
1436
+ * keeps it zero-GC is the SkipList one: nodes are slot INDICES in flat typed-array
1437
+ * columns over the same private free-list (NodePool), never heap objects.
1438
+ *
1439
+ * Two orders held at once (the treap invariant):
1440
+ * - BST order on `_key` (an in-order walk is ascending by key); and
1441
+ * - MAX-HEAP order on `_prio` (every parent's priority >= its children's), where
1442
+ * `_prio` is one instance-local NR-LCG draw per inserted node. A random priority
1443
+ * heap over a BST is provably balanced IN EXPECTATION.
1444
+ * The augmentation is a third invariant: `_size[x]` is the number of nodes in x's
1445
+ * subtree, maintained in the SAME pass as every link rewrite, so `rank` (how many
1446
+ * keys are < x) and `select` (the k-th smallest key) are O(log n) via subtree counts.
1447
+ *
1448
+ * Storage (allocated once, sized to capacity + 1; slot 0 is the NIL sentinel whose
1449
+ * `_size` is a permanent 0 -- null is not zero: slot 0 is "no subtree", never data):
1450
+ * - `_key` / `_value` `Float64Array` -- key and value at each slot.
1451
+ * - `_left` / `_right` `Uint32Array` -- child slot indices, `NIL = 0`.
1452
+ * - `_prio` `Uint32Array` -- the random heap priority at each slot.
1453
+ * - `_size` `Uint32Array` -- the subtree node count at each slot.
1454
+ * - `_pool` NodePool -- the free-list handing out slot indices [1, capacity].
1455
+ *
1456
+ * Honesty (randomized member): a hot op is EXPECTED O(log n), not worst-case -- the
1457
+ * same contract as SkipList. An unlucky priority draw can build a tall thin tree and
1458
+ * spike a single op; the MAX single insert (the rotation chain) is DISCLOSED, never
1459
+ * gated (decisions/0007-treap.md, D-06). RECURSION DEPTH: `set` / `delete` / `split`
1460
+ * / `merge` recurse over slot indices; the recursion depth equals the tree height,
1461
+ * which is O(log n) EXPECTED and O(n) worst-case on a pathological priority draw.
1462
+ * Because priorities come from the instance-local LCG (NOT caller-controlled), an
1463
+ * adversary cannot force the worst case with chosen keys, so the expected bound holds
1464
+ * for the fixed public surface -- this matches the EXPECTED contract and is DISCLOSED
1465
+ * here + in the ADR, not silently shipped. The recursion uses the native call stack,
1466
+ * not the GC heap, so every hot op is still 0 B/op.
1467
+ *
1468
+ * Keys and values are FINITE numbers (typeof-guarded BEFORE coercion -- Symbol /
1469
+ * BigInt / NaN / +-Infinity fail closed with a `[lite-logn]` throw). `set` on an
1470
+ * EXISTING key updates its value in place (no new node). A missing / empty query
1471
+ * returns `undefined` (never throws). Fixed capacity: a full pool throws, never
1472
+ * silently drops. `rangeIter` is a VERSION-STAMPED iterator -- any structural OR
1473
+ * value mutation mid-iteration throws `[lite-logn]` rather than yield stale data.
1474
+ *
1475
+ * `split` / `merge` are O(log n) EXPECTED because they REWIRE nodes in place rather
1476
+ * than copy: the two treaps a `split` returns (and the two a `merge` consumes) SHARE
1477
+ * the source's backing arena (columns + free-list). `split` and `merge` therefore
1478
+ * CONSUME their inputs (leaving them empty) and hand back views over the same store
1479
+ * -- the only way to keep the structural ops sub-linear under a pooled allocator.
1480
+ */
1481
+ export class Treap {
1482
+ /**
1483
+ * @param {number} capacity exact max live entries; integer in [1, 2^31-1].
1484
+ * @param {number} [seed] PRNG seed; unsigned 32-bit integer (default fixed).
1485
+ */
1486
+ constructor(capacity, seed) {
1487
+ // typeof guard BEFORE coercion (Number.isInteger is Symbol/BigInt-safe).
1488
+ if (typeof capacity !== 'number' || !Number.isInteger(capacity) ||
1489
+ capacity < 1 || capacity > TR_MAX_CAPACITY) {
1490
+ throw new RangeError(
1491
+ '[lite-logn] Treap capacity must be an integer in [1, 2^31-1], got ' +
1492
+ String(capacity));
1493
+ }
1494
+ let s;
1495
+ if (seed === undefined) {
1496
+ s = TR_DEFAULT_SEED;
1497
+ } else if (typeof seed !== 'number' || !Number.isInteger(seed) ||
1498
+ seed < 0 || seed > 0xFFFFFFFF) {
1499
+ throw new RangeError(
1500
+ '[lite-logn] Treap seed must be an unsigned 32-bit integer, got ' +
1501
+ String(seed));
1502
+ } else {
1503
+ s = seed >>> 0;
1504
+ }
1505
+ s = s | 0; // store the LCG state as a SIGNED int32 (an unboxed Smi) -- see _lcgNext
1506
+ this._cap = capacity; // max live entries
1507
+ this._key = new Float64Array(capacity + 1); // key at each slot
1508
+ this._value = new Float64Array(capacity + 1); // value at each slot
1509
+ this._left = new Uint32Array(capacity + 1); // left child slot; NIL = 0
1510
+ this._right = new Uint32Array(capacity + 1); // right child slot; NIL = 0
1511
+ this._prio = new Uint32Array(capacity + 1); // random heap priority
1512
+ this._size = new Uint32Array(capacity + 1); // subtree node count; _size[0] = 0
1513
+ this._pool = new NodePool(capacity); // free-list over slots [1, capacity]
1514
+ this._root = 0; // NIL == empty tree
1515
+ this._seed0 = s; // initial seed (clear resets to it)
1516
+ this._seed = s; // live LCG state
1517
+ this._version = 0; // iterator invalidation stamp
1518
+ this._sr = 0; // split scratch (the "right" root)
1519
+ }
1520
+
1521
+ /** Live entry count. O(1) (the root subtree count). */
1522
+ get size() { return this._root === 0 ? 0 : this._size[this._root]; }
1523
+
1524
+ /** The fixed capacity this treap was sized for. O(1). */
1525
+ get capacity() { return this._cap; }
1526
+
1527
+ /**
1528
+ * The value stored under `key`, or `undefined` if absent (never throws on a
1529
+ * missing / empty query). EXPECTED O(log n): a plain BST descent. Fails closed on
1530
+ * a non-number / non-finite key (typeof-guarded first) with a `[lite-logn]` throw.
1531
+ * @param {number} key a finite number
1532
+ * @returns {number|undefined}
1533
+ */
1534
+ get(key) {
1535
+ if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
1536
+ const L = this._left, R = this._right, K = this._key;
1537
+ let t = this._root;
1538
+ while (t !== 0) {
1539
+ if (key < K[t]) t = L[t];
1540
+ else if (key > K[t]) t = R[t];
1541
+ else return this._value[t];
1542
+ }
1543
+ return undefined;
1544
+ }
1545
+
1546
+ /**
1547
+ * True iff `key` is currently in the treap. EXPECTED O(log n). Fails closed on a
1548
+ * non-finite key (typeof-guarded first).
1549
+ * @param {number} key a finite number
1550
+ * @returns {boolean}
1551
+ */
1552
+ has(key) {
1553
+ if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
1554
+ const L = this._left, R = this._right, K = this._key;
1555
+ let t = this._root;
1556
+ while (t !== 0) {
1557
+ if (key < K[t]) t = L[t];
1558
+ else if (key > K[t]) t = R[t];
1559
+ else return true;
1560
+ }
1561
+ return false;
1562
+ }
1563
+
1564
+ /**
1565
+ * Insert `key -> value`, or UPDATE the value in place if `key` already exists (no
1566
+ * new node). EXPECTED O(log n): a descent to check membership, then (on insert) a
1567
+ * recursive splice that rotates the new node up until heap order is restored,
1568
+ * fixing `_size` on the unwind. Fails closed: a non-finite key or value
1569
+ * (typeof-guarded first), or a full pool, each throw `[lite-logn]` as a no-op.
1570
+ * @param {number} key a finite number
1571
+ * @param {number} value a finite number
1572
+ * @returns {this}
1573
+ */
1574
+ set(key, value) {
1575
+ if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
1576
+ if (typeof value !== 'number' || !Number.isFinite(value)) return this._badValue(value);
1577
+ const L = this._left, R = this._right, K = this._key;
1578
+ let t = this._root;
1579
+ while (t !== 0) { // update in place if present -- no new node, no rebalance
1580
+ if (key < K[t]) t = L[t];
1581
+ else if (key > K[t]) t = R[t];
1582
+ else { this._value[t] = value; this._version = (this._version + 1) | 0; return this; }
1583
+ }
1584
+ const slot = this._pool.alloc();
1585
+ if (slot === 0) return this._full();
1586
+ K[slot] = key;
1587
+ this._value[slot] = value;
1588
+ L[slot] = 0; R[slot] = 0;
1589
+ this._size[slot] = 1;
1590
+ this._seed = _lcgNext(this._seed);
1591
+ this._prio[slot] = this._seed; // stored unsigned in the Uint32 column
1592
+ this._root = this._insert(this._root, slot);
1593
+ this._version = (this._version + 1) | 0;
1594
+ return this;
1595
+ }
1596
+
1597
+ /**
1598
+ * Remove `key`. EXPECTED O(log n). Idempotent: returns `false` if `key` is absent
1599
+ * (no throw), `true` if it was present and removed. Deletion MERGES the removed
1600
+ * node's two subtrees (priority-ordered) then frees its slot, fixing `_size` on
1601
+ * the unwind. Fails closed on a non-finite key (typeof-guarded first).
1602
+ * @param {number} key a finite number
1603
+ * @returns {boolean}
1604
+ */
1605
+ delete(key) {
1606
+ if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
1607
+ const L = this._left, R = this._right, K = this._key;
1608
+ let t = this._root, found = false;
1609
+ while (t !== 0) {
1610
+ if (key < K[t]) t = L[t];
1611
+ else if (key > K[t]) t = R[t];
1612
+ else { found = true; break; }
1613
+ }
1614
+ if (!found) return false; // absent (no throw)
1615
+ this._root = this._delete(this._root, key);
1616
+ this._version = (this._version + 1) | 0;
1617
+ return true;
1618
+ }
1619
+
1620
+ /**
1621
+ * The number of stored keys STRICTLY LESS than `x` (its rank / position). EXPECTED
1622
+ * O(log n) via subtree counts: at each node, when the node's key is < `x`, its
1623
+ * whole left subtree plus itself precede `x`. `x` need not be present; `rank` of
1624
+ * the smallest key is 0, of a key past the max is `size`. Fails closed on a
1625
+ * non-finite `x`.
1626
+ * @param {number} x a finite number
1627
+ * @returns {number} count of keys < x, in [0, size]
1628
+ */
1629
+ rank(x) {
1630
+ if (typeof x !== 'number' || !Number.isFinite(x)) return this._badKey(x);
1631
+ const L = this._left, R = this._right, K = this._key, S = this._size;
1632
+ let t = this._root, r = 0;
1633
+ while (t !== 0) {
1634
+ if (x <= K[t]) t = L[t]; // t (and its right) are >= x
1635
+ else { r += S[L[t]] + 1; t = R[t]; } // t's left subtree + t precede x
1636
+ }
1637
+ return r;
1638
+ }
1639
+
1640
+ /**
1641
+ * The k-th smallest KEY (0-based order statistic), or `undefined` if `k` is out of
1642
+ * range [0, size). EXPECTED O(log n) via subtree counts. Fails closed on a
1643
+ * non-integer `k` (typeof-guarded first); an in-type out-of-range `k` returns
1644
+ * `undefined` (matching the soft-miss of `get`).
1645
+ * @param {number} k integer in [0, size)
1646
+ * @returns {number|undefined} the k-th smallest key
1647
+ */
1648
+ select(k) {
1649
+ if (typeof k !== 'number' || !Number.isInteger(k)) return this._badRank(k);
1650
+ if (k < 0 || k >= this.size) return undefined;
1651
+ const L = this._left, R = this._right, K = this._key, S = this._size;
1652
+ let t = this._root;
1653
+ for (;;) {
1654
+ const ls = S[L[t]];
1655
+ if (k < ls) t = L[t];
1656
+ else if (k > ls) { k -= ls + 1; t = R[t]; }
1657
+ else return K[t];
1658
+ }
1659
+ }
1660
+
1661
+ /**
1662
+ * The smallest key STRICTLY greater than `key`, or `undefined` if none. EXPECTED
1663
+ * O(log n). `key` itself need not be present. Fails closed on a non-finite key.
1664
+ * @param {number} key a finite number
1665
+ * @returns {number|undefined}
1666
+ */
1667
+ successor(key) {
1668
+ if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
1669
+ const L = this._left, R = this._right, K = this._key;
1670
+ let t = this._root, best;
1671
+ while (t !== 0) {
1672
+ if (K[t] > key) { best = K[t]; t = L[t]; }
1673
+ else t = R[t];
1674
+ }
1675
+ return best;
1676
+ }
1677
+
1678
+ /**
1679
+ * The largest key STRICTLY less than `key`, or `undefined` if none. EXPECTED
1680
+ * O(log n). `key` itself need not be present. Fails closed on a non-finite key.
1681
+ * @param {number} key a finite number
1682
+ * @returns {number|undefined}
1683
+ */
1684
+ predecessor(key) {
1685
+ if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
1686
+ const L = this._left, R = this._right, K = this._key;
1687
+ let t = this._root, best;
1688
+ while (t !== 0) {
1689
+ if (K[t] < key) { best = K[t]; t = R[t]; }
1690
+ else t = L[t];
1691
+ }
1692
+ return best;
1693
+ }
1694
+
1695
+ /**
1696
+ * A VERSION-STAMPED iterator over the keys in `[lo, hi]` INCLUSIVE, ascending.
1697
+ * Bounds may be any number INCLUDING +-Infinity (an unbounded end); `NaN` fails
1698
+ * closed, as does `lo > hi`. The generator captures the treap's version and throws
1699
+ * `[lite-logn]` if any STRUCTURAL or VALUE mutation happens mid-iteration, rather
1700
+ * than yield stale data. It walks by repeated `successor` (each step a fresh
1701
+ * O(log n) descent, so NO scratch stack is allocated); the one documented per-
1702
+ * protocol allocator is the {value, done} per step.
1703
+ * @param {number} lo lower bound (inclusive); may be -Infinity
1704
+ * @param {number} hi upper bound (inclusive); may be +Infinity
1705
+ * @returns {IterableIterator<number>} the keys in [lo, hi], ascending
1706
+ */
1707
+ rangeIter(lo, hi) {
1708
+ if (typeof lo !== 'number' || Number.isNaN(lo)) return this._badBound(lo);
1709
+ if (typeof hi !== 'number' || Number.isNaN(hi)) return this._badBound(hi);
1710
+ if (lo > hi) return this._badRange(lo, hi);
1711
+ return this._rangeGen(lo, hi);
1712
+ }
1713
+
1714
+ /** @private version-stamped range generator (see rangeIter). */
1715
+ *_rangeGen(lo, hi) {
1716
+ const ver = this._version;
1717
+ let cur = this._ceil(lo); // smallest key >= lo, or undefined
1718
+ while (cur !== undefined && cur <= hi) {
1719
+ if (this._version !== ver) {
1720
+ throw new Error('[lite-logn] Treap mutated during iteration');
1721
+ }
1722
+ yield cur;
1723
+ cur = this.successor(cur);
1724
+ }
1725
+ }
1726
+
1727
+ /**
1728
+ * Visit every live `(key, value)` pair in ASCENDING key order. O(n) cold in-order
1729
+ * walk (recursion depth = tree height), allocation-free in the loop body (pass a
1730
+ * hoisted callback). Unlike rangeIter this is NOT version-stamped -- mutating from
1731
+ * within the callback is the caller's responsibility (matching the other members).
1732
+ * @param {(key:number, value:number, treap:Treap)=>void} fn
1733
+ */
1734
+ forEach(fn) {
1735
+ this._forEach(this._root, fn);
1736
+ }
1737
+
1738
+ /** @private recursive in-order walk. */
1739
+ _forEach(t, fn) {
1740
+ if (t === 0) return;
1741
+ this._forEach(this._left[t], fn);
1742
+ fn(this._key[t], this._value[t], this);
1743
+ this._forEach(this._right[t], fn);
1744
+ }
1745
+
1746
+ /**
1747
+ * Empty the treap, keeping the fixed capacity. O(capacity) cold path: returns
1748
+ * every node to the pool, points the root at NIL, and restores the PRNG to its
1749
+ * initial seed (a cleared treap replays a fresh one). @returns {this}
1750
+ */
1751
+ clear() {
1752
+ this._pool.clear();
1753
+ this._root = 0;
1754
+ this._seed = this._seed0;
1755
+ this._version = (this._version + 1) | 0;
1756
+ return this;
1757
+ }
1758
+
1759
+ /**
1760
+ * SPLIT `this` at `key` into two treaps: `[left, right]` where `left` holds every
1761
+ * key STRICTLY LESS than `key` and `right` holds every key >= `key`. EXPECTED
1762
+ * O(log n) -- it REWIRES nodes in place (no copy), so the returned treaps SHARE
1763
+ * this treap's backing arena, and `this` is CONSUMED (left empty). Fails closed on
1764
+ * a non-finite key.
1765
+ * @param {number} key a finite number
1766
+ * @returns {[Treap, Treap]} [keys < key, keys >= key]
1767
+ */
1768
+ split(key) {
1769
+ if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
1770
+ const l = this._split(this._root, key);
1771
+ const r = this._sr;
1772
+ const left = Treap._view(this, l);
1773
+ const right = Treap._view(this, r);
1774
+ this._root = 0; // consumed: its nodes now belong to left / right
1775
+ this._version = (this._version + 1) | 0;
1776
+ return [left, right];
1777
+ }
1778
+
1779
+ /**
1780
+ * MERGE two treaps `a` and `b` -- where EVERY key of `a` is STRICTLY LESS than
1781
+ * every key of `b` -- into one, returning it. EXPECTED O(log n): it rewires nodes
1782
+ * in place, so `a` and `b` MUST share a backing arena (i.e. both came from a prior
1783
+ * `split`), and BOTH are CONSUMED. Fails closed: non-Treap inputs, treaps from
1784
+ * different arenas, or an overlapping key range each throw `[lite-logn]`.
1785
+ * @param {Treap} a all keys strictly less than every key of b
1786
+ * @param {Treap} b all keys strictly greater than every key of a
1787
+ * @returns {Treap}
1788
+ */
1789
+ static merge(a, b) {
1790
+ if (!(a instanceof Treap) || !(b instanceof Treap)) {
1791
+ throw new TypeError('[lite-logn] Treap.merge needs two Treap instances');
1792
+ }
1793
+ if (a._key !== b._key) {
1794
+ throw new Error(
1795
+ '[lite-logn] Treap.merge requires two treaps sharing an arena (from the same split)');
1796
+ }
1797
+ if (a._root !== 0 && b._root !== 0) {
1798
+ let m = a._root; while (a._right[m] !== 0) m = a._right[m]; // max key of a
1799
+ let n = b._root; while (b._left[n] !== 0) n = b._left[n]; // min key of b
1800
+ if (a._key[m] >= b._key[n]) {
1801
+ throw new Error(
1802
+ '[lite-logn] Treap.merge requires all keys of a < all keys of b');
1803
+ }
1804
+ }
1805
+ const root = a._merge(a._root, b._root);
1806
+ const out = Treap._view(a, root);
1807
+ a._root = 0; b._root = 0; // both consumed
1808
+ a._version = (a._version + 1) | 0;
1809
+ b._version = (b._version + 1) | 0;
1810
+ return out;
1811
+ }
1812
+
1813
+ // ---- private rotations + recursive structure (hot bodies) ---------------
1814
+
1815
+ /**
1816
+ * @private true iff node `a` outranks node `b` in the priority MAX-heap. Priority
1817
+ * ties (astronomically rare across 32-bit LCG draws) break by key, so the tree
1818
+ * shape is a deterministic function of the (key, priority) set -- never ambiguous.
1819
+ */
1820
+ _higher(a, b) {
1821
+ const pa = this._prio[a], pb = this._prio[b];
1822
+ return pa > pb || (pa === pb && this._key[a] < this._key[b]);
1823
+ }
1824
+
1825
+ /**
1826
+ * @private right rotation: `y`'s left child `x` becomes the subtree root. Rewrites
1827
+ * two child links and recomputes the two affected `_size` cells. Returns `x`.
1828
+ */
1829
+ _rotR(y) {
1830
+ const L = this._left, R = this._right, S = this._size;
1831
+ const x = L[y];
1832
+ L[y] = R[x];
1833
+ R[x] = y;
1834
+ S[y] = S[L[y]] + S[R[y]] + 1;
1835
+ S[x] = S[L[x]] + S[R[x]] + 1;
1836
+ return x;
1837
+ }
1838
+
1839
+ /**
1840
+ * @private left rotation: `y`'s right child `x` becomes the subtree root. Rewrites
1841
+ * two child links and recomputes the two affected `_size` cells. Returns `x`.
1842
+ */
1843
+ _rotL(y) {
1844
+ const L = this._left, R = this._right, S = this._size;
1845
+ const x = R[y];
1846
+ R[y] = L[x];
1847
+ L[x] = y;
1848
+ S[y] = S[L[y]] + S[R[y]] + 1;
1849
+ S[x] = S[L[x]] + S[R[x]] + 1;
1850
+ return x;
1851
+ }
1852
+
1853
+ /** @private recursive BST insert of leaf slot `s`, rotating up to fix heap order. */
1854
+ _insert(t, s) {
1855
+ if (t === 0) return s; // s already has size 1, NIL children, its priority set
1856
+ const L = this._left, R = this._right, S = this._size, K = this._key;
1857
+ if (K[s] < K[t]) {
1858
+ L[t] = this._insert(L[t], s);
1859
+ S[t] = S[L[t]] + S[R[t]] + 1;
1860
+ if (this._higher(L[t], t)) return this._rotR(t);
1861
+ } else {
1862
+ R[t] = this._insert(R[t], s);
1863
+ S[t] = S[L[t]] + S[R[t]] + 1;
1864
+ if (this._higher(R[t], t)) return this._rotL(t);
1865
+ }
1866
+ return t;
1867
+ }
1868
+
1869
+ /** @private recursive delete of `key` from subtree `t`; frees the removed slot. */
1870
+ _delete(t, key) {
1871
+ const L = this._left, R = this._right, S = this._size, K = this._key;
1872
+ if (key < K[t]) {
1873
+ L[t] = this._delete(L[t], key);
1874
+ S[t] = S[L[t]] + S[R[t]] + 1;
1875
+ return t;
1876
+ }
1877
+ if (key > K[t]) {
1878
+ R[t] = this._delete(R[t], key);
1879
+ S[t] = S[L[t]] + S[R[t]] + 1;
1880
+ return t;
1881
+ }
1882
+ const merged = this._merge(L[t], R[t]); // t removed: fuse its two subtrees
1883
+ this._pool.free(t);
1884
+ return merged;
1885
+ }
1886
+
1887
+ /** @private recursive priority merge of two subtrees (all keys in a < all in b). */
1888
+ _merge(a, b) {
1889
+ if (a === 0) return b;
1890
+ if (b === 0) return a;
1891
+ const L = this._left, R = this._right, S = this._size;
1892
+ if (this._higher(a, b)) {
1893
+ R[a] = this._merge(R[a], b);
1894
+ S[a] = S[L[a]] + S[R[a]] + 1;
1895
+ return a;
1896
+ }
1897
+ L[b] = this._merge(a, L[b]);
1898
+ S[b] = S[L[b]] + S[R[b]] + 1;
1899
+ return b;
1900
+ }
1901
+
1902
+ /**
1903
+ * @private recursive split of subtree `t` by `key`. Returns the LEFT root (keys <
1904
+ * key); the RIGHT root (keys >= key) is left in `this._sr` (read by the caller
1905
+ * immediately, before any sibling recursion, so a single scratch field suffices).
1906
+ */
1907
+ _split(t, key) {
1908
+ if (t === 0) { this._sr = 0; return 0; }
1909
+ const L = this._left, R = this._right, K = this._key, S = this._size;
1910
+ if (K[t] < key) {
1911
+ const l1 = this._split(R[t], key); // this._sr := the right part
1912
+ R[t] = l1;
1913
+ S[t] = S[L[t]] + S[R[t]] + 1;
1914
+ return t; // pair (t, this._sr)
1915
+ }
1916
+ const l1 = this._split(L[t], key); // this._sr := R1 ; l1 := L1
1917
+ L[t] = this._sr;
1918
+ S[t] = S[L[t]] + S[R[t]] + 1;
1919
+ this._sr = t;
1920
+ return l1; // pair (l1, t)
1921
+ }
1922
+
1923
+ /** @private smallest key >= `lo`, or undefined (the range-iter start). */
1924
+ _ceil(lo) {
1925
+ const L = this._left, R = this._right, K = this._key;
1926
+ let t = this._root, best;
1927
+ while (t !== 0) {
1928
+ if (K[t] >= lo) { best = K[t]; t = L[t]; }
1929
+ else t = R[t];
1930
+ }
1931
+ return best;
1932
+ }
1933
+
1934
+ /** @private build a Treap VIEW sharing `src`'s arena with a given root (split/merge). */
1935
+ static _view(src, root) {
1936
+ const t = Object.create(Treap.prototype);
1937
+ t._cap = src._cap;
1938
+ t._key = src._key; t._value = src._value;
1939
+ t._left = src._left; t._right = src._right;
1940
+ t._prio = src._prio; t._size = src._size;
1941
+ t._pool = src._pool;
1942
+ t._root = root;
1943
+ t._seed0 = src._seed0; t._seed = src._seed;
1944
+ t._version = 0; t._sr = 0;
1945
+ return t;
1946
+ }
1947
+
1948
+ // ---- cold path only: throw builders (string concat off the hot body) ----
1949
+
1950
+ /** @private */
1951
+ _badKey(key) {
1952
+ throw new TypeError(
1953
+ '[lite-logn] Treap key must be a finite number, got ' + String(key));
1954
+ }
1955
+
1956
+ /** @private */
1957
+ _badValue(value) {
1958
+ throw new TypeError(
1959
+ '[lite-logn] Treap value must be a finite number, got ' + String(value));
1960
+ }
1961
+
1962
+ /** @private */
1963
+ _badRank(k) {
1964
+ throw new TypeError(
1965
+ '[lite-logn] Treap select index must be an integer, got ' + String(k));
1966
+ }
1967
+
1968
+ /** @private */
1969
+ _badBound(b) {
1970
+ throw new TypeError(
1971
+ '[lite-logn] Treap rangeIter bound must be a number (not NaN), got ' + String(b));
1972
+ }
1973
+
1974
+ /** @private */
1975
+ _badRange(lo, hi) {
1976
+ throw new RangeError(
1977
+ '[lite-logn] Treap rangeIter needs lo <= hi, got lo=' + String(lo) +
1978
+ ' hi=' + String(hi));
1979
+ }
1980
+
1981
+ /** @private */
1982
+ _full() {
1983
+ throw new RangeError('[lite-logn] Treap full (capacity ' + this._cap + ')');
1984
+ }
1985
+ }
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @zakkster/lite-logn
2
2
 
3
- > Zero-GC, O(log n) data structures that PROVE their logarithm. The O(log n) sibling of `@zakkster/lite-o1`: where lite-o1 holds the constant (a flat ops/ms line), lite-logn holds the logarithm (a straight line on a log-x axis -- one added level per doubling of n). v0.4.0 ships four members: BinaryHeap (array-embedded O(log n) push / pop min|max heap), Fenwick / BIT (O(log n) point-update AND prefix-sum via the `i & -i` walk), SegmentTree (O(log n) associative range-query -- min / max / sum / gcd -- plus point-update over a flat 2n array), and SkipList (pointer-free expected-O(log n) ordered map over a private free-list node pool) -- each zero-GC, each shipped with a log-linear Witness that fits `nsPerOp = intercept + slope*log2(n)` and shows the straight log line while an O(n) foil leaves it.
3
+ > Zero-GC, O(log n) data structures that PROVE their logarithm. The O(log n) sibling of `@zakkster/lite-o1`: where lite-o1 holds the constant (a flat ops/ms line), lite-logn holds the logarithm (a straight line on a log-x axis -- one added level per doubling of n). v0.5.0 ships five members: BinaryHeap (array-embedded O(log n) push / pop min|max heap), Fenwick / BIT (O(log n) point-update AND prefix-sum via the `i & -i` walk), SegmentTree (O(log n) associative range-query -- min / max / sum / gcd -- plus point-update over a flat 2n array), SkipList (pointer-free expected-O(log n) ordered map over a private free-list node pool), and Treap (a randomized-balanced augmented ordered map with O(log n) rank / select / split / merge) -- each zero-GC, each shipped with a log-linear Witness that fits `nsPerOp = intercept + slope*log2(n)` and shows the straight log line while an O(n) foil leaves it.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@zakkster/lite-logn.svg?style=for-the-badge&color=latest)](https://www.npmjs.com/package/@zakkster/lite-logn)
6
6
  [![sponsor](https://img.shields.io/badge/sponsor-PeshoVurtoleta-ea4aaa.svg?logo=github)](https://github.com/sponsors/PeshoVurtoleta)
@@ -19,7 +19,7 @@ Almost no JavaScript data-structure library ships the evidence that its Big-O cl
19
19
 
20
20
  lite-logn is the O(log n) sibling of [`@zakkster/lite-o1`](https://www.npmjs.com/package/@zakkster/lite-o1). lite-o1 proves a FLAT ops/ms line on a log-x axis (the constant -- slope ~ 0); lite-logn proves a STRAIGHT line on that same axis (one added level per doubling of `n` -- slope > 0, within a per-member band). The gate SHAPE differs; the discipline is identical: zero allocation on every hot path and a witness that turns "trust me, it is O(log n)" into a straight line you can see, with a foil that leaves it.
21
21
 
22
- **v0.4.0 ships four members: BinaryHeap, Fenwick, SegmentTree and SkipList.** Members land one per session, each append-only so prior members stay byte-identical. The planned roster below fills in per release.
22
+ **v0.5.0 ships five members: BinaryHeap, Fenwick, SegmentTree, SkipList and Treap.** Members land one per session, each append-only so prior members stay byte-identical. The planned roster below fills in per release.
23
23
 
24
24
  ```bash
25
25
  npm install @zakkster/lite-logn
@@ -53,12 +53,14 @@ Every hot op allocates zero bytes after construction, and `npm run witness` prov
53
53
  - [What you get](#what-you-get)
54
54
  - [The roster](#the-roster)
55
55
  - [The O(log n) Witness](#the-olog-n-witness)
56
+ - [Benchmarks](#benchmarks)
56
57
  - [API reference](#api-reference)
57
58
  - [Constants](#constants)
58
59
  - [BinaryHeap](#binaryheap)
59
60
  - [Fenwick](#fenwick)
60
61
  - [SegmentTree](#segmenttree)
61
62
  - [SkipList](#skiplist)
63
+ - [Treap](#treap)
62
64
  - [Zero-GC design notes](#zero-gc-design-notes)
63
65
  - [Testing](#testing)
64
66
  - [What this is not](#what-this-is-not)
@@ -83,7 +85,7 @@ lite-logn ships the O(log n) structures that matter with the allocation removed
83
85
 
84
86
  ## The roster
85
87
 
86
- One member per session, each landing append-only (prior members stay byte-identical). At v0.4.0, BinaryHeap, Fenwick, SegmentTree and SkipList are shipped.
88
+ One member per session, each landing append-only (prior members stay byte-identical). At v0.5.0, BinaryHeap, Fenwick, SegmentTree, SkipList and Treap are shipped.
87
89
 
88
90
  | Member | Version | Status | Shape | Hot ops |
89
91
  | --- | --- | --- | --- | --- |
@@ -91,8 +93,9 @@ One member per session, each landing append-only (prior members stay byte-identi
91
93
  | **Fenwick** (BIT) | 0.2.0 | shipped | flat `Float64Array`, lowest-set-bit walk (`i & -i`) | `update` / `prefix` / `rangeSum` / `at` / `set` O(log n) |
92
94
  | **SegmentTree** | 0.3.0 | shipped | single flat `Float64Array(2n)` (leaves n..2n-1); associative fold (min/max/sum/gcd) chosen at construction | `query` / `update` O(log n), `at` O(1) |
93
95
  | **SkipList** | 0.4.0 | shipped | pointer-free over a private free-list node pool; expected O(log n) | `get` / `set` / `delete` / `successor` / `predecessor` |
96
+ | **Treap** | 0.5.0 | shipped | randomized-balanced augmented BST over the same node pool; expected O(log n) | `get` / `has` / `set` / `delete` / `rank` / `select` / `successor` / `predecessor` / `forEach` / `rangeIter` / `split` + `merge` |
94
97
 
95
- Later tiers (Treap / Scapegoat, OrderStatTree, IndexedHeap, SortedArray, MinMaxHeap, SplayTree, and presets) are queued in [`ROADMAP.md`](./ROADMAP.md).
98
+ Later tiers (Scapegoat, OrderStatTree, IndexedHeap, SortedArray, MinMaxHeap, SplayTree, and presets) are queued in [`ROADMAP.md`](./ROADMAP.md).
96
99
 
97
100
  ## The O(log n) Witness
98
101
 
@@ -102,7 +105,81 @@ The family anchor. Time a fixed batch of the hot op at each `n` in a geometric s
102
105
  - `slope` inside the member's band (the per-level cost, ns/level), AND
103
106
  - the FOIL leaves the line (low `R^2` -- the O(n) default a working programmer reaches for, shown losing as `n` grows).
104
107
 
105
- For amortized / randomized members the witness also prints the MAX single-op time -- the honesty hook: a rebuild spike or a degenerate tail shows as a tall bar even when the mean still fits the line. The `R^2` floor (0.958) is frozen family-wide in BinaryHeap; each member then calibrates its OWN per-op slope band (median-of-15 fit-runs x `[0.6, 1.4]`), because a cheaper op honestly has a lower per-level slope (see [`decisions/0004-witness-band.md`](./decisions/0004-witness-band.md)). At v0.4.0 the witness gates seven ops: BinaryHeap `pop` (R^2 ~ 0.99, slope ~ 8-10 ns/level), Fenwick `update` (R^2 ~ 0.98-0.99, slope ~ 2.9-3.0 ns/level) and `prefix` (R^2 ~ 0.97, slope ~ 2.6-2.7 ns/level), SegmentTree `update` (R^2 ~ 0.99, slope ~ 3.2 ns/level, band `[2.29, 5.35]`) and `query` (R^2 ~ 0.99, slope ~ 7 ns/level, band `[4.30, 10.04]`), and SkipList `get` (R^2 ~ 0.97-0.99, slope ~ 9 ns/level, band `[5.27, 12.30]`) and `set` (R^2 ~ 0.97-0.99, slope ~ 14 ns/level, band `[8.36, 19.50]`) all ON the line. SkipList's two ops are gated over DIFFERENT sweeps -- each measured where its logarithm is visible, not where the cache wall is: `get` (a clean search with no per-op randomness) over `[2^11, 2^17]` for dynamic range; `set` (a heavier insert+delete churn whose per-insert tower height is random) over the smaller, fully cache-resident `[2^9, 2^14]` so the fit sees the structural level count, not DRAM latency. Because SkipList is EXPECTED (not worst-case) O(log n), the witness also prints the MAX single insert over a realistic randomized build trace -- the unlucky-tower tail a mean hides. Each op's O(n) foil fits well below the floor: the sorted-array insert (BinaryHeap / SkipList) foil runs R^2 ~ 0.77-0.87, the Fenwick foils (prefix-array rebuild, naive re-sum) and SkipList's linear-scan search foil hold at R^2 ~ 0.75-0.82, and SegmentTree's foils (whole-tree rebuild per update, scan-fold per query) fit at R^2 ~ 0.72-0.85 -- all foil families sit comfortably under the 0.958 floor.
108
+ For amortized / randomized members the witness also prints the MAX single-op time -- the honesty hook: a rebuild spike or a degenerate tail shows as a tall bar even when the mean still fits the line. The `R^2` floor (0.958) is frozen family-wide in BinaryHeap; each member then calibrates its OWN per-op slope band (median-of-15 fit-runs x `[0.6, 1.4]`), because a cheaper op honestly has a lower per-level slope (see [`decisions/0004-witness-band.md`](./decisions/0004-witness-band.md)). At v0.5.0 the witness gates eight ops: BinaryHeap `pop` (R^2 ~ 0.99, slope ~ 8-10 ns/level), Fenwick `update` (R^2 ~ 0.98-0.99, slope ~ 2.9-3.0 ns/level) and `prefix` (R^2 ~ 0.97, slope ~ 2.6-2.7 ns/level), SegmentTree `update` (R^2 ~ 0.99, slope ~ 3.2 ns/level, band `[2.29, 5.35]`) and `query` (R^2 ~ 0.99, slope ~ 7 ns/level, band `[4.30, 10.04]`), SkipList `get` (R^2 ~ 0.97-0.99, slope ~ 9 ns/level, band `[5.27, 12.30]`) and `set` (R^2 ~ 0.97-0.99, slope ~ 14 ns/level, band `[8.36, 19.50]`), and Treap `get` (R^2 ~ 0.99, slope ~ 4 ns/level, band `[2.55, 5.95]`) all ON the line. Treap's descent touches one node per level, so its per-level slope is lower than SkipList's tower search -- expected, which is why only the R^2 floor is shared and each op calibrates its own band. SkipList's two ops are gated over DIFFERENT sweeps -- each measured where its logarithm is visible, not where the cache wall is: `get` (a clean search with no per-op randomness) over `[2^11, 2^17]` for dynamic range; `set` (a heavier insert+delete churn whose per-insert tower height is random) over the smaller, fully cache-resident `[2^9, 2^14]` so the fit sees the structural level count, not DRAM latency. Because SkipList is EXPECTED (not worst-case) O(log n), the witness also prints the MAX single insert over a realistic randomized build trace -- the unlucky-tower tail a mean hides. Each op's O(n) foil fits well below the floor: the sorted-array insert (BinaryHeap / SkipList) foil runs R^2 ~ 0.77-0.87, the Fenwick foils (prefix-array rebuild, naive re-sum) and SkipList's linear-scan search foil hold at R^2 ~ 0.75-0.82, and SegmentTree's foils (whole-tree rebuild per update, scan-fold per query) fit at R^2 ~ 0.72-0.85 -- all foil families sit comfortably under the 0.958 floor.
109
+
110
+ ## Benchmarks
111
+
112
+ A repo-only, eight-dimension benchmark suite (`benchmark/`, ADOPTED field-for-field from `@zakkster/lite-o1`'s "Bench v2") surrounds the witness anchor. It is dev infra: NOT in the published tarball, imports NOTHING from the package but `LogN.js`, and spawns one child process per `(member x dimension)` cell for a clean GC/JIT state. **D1 is the O(log n) Witness itself** -- it DELEGATES to the shipped `test/witness.mjs` (the same frozen kernels, per-op sweeps, `R^2` floor and slope bands), so the headline dimension never re-implements the fit. Run it yourself:
113
+
114
+ ```sh
115
+ npm run bench # 32 cells -> benchmark/results.json + summary tables
116
+ npm run bench:report # the above, then benchmark/report.html (hand-rolled inline-SVG graphs)
117
+ ```
118
+
119
+ Numbers below are one run on an Apple M4 Pro (arm64), Node v26 -- machine-specific, reproducible from a fixed seed (`0x9e3779b1`). Every applicable cell is a positive number; every inapplicable cell is the string `n/a` (never a numeric 0).
120
+
121
+ ### D1 -- the O(log n) Witness fit (per gated op-row)
122
+
123
+ Each op fits `nsPerOp = intercept + slope*log2(n)`. ON-LINE = `R^2 >= 0.958` (the frozen family floor) AND `slope` inside the member's per-op band; the O(n) foil MUST leave the line (`foil R^2 < 0.958`). All eight op-rows sit ON the line; all eight foils leave it.
124
+
125
+ <svg width="640" height="200" viewBox="0 0 640 200" role="img" aria-label="D1 slope per op-row (ns/level)" xmlns="http://www.w3.org/2000/svg">
126
+ <text x="8" y="16" font-size="12" fill="#475569">D1 slope (ns/level) -- lower is a cheaper per-level cost</text>
127
+ <g font-size="10" fill="#334155" text-anchor="middle">
128
+ <rect x="24" y="84" width="60" height="96" fill="#2563eb"/><text x="54" y="194">BH.pop 8.4</text>
129
+ <rect x="112" y="148" width="60" height="32" fill="#059669"/><text x="142" y="194">Fen.upd 2.8</text>
130
+ <rect x="200" y="150" width="60" height="30" fill="#059669"/><text x="230" y="194">Fen.pre 2.6</text>
131
+ <rect x="288" y="145" width="60" height="35" fill="#d97706"/><text x="318" y="194">Seg.upd 3.1</text>
132
+ <rect x="376" y="99" width="60" height="81" fill="#d97706"/><text x="406" y="194">Seg.qry 7.0</text>
133
+ <rect x="464" y="88" width="60" height="92" fill="#7c3aed"/><text x="494" y="194">SL.get 8.0</text>
134
+ <rect x="552" y="40" width="60" height="140" fill="#7c3aed"/><text x="582" y="194">SL.set 12.1</text>
135
+ </g>
136
+ </svg>
137
+
138
+ | op-row | `R^2` | slope (ns/level) | slope band | on line? | foil | foil `R^2` | foil off? |
139
+ | --- | --- | --- | --- | --- | --- | --- | --- |
140
+ | `BinaryHeap.pop` | 0.996 | 8.4 | `[5.76, 13.44]` | ON | sorted-array insert | 0.83 | off |
141
+ | `Fenwick.update` | 0.980 | 2.8 | `[1.84, 4.30]` | ON | prefix-array rebuild | 0.76 | off |
142
+ | `Fenwick.prefix` | 0.968 | 2.6 | `[1.76, 4.10]` | ON | naive re-sum | 0.75 | off |
143
+ | `SegmentTree.update` | 0.989 | 3.1 | `[2.29, 5.35]` | ON | whole-tree rebuild | 0.82 | off |
144
+ | `SegmentTree.query` | 0.998 | 7.0 | `[4.30, 10.04]` | ON | scan-fold | 0.73 | off |
145
+ | `SkipList.get` | 0.988 | 8.0 | `[5.27, 12.30]` | ON | linear scan | 0.79 | off |
146
+ | `SkipList.set` | 0.985 | 12.1 | `[8.36, 19.50]` | ON | sorted-array insert | 0.77 | off |
147
+ | `Treap.get` | 0.988 | 4.0 | `[2.55, 5.95]` | ON | linear scan | 0.79 | off |
148
+
149
+ **SkipList counter-foil (the order tax).** A native `Map` is O(1) at get/set (`~27 ns/op`, FLATTER than any log line) but ORDER-BLIND: it cannot answer `successor` / `predecessor` / `rangeIter`. The log factor SkipList pays buys exactly the ordered queries Map cannot. SkipList is EXPECTED O(log n), so D1 also DISCLOSES its MAX single insert (an unlucky tall tower over a randomized build: `~18-130 us`, not gated).
150
+
151
+ ### D3 -- memory (bytes / live vs a theoretical floor)
152
+
153
+ | member | peak bytes @ 64Ki | B/live | theo min | overhead x | note |
154
+ | --- | --- | --- | --- | --- | --- |
155
+ | BinaryHeap | 1,048,576 | 16.0 | 12 | 1.33 | key (8) + id (4) dense; `_pos` reverse map is the universe overhead |
156
+ | Fenwick | 524,296 | 8.0 | 8 | 1.00 | one `Float64` tree cell per element -- exact |
157
+ | SegmentTree | 1,048,576 | 16.0 | 16 | 1.00 | the `2n` array -- exact |
158
+ | SkipList | 5,767,320 | 88.0 | 16 | 5.50 | key + value dense; the `ceil(log2 cap)+1` link columns are the tower overhead |
159
+
160
+ The overhead-x load-factor curve RISES as load falls for BinaryHeap + SkipList (fixed backing over fewer live) and is FLAT for the INDEX-ADDRESSED Fenwick + SegmentTree (every cell is always live) -- another honest `n/a` where insertion order does not apply.
161
+
162
+ ### D5 -- bundle size + tree-shaking (esbuild min + gzip)
163
+
164
+ A single-member import must be `< 40%` of the all-member import. Three of four clear it; SkipList (the heaviest lone member) is the ONE honest exception at `~41%` -- stated, not rounded down, and never by moving the budget. The median lone-import ratio is `~0.32 (< 0.40)`; every member's lone import still drops the majority of the others (`< 0.50`).
165
+
166
+ | member | single gz (B) | all gz (B) | ratio | `< 40%`? |
167
+ | --- | --- | --- | --- | --- |
168
+ | BinaryHeap | 1,365 | 3,758 | 0.363 | yes |
169
+ | Fenwick | 826 | 3,758 | 0.220 | yes |
170
+ | SegmentTree | 1,071 | 3,758 | 0.285 | yes |
171
+ | SkipList | 1,546 | 3,758 | 0.411 | NO (the stated exception) |
172
+
173
+ ### D6 -- GC pressure (the 0 B/op gate as a curve, per op-row)
174
+
175
+ All seven gated op-rows report **0 B/op** across the `n = 1e3..1e6` sweep, with `max major GC = 0`. The precise proof stays `node --expose-gc test/torture.mjs` (via `@zakkster/lite-gc-profiler`); D6 is the portable curve (min heap-delta over independent passes -- heap-accounting jitter only ADDS, so a truly-zero kernel hits 0 on its best pass while a per-op allocator stays positive on every pass).
176
+
177
+ ### The other dimensions
178
+
179
+ - **D2 amortized cost** -- cumulative ns/op stays bounded over a `~1M`-op mixed trace (drift `< 1.0` here: the trace speeds up as the JIT warms, never degrades).
180
+ - **D4 cache (PROXY, labelled)** -- dense `forEach` iteration vs random single-element lookup; the random/dense gap is `~1.9x` (SegmentTree) to `~2.9x` (SkipList). No native perf counters.
181
+ - **D7 scalability** -- numeric substrates: string + object keys read `n/a`. Load factors `0.3/0.5/0.7/0.9`; insertion order (sorted / random / adversarial-reverse) applies to the comparison-ordered BinaryHeap + SkipList + Treap, `n/a` for the index-addressed Fenwick + SegmentTree.
182
+ - **D8 workloads** -- churn (all members) + an ordered scan (`successor` + `rangeIter`, SkipList + Treap; `n/a` elsewhere).
106
183
 
107
184
  ## API reference
108
185
 
@@ -110,7 +187,7 @@ For amortized / randomized members the witness also prints the MAX single-op tim
110
187
 
111
188
  | Export | Type | Value | Meaning |
112
189
  | --- | --- | --- | --- |
113
- | `VERSION` | `string` | `'0.4.0'` | The package version. One of the three version sites (package.json / `LogN.js` `VERSION` const / `llms.txt`), kept in lockstep and enforced in review. |
190
+ | `VERSION` | `string` | `'0.5.0'` | The package version. One of the three version sites (package.json / `LogN.js` `VERSION` const / `llms.txt`), kept in lockstep and enforced in review. |
114
191
 
115
192
  ### BinaryHeap
116
193
 
@@ -256,6 +333,45 @@ sl.delete(50); // -> true (idempotent: false if absent)
256
333
  | `clear` | `clear() -> this` | O(capacity) | Empties the list, keeps capacity, resets the PRNG to its initial seed. |
257
334
  | `size` / `capacity` | getters | O(1) | Live entry count / fixed capacity. |
258
335
 
336
+ ### Treap
337
+
338
+ A **treap**: a randomized, self-balancing **binary search tree** that is also an **order-statistic tree** -- an AUGMENTED ordered map (key -> value) -- the family's balanced BST. It holds two orders at once: a **BST order** on the key and a **max-heap order** on a per-node random priority; a random-priority heap over a BST is provably balanced **in expectation**, so `get` / `set` / `delete` are **expected O(log n)**. A third invariant, a subtree-size column maintained in the SAME pass as every link rewrite, adds `rank(x)` (how many keys are `< x`), `select(k)` (the k-th smallest key), and O(log n) `split` / `merge`. Nodes are slot **indices** in six flat columns (`_key` / `_value` `Float64`; `_left` / `_right` / `_prio` / `_size` `Uint32`, `NIL = 0`) over the SAME private free-list (`NodePool`) SkipList uses -- design-parity, never a heap object per op. Priority is one instance-local Numerical-Recipes LCG draw per insert (deterministic from a seed; ties break by key). Keys and values are finite numbers (typeof-guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail closed); `set` on an existing key updates the value in place. Every hot op allocates zero bytes after construction.
339
+
340
+ Honesty note: the hot ops are **expected** O(log n), not worst-case -- an unlucky priority draw can build a tall thin tree and spike a single op (the rotation chain). The witness fits the clean average line **and** separately prints the MAX single insert. `set` / `delete` / `split` / `merge` recurse to a depth equal to the tree height (O(log n) expected, O(n) worst-case) on the native call stack -- but priorities come from the instance-local LCG, NOT caller-chosen keys, so an adversary cannot force the worst case through the public surface; the recursion allocates zero heap bytes (see [`decisions/0007-treap.md`](./decisions/0007-treap.md)).
341
+
342
+ ```js
343
+ import { Treap } from '@zakkster/lite-logn';
344
+
345
+ const tr = new Treap(1000, 42); // capacity 1000, seed 42 (deterministic)
346
+ tr.set(50, 500); // insert key 50 -> value 500
347
+ tr.set(20, 200);
348
+ tr.set(80, 800);
349
+ tr.get(20); // -> 200
350
+ tr.rank(50); // -> 1 (one key, 20, is strictly less than 50)
351
+ tr.select(0); // -> 20 (the smallest key)
352
+ tr.successor(20); // -> 50 (smallest key strictly greater)
353
+ const [lo, hi] = tr.split(50); // lo: keys < 50; hi: keys >= 50 (share the arena; tr consumed)
354
+ const whole = Treap.merge(lo, hi);// fuse back (all lo keys < all hi keys); both consumed
355
+ ```
356
+
357
+ | Member | Signature | Complexity | Notes |
358
+ | --- | --- | --- | --- |
359
+ | constructor | `new Treap(capacity, seed?)` | O(capacity) | `capacity` integer in `[1, 2^31-1]` (slot indices + subtree counts are `Uint32`, `NIL = 0` reserves slot 0); `seed` an unsigned 32-bit integer (default fixed). Allocates the six columns + private pool once. |
360
+ | `get` | `get(key) -> number \| undefined` | expected O(log n) | The value under `key`, or `undefined` if absent (no throw). Non-finite key throws. |
361
+ | `has` | `has(key) -> boolean` | expected O(log n) | True iff `key` is stored. Non-finite key throws. |
362
+ | `set` | `set(key, value) -> this` | expected O(log n) | Insert `key -> value`, or update the value in place if `key` exists. Non-finite key/value throws; a full pool throws. |
363
+ | `delete` | `delete(key) -> boolean` | expected O(log n) | Idempotent: `false` if absent, `true` if removed. Non-finite key throws. |
364
+ | `rank` | `rank(x) -> number` | expected O(log n) | Count of stored keys STRICTLY less than `x`, in `[0, size]`. `x` need not be present. Non-finite `x` throws. |
365
+ | `select` | `select(k) -> number \| undefined` | expected O(log n) | The k-th smallest key (0-based), or `undefined` if `k` is out of `[0, size)`. Non-integer `k` throws. |
366
+ | `successor` | `successor(key) -> number \| undefined` | expected O(log n) | The smallest key STRICTLY greater than `key`, or `undefined`. |
367
+ | `predecessor` | `predecessor(key) -> number \| undefined` | expected O(log n) | The largest key STRICTLY less than `key`, or `undefined`. |
368
+ | `rangeIter` | `rangeIter(lo, hi) -> IterableIterator<number>` | O(k log n) | Version-stamped iterator over keys in `[lo, hi]` INCLUSIVE, ascending. Bounds may be `+-Infinity`; `NaN` or `lo > hi` throws; a structural OR value mutation mid-iteration throws. |
369
+ | `forEach` | `forEach(fn) -> void` | O(n) | Visits `(key, value, treap)` in ascending key order. |
370
+ | `clear` | `clear() -> this` | O(capacity) | Empties the treap, keeps capacity, resets the PRNG to its initial seed. |
371
+ | `split` | `split(key) -> [Treap, Treap]` | expected O(log n) | `[left (keys < key), right (keys >= key)]`; rewires in place, so the two treaps SHARE this treap's arena and this is CONSUMED (left empty). |
372
+ | `merge` (static) | `Treap.merge(a, b) -> Treap` | expected O(log n) | Fuse two arena-sharing treaps where every key of `a` < every key of `b`; CONSUMES both. Non-Treap inputs, cross-arena treaps, or an overlapping range throw. |
373
+ | `size` / `capacity` | getters | O(1) | Live entry count / fixed capacity. |
374
+
259
375
  Member signatures for later members are appended here as each ships.
260
376
 
261
377
  ## Zero-GC design notes
@@ -278,8 +394,13 @@ Member signatures for later members are appended here as each ships.
278
394
  | `SkipList` constructor / `clear` | O(capacity) typed arrays + pool, once (cold) |
279
395
  | `SkipList` forEach | 0 B/op in the loop body (pass a hoisted callback) |
280
396
  | `SkipList` rangeIter | one iterator + `{value, done}` per step (the documented per-protocol allocator; transient, not retained) |
397
+ | `Treap` get / has / set / delete / rank / select / successor / predecessor | 0 B/op (nodes are slot indices; the recursive set/delete run on the native call stack, not the heap) |
398
+ | `Treap` constructor / `clear` | O(capacity) six columns + pool, once (cold) |
399
+ | `Treap` forEach | 0 B/op in the loop body (recursive in-order walk, hoisted callback) |
400
+ | `Treap` rangeIter | one iterator + `{value, done}` per step (the documented per-protocol allocator; transient, not retained) |
401
+ | `Treap` split / merge | 0 B/op beyond the returned Treap view(s); rewire in place, share the arena, consume the input(s) |
281
402
 
282
- Gated witness numbers (this machine, shared R^2 floor 0.958): BinaryHeap `pop` R^2 ~ 0.99, slope ~ 8-10 ns/level; Fenwick `update` R^2 ~ 0.98-0.99, slope ~ 2.9-3.0 ns/level (band `[1.84, 4.30]`); Fenwick `prefix` R^2 ~ 0.97, slope ~ 2.6-2.7 ns/level (band `[1.76, 4.10]`); SegmentTree `update` R^2 ~ 0.99, slope ~ 3.2 ns/level (band `[2.29, 5.35]`); SegmentTree `query` R^2 ~ 0.99, slope ~ 7 ns/level (band `[4.30, 10.04]`); SkipList `get` R^2 ~ 0.97-0.99, slope ~ 9 ns/level (band `[5.27, 12.30]`, sweep `[2^11, 2^17]`); SkipList `set` R^2 ~ 0.97-0.99, slope ~ 14 ns/level (band `[8.36, 19.50]`, cache-resident sweep `[2^9, 2^14]`). The allocation table is extended per member as each lands.
403
+ Gated witness numbers (this machine, shared R^2 floor 0.958): BinaryHeap `pop` R^2 ~ 0.99, slope ~ 8-10 ns/level; Fenwick `update` R^2 ~ 0.98-0.99, slope ~ 2.9-3.0 ns/level (band `[1.84, 4.30]`); Fenwick `prefix` R^2 ~ 0.97, slope ~ 2.6-2.7 ns/level (band `[1.76, 4.10]`); SegmentTree `update` R^2 ~ 0.99, slope ~ 3.2 ns/level (band `[2.29, 5.35]`); SegmentTree `query` R^2 ~ 0.99, slope ~ 7 ns/level (band `[4.30, 10.04]`); SkipList `get` R^2 ~ 0.97-0.99, slope ~ 9 ns/level (band `[5.27, 12.30]`, sweep `[2^11, 2^17]`); SkipList `set` R^2 ~ 0.97-0.99, slope ~ 14 ns/level (band `[8.36, 19.50]`, cache-resident sweep `[2^9, 2^14]`); Treap `get` R^2 ~ 0.99, slope ~ 4 ns/level (band `[2.55, 5.95]`, sweep `[2^11, 2^17]`). The allocation table is extended per member as each lands.
283
404
 
284
405
  ## Testing
285
406
 
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # @zakkster/lite-logn
2
2
 
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  License: MIT (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
5
5
  Runtime dependencies: none. ESM only. ASCII-only source. sideEffects: false.
6
6
  Node: >= 18.
@@ -19,7 +19,8 @@ FLAT ops/ms line on a log-x axis), lite-logn holds the logarithm (a STRAIGHT
19
19
  line on that same axis, one added level per doubling of n).
20
20
 
21
21
  v0.1.0 shipped BinaryHeap; v0.2.0 adds Fenwick; v0.3.0 adds SegmentTree; v0.4.0
22
- adds SkipList. The roster (one member per session, each landing append-only):
22
+ adds SkipList; v0.5.0 adds Treap. The roster (one member per session, each landing
23
+ append-only):
23
24
 
24
25
  - BinaryHeap (v0.1.0) -- an INDEXED binary heap (addressable priority queue): a
25
26
  min|max binary heap over three parallel typed arrays (`_key` Float64Array,
@@ -47,6 +48,17 @@ adds SkipList. The roster (one member per session, each landing append-only):
47
48
  seed, deterministic. set updates an existing key's value in place. EXPECTED (not
48
49
  worst-case) O(log n): the witness fits the average line AND prints the MAX single
49
50
  insert. Zero allocation on every hot op.
51
+ - Treap (v0.5.0) -- an AUGMENTED ordered map (key -> value) that is also an order-
52
+ statistic tree: a randomized, self-balancing BST (BST order on keys x max-heap order
53
+ on a per-node random priority -> EXPECTED O(log n) height) with a subtree-size column
54
+ that adds rank(x) (count of keys < x) / select(k) (the k-th smallest key) / split(key)
55
+ / merge(a, b), all EXPECTED O(log n). Nodes are slot INDICES in flat typed-array
56
+ columns (_key / _value / _left / _right / _prio / _size) over the SAME private free-
57
+ list (NodePool) SkipList uses. Priority is one instance-local NR-LCG draw per insert
58
+ (deterministic, seed-reproducible; ties break by key). set updates an existing key's
59
+ value in place. split / merge REWIRE in place (O(log n)) so the two treaps SHARE a
60
+ backing arena and CONSUME their inputs. EXPECTED, not worst-case: the MAX single
61
+ insert (rotation chain) is DISCLOSED, never gated. Zero allocation on every hot op.
50
62
 
51
63
  ## Exports (from the single main file LogN.js)
52
64
 
@@ -141,6 +153,42 @@ adds SkipList. The roster (one member per session, each landing append-only):
141
153
  - `size` / `capacity` getters; `clear()` -> this (empty, keep capacity, reset the
142
154
  PRNG to its initial seed); `forEach(fn)` visits (key, value, list) ascending.
143
155
 
156
+ - `Treap` -- class. A randomized-balanced AUGMENTED ordered map (key -> value) that is
157
+ also an order-statistic tree. A BST on `_key` x a max-heap on a per-node random
158
+ `_prio` gives EXPECTED O(log n) height; a `_size` subtree-count column adds O(log n)
159
+ order statistics. Nodes are slot INDICES in flat typed-array columns (`_key` /
160
+ `_value` Float64, `_left` / `_right` / `_prio` / `_size` Uint32, `NIL = 0`) over a
161
+ PRIVATE free-list (NodePool) -- no heap object per op. Keys and values are finite
162
+ numbers (typeof-guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail
163
+ closed). EXPECTED, not worst-case (an unlucky priority draw can spike one op; the MAX
164
+ single insert is disclosed). set / delete / split / merge recurse to a depth = tree
165
+ height (O(log n) expected, O(n) worst-case on a pathological priority draw), on the
166
+ native call stack, so every hot op is still 0 B/op.
167
+ - `new Treap(capacity, seed?)` -- capacity an integer in [1, 2^31-1] (slot indices +
168
+ subtree counts are Uint32, `NIL = 0` reserves slot 0); seed an unsigned 32-bit
169
+ integer (default fixed). Allocates six columns + a free-list once.
170
+ - `get(key)` -> value | undefined. Absent -> undefined (no throw).
171
+ - `has(key)` -> boolean.
172
+ - `set(key, value)` -> this. Insert, or update the value in place if key exists (no
173
+ new node). Non-finite key/value throws; a full pool throws.
174
+ - `delete(key)` -> boolean. Idempotent: false if absent, true if removed.
175
+ - `rank(x)` -> number. Count of stored keys STRICTLY LESS than x, in [0, size].
176
+ - `select(k)` -> key | undefined. The k-th smallest key (0-based); undefined if k is
177
+ out of [0, size). Non-integer k throws.
178
+ - `successor(key)` -> key | undefined. Smallest key STRICTLY greater than key.
179
+ - `predecessor(key)` -> key | undefined. Largest key STRICTLY less than key.
180
+ - `rangeIter(lo, hi)` -> iterator of keys in [lo, hi] INCLUSIVE, ascending; VERSION-
181
+ STAMPED (structural OR value mutation mid-iteration throws). Bounds may be
182
+ +-Infinity; NaN or lo > hi throws.
183
+ - `size` / `capacity` getters; `clear()` -> this (empty, keep capacity, reset the
184
+ PRNG to its initial seed); `forEach(fn)` visits (key, value, treap) ascending.
185
+ - `split(key)` -> [left, right]. left holds keys < key, right holds keys >= key;
186
+ O(log n) EXPECTED (rewires in place), so the two treaps SHARE this treap's arena
187
+ and this is CONSUMED (left empty).
188
+ - `Treap.merge(a, b)` -> Treap. Merge two arena-sharing treaps where every key of a
189
+ < every key of b; O(log n) EXPECTED, CONSUMES both. Non-Treap inputs, treaps from
190
+ different arenas, or an overlapping key range each throw.
191
+
144
192
  Member exports (one tree-shakeable class each) are appended here as each member
145
193
  ships.
146
194
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@zakkster/lite-logn",
3
3
  "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
4
- "version": "0.4.0",
5
- "description": "Zero-dependency, zero-GC family of O(log n) data structures that proves its logarithm: BinaryHeap (array-embedded O(log n) push/pop min-heap), Fenwick/BIT (O(log n) point-update AND prefix-sum via the i & -i walk), SegmentTree (O(log n) associative range-query + point-update), and SkipList (pointer-free expected-O(log n) ordered map over a private free-list node pool) with a log-linear O(log n) Witness harness that fits nsPerOp = intercept + slope*log2(n) and shows the straight log line while an O(n) foil leaves it. Tree-shakeable named exports.",
4
+ "version": "0.5.0",
5
+ "description": "Zero-dependency, zero-GC family of O(log n) data structures that proves its logarithm: BinaryHeap (array-embedded O(log n) push/pop min-heap), Fenwick/BIT (O(log n) point-update AND prefix-sum via the i & -i walk), SegmentTree (O(log n) associative range-query + point-update), SkipList (pointer-free expected-O(log n) ordered map over a private free-list node pool), and Treap (randomized-balanced augmented ordered map with O(log n) rank/select/split/merge) with a log-linear O(log n) Witness harness that fits nsPerOp = intercept + slope*log2(n) and shows the straight log line while an O(n) foil leaves it. Tree-shakeable named exports.",
6
6
  "type": "module",
7
7
  "main": "./LogN.js",
8
8
  "module": "./LogN.js",
@@ -24,13 +24,15 @@
24
24
  "LICENSE"
25
25
  ],
26
26
  "scripts": {
27
- "test": "node --test test/*.test.js test/Bench.test.mjs",
27
+ "test": "node --test test/*.test.js test/*.test.mjs",
28
28
  "test:types": "tsc -p test/types/tsconfig.json",
29
29
  "torture": "node --expose-gc test/torture.mjs",
30
30
  "witness": "node test/witness.mjs",
31
31
  "test:perf": "node --expose-gc --max-semi-space-size=4 --test test/perf/PerfGate.test.mjs",
32
32
  "bench": "node benchmark/Bench.mjs",
33
33
  "bench:report": "node benchmark/Bench.mjs && node benchmark/Report.mjs",
34
+ "demo": "node --expose-gc --test demo/Demo.test.mjs",
35
+ "demo:serve": "node demo/serve.mjs",
34
36
  "verify": "npm test && npm run test:types && npm run torture && npm run witness && npm run test:perf"
35
37
  },
36
38
  "keywords": [
@@ -46,7 +48,9 @@
46
48
  "range-query",
47
49
  "range-sum",
48
50
  "skip-list",
51
+ "treap",
49
52
  "ordered-set",
53
+ "ordered-map",
50
54
  "balanced-bst",
51
55
  "bst",
52
56
  "log-n",