@zakkster/lite-logn 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.6.0 ships six 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), and Scapegoat (a DETERMINISTIC weight-balanced augmented ordered map: worst-case-O(log n) get, amortized-O(log n) set / delete, zero-GC rebuild) -- 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.6.0 ships six members: BinaryHeap, Fenwick, SegmentTree, SkipList, Treap and Scapegoat.** 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,15 @@ 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)
64
+ - [Scapegoat](#scapegoat)
62
65
  - [Zero-GC design notes](#zero-gc-design-notes)
63
66
  - [Testing](#testing)
64
67
  - [What this is not](#what-this-is-not)
@@ -83,7 +86,7 @@ lite-logn ships the O(log n) structures that matter with the allocation removed
83
86
 
84
87
  ## The roster
85
88
 
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.
89
+ One member per session, each landing append-only (prior members stay byte-identical). At v0.6.0, BinaryHeap, Fenwick, SegmentTree, SkipList, Treap and Scapegoat are shipped.
87
90
 
88
91
  | Member | Version | Status | Shape | Hot ops |
89
92
  | --- | --- | --- | --- | --- |
@@ -91,8 +94,10 @@ One member per session, each landing append-only (prior members stay byte-identi
91
94
  | **Fenwick** (BIT) | 0.2.0 | shipped | flat `Float64Array`, lowest-set-bit walk (`i & -i`) | `update` / `prefix` / `rangeSum` / `at` / `set` O(log n) |
92
95
  | **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
96
  | **SkipList** | 0.4.0 | shipped | pointer-free over a private free-list node pool; expected O(log n) | `get` / `set` / `delete` / `successor` / `predecessor` |
97
+ | **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` |
98
+ | **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) |
94
99
 
95
- Later tiers (Treap / Scapegoat, OrderStatTree, IndexedHeap, SortedArray, MinMaxHeap, SplayTree, and presets) are queued in [`ROADMAP.md`](./ROADMAP.md).
100
+ Later tiers (OrderStatTree, IndexedHeap, SortedArray, MinMaxHeap, SplayTree, and presets) are queued in [`ROADMAP.md`](./ROADMAP.md).
96
101
 
97
102
  ## The O(log n) Witness
98
103
 
@@ -102,7 +107,84 @@ The family anchor. Time a fixed batch of the hot op at each `n` in a geometric s
102
107
  - `slope` inside the member's band (the per-level cost, ns/level), AND
103
108
  - the FOIL leaves the line (low `R^2` -- the O(n) default a working programmer reaches for, shown losing as `n` grows).
104
109
 
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.
110
+ 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.6.0 the witness gates nine 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]`), and Scapegoat `get` (R^2 ~ 0.99, slope ~ 4 ns/level, band `[2.41, 5.63]`) 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.
111
+
112
+ ## Benchmarks
113
+
114
+ 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:
115
+
116
+ ```sh
117
+ npm run bench # 32 cells -> benchmark/results.json + summary tables
118
+ npm run bench:report # the above, then benchmark/report.html (hand-rolled inline-SVG graphs)
119
+ ```
120
+
121
+ 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).
122
+
123
+ ### D1 -- the O(log n) Witness fit (per gated op-row)
124
+
125
+ 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.
126
+
127
+ <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">
128
+ <text x="8" y="16" font-size="12" fill="#475569">D1 slope (ns/level) -- lower is a cheaper per-level cost</text>
129
+ <g font-size="10" fill="#334155" text-anchor="middle">
130
+ <rect x="24" y="84" width="60" height="96" fill="#2563eb"/><text x="54" y="194">BH.pop 8.4</text>
131
+ <rect x="112" y="148" width="60" height="32" fill="#059669"/><text x="142" y="194">Fen.upd 2.8</text>
132
+ <rect x="200" y="150" width="60" height="30" fill="#059669"/><text x="230" y="194">Fen.pre 2.6</text>
133
+ <rect x="288" y="145" width="60" height="35" fill="#d97706"/><text x="318" y="194">Seg.upd 3.1</text>
134
+ <rect x="376" y="99" width="60" height="81" fill="#d97706"/><text x="406" y="194">Seg.qry 7.0</text>
135
+ <rect x="464" y="88" width="60" height="92" fill="#7c3aed"/><text x="494" y="194">SL.get 8.0</text>
136
+ <rect x="552" y="40" width="60" height="140" fill="#7c3aed"/><text x="582" y="194">SL.set 12.1</text>
137
+ </g>
138
+ </svg>
139
+
140
+ | op-row | `R^2` | slope (ns/level) | slope band | on line? | foil | foil `R^2` | foil off? |
141
+ | --- | --- | --- | --- | --- | --- | --- | --- |
142
+ | `BinaryHeap.pop` | 0.996 | 8.4 | `[5.76, 13.44]` | ON | sorted-array insert | 0.83 | off |
143
+ | `Fenwick.update` | 0.980 | 2.8 | `[1.84, 4.30]` | ON | prefix-array rebuild | 0.76 | off |
144
+ | `Fenwick.prefix` | 0.968 | 2.6 | `[1.76, 4.10]` | ON | naive re-sum | 0.75 | off |
145
+ | `SegmentTree.update` | 0.989 | 3.1 | `[2.29, 5.35]` | ON | whole-tree rebuild | 0.82 | off |
146
+ | `SegmentTree.query` | 0.998 | 7.0 | `[4.30, 10.04]` | ON | scan-fold | 0.73 | off |
147
+ | `SkipList.get` | 0.988 | 8.0 | `[5.27, 12.30]` | ON | linear scan | 0.79 | off |
148
+ | `SkipList.set` | 0.985 | 12.1 | `[8.36, 19.50]` | ON | sorted-array insert | 0.77 | off |
149
+ | `Treap.get` | 0.988 | 4.0 | `[2.55, 5.95]` | ON | linear scan | 0.79 | off |
150
+ | `Scapegoat.get` | 0.988 | 3.8 | `[2.41, 5.63]` | ON | linear scan | 0.79 | off |
151
+
152
+ **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`.
153
+
154
+ **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).
155
+
156
+ ### D3 -- memory (bytes / live vs a theoretical floor)
157
+
158
+ | member | peak bytes @ 64Ki | B/live | theo min | overhead x | note |
159
+ | --- | --- | --- | --- | --- | --- |
160
+ | BinaryHeap | 1,048,576 | 16.0 | 12 | 1.33 | key (8) + id (4) dense; `_pos` reverse map is the universe overhead |
161
+ | Fenwick | 524,296 | 8.0 | 8 | 1.00 | one `Float64` tree cell per element -- exact |
162
+ | SegmentTree | 1,048,576 | 16.0 | 16 | 1.00 | the `2n` array -- exact |
163
+ | SkipList | 5,767,320 | 88.0 | 16 | 5.50 | key + value dense; the `ceil(log2 cap)+1` link columns are the tower overhead |
164
+
165
+ 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.
166
+
167
+ ### D5 -- bundle size + tree-shaking (esbuild min + gzip)
168
+
169
+ 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`).
170
+
171
+ | member | single gz (B) | all gz (B) | ratio | `< 40%`? |
172
+ | --- | --- | --- | --- | --- |
173
+ | BinaryHeap | 1,365 | 3,758 | 0.363 | yes |
174
+ | Fenwick | 826 | 3,758 | 0.220 | yes |
175
+ | SegmentTree | 1,071 | 3,758 | 0.285 | yes |
176
+ | SkipList | 1,546 | 3,758 | 0.411 | NO (the stated exception) |
177
+
178
+ ### D6 -- GC pressure (the 0 B/op gate as a curve, per op-row)
179
+
180
+ 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).
181
+
182
+ ### The other dimensions
183
+
184
+ - **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).
185
+ - **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.
186
+ - **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, `n/a` for the index-addressed Fenwick + SegmentTree.
187
+ - **D8 workloads** -- churn (all members) + an ordered scan (`successor` + `rangeIter`, SkipList + Treap + Scapegoat; `n/a` elsewhere).
106
188
 
