@zakkster/lite-logn 0.3.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 +127 -1
- package/LogN.d.ts +96 -0
- package/LogN.js +1062 -11
- package/README.md +169 -9
- package/llms.txt +83 -6
- package/package.json +7 -3
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.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
|
[](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.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,11 +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)
|
|
62
|
+
- [SkipList](#skiplist)
|
|
63
|
+
- [Treap](#treap)
|
|
61
64
|
- [Zero-GC design notes](#zero-gc-design-notes)
|
|
62
65
|
- [Testing](#testing)
|
|
63
66
|
- [What this is not](#what-this-is-not)
|
|
@@ -82,16 +85,17 @@ lite-logn ships the O(log n) structures that matter with the allocation removed
|
|
|
82
85
|
|
|
83
86
|
## The roster
|
|
84
87
|
|
|
85
|
-
One member per session, each landing append-only (prior members stay byte-identical). At v0.
|
|
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.
|
|
86
89
|
|
|
87
90
|
| Member | Version | Status | Shape | Hot ops |
|
|
88
91
|
| --- | --- | --- | --- | --- |
|
|
89
92
|
| **BinaryHeap** | 0.1.0 | shipped | array-embedded complete binary min|max heap over a flat `Float64Array` | `push` / `pop` O(log n), `peek` O(1) |
|
|
90
93
|
| **Fenwick** (BIT) | 0.2.0 | shipped | flat `Float64Array`, lowest-set-bit walk (`i & -i`) | `update` / `prefix` / `rangeSum` / `at` / `set` O(log n) |
|
|
91
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) |
|
|
92
|
-
| **SkipList** | 0.4.0 |
|
|
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` |
|
|
93
97
|
|
|
94
|
-
Later tiers (
|
|
98
|
+
Later tiers (Scapegoat, OrderStatTree, IndexedHeap, SortedArray, MinMaxHeap, SplayTree, and presets) are queued in [`ROADMAP.md`](./ROADMAP.md).
|
|
95
99
|
|
|
96
100
|
## The O(log n) Witness
|
|
97
101
|
|
|
@@ -101,7 +105,81 @@ The family anchor. Time a fixed batch of the hot op at each `n` in a geometric s
|
|
|
101
105
|
- `slope` inside the member's band (the per-level cost, ns/level), AND
|
|
102
106
|
- the FOIL leaves the line (low `R^2` -- the O(n) default a working programmer reaches for, shown losing as `n` grows).
|
|
103
107
|
|
|
104
|
-
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.
|
|
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).
|
|
105
183
|
|
|
106
184
|
## API reference
|
|
107
185
|
|
|
@@ -109,7 +187,7 @@ For amortized / randomized members the witness also prints the MAX single-op tim
|
|
|
109
187
|
|
|
110
188
|
| Export | Type | Value | Meaning |
|
|
111
189
|
| --- | --- | --- | --- |
|
|
112
|
-
| `VERSION` | `string` | `'0.
|
|
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. |
|
|
113
191
|
|
|
114
192
|
### BinaryHeap
|
|
115
193
|
|
|
@@ -221,6 +299,79 @@ The iterative `2n` layout is **order-agnostic** -- `query` mixes left- and right
|
|
|
221
299
|
| `length` / `kind` | getters | O(1) | Element count / the frozen fold `'min'` \| `'max'` \| `'sum'` \| `'gcd'`. |
|
|
222
300
|
| `SegmentTree.build` | `build(values, kind) -> SegmentTree` | O(n) | Bottom-up bulk build (seed leaves, then fold each internal node once deepest-first -- NOT n incremental updates); fails closed on a non-array-like, any non-finite value, or (gcd) any negative / non-integer. |
|
|
223
301
|
|
|
302
|
+
### SkipList
|
|
303
|
+
|
|
304
|
+
A **skip list**: a pointer-free **ordered map** (key -> value) whose `get` / `set` / `delete` / `successor` / `predecessor` are **expected O(log n)** via a probabilistic tower of forward links -- the family's first randomized member and its first pointer-based one. Where the array-embedded members bury a fixed-shape tree in index arithmetic, a skip list's shape is random, so it needs real per-node links; the trick that keeps it zero-GC is storing those links as slot **indices** in flat `Uint32Array` columns over a private free-list (`NodePool`), never as heap objects. `NIL = 0`, slot 0 is the head sentinel, and level generation is one step of the repo's Numerical-Recipes LCG whose high bits draw a geometric height (`1 + clz32(word)`) -- deterministic from an instance-local seed, no `Math.random`. Keys are finite numbers (typeof-guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail closed); values are finite numbers; `set` on an existing key updates the value in place (no new node). Every hot op allocates zero bytes after construction.
|
|
305
|
+
|
|
306
|
+
Honesty note: the hot ops are **expected** O(log n), not worst-case -- an unlucky seed can build a tall thin tower and spike a single op. The witness fits the clean average line **and** separately prints the MAX single insert over a realistic randomized build trace, so the expectation is never sold as a guarantee.
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
import { SkipList } from '@zakkster/lite-logn';
|
|
310
|
+
|
|
311
|
+
const sl = new SkipList(1000, 42); // capacity 1000, seed 42 (deterministic)
|
|
312
|
+
sl.set(50, 500); // insert key 50 -> value 500
|
|
313
|
+
sl.set(20, 200);
|
|
314
|
+
sl.set(80, 800);
|
|
315
|
+
sl.get(20); // -> 200
|
|
316
|
+
sl.set(20, 222); // update value in place (no new node)
|
|
317
|
+
sl.successor(20); // -> 50 (smallest key strictly greater)
|
|
318
|
+
sl.predecessor(80); // -> 50 (largest key strictly less)
|
|
319
|
+
[...sl.rangeIter(20, 60)]; // -> [20, 50] (keys in [lo, hi], ascending)
|
|
320
|
+
sl.delete(50); // -> true (idempotent: false if absent)
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
| Member | Signature | Complexity | Notes |
|
|
324
|
+
| --- | --- | --- | --- |
|
|
325
|
+
| constructor | `new SkipList(capacity, seed?)` | O(capacity) | `capacity` integer in `[1, 2^26-1]` (slot indices are `Uint32`, `NIL = 0` reserves slot 0, the `MAXLEVEL`-column stride must stay addressable); `seed` an unsigned 32-bit integer (default fixed). Allocates the typed-array columns + private pool once. |
|
|
326
|
+
| `get` | `get(key) -> number \| undefined` | expected O(log n) | The value under `key`, or `undefined` if absent (no throw). Non-finite key throws. |
|
|
327
|
+
| `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. |
|
|
328
|
+
| `delete` | `delete(key) -> boolean` | expected O(log n) | Idempotent: `false` if absent, `true` if removed. Non-finite key throws. |
|
|
329
|
+
| `successor` | `successor(key) -> number \| undefined` | expected O(log n) | The smallest key STRICTLY greater than `key`, or `undefined`. `key` need not be present. |
|
|
330
|
+
| `predecessor` | `predecessor(key) -> number \| undefined` | expected O(log n) | The largest key STRICTLY less than `key`, or `undefined`. `key` need not be present. |
|
|
331
|
+
| `rangeIter` | `rangeIter(lo, hi) -> IterableIterator<number>` | O(log n + k) | Version-stamped iterator over keys in `[lo, hi]` INCLUSIVE, ascending. Bounds may be `+-Infinity` (unbounded ends); `NaN` or `lo > hi` throws; a structural mutation mid-iteration throws. |
|
|
332
|
+
| `forEach` | `forEach(fn) -> void` | O(n) | Visits `(key, value, list)` in ascending key order. |
|
|
333
|
+
| `clear` | `clear() -> this` | O(capacity) | Empties the list, keeps capacity, resets the PRNG to its initial seed. |
|
|
334
|
+
| `size` / `capacity` | getters | O(1) | Live entry count / fixed capacity. |
|
|
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
|
+
|
|
224
375
|
Member signatures for later members are appended here as each ships.
|
|
225
376
|
|
|
226
377
|
## Zero-GC design notes
|
|
@@ -239,8 +390,17 @@ Member signatures for later members are appended here as each ships.
|
|
|
239
390
|
| `SegmentTree` query / update / at | 0 B/op |
|
|
240
391
|
| `SegmentTree` constructor / `build` / `clear` | O(length) typed array (`2n` cells), once (cold) |
|
|
241
392
|
| `SegmentTree` forEach | 0 B/op in the loop body (pass a hoisted callback) |
|
|
242
|
-
|
|
243
|
-
|
|
393
|
+
| `SkipList` get / set / delete / successor / predecessor | 0 B/op (links are slot indices from a private free-list, never heap objects) |
|
|
394
|
+
| `SkipList` constructor / `clear` | O(capacity) typed arrays + pool, once (cold) |
|
|
395
|
+
| `SkipList` forEach | 0 B/op in the loop body (pass a hoisted callback) |
|
|
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) |
|
|
402
|
+
|
|
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.
|
|
244
404
|
|
|
245
405
|
## Testing
|
|
246
406
|
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-logn
|
|
2
2
|
|
|
3
|
-
Version: 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.
|
|
@@ -18,8 +18,9 @@ It is the O(log n) sibling of @zakkster/lite-o1: lite-o1 holds the constant (a
|
|
|
18
18
|
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
|
-
v0.1.0 shipped BinaryHeap; v0.2.0 adds Fenwick; v0.3.0 adds SegmentTree.
|
|
22
|
-
roster (one member per session, each landing
|
|
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. 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,
|
|
@@ -38,9 +39,26 @@ roster (one member per session, each landing append-only):
|
|
|
38
39
|
construction (ctor-cached small-int `_k` combined by an inline switch). The
|
|
39
40
|
complement to Fenwick: Fenwick's rangeSum needs an INVERSE (subtraction), so it
|
|
40
41
|
is sum-only; SegmentTree folds any associative + commutative op over a range.
|
|
41
|
-
- SkipList (v0.4.0) --
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
- SkipList (v0.4.0) -- a pointer-free ordered map (key -> value): get / set /
|
|
43
|
+
delete / successor / predecessor / rangeIter, EXPECTED O(log n) via a
|
|
44
|
+
probabilistic tower of forward links stored as slot INDICES in flat Uint32Array
|
|
45
|
+
columns (`NIL = 0`, slot 0 the head sentinel) over a PRIVATE free-list (NodePool,
|
|
46
|
+
design-parity with lite-o1's private pools, NOT a runtime dep). Level generation
|
|
47
|
+
is one NR-LCG step, HIGH bits -> geometric height (`1 + clz32`), instance-local
|
|
48
|
+
seed, deterministic. set updates an existing key's value in place. EXPECTED (not
|
|
49
|
+
worst-case) O(log n): the witness fits the average line AND prints the MAX single
|
|
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.
|
|
44
62
|
|
|
45
63
|
## Exports (from the single main file LogN.js)
|
|
46
64
|
|
|
@@ -112,6 +130,65 @@ roster (one member per session, each landing append-only):
|
|
|
112
130
|
incremental updates); fails closed on a non-array-like, any non-finite value,
|
|
113
131
|
or (gcd) any negative / non-integer.
|
|
114
132
|
|
|
133
|
+
- `SkipList` -- class. A pointer-free ordered map (key -> value) whose get / set /
|
|
134
|
+
delete / successor / predecessor are EXPECTED O(log n) via a probabilistic tower
|
|
135
|
+
of forward links stored as slot INDICES in a single flat `Uint32Array` of
|
|
136
|
+
`columns * (capacity + 1)` cells (`NIL = 0`, slot 0 the head sentinel) over a
|
|
137
|
+
PRIVATE free-list (NodePool) -- no heap object per op. Keys are finite numbers
|
|
138
|
+
(typeof-guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail closed);
|
|
139
|
+
values are finite numbers. EXPECTED, not worst-case (an unlucky seed can spike one
|
|
140
|
+
op; the witness prints the MAX single insert).
|
|
141
|
+
- `new SkipList(capacity, seed?)` -- capacity an integer in [1, 2^26-1] (slot
|
|
142
|
+
indices are Uint32, `NIL = 0` reserves slot 0); seed an unsigned 32-bit integer
|
|
143
|
+
(default fixed). Level columns sized to ceil(log2 cap)+1 up front (never grown).
|
|
144
|
+
- `get(key)` -> value | undefined. Absent -> undefined (no throw).
|
|
145
|
+
- `set(key, value)` -> this. Insert, or update the value in place if key exists
|
|
146
|
+
(no new node). Non-finite key/value throws; a full pool throws.
|
|
147
|
+
- `delete(key)` -> boolean. Idempotent: false if absent, true if removed.
|
|
148
|
+
- `successor(key)` -> key | undefined. Smallest key STRICTLY greater than key.
|
|
149
|
+
- `predecessor(key)` -> key | undefined. Largest key STRICTLY less than key.
|
|
150
|
+
- `rangeIter(lo, hi)` -> iterator of keys in [lo, hi] INCLUSIVE, ascending;
|
|
151
|
+
VERSION-STAMPED (mutation mid-iteration throws). Bounds may be +-Infinity;
|
|
152
|
+
NaN or lo > hi throws.
|
|
153
|
+
- `size` / `capacity` getters; `clear()` -> this (empty, keep capacity, reset the
|
|
154
|
+
PRNG to its initial seed); `forEach(fn)` visits (key, value, list) ascending.
|
|
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
|
+
|
|
115
192
|
Member exports (one tree-shakeable class each) are appended here as each member
|
|
116
193
|
ships.
|
|
117
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.
|
|
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),
|
|
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
|
|
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",
|