@zakkster/lite-logn 0.5.0 → 0.7.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 +116 -1
- package/LogN.d.ts +103 -0
- package/LogN.js +890 -1
- package/README.md +96 -9
- package/llms.txt +85 -3
- package/package.json +8 -2
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.
|
|
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.7.0 ships seven 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), Treap (a randomized-balanced augmented ordered map with O(log n) rank / select / split / merge), Scapegoat (a DETERMINISTIC weight-balanced augmented ordered map: worst-case-O(log n) get, amortized-O(log n) set / delete, zero-GC rebuild), and MinMaxHeap (an array-embedded double-ended priority queue: O(1) peekMin / peekMax, O(log n) push / popMin / popMax) -- 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
|
[](https://www.npmjs.com/package/@zakkster/lite-logn)
|
|
6
6
|
[](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.
|
|
22
|
+
**v0.7.0 ships seven members: BinaryHeap, Fenwick, SegmentTree, SkipList, Treap, Scapegoat and MinMaxHeap.** 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
|
|
@@ -61,6 +61,8 @@ Every hot op allocates zero bytes after construction, and `npm run witness` prov
|
|
|
61
61
|
- [SegmentTree](#segmenttree)
|
|
62
62
|
- [SkipList](#skiplist)
|
|
63
63
|
- [Treap](#treap)
|
|
64
|
+
- [Scapegoat](#scapegoat)
|
|
65
|
+
- [MinMaxHeap](#minmaxheap)
|
|
64
66
|
- [Zero-GC design notes](#zero-gc-design-notes)
|
|
65
67
|
- [Testing](#testing)
|
|
66
68
|
- [What this is not](#what-this-is-not)
|
|
@@ -85,7 +87,7 @@ lite-logn ships the O(log n) structures that matter with the allocation removed
|
|
|
85
87
|
|
|
86
88
|
## The roster
|
|
87
89
|
|
|
88
|
-
One member per session, each landing append-only (prior members stay byte-identical). At v0.
|
|
90
|
+
One member per session, each landing append-only (prior members stay byte-identical). At v0.7.0, BinaryHeap, Fenwick, SegmentTree, SkipList, Treap, Scapegoat and MinMaxHeap are shipped.
|
|
89
91
|
|
|
90
92
|
| Member | Version | Status | Shape | Hot ops |
|
|
91
93
|
| --- | --- | --- | --- | --- |
|
|
@@ -94,8 +96,10 @@ One member per session, each landing append-only (prior members stay byte-identi
|
|
|
94
96
|
| **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) |
|
|
95
97
|
| **SkipList** | 0.4.0 | shipped | pointer-free over a private free-list node pool; expected O(log n) | `get` / `set` / `delete` / `successor` / `predecessor` |
|
|
96
98
|
| **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` |
|
|
99
|
+
| **Scapegoat** | 0.6.0 | shipped | DETERMINISTIC weight-balanced augmented BST over the same node pool; worst-case O(log n) get, amortized O(log n) set/delete (zero-GC rebuild) | `get` / `has` / `set` / `delete` / `rank` / `select` / `successor` / `predecessor` / `forEach` / `rangeIter` (NO split/merge) |
|
|
100
|
+
| **MinMaxHeap** | 0.7.0 | shipped | array-embedded double-ended PQ (DEPQ): one binary heap whose levels alternate min/max, two flat columns (`_key`/`_id`) | `push` / `popMin` / `popMax` O(log n), `peekMin` / `peekMax` / `peekMinKey` / `peekMaxKey` O(1) (non-addressable: NO changeKey/remove) |
|
|
97
101
|
|
|
98
|
-
Later tiers (
|
|
102
|
+
Later tiers (OrderStatTree, IndexedHeap, SortedArray, MinMaxHeap, SplayTree, and presets) are queued in [`ROADMAP.md`](./ROADMAP.md).
|
|
99
103
|
|
|
100
104
|
## The O(log n) Witness
|
|
101
105
|
|
|
@@ -105,7 +109,7 @@ The family anchor. Time a fixed batch of the hot op at each `n` in a geometric s
|
|
|
105
109
|
- `slope` inside the member's band (the per-level cost, ns/level), AND
|
|
106
110
|
- the FOIL leaves the line (low `R^2` -- the O(n) default a working programmer reaches for, shown losing as `n` grows).
|
|
107
111
|
|
|
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.
|
|
112
|
+
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.7.0 the witness gates ten 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]`), Treap `get` (R^2 ~ 0.99, slope ~ 4 ns/level, band `[2.55, 5.95]`), Scapegoat `get` (R^2 ~ 0.99, slope ~ 4 ns/level, band `[2.41, 5.63]`), and MinMaxHeap `popMin` (R^2 ~ 0.99, slope ~ 10.3 ns/level, band `[6.18, 14.42]`) all ON the line. Scapegoat is DETERMINISTIC, so its `get` is WORST-case (not expected) O(log n); its rebuild spike lives on the AMORTIZED `set` path and is proven not by a per-op line but by an amortized-trace assertion -- the cumulative ascending-insert (rebuild-heavy) cost/op tracks a LOG curve (last/first ratio ~1.5x over `[2^11, 2^17]`, gated `< 4x`) where a rebuild-less BST would degenerate to an O(n)-amortized chain and blow the ratio to ~64x. 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
113
|
|
|
110
114
|
## Benchmarks
|
|
111
115
|
|
|
@@ -145,6 +149,10 @@ Each op fits `nsPerOp = intercept + slope*log2(n)`. ON-LINE = `R^2 >= 0.958` (th
|
|
|
145
149
|
| `SkipList.get` | 0.988 | 8.0 | `[5.27, 12.30]` | ON | linear scan | 0.79 | off |
|
|
146
150
|
| `SkipList.set` | 0.985 | 12.1 | `[8.36, 19.50]` | ON | sorted-array insert | 0.77 | off |
|
|
147
151
|
| `Treap.get` | 0.988 | 4.0 | `[2.55, 5.95]` | ON | linear scan | 0.79 | off |
|
|
152
|
+
| `Scapegoat.get` | 0.988 | 3.8 | `[2.41, 5.63]` | ON | linear scan | 0.79 | off |
|
|
153
|
+
| `MinMaxHeap.popMin` | 0.99 | 10.3 | `[6.18, 14.42]` | ON | linear min-scan-and-splice | 0.77 | off |
|
|
154
|
+
|
|
155
|
+
**Scapegoat amortized-trace (the rebuild honesty).** Scapegoat's `get` is WORST-case O(log n) (a deterministic weight-balance height bound), so it carries no expected-op MAX-single-op disclosure. The rebuild spike lives on the AMORTIZED `set` path; D1 proves the amortization not with a per-op line but with an amortized-trace assertion -- the cumulative ascending-insert (rebuild-heavy) cost/op tracks a LOG curve (last/first ratio `~1.5x` over `[2^11, 2^17]`, gated `< 4x`) where a rebuild-less BST would blow to `~64x`.
|
|
148
156
|
|
|
149
157
|
**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
158
|
|
|
@@ -178,8 +186,8 @@ All seven gated op-rows report **0 B/op** across the `n = 1e3..1e6` sweep, with
|
|
|
178
186
|
|
|
179
187
|
- **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
188
|
- **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).
|
|
189
|
+
- **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 + Scapegoat + MinMaxHeap, `n/a` for the index-addressed Fenwick + SegmentTree.
|
|
190
|
+
- **D8 workloads** -- churn (all members) + an ordered scan (`successor` + `rangeIter`, SkipList + Treap + Scapegoat; `n/a` elsewhere, including MinMaxHeap -- a DEPQ, not an ordered map).
|
|
183
191
|
|
|
184
192
|
## API reference
|
|
185
193
|
|
|
@@ -187,7 +195,7 @@ All seven gated op-rows report **0 B/op** across the `n = 1e3..1e6` sweep, with
|
|
|
187
195
|
|
|
188
196
|
| Export | Type | Value | Meaning |
|
|
189
197
|
| --- | --- | --- | --- |
|
|
190
|
-
| `VERSION` | `string` | `'0.
|
|
198
|
+
| `VERSION` | `string` | `'0.6.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. |
|
|
191
199
|
|
|
192
200
|
### BinaryHeap
|
|
193
201
|
|
|
@@ -372,6 +380,77 @@ const whole = Treap.merge(lo, hi);// fuse back (all lo keys < all hi keys); both
|
|
|
372
380
|
| `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
381
|
| `size` / `capacity` | getters | O(1) | Live entry count / fixed capacity. |
|
|
374
382
|
|
|
383
|
+
### Scapegoat
|
|
384
|
+
|
|
385
|
+
A **scapegoat tree**: a **DETERMINISTIC**, weight-balanced **binary search tree** that is also an **order-statistic tree** -- an AUGMENTED ordered map (key -> value) -- the honest **pair to Treap**. Where a treap randomizes its shape to be balanced *in expectation*, a scapegoat keeps a hard **worst-case height bound** (`height <= log_{1/alpha}(n) + 1`), so `get` is **worst-case O(log n)** (never merely expected). It pays for that with **amortized O(log n)** `set` / `delete`: after a mutation makes the tree too deep (or, on delete, too sparse), an occasional **subtree rebuild** restores balance in bulk. A subtree-size column (maintained in the same pass as every link rewrite and every rebuild) adds `rank(x)` / `select(k)`, O(log n). **No priorities, no RNG anywhere** -- the shape is a deterministic function of the insert / delete order. Nodes are slot **indices** in five flat columns (`_key` / `_value` `Float64`; `_left` / `_right` / `_size` `Uint32`, `NIL = 0`) over the SAME private free-list (`NodePool`) SkipList and Treap use. 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.
|
|
386
|
+
|
|
387
|
+
Zero-GC rebuild (the load-bearing design call): there is **no fresh array per rebuild**. One `_flat` (`Uint32Array(capacity)`) + one `_stack` (`Uint32Array(capacity+1)`) are allocated at construction and reused every rebuild -- an ITERATIVE, Morris-free in-order flatten (via `_stack`) writes sorted slot indices into `_flat`, and a bounded log-depth balanced rebuild re-links `_left` / `_right` / `_size` on the native call stack. Proven 0 B/op even under a rebuild-HEAVY ascending-insert trace (see [`decisions/0008-scapegoat.md`](./decisions/0008-scapegoat.md)). Unlike Treap there is **no `split` / `merge`**: a scapegoat has no priority heap to merge by, and an honest deterministic split/merge would be O(n) rebuilds -- the documented asymmetry vs Treap. `alpha` (the weight-balance factor) is validated to the OPEN interval `(0.55, 0.75)` -- both ends throw -- and frozen at construction (default `2/3`).
|
|
388
|
+
|
|
389
|
+
```js
|
|
390
|
+
import { Scapegoat } from '@zakkster/lite-logn';
|
|
391
|
+
|
|
392
|
+
const sg = new Scapegoat(1000); // capacity 1000, alpha = 2/3 (deterministic, no seed)
|
|
393
|
+
sg.set(50, 500); // insert key 50 -> value 500
|
|
394
|
+
sg.set(20, 200);
|
|
395
|
+
sg.set(80, 800);
|
|
396
|
+
sg.get(20); // -> 200 -- worst-case O(log n)
|
|
397
|
+
sg.rank(50); // -> 1 (one key, 20, is strictly less than 50)
|
|
398
|
+
sg.select(0); // -> 20 (the smallest key)
|
|
399
|
+
sg.successor(20); // -> 50 (smallest key strictly greater)
|
|
400
|
+
[...sg.rangeIter(20, 80)]; // -> [20, 50, 80] (inclusive, ascending)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
| Member | Signature | Complexity | Notes |
|
|
404
|
+
| --- | --- | --- | --- |
|
|
405
|
+
| constructor | `new Scapegoat(capacity, alpha?)` | O(capacity) | `capacity` integer in `[1, 2^31-1]` (slot indices + subtree counts are `Uint32`, `NIL = 0` reserves slot 0); `alpha` in the OPEN interval `(0.55, 0.75)` (both ends throw), frozen at construction (default `2/3`). Allocates five columns + private pool + two rebuild scratch buffers once. |
|
|
406
|
+
| `get` | `get(key) -> number \| undefined` | worst-case O(log n) | The value under `key`, or `undefined` if absent (no throw). Non-finite key throws. |
|
|
407
|
+
| `has` | `has(key) -> boolean` | worst-case O(log n) | True iff `key` is stored. Non-finite key throws. |
|
|
408
|
+
| `set` | `set(key, value) -> this` | amortized O(log n) | Insert `key -> value`, or update the value in place if `key` exists (no rebuild). Non-finite key/value throws; a full pool throws. |
|
|
409
|
+
| `delete` | `delete(key) -> boolean` | amortized O(log n) | Idempotent: `false` if absent, `true` if removed. Non-finite key throws. |
|
|
410
|
+
| `rank` | `rank(x) -> number` | worst-case O(log n) | Count of stored keys STRICTLY less than `x`, in `[0, size]`. `x` need not be present. Non-finite `x` throws. |
|
|
411
|
+
| `select` | `select(k) -> number \| undefined` | worst-case O(log n) | The k-th smallest key (0-based), or `undefined` if `k` is out of `[0, size)`. Non-integer `k` throws. |
|
|
412
|
+
| `successor` | `successor(key) -> number \| undefined` | worst-case O(log n) | The smallest key STRICTLY greater than `key`, or `undefined`. |
|
|
413
|
+
| `predecessor` | `predecessor(key) -> number \| undefined` | worst-case O(log n) | The largest key STRICTLY less than `key`, or `undefined`. |
|
|
414
|
+
| `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. |
|
|
415
|
+
| `forEach` | `forEach(fn) -> void` | O(n) | Visits `(key, value, tree)` in ascending key order. |
|
|
416
|
+
| `clear` | `clear() -> this` | O(capacity) | Empties the tree, keeps capacity. |
|
|
417
|
+
| `size` / `capacity` / `alpha` | getters | O(1) | Live entry count / fixed capacity / frozen weight-balance factor. |
|
|
418
|
+
|
|
419
|
+
Member signatures for later members are appended here as each ships.
|
|
420
|
+
|
|
421
|
+
### MinMaxHeap
|
|
422
|
+
|
|
423
|
+
A **min-max heap**: a **double-ended priority queue (DEPQ)** held in ONE array-embedded binary heap whose levels **alternate min / max** (Atkinson, Sack, Santoro & Strothotte 1986). Even depth (the root is depth 0) is a **MIN** level, odd depth a **MAX** level, so the global minimum is the root and the global maximum is the **larger of the root's up-to-two children**. That single alternating heap answers BOTH ends: `peekMin` / `peekMax` / `peekMinKey` / `peekMaxKey` are **O(1)**; `push` / `popMin` / `popMax` are all **worst-case O(log n)** -- no second heap, no paired-heap correspondence to maintain. It uses the BinaryHeap **id + key** idiom (two parallel pointer-free columns: `_id` `Uint32Array`, `_key` `Float64Array`), so it carries an opaque payload per entry with no object nodes. Keys are finite numbers (typeof-guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail closed -- the key is checked FIRST, then the id, then a full heap). Every hot op allocates zero bytes after construction.
|
|
424
|
+
|
|
425
|
+
The asymmetry vs BinaryHeap: MinMaxHeap is **non-addressable**. There is no `_pos` reverse map and therefore deliberately **no `changeKey` / `remove`**; the id is an OPAQUE `Uint32` payload (NOT unique -- duplicates allowed -- over the full `[0, 2^32)` domain, wider than BinaryHeap's `[0, capacity)`). A DEPQ's job is the two extremes; addressability is the separable concern BinaryHeap already carries. There is also **no `kind` argument / getter** (a DEPQ has both ends; a kind getter would be a lie). This is the classic **one-element-per-node** min-max heap; the interval-heap DEPQ (two elements per node) is a deliberately deferred alternative (see [`decisions/0009-minmaxheap.md`](./decisions/0009-minmaxheap.md)). Level parity is computed zero-alloc as `((31 - Math.clz32(i + 1)) & 1) === 0` (min iff even depth); the sifts are hole-punching (one write per level) and every grandchild index is bound-checked against the live size (the classic min-max off-by-one, verified at n = 1, 2, 3, 4).
|
|
426
|
+
|
|
427
|
+
```js
|
|
428
|
+
import { MinMaxHeap } from '@zakkster/lite-logn';
|
|
429
|
+
|
|
430
|
+
const h = new MinMaxHeap(1000); // capacity 1000 (no kind: a DEPQ serves both ends)
|
|
431
|
+
h.push(1, 5.0); // push id 1 with key 5.0
|
|
432
|
+
h.push(2, 1.0);
|
|
433
|
+
h.push(3, 9.0);
|
|
434
|
+
h.peekMinKey(); // -> 1.0 -- O(1)
|
|
435
|
+
h.peekMaxKey(); // -> 9.0 -- O(1)
|
|
436
|
+
h.popMin(); // -> 2 (the id at the minimum key) -- worst-case O(log n)
|
|
437
|
+
h.popMax(); // -> 3 (the id at the maximum key) -- worst-case O(log n)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
| Member | Signature | Complexity | Notes |
|
|
441
|
+
| --- | --- | --- | --- |
|
|
442
|
+
| constructor | `new MinMaxHeap(capacity)` | O(capacity) | `capacity` integer in `[1, 2^31-1]`. Allocates two typed arrays (`_key` `Float64`, `_id` `Uint32`) once. NO `kind` argument. |
|
|
443
|
+
| `push` | `push(id, key) -> void` | worst-case O(log n) | `id` integer in `[0, 2^32)` (opaque, not required unique); `key` finite. Key checked FIRST, then id, then a full heap -- each throws `[lite-logn]` as a no-op (size unchanged). |
|
|
444
|
+
| `popMin` | `popMin() -> number \| undefined` | worst-case O(log n) | Removes and returns the id at the minimum key; `undefined` if empty (no throw). |
|
|
445
|
+
| `popMax` | `popMax() -> number \| undefined` | worst-case O(log n) | Removes and returns the id at the maximum key; `undefined` if empty (no throw). |
|
|
446
|
+
| `peekMin` / `peekMax` | `-> number \| undefined` | O(1) | The id at the minimum / maximum key; `undefined` if empty (never throw). |
|
|
447
|
+
| `peekMinKey` / `peekMaxKey` | `-> number \| undefined` | O(1) | The minimum / maximum key; `undefined` if empty (never throw). |
|
|
448
|
+
| `clear` | `clear() -> void` | O(1) | Empties the heap, keeps capacity. |
|
|
449
|
+
| `forEach` | `forEach(fn) -> void` | O(n) | Visits `(id, key, heap)` in UNSPECIFIED (heap-array) order -- NOT sorted / pop order. |
|
|
450
|
+
| `[Symbol.iterator]` | `-> IterableIterator<number>` | O(n) | Yields live ids in UNSPECIFIED (heap-array) order. |
|
|
451
|
+
| `size` / `capacity` | getters | O(1) | Live entry count / fixed capacity. |
|
|
452
|
+
| `MinMaxHeap.build` | `build(ids, keys, capacity) -> MinMaxHeap` | O(n) | Floyd bulk build from parallel arrays (deepest-first, level-aware sift-down); fails closed on non-array-like / length mismatch, count > capacity, out-of-range id, or non-finite key. |
|
|
453
|
+
|
|
375
454
|
Member signatures for later members are appended here as each ships.
|
|
376
455
|
|
|
377
456
|
## Zero-GC design notes
|
|
@@ -399,8 +478,15 @@ Member signatures for later members are appended here as each ships.
|
|
|
399
478
|
| `Treap` forEach | 0 B/op in the loop body (recursive in-order walk, hoisted callback) |
|
|
400
479
|
| `Treap` rangeIter | one iterator + `{value, done}` per step (the documented per-protocol allocator; transient, not retained) |
|
|
401
480
|
| `Treap` split / merge | 0 B/op beyond the returned Treap view(s); rewire in place, share the arena, consume the input(s) |
|
|
481
|
+
| `Scapegoat` get / has / set / delete / rank / select / successor / predecessor | 0 B/op (nodes are slot indices; the rebuild reuses the preallocated `_flat` + `_stack` scratch and the native call stack, so even a rebuild storm is 0 B/op) |
|
|
482
|
+
| `Scapegoat` constructor / `clear` | O(capacity) five columns + pool + two rebuild scratch buffers, once (cold) |
|
|
483
|
+
| `Scapegoat` forEach | 0 B/op in the loop body (recursive in-order walk, hoisted callback) |
|
|
484
|
+
| `Scapegoat` rangeIter | one iterator + `{value, done}` per step (the documented per-protocol allocator; transient, not retained) |
|
|
485
|
+
| `MinMaxHeap` push / popMin / popMax / peekMin / peekMax / peekMinKey / peekMaxKey | 0 B/op (two flat columns; the hole-punching sifts use only local scalar temporaries) |
|
|
486
|
+
| `MinMaxHeap` constructor / `build` / `clear` | O(capacity) two typed arrays, once (cold) |
|
|
487
|
+
| `MinMaxHeap` forEach | 0 B/op in the loop body (pass a hoisted callback) |
|
|
402
488
|
|
|
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.
|
|
489
|
+
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]`); Scapegoat `get` R^2 ~ 0.99, slope ~ 3.8-4.0 ns/level (band `[2.41, 5.63]`, sweep `[2^11, 2^17]`) -- WORST-case (deterministic), with the AMORTIZED `set` rebuild spike proven by the amortized-trace assertion (ratio `< 4x`), not a per-op line; MinMaxHeap `popMin` R^2 ~ 0.99, slope ~ 10.3 ns/level (band `[6.18, 14.42]`, sweep `[1e4, 1e6]`) -- WORST-case (a DEPQ whose push / popMin / popMax are all worst-case, so no MAX-single-op line), a touch ABOVE BinaryHeap.pop because a min-max trickle-down compares against up to six descendants per level. The allocation table is extended per member as each lands.
|
|
404
490
|
|
|
405
491
|
## Testing
|
|
406
492
|
|
|
@@ -418,6 +504,7 @@ Gated witness numbers (this machine, shared R^2 floor 0.958): BinaryHeap `pop` R
|
|
|
418
504
|
- **Not a bounded-integer priority queue.** If your priorities are small bounded integers, a heap's O(log n) is the wrong tool -- use `@zakkster/lite-o1`'s `BucketQueue` (Dial, O(1)) or `@zakkster/lite-scheduler`'s `FastBitScheduler`. lite-logn's heap is the GENERAL comparator PQ at O(log n).
|
|
419
505
|
- **Not an approximate-membership library.** Bloom / cuckoo / binary-fuse filters live in `@zakkster/lite-filter`. lite-logn owns exact ordered structures.
|
|
420
506
|
- **Not a cache.** `@zakkster/lite-lru` uses ordering internally for eviction but is a cache, not an ordered-collection library.
|
|
507
|
+
- **Not an addressable double-ended queue.** MinMaxHeap is a DEPQ, but it is NON-addressable: its id is an opaque, non-unique payload (no reverse map), so it has no `changeKey` / `remove`. For an addressable single-ended priority queue (reprioritize / remove by entity id) use **BinaryHeap**. MinMaxHeap also ships the classic one-element-per-node min-max heap only -- the interval-heap DEPQ (two elements per node) is a deliberately deferred alternative (see [`decisions/0009-minmaxheap.md`](./decisions/0009-minmaxheap.md)).
|
|
421
508
|
- **Not a grow-on-demand collection.** Capacity is fixed at construction and overflow fails closed.
|
|
422
509
|
|
|
423
510
|
## Ecosystem
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-logn
|
|
2
2
|
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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,8 +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; v0.5.0 adds Treap.
|
|
23
|
-
append-only):
|
|
22
|
+
adds SkipList; v0.5.0 adds Treap; v0.6.0 adds Scapegoat; v0.7.0 adds MinMaxHeap. The
|
|
23
|
+
roster (one member per session, each landing append-only):
|
|
24
24
|
|
|
25
25
|
- BinaryHeap (v0.1.0) -- an INDEXED binary heap (addressable priority queue): a
|
|
26
26
|
min|max binary heap over three parallel typed arrays (`_key` Float64Array,
|
|
@@ -59,6 +59,35 @@ append-only):
|
|
|
59
59
|
value in place. split / merge REWIRE in place (O(log n)) so the two treaps SHARE a
|
|
60
60
|
backing arena and CONSUME their inputs. EXPECTED, not worst-case: the MAX single
|
|
61
61
|
insert (rotation chain) is DISCLOSED, never gated. Zero allocation on every hot op.
|
|
62
|
+
- Scapegoat (v0.6.0) -- a DETERMINISTIC, weight-balanced AUGMENTED ordered map (key ->
|
|
63
|
+
value) that is also an order-statistic tree -- the honest PAIR to Treap. get is
|
|
64
|
+
WORST-case O(log n) (a hard height bound <= log_{1/alpha}(n) + 1, never merely
|
|
65
|
+
expected); set / delete are AMORTIZED O(log n) (an occasional subtree rebuild absorbs
|
|
66
|
+
the imbalance). A subtree-size column adds rank(x) / select(k), O(log n). NO priorities,
|
|
67
|
+
NO RNG anywhere: the shape is a deterministic function of the insert / delete order.
|
|
68
|
+
Nodes are slot INDICES in flat typed-array columns (_key / _value / _left / _right /
|
|
69
|
+
_size) over the SAME private free-list (NodePool); the ZERO-GC rebuild reuses ONE
|
|
70
|
+
preallocated scratch buffer (_flat) + ONE preallocated index-stack (_stack) -- an
|
|
71
|
+
iterative Morris-free flatten + a bounded log-depth balanced rebuild on the native
|
|
72
|
+
stack -- so even a rebuild-heavy trace is 0 B/op. alpha is frozen at construction in the
|
|
73
|
+
OPEN interval (0.55, 0.75) (both ends throw); default 2/3. Unlike Treap there is NO
|
|
74
|
+
split / merge (no priority heap to merge by; an honest deterministic split/merge would
|
|
75
|
+
be O(n) rebuilds) -- the documented asymmetry vs Treap. set updates an existing key's
|
|
76
|
+
value in place. The amortized-trace witness shows cumulative insert cost/op tracks log n
|
|
77
|
+
despite the rebuild spikes. Zero allocation on every hot op.
|
|
78
|
+
- MinMaxHeap (v0.7.0) -- a DOUBLE-ENDED priority queue (DEPQ) held in ONE array-embedded
|
|
79
|
+
binary heap whose levels ALTERNATE min / max (Atkinson et al. 1986). Even depth is a MIN
|
|
80
|
+
level, odd depth a MAX level, so the minimum is the root and the maximum is the LARGER of
|
|
81
|
+
the root's up-to-two children: peekMin / peekMax are O(1); push / popMin / popMax are all
|
|
82
|
+
WORST-case O(log n) from that single heap (no second heap, no paired-heap correspondence).
|
|
83
|
+
Two parallel typed-array columns (`_key` Float64, `_id` Uint32) in the BinaryHeap id+key
|
|
84
|
+
idiom; the id is an OPAQUE Uint32 payload in [0, 2^32) (NOT unique, no reverse map), so
|
|
85
|
+
there is deliberately NO changeKey / remove -- the documented asymmetry vs BinaryHeap.
|
|
86
|
+
Level parity is `((31 - Math.clz32(i + 1)) & 1) === 0` (min iff even depth), computed
|
|
87
|
+
zero-alloc; the sifts are hole-punching (one write per level). Keys are finite numbers
|
|
88
|
+
(typeof-guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail closed). This is
|
|
89
|
+
the classic one-element-per-node min-max heap only; the interval-heap DEPQ is deferred.
|
|
90
|
+
Zero allocation on every hot op.
|
|
62
91
|
|
|
63
92
|
## Exports (from the single main file LogN.js)
|
|
64
93
|
|
|
@@ -189,6 +218,59 @@ append-only):
|
|
|
189
218
|
< every key of b; O(log n) EXPECTED, CONSUMES both. Non-Treap inputs, treaps from
|
|
190
219
|
different arenas, or an overlapping key range each throw.
|
|
191
220
|
|
|
221
|
+
- `Scapegoat` -- class. A DETERMINISTIC, weight-balanced AUGMENTED ordered map (key ->
|
|
222
|
+
value) that is also an order-statistic tree -- the honest PAIR to Treap. A BST on
|
|
223
|
+
`_key` kept alpha-weight-balanced (get WORST-case O(log n); set / delete AMORTIZED
|
|
224
|
+
O(log n) via an occasional subtree rebuild); a `_size` subtree-count column adds O(log n)
|
|
225
|
+
order statistics. NO priorities, NO RNG. Nodes are slot INDICES in flat typed-array
|
|
226
|
+
columns (`_key` / `_value` Float64, `_left` / `_right` / `_size` Uint32, `NIL = 0`) over
|
|
227
|
+
a PRIVATE free-list (NodePool); the zero-GC rebuild reuses ONE preallocated `_flat`
|
|
228
|
+
scratch + ONE `_stack` index-stack (iterative Morris-free flatten + bounded log-depth
|
|
229
|
+
balanced rebuild on the native stack) -- no heap object, no fresh array, 0 B/op even
|
|
230
|
+
under a rebuild storm. Keys and values are finite numbers (typeof-guarded before
|
|
231
|
+
coercion; Symbol / BigInt / NaN / +-Infinity fail closed).
|
|
232
|
+
- `new Scapegoat(capacity, alpha?)` -- capacity an integer in [1, 2^31-1] (slot indices
|
|
233
|
+
+ subtree counts are Uint32, `NIL = 0` reserves slot 0); alpha the weight-balance
|
|
234
|
+
factor in the OPEN interval (0.55, 0.75) (both ends throw), frozen at construction;
|
|
235
|
+
default 2/3. Allocates five columns + a free-list + two rebuild scratch buffers once.
|
|
236
|
+
- `get(key)` -> value | undefined. Absent -> undefined (no throw).
|
|
237
|
+
- `has(key)` -> boolean.
|
|
238
|
+
- `set(key, value)` -> this. Insert, or update the value in place if key exists (no new
|
|
239
|
+
node, no rebuild). Non-finite key/value throws; a full pool throws.
|
|
240
|
+
- `delete(key)` -> boolean. Idempotent: false if absent, true if removed.
|
|
241
|
+
- `rank(x)` -> number. Count of stored keys STRICTLY LESS than x, in [0, size].
|
|
242
|
+
- `select(k)` -> key | undefined. The k-th smallest key (0-based); undefined if k is out
|
|
243
|
+
of [0, size). Non-integer k throws.
|
|
244
|
+
- `successor(key)` -> key | undefined. Smallest key STRICTLY greater than key.
|
|
245
|
+
- `predecessor(key)` -> key | undefined. Largest key STRICTLY less than key.
|
|
246
|
+
- `rangeIter(lo, hi)` -> iterator of keys in [lo, hi] INCLUSIVE, ascending; VERSION-
|
|
247
|
+
STAMPED (structural OR value mutation mid-iteration throws). Bounds may be +-Infinity;
|
|
248
|
+
NaN or lo > hi throws.
|
|
249
|
+
- `size` / `capacity` / `alpha` getters; `clear()` -> this (empty, keep capacity);
|
|
250
|
+
`forEach(fn)` visits (key, value, tree) ascending.
|
|
251
|
+
- There is deliberately NO `split` / `merge` (the documented asymmetry vs Treap: no
|
|
252
|
+
priority heap to merge by; an honest deterministic split/merge would be O(n) rebuilds).
|
|
253
|
+
|
|
254
|
+
- `MinMaxHeap` -- class. A DOUBLE-ENDED priority queue (DEPQ) in ONE array-embedded binary
|
|
255
|
+
heap whose levels ALTERNATE min / max (Atkinson et al. 1986). The minimum is the root; the
|
|
256
|
+
maximum is the larger of the root's up-to-two children. Two parallel typed-array columns
|
|
257
|
+
(`_key` Float64, `_id` Uint32); the id is an OPAQUE Uint32 payload in [0, 2^32) (not unique,
|
|
258
|
+
no reverse map). Keys are finite numbers (typeof-guarded before coercion; Symbol / BigInt /
|
|
259
|
+
NaN / +-Infinity fail closed). Non-addressable: deliberately NO changeKey / remove.
|
|
260
|
+
- `new MinMaxHeap(capacity)` -- capacity an integer in [1, 2^31-1]. Allocates the two typed
|
|
261
|
+
arrays once. NO `kind` argument (a DEPQ serves both ends).
|
|
262
|
+
- `push(id, key)` -> void. id integer in [0, 2^32); key finite. The key is checked FIRST,
|
|
263
|
+
then the id, then a full heap; each throws `[lite-logn]` as a no-op (size unchanged).
|
|
264
|
+
- `popMin()` -> id | undefined. Removes the minimum; undefined if empty (no throw).
|
|
265
|
+
- `popMax()` -> id | undefined. Removes the maximum; undefined if empty (no throw).
|
|
266
|
+
- `peekMin()` / `peekMax()` -> id | undefined. `peekMinKey()` / `peekMaxKey()` -> key |
|
|
267
|
+
undefined. All O(1), read-only, undefined if empty (never throw).
|
|
268
|
+
- `size` / `capacity` getters; `clear()`; `forEach(fn)` and `[Symbol.iterator]()` yield
|
|
269
|
+
live ids in UNSPECIFIED (heap-array) order -- NOT sorted / pop order.
|
|
270
|
+
- `MinMaxHeap.build(ids, keys, capacity)` -> MinMaxHeap. Floyd O(n) bulk build from parallel
|
|
271
|
+
arrays (deepest-first, level-aware sift-down); fails closed on non-array-like / length
|
|
272
|
+
mismatch, count > capacity, out-of-range id, or non-finite key.
|
|
273
|
+
|
|
192
274
|
Member exports (one tree-shakeable class each) are appended here as each member
|
|
193
275
|
ships.
|
|
194
276
|
|
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.
|
|
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),
|
|
4
|
+
"version": "0.7.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), Treap (randomized-balanced augmented ordered map with O(log n) rank/select/split/merge), Scapegoat (DETERMINISTIC weight-balanced augmented ordered map: worst-case-O(log n) get, amortized-O(log n) set/delete, zero-GC rebuild), and MinMaxHeap (array-embedded double-ended priority queue: O(1) peekMin/peekMax, O(log n) push/popMin/popMax) 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",
|
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
"binary-heap",
|
|
40
40
|
"heap",
|
|
41
41
|
"priority-queue",
|
|
42
|
+
"min-max-heap",
|
|
43
|
+
"double-ended-priority-queue",
|
|
44
|
+
"depq",
|
|
42
45
|
"d-ary-heap",
|
|
43
46
|
"fenwick",
|
|
44
47
|
"bit",
|
|
@@ -49,6 +52,9 @@
|
|
|
49
52
|
"range-sum",
|
|
50
53
|
"skip-list",
|
|
51
54
|
"treap",
|
|
55
|
+
"scapegoat",
|
|
56
|
+
"scapegoat-tree",
|
|
57
|
+
"weight-balanced",
|
|
52
58
|
"ordered-set",
|
|
53
59
|
"ordered-map",
|
|
54
60
|
"balanced-bst",
|