107
189
  ## API reference
108
190
 
@@ -110,7 +192,7 @@ For amortized / randomized members the witness also prints the MAX single-op tim
110
192
 
111
193
  | Export | Type | Value | Meaning |
112
194
  | --- | --- | --- | --- |
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. |
195
+ | `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. |
114
196
 
115
197
  ### BinaryHeap
116
198
 
@@ -256,6 +338,81 @@ sl.delete(50); // -> true (idempotent: false if absent)
256
338
  | `clear` | `clear() -> this` | O(capacity) | Empties the list, keeps capacity, resets the PRNG to its initial seed. |
257
339
  | `size` / `capacity` | getters | O(1) | Live entry count / fixed capacity. |
258
340
 
341
+ ### Treap
342
+
343
+ 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.
344
+
345
+ 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)).
346
+
347
+ ```js
348
+ import { Treap } from '@zakkster/lite-logn';
349
+
350
+ const tr = new Treap(1000, 42); // capacity 1000, seed 42 (deterministic)
351
+ tr.set(50, 500); // insert key 50 -> value 500
352
+ tr.set(20, 200);
353
+ tr.set(80, 800);
354
+ tr.get(20); // -> 200
355
+ tr.rank(50); // -> 1 (one key, 20, is strictly less than 50)
356
+ tr.select(0); // -> 20 (the smallest key)
357
+ tr.successor(20); // -> 50 (smallest key strictly greater)
358
+ const [lo, hi] = tr.split(50); // lo: keys < 50; hi: keys >= 50 (share the arena; tr consumed)
359
+ const whole = Treap.merge(lo, hi);// fuse back (all lo keys < all hi keys); both consumed
360
+ ```
361
+
362
+ | Member | Signature | Complexity | Notes |
363
+ | --- | --- | --- | --- |
364
+ | 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. |
365
+ | `get` | `get(key) -> number \| undefined` | expected O(log n) | The value under `key`, or `undefined` if absent (no throw). Non-finite key throws. |
366
+ | `has` | `has(key) -> boolean` | expected O(log n) | True iff `key` is stored. Non-finite key throws. |
367
+ | `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. |
368
+ | `delete` | `delete(key) -> boolean` | expected O(log n) | Idempotent: `false` if absent, `true` if removed. Non-finite key throws. |
369
+ | `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. |
370
+ | `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. |
371
+ | `successor` | `successor(key) -> number \| undefined` | expected O(log n) | The smallest key STRICTLY greater than `key`, or `undefined`. |
372
+ | `predecessor` | `predecessor(key) -> number \| undefined` | expected O(log n) | The largest key STRICTLY less than `key`, or `undefined`. |
373
+ | `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. |
374
+ | `forEach` | `forEach(fn) -> void` | O(n) | Visits `(key, value, treap)` in ascending key order. |
375
+ | `clear` | `clear() -> this` | O(capacity) | Empties the treap, keeps capacity, resets the PRNG to its initial seed. |
376
+ | `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). |
377
+ | `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. |
378
+ | `size` / `capacity` | getters | O(1) | Live entry count / fixed capacity. |
379
+
380
+ ### Scapegoat
381
+
382
+ 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.
383
+
384
+ 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`).
385
+
386
+ ```js
387
+ import { Scapegoat } from '@zakkster/lite-logn';
388
+
389
+ const sg = new Scapegoat(1000); // capacity 1000, alpha = 2/3 (deterministic, no seed)
390
+ sg.set(50, 500); // insert key 50 -> value 500
391
+ sg.set(20, 200);
392
+ sg.set(80, 800);
393
+ sg.get(20); // -> 200 -- worst-case O(log n)
394
+ sg.rank(50); // -> 1 (one key, 20, is strictly less than 50)
395
+ sg.select(0); // -> 20 (the smallest key)
396
+ sg.successor(20); // -> 50 (smallest key strictly greater)
397
+ [...sg.rangeIter(20, 80)]; // -> [20, 50, 80] (inclusive, ascending)
398
+ ```
399
+
400
+ | Member | Signature | Complexity | Notes |
401
+ | --- | --- | --- | --- |
402
+ | 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. |
403
+ | `get` | `get(key) -> number \| undefined` | worst-case O(log n) | The value under `key`, or `undefined` if absent (no throw). Non-finite key throws. |
404
+ | `has` | `has(key) -> boolean` | worst-case O(log n) | True iff `key` is stored. Non-finite key throws. |
405
+ | `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. |
406
+ | `delete` | `delete(key) -> boolean` | amortized O(log n) | Idempotent: `false` if absent, `true` if removed. Non-finite key throws. |
407
+ | `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. |
408
+ | `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. |
409
+ | `successor` | `successor(key) -> number \| undefined` | worst-case O(log n) | The smallest key STRICTLY greater than `key`, or `undefined`. |
410
+ | `predecessor` | `predecessor(key) -> number \| undefined` | worst-case O(log n) | The largest key STRICTLY less than `key`, or `undefined`. |
411
+ | `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. |
412
+ | `forEach` | `forEach(fn) -> void` | O(n) | Visits `(key, value, tree)` in ascending key order. |
413
+ | `clear` | `clear() -> this` | O(capacity) | Empties the tree, keeps capacity. |
414
+ | `size` / `capacity` / `alpha` | getters | O(1) | Live entry count / fixed capacity / frozen weight-balance factor. |
415
+
259
416
  Member signatures for later members are appended here as each ships.
260
417
 
261
418
  ## Zero-GC design notes
@@ -278,8 +435,17 @@ Member signatures for later members are appended here as each ships.
278
435
  | `SkipList` constructor / `clear` | O(capacity) typed arrays + pool, once (cold) |
279
436
  | `SkipList` forEach | 0 B/op in the loop body (pass a hoisted callback) |
280
437
  | `SkipList` rangeIter | one iterator + `{value, done}` per step (the documented per-protocol allocator; transient, not retained) |
281
-
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.
438
+ | `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) |
439
+ | `Treap` constructor / `clear` | O(capacity) six columns + pool, once (cold) |
440
+ | `Treap` forEach | 0 B/op in the loop body (recursive in-order walk, hoisted callback) |
441
+ | `Treap` rangeIter | one iterator + `{value, done}` per step (the documented per-protocol allocator; transient, not retained) |
442
+ | `Treap` split / merge | 0 B/op beyond the returned Treap view(s); rewire in place, share the arena, consume the input(s) |
443
+ | `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) |
444
+ | `Scapegoat` constructor / `clear` | O(capacity) five columns + pool + two rebuild scratch buffers, once (cold) |
445
+ | `Scapegoat` forEach | 0 B/op in the loop body (recursive in-order walk, hoisted callback) |
446
+ | `Scapegoat` rangeIter | one iterator + `{value, done}` per step (the documented per-protocol allocator; transient, not retained) |
447
+
448
+ 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. The allocation table is extended per member as each lands.
283
449
 
284
450
  ## Testing
285
451
 
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # @zakkster/lite-logn
2
2
 
3
- Version: 0.4.0
3
+ Version: 0.6.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; v0.6.0 adds Scapegoat. The roster (one member per
23
+ session, each landing 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,33 @@ 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.
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.
50
78
 
51
79
  ## Exports (from the single main file LogN.js)
52
80
 
@@ -141,6 +169,75 @@ adds SkipList. The roster (one member per session, each landing append-only):
141
169
  - `size` / `capacity` getters; `clear()` -> this (empty, keep capacity, reset the
142
170
  PRNG to its initial seed); `forEach(fn)` visits (key, value, list) ascending.
143
171
 
172
+ - `Treap` -- class. A randomized-balanced AUGMENTED ordered map (key -> value) that is
173
+ also an order-statistic tree. A BST on `_key` x a max-heap on a per-node random
174
+ `_prio` gives EXPECTED O(log n) height; a `_size` subtree-count column adds O(log n)
175
+ order statistics. Nodes are slot INDICES in flat typed-array columns (`_key` /
176
+ `_value` Float64, `_left` / `_right` / `_prio` / `_size` Uint32, `NIL = 0`) over a
177
+ PRIVATE free-list (NodePool) -- no heap object per op. Keys and values are finite
178
+ numbers (typeof-guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail
179
+ closed). EXPECTED, not worst-case (an unlucky priority draw can spike one op; the MAX
180
+ single insert is disclosed). set / delete / split / merge recurse to a depth = tree
181
+ height (O(log n) expected, O(n) worst-case on a pathological priority draw), on the
182
+ native call stack, so every hot op is still 0 B/op.
183
+ - `new Treap(capacity, seed?)` -- capacity an integer in [1, 2^31-1] (slot indices +
184
+ subtree counts are Uint32, `NIL = 0` reserves slot 0); seed an unsigned 32-bit
185
+ integer (default fixed). Allocates six columns + a free-list once.
186
+ - `get(key)` -> value | undefined. Absent -> undefined (no throw).
187
+ - `has(key)` -> boolean.
188
+ - `set(key, value)` -> this. Insert, or update the value in place if key exists (no
189
+ new node). Non-finite key/value throws; a full pool throws.
190
+ - `delete(key)` -> boolean. Idempotent: false if absent, true if removed.
191
+ - `rank(x)` -> number. Count of stored keys STRICTLY LESS than x, in [0, size].
192
+ - `select(k)` -> key | undefined. The k-th smallest key (0-based); undefined if k is
193
+ out of [0, size). Non-integer k throws.
194
+ - `successor(key)` -> key | undefined. Smallest key STRICTLY greater than key.
195
+ - `predecessor(key)` -> key | undefined. Largest key STRICTLY less than key.
196
+ - `rangeIter(lo, hi)` -> iterator of keys in [lo, hi] INCLUSIVE, ascending; VERSION-
197
+ STAMPED (structural OR value mutation mid-iteration throws). Bounds may be
198
+ +-Infinity; NaN or lo > hi throws.
199
+ - `size` / `capacity` getters; `clear()` -> this (empty, keep capacity, reset the
200
+ PRNG to its initial seed); `forEach(fn)` visits (key, value, treap) ascending.
201
+ - `split(key)` -> [left, right]. left holds keys < key, right holds keys >= key;
202
+ O(log n) EXPECTED (rewires in place), so the two treaps SHARE this treap's arena
203
+ and this is CONSUMED (left empty).
204
+ - `Treap.merge(a, b)` -> Treap. Merge two arena-sharing treaps where every key of a
205
+ < every key of b; O(log n) EXPECTED, CONSUMES both. Non-Treap inputs, treaps from
206
+ different arenas, or an overlapping key range each throw.
207
+
208
+ - `Scapegoat` -- class. A DETERMINISTIC, weight-balanced AUGMENTED ordered map (key ->
209
+ value) that is also an order-statistic tree -- the honest PAIR to Treap. A BST on
210
+ `_key` kept alpha-weight-balanced (get WORST-case O(log n); set / delete AMORTIZED
211
+ O(log n) via an occasional subtree rebuild); a `_size` subtree-count column adds O(log n)
212
+ order statistics. NO priorities, NO RNG. Nodes are slot INDICES in flat typed-array
213
+ columns (`_key` / `_value` Float64, `_left` / `_right` / `_size` Uint32, `NIL = 0`) over
214
+ a PRIVATE free-list (NodePool); the zero-GC rebuild reuses ONE preallocated `_flat`
215
+ scratch + ONE `_stack` index-stack (iterative Morris-free flatten + bounded log-depth
216
+ balanced rebuild on the native stack) -- no heap object, no fresh array, 0 B/op even
217
+ under a rebuild storm. Keys and values are finite numbers (typeof-guarded before
218
+ coercion; Symbol / BigInt / NaN / +-Infinity fail closed).
219
+ - `new Scapegoat(capacity, alpha?)` -- capacity an integer in [1, 2^31-1] (slot indices
220
+ + subtree counts are Uint32, `NIL = 0` reserves slot 0); alpha the weight-balance
221
+ factor in the OPEN interval (0.55, 0.75) (both ends throw), frozen at construction;
222
+ default 2/3. Allocates five columns + a free-list + two rebuild scratch buffers once.
223
+ - `get(key)` -> value | undefined. Absent -> undefined (no throw).
224
+ - `has(key)` -> boolean.
225
+ - `set(key, value)` -> this. Insert, or update the value in place if key exists (no new
226
+ node, no rebuild). Non-finite key/value throws; a full pool throws.
227
+ - `delete(key)` -> boolean. Idempotent: false if absent, true if removed.
228
+ - `rank(x)` -> number. Count of stored keys STRICTLY LESS than x, in [0, size].
229
+ - `select(k)` -> key | undefined. The k-th smallest key (0-based); undefined if k is out
230
+ of [0, size). Non-integer k throws.
231
+ - `successor(key)` -> key | undefined. Smallest key STRICTLY greater than key.
232
+ - `predecessor(key)` -> key | undefined. Largest key STRICTLY less than key.
233
+ - `rangeIter(lo, hi)` -> iterator of keys in [lo, hi] INCLUSIVE, ascending; VERSION-
234
+ STAMPED (structural OR value mutation mid-iteration throws). Bounds may be +-Infinity;
235
+ NaN or lo > hi throws.
236
+ - `size` / `capacity` / `alpha` getters; `clear()` -> this (empty, keep capacity);
237
+ `forEach(fn)` visits (key, value, tree) ascending.
238
+ - There is deliberately NO `split` / `merge` (the documented asymmetry vs Treap: no
239
+ priority heap to merge by; an honest deterministic split/merge would be O(n) rebuilds).
240
+
144
241
  Member exports (one tree-shakeable class each) are appended here as each member
145
242
  ships.
146
243
 
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.6.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), and Scapegoat (DETERMINISTIC weight-balanced augmented ordered map: worst-case-O(log n) get, amortized-O(log n) set/delete, zero-GC rebuild) 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,12 @@
46
48
  "range-query",
47
49
  "range-sum",
48
50
  "skip-list",
51
+ "treap",
52
+ "scapegoat",
53
+ "scapegoat-tree",
54
+ "weight-balanced",
49
55
  "ordered-set",
56
+ "ordered-map",
50
57
  "balanced-bst",
51
58
  "bst",
52
59
  "log-n",