@zakkster/lite-logn 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +146 -1
- package/LogN.d.ts +107 -0
- package/LogN.js +1031 -13
- package/README.md +150 -19
- package/llms.txt +91 -10
- package/package.json +2 -2
package/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.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.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@zakkster/lite-logn)
|
|
6
6
|
[](https://github.com/sponsors/PeshoVurtoleta)
|
|
@@ -19,19 +19,31 @@ 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.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.
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
npm install @zakkster/lite-logn
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
```js
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
import { Fenwick } from '@zakkster/lite-logn';
|
|
30
|
+
|
|
31
|
+
// A Fenwick tree (Binary Indexed Tree): point-update AND prefix-sum both O(log n).
|
|
32
|
+
const f = new Fenwick(1000); // 1000 slots, all zero
|
|
33
|
+
f.update(10, 5); // add 5 at index 10 -- O(log n)
|
|
34
|
+
f.update(20, 3); // add 3 at index 20 -- O(log n)
|
|
35
|
+
f.prefix(15); // -> 5 (sum of [0..15] inclusive) -- O(log n)
|
|
36
|
+
f.rangeSum(10, 20); // -> 8 (sum of [10..20] inclusive) -- O(log n)
|
|
37
|
+
f.at(10); // -> 5 (the single element at 10) -- O(log n)
|
|
38
|
+
f.set(10, 100); // set index 10 to 100 (absolute) -- O(log n)
|
|
39
|
+
f.prefix(20); // -> 103
|
|
40
|
+
|
|
41
|
+
// O(n) LINEAR bulk build (each cell adds itself to its parent in one pass):
|
|
42
|
+
const g = Fenwick.build([1, 2, 3, 4, 5]);
|
|
43
|
+
g.prefix(4); // -> 15
|
|
32
44
|
```
|
|
33
45
|
|
|
34
|
-
|
|
46
|
+
Every hot op allocates zero bytes after construction, and `npm run witness` proves BOTH `update` and `prefix` hold the straight log line while their O(n) foils (a prefix-array rebuild and a naive re-sum) leave it.
|
|
35
47
|
|
|
36
48
|
---
|
|
37
49
|
|
|
@@ -39,10 +51,14 @@ Once BinaryHeap ships (v0.1.0 member session), the quick-start becomes a heap pu
|
|
|
39
51
|
|
|
40
52
|
- [Why this exists](#why-this-exists)
|
|
41
53
|
- [What you get](#what-you-get)
|
|
42
|
-
- [The
|
|
54
|
+
- [The roster](#the-roster)
|
|
43
55
|
- [The O(log n) Witness](#the-olog-n-witness)
|
|
44
56
|
- [API reference](#api-reference)
|
|
45
57
|
- [Constants](#constants)
|
|
58
|
+
- [BinaryHeap](#binaryheap)
|
|
59
|
+
- [Fenwick](#fenwick)
|
|
60
|
+
- [SegmentTree](#segmenttree)
|
|
61
|
+
- [SkipList](#skiplist)
|
|
46
62
|
- [Zero-GC design notes](#zero-gc-design-notes)
|
|
47
63
|
- [Testing](#testing)
|
|
48
64
|
- [What this is not](#what-this-is-not)
|
|
@@ -65,16 +81,16 @@ lite-logn ships the O(log n) structures that matter with the allocation removed
|
|
|
65
81
|
- **Tree-shakeable named exports.** Members share no mutable module state, so a bundler that imports one drops the others.
|
|
66
82
|
- **Fail closed.** Fixed, preallocated capacity; a `typeof`-guard at the door of every mutating op; `null` is not zero; an unknown option key is an error with a hint, never a silent ignore.
|
|
67
83
|
|
|
68
|
-
## The
|
|
84
|
+
## The roster
|
|
69
85
|
|
|
70
|
-
One member per session, each landing append-only (prior members stay byte-identical). At v0.
|
|
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.
|
|
71
87
|
|
|
72
|
-
| Member | Version | Shape | Hot ops |
|
|
73
|
-
| --- | --- | --- | --- |
|
|
74
|
-
| **BinaryHeap** | 0.1.0 | array-embedded complete binary min
|
|
75
|
-
| **Fenwick** (BIT) | 0.2.0 | flat
|
|
76
|
-
| **SegmentTree** | 0.3.0 | flat
|
|
77
|
-
| **SkipList** | 0.4.0 | pointer-free over a
|
|
88
|
+
| Member | Version | Status | Shape | Hot ops |
|
|
89
|
+
| --- | --- | --- | --- | --- |
|
|
90
|
+
| **BinaryHeap** | 0.1.0 | shipped | array-embedded complete binary min|max heap over a flat `Float64Array` | `push` / `pop` O(log n), `peek` O(1) |
|
|
91
|
+
| **Fenwick** (BIT) | 0.2.0 | shipped | flat `Float64Array`, lowest-set-bit walk (`i & -i`) | `update` / `prefix` / `rangeSum` / `at` / `set` O(log n) |
|
|
92
|
+
| **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
|
+
| **SkipList** | 0.4.0 | shipped | pointer-free over a private free-list node pool; expected O(log n) | `get` / `set` / `delete` / `successor` / `predecessor` |
|
|
78
94
|
|
|
79
95
|
Later tiers (Treap / Scapegoat, OrderStatTree, IndexedHeap, SortedArray, MinMaxHeap, SplayTree, and presets) are queued in [`ROADMAP.md`](./ROADMAP.md).
|
|
80
96
|
|
|
@@ -86,7 +102,7 @@ The family anchor. Time a fixed batch of the hot op at each `n` in a geometric s
|
|
|
86
102
|
- `slope` inside the member's band (the per-level cost, ns/level), AND
|
|
87
103
|
- the FOIL leaves the line (low `R^2` -- the O(n) default a working programmer reaches for, shown losing as `n` grows).
|
|
88
104
|
|
|
89
|
-
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
|
|
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.
|
|
90
106
|
|
|
91
107
|
## API reference
|
|
92
108
|
|
|
@@ -94,7 +110,7 @@ For amortized / randomized members the witness also prints the MAX single-op tim
|
|
|
94
110
|
|
|
95
111
|
| Export | Type | Value | Meaning |
|
|
96
112
|
| --- | --- | --- | --- |
|
|
97
|
-
| `VERSION` | `string` | `'0.
|
|
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. |
|
|
98
114
|
|
|
99
115
|
### BinaryHeap
|
|
100
116
|
|
|
@@ -135,6 +151,111 @@ heap.pop(); // -> 2 (the id whose key 9.0 is the max)
|
|
|
135
151
|
| `size` / `capacity` / `kind` | getters | O(1) | Live count / fixed capacity / `'min'` \| `'max'`. |
|
|
136
152
|
| `BinaryHeap.build` | `build(kind, ids, keys, capacity) -> BinaryHeap` | O(n) | Floyd bulk build from parallel arrays; fails closed on duplicate/out-of-range id, non-finite key, or `count > capacity`. |
|
|
137
153
|
|
|
154
|
+
### Fenwick
|
|
155
|
+
|
|
156
|
+
A **Fenwick tree** (Binary Indexed Tree): BOTH point-update AND prefix-sum in O(log n) over a single flat `Float64Array`, using nothing but the lowest-set-bit walk (`i & -i`). It answers the most delightfully non-obvious complexity question in the family -- "how can update AND query both be logarithmic on a plain array?" -- and the witness proves it with TWO straight log lines. Public indices are **0-based** in `[0, length)`; internally the tree is 1-based, so `_t[0]` is the unused identity sentinel and is never read as data (null is not zero). `update` climbs by `i & -i` (one `_t` touch per level); `prefix` descends by `i & -i` (one read per level); `rangeSum` and `at` are pairs of inlined prefix walks. Values are finite numbers (negatives allowed); NaN / +-Infinity / non-number fail closed. Every hot op allocates zero bytes after construction.
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
import { Fenwick } from '@zakkster/lite-logn';
|
|
160
|
+
|
|
161
|
+
const f = new Fenwick(1000);
|
|
162
|
+
f.update(10, 5); // add 5 at index 10
|
|
163
|
+
f.update(20, 3); // add 3 at index 20
|
|
164
|
+
f.prefix(15); // -> 5 (sum of [0..15] inclusive)
|
|
165
|
+
f.prefix(-1); // -> 0 (the empty-prefix base case)
|
|
166
|
+
f.rangeSum(10, 20); // -> 8 (sum of [10..20] inclusive)
|
|
167
|
+
f.at(10); // -> 5 (single element = prefix(10) - prefix(9))
|
|
168
|
+
f.set(10, 100); // set index 10 to 100 (absolute)
|
|
169
|
+
f.prefix(20); // -> 103
|
|
170
|
+
|
|
171
|
+
// O(n) LINEAR bulk build (not n incremental updates):
|
|
172
|
+
const g = Fenwick.build([1, 2, 3, 4, 5]);
|
|
173
|
+
g.rangeSum(1, 3); // -> 9
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
| Member | Signature | Complexity | Notes |
|
|
177
|
+
| --- | --- | --- | --- |
|
|
178
|
+
| constructor | `new Fenwick(length)` | O(length) | `length` integer in `[1, 2^31-1]`. Allocates one `Float64Array(length + 1)`, zero-initialized. |
|
|
179
|
+
| `update` | `update(i, delta) -> this` | O(log n) | Add `delta` at 0-based index `i` (climb by `i & -i`). `delta` finite (typeof-guarded first); out-of-range `i` throws. |
|
|
180
|
+
| `prefix` | `prefix(i) -> number` | O(log n) | Sum of `[0, i]` INCLUSIVE (descend by `i & -i`). `prefix(-1) === 0`; valid domain `[-1, length)`. |
|
|
181
|
+
| `rangeSum` | `rangeSum(lo, hi) -> number` | O(log n) | Sum of `[lo, hi]` INCLUSIVE both ends = `prefix(hi) - prefix(lo-1)`. Throws on out-of-range or `lo > hi`. |
|
|
182
|
+
| `at` | `at(i) -> number` | O(log n) | The single element = `prefix(i) - prefix(i-1)`. Out-of-range `i` throws. |
|
|
183
|
+
| `set` | `set(i, value) -> this` | O(log n) | Set element `i` to `value` (absolute), via `update(i, value - at(i))`. `value` finite. |
|
|
184
|
+
| `clear` | `clear() -> this` | O(length) | Zeros every element in place, keeping capacity. |
|
|
185
|
+
| `forEach` | `forEach(fn) -> void` | O(n log n) | Visits `(value, index, fenwick)` in ascending index order (each element is an `at` walk). |
|
|
186
|
+
| `length` | getter | O(1) | Element count this tree was sized for. |
|
|
187
|
+
| `Fenwick.build` | `build(values) -> Fenwick` | O(n) | LINEAR bulk build (each cell adds itself to its parent in one forward pass); fails closed on a non-array-like or any non-finite value. |
|
|
188
|
+
|
|
189
|
+
### SegmentTree
|
|
190
|
+
|
|
191
|
+
A **segment tree**: an associative range-query AND a point-update, BOTH O(log n), over a SINGLE flat `Float64Array(2n)` -- no nodes, no pointers, no recursion on the hot path. It is the complement to Fenwick: Fenwick's `rangeSum` works only because subtraction inverts addition, so it is a SUM machine; SegmentTree folds ANY associative + commutative operation over a range -- **min / max / sum / gcd** -- because it stores a fold of each subtree at its internal node rather than a prefix. The fold is chosen ONCE at construction and cached as a small-int combined by an INLINE switch on the hot path (no function ref, no closure, no megamorphic call site). Leaves live at `_t[n + i]`; internal node `p` holds the fold of its children `_t[2p]` / `_t[2p+1]`, so `_t[1]` is the fold of the whole array and `_t[0]` is unused (null is not zero). `update` sets a leaf and climbs to the root recomputing each ancestor (one write per level); `query` walks the two boundaries up the tree, folding each node that lies fully inside `[lo, hi]` into one accumulator. Every hot op allocates zero bytes after construction.
|
|
192
|
+
|
|
193
|
+
The fold's **identity** fills query accumulators and cleared / fresh leaves -- `sum -> 0`, `min -> +Infinity`, `max -> -Infinity`, `gcd -> 0` -- so a fresh or cleared tree queries to the identity. Identity is a legal RESULT but NEVER a legal INPUT: the value door rejects user `NaN` / `+-Infinity` (and, for the `gcd` kind, any negative or non-integer value), typeof-guarded before coercion.
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
import { SegmentTree } from '@zakkster/lite-logn';
|
|
197
|
+
|
|
198
|
+
const st = new SegmentTree(1000, 'min'); // 1000 slots, all +Infinity (min identity)
|
|
199
|
+
st.update(10, 5); // set index 10 to 5 (absolute)
|
|
200
|
+
st.update(20, 3); // set index 20 to 3
|
|
201
|
+
st.query(0, 999); // -> 3 (min over [0..999] inclusive)
|
|
202
|
+
st.query(10, 10); // -> 5 (a one-element range = the leaf)
|
|
203
|
+
st.at(20); // -> 3 (the single leaf value, O(1))
|
|
204
|
+
|
|
205
|
+
// A different fold, chosen at construction:
|
|
206
|
+
const sum = SegmentTree.build([1, 2, 3, 4, 5], 'sum'); // O(n) bottom-up bulk build
|
|
207
|
+
sum.query(1, 3); // -> 9 (2 + 3 + 4)
|
|
208
|
+
const g = SegmentTree.build([12, 18, 24], 'gcd');
|
|
209
|
+
g.query(0, 2); // -> 6
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The iterative `2n` layout is **order-agnostic** -- `query` mixes left- and right-boundary contributions into one accumulator, so it is correct ONLY because min / max / sum / gcd are all COMMUTATIVE as well as associative. A future non-commutative fold (matrix product, string concat) would need a pow2 layout with separate ordered accumulators (see [`decisions/0005-segtree.md`](./decisions/0005-segtree.md)).
|
|
213
|
+
|
|
214
|
+
| Member | Signature | Complexity | Notes |
|
|
215
|
+
| --- | --- | --- | --- |
|
|
216
|
+
| constructor | `new SegmentTree(length, kind)` | O(length) | `length` integer in `[1, 2^30-1]` (HALF of Fenwick's ceiling: the `2n` array must keep `2n` a positive int32); `kind` is `'min'` \| `'max'` \| `'sum'` \| `'gcd'`. Allocates one `Float64Array(2 * length)`. |
|
|
217
|
+
| `query` | `query(lo, hi) -> number` | O(log n) | The fold over `[lo, hi]` INCLUSIVE both ends. Throws on out-of-range or `lo > hi`. `lo == hi` returns that single leaf. |
|
|
218
|
+
| `update` | `update(i, value) -> this` | O(log n) | Set leaf `i` to `value` (ABSOLUTE), then fix ancestors. `value` finite (nonnegative integer for the `gcd` kind); out-of-range `i` throws. |
|
|
219
|
+
| `at` | `at(i) -> number` | O(1) | The single leaf value. Out-of-range `i` throws. |
|
|
220
|
+
| `clear` | `clear() -> this` | O(n) | Resets every element to the fold identity, keeping capacity. |
|
|
221
|
+
| `forEach` | `forEach(fn) -> void` | O(n) | Visits `(value, index, tree)` in ascending leaf order. |
|
|
222
|
+
| `length` / `kind` | getters | O(1) | Element count / the frozen fold `'min'` \| `'max'` \| `'sum'` \| `'gcd'`. |
|
|
223
|
+
| `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. |
|
|
224
|
+
|
|
225
|
+
### SkipList
|
|
226
|
+
|
|
227
|
+
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.
|
|
228
|
+
|
|
229
|
+
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.
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
import { SkipList } from '@zakkster/lite-logn';
|
|
233
|
+
|
|
234
|
+
const sl = new SkipList(1000, 42); // capacity 1000, seed 42 (deterministic)
|
|
235
|
+
sl.set(50, 500); // insert key 50 -> value 500
|
|
236
|
+
sl.set(20, 200);
|
|
237
|
+
sl.set(80, 800);
|
|
238
|
+
sl.get(20); // -> 200
|
|
239
|
+
sl.set(20, 222); // update value in place (no new node)
|
|
240
|
+
sl.successor(20); // -> 50 (smallest key strictly greater)
|
|
241
|
+
sl.predecessor(80); // -> 50 (largest key strictly less)
|
|
242
|
+
[...sl.rangeIter(20, 60)]; // -> [20, 50] (keys in [lo, hi], ascending)
|
|
243
|
+
sl.delete(50); // -> true (idempotent: false if absent)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
| Member | Signature | Complexity | Notes |
|
|
247
|
+
| --- | --- | --- | --- |
|
|
248
|
+
| 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. |
|
|
249
|
+
| `get` | `get(key) -> number \| undefined` | expected O(log n) | The value under `key`, or `undefined` if absent (no throw). Non-finite key throws. |
|
|
250
|
+
| `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. |
|
|
251
|
+
| `delete` | `delete(key) -> boolean` | expected O(log n) | Idempotent: `false` if absent, `true` if removed. Non-finite key throws. |
|
|
252
|
+
| `successor` | `successor(key) -> number \| undefined` | expected O(log n) | The smallest key STRICTLY greater than `key`, or `undefined`. `key` need not be present. |
|
|
253
|
+
| `predecessor` | `predecessor(key) -> number \| undefined` | expected O(log n) | The largest key STRICTLY less than `key`, or `undefined`. `key` need not be present. |
|
|
254
|
+
| `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. |
|
|
255
|
+
| `forEach` | `forEach(fn) -> void` | O(n) | Visits `(key, value, list)` in ascending key order. |
|
|
256
|
+
| `clear` | `clear() -> this` | O(capacity) | Empties the list, keeps capacity, resets the PRNG to its initial seed. |
|
|
257
|
+
| `size` / `capacity` | getters | O(1) | Live entry count / fixed capacity. |
|
|
258
|
+
|
|
138
259
|
Member signatures for later members are appended here as each ships.
|
|
139
260
|
|
|
140
261
|
## Zero-GC design notes
|
|
@@ -147,8 +268,18 @@ Member signatures for later members are appended here as each ships.
|
|
|
147
268
|
| --- | --- |
|
|
148
269
|
| `BinaryHeap` push / pop / peek / topKey / keyOf / has / changeKey / remove | 0 B/op |
|
|
149
270
|
| `BinaryHeap` constructor / `build` / `clear` | O(capacity) typed arrays, once (cold) |
|
|
150
|
-
|
|
151
|
-
|
|
271
|
+
| `Fenwick` update / prefix / rangeSum / at / set | 0 B/op |
|
|
272
|
+
| `Fenwick` constructor / `build` / `clear` | O(length) typed array, once (cold) |
|
|
273
|
+
| `Fenwick` forEach | 0 B/op in the loop body (pass a hoisted callback) |
|
|
274
|
+
| `SegmentTree` query / update / at | 0 B/op |
|
|
275
|
+
| `SegmentTree` constructor / `build` / `clear` | O(length) typed array (`2n` cells), once (cold) |
|
|
276
|
+
| `SegmentTree` forEach | 0 B/op in the loop body (pass a hoisted callback) |
|
|
277
|
+
| `SkipList` get / set / delete / successor / predecessor | 0 B/op (links are slot indices from a private free-list, never heap objects) |
|
|
278
|
+
| `SkipList` constructor / `clear` | O(capacity) typed arrays + pool, once (cold) |
|
|
279
|
+
| `SkipList` forEach | 0 B/op in the loop body (pass a hoisted callback) |
|
|
280
|
+
| `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.
|
|
152
283
|
|
|
153
284
|
## Testing
|
|
154
285
|
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-logn
|
|
2
2
|
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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,8 @@ 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
|
|
22
|
-
session, each landing append-only):
|
|
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):
|
|
23
23
|
|
|
24
24
|
- BinaryHeap (v0.1.0) -- an INDEXED binary heap (addressable priority queue): a
|
|
25
25
|
min|max binary heap over three parallel typed arrays (`_key` Float64Array,
|
|
@@ -29,14 +29,24 @@ session, each landing append-only):
|
|
|
29
29
|
addressable by a caller-supplied entity id. The cleanest witness in the family
|
|
30
30
|
and the one that calibrates the shared R^2 floor + slope band gate (pop).
|
|
31
31
|
- Fenwick / BIT (v0.2.0) -- BOTH point-update AND prefix-sum in O(log n) over a
|
|
32
|
-
single flat
|
|
33
|
-
prefix
|
|
32
|
+
single flat `Float64Array` via the lowest-set-bit walk (`i & -i`); rangeSum is
|
|
33
|
+
two prefix walks. 0-based public indices; 1-based internally (`_t[0]` the unused
|
|
34
|
+
identity sentinel). Two straight log lines (update + prefix).
|
|
34
35
|
- SegmentTree (v0.3.0) -- general associative range-query (min / max / sum / gcd)
|
|
35
|
-
+ point-update over a flat
|
|
36
|
-
is chosen
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
+ point-update over a SINGLE flat `Float64Array(2n)` (leaves at n..2n-1, `_t[0]`
|
|
37
|
+
unused), BOTH O(log n) via iterative bottom-up walks; the fold is chosen ONCE at
|
|
38
|
+
construction (ctor-cached small-int `_k` combined by an inline switch). The
|
|
39
|
+
complement to Fenwick: Fenwick's rangeSum needs an INVERSE (subtraction), so it
|
|
40
|
+
is sum-only; SegmentTree folds any associative + commutative op over a range.
|
|
41
|
+
- SkipList (v0.4.0) -- a pointer-free ordered map (key -> value): get / set /
|
|
42
|
+
delete / successor / predecessor / rangeIter, EXPECTED O(log n) via a
|
|
43
|
+
probabilistic tower of forward links stored as slot INDICES in flat Uint32Array
|
|
44
|
+
columns (`NIL = 0`, slot 0 the head sentinel) over a PRIVATE free-list (NodePool,
|
|
45
|
+
design-parity with lite-o1's private pools, NOT a runtime dep). Level generation
|
|
46
|
+
is one NR-LCG step, HIGH bits -> geometric height (`1 + clz32`), instance-local
|
|
47
|
+
seed, deterministic. set updates an existing key's value in place. EXPECTED (not
|
|
48
|
+
worst-case) O(log n): the witness fits the average line AND prints the MAX single
|
|
49
|
+
insert. Zero allocation on every hot op.
|
|
40
50
|
|
|
41
51
|
## Exports (from the single main file LogN.js)
|
|
42
52
|
|
|
@@ -60,6 +70,77 @@ session, each landing append-only):
|
|
|
60
70
|
build from parallel arrays; fails closed on duplicate/out-of-range id,
|
|
61
71
|
non-finite key, or count > capacity.
|
|
62
72
|
|
|
73
|
+
- `Fenwick` -- class. A Fenwick tree (Binary Indexed Tree): BOTH point-update AND
|
|
74
|
+
prefix-sum in O(log n) over a single flat `Float64Array` via the lowest-set-bit
|
|
75
|
+
walk (`i & -i`). Public indices 0-based in [0, length); internally 1-based
|
|
76
|
+
(`_t[0]` the unused identity sentinel -- null is not zero). Values are finite
|
|
77
|
+
numbers (negatives allowed); NaN / +-Infinity / non-number fail closed.
|
|
78
|
+
- `new Fenwick(length)` -- length is an integer in [1, 2^31-1]. Allocates one
|
|
79
|
+
`Float64Array(length + 1)`, zero-initialized.
|
|
80
|
+
- `update(i, delta)` -> this. Add delta at 0-based index i (climb by `i & -i`);
|
|
81
|
+
delta must be a finite number (typeof-guarded first); out-of-range i throws.
|
|
82
|
+
- `prefix(i)` -> number. Sum of [0, i] INCLUSIVE (descend by `i & -i`).
|
|
83
|
+
`prefix(-1) === 0` is the clean base case; valid domain [-1, length).
|
|
84
|
+
- `rangeSum(lo, hi)` -> number. Sum of [lo, hi] INCLUSIVE both ends =
|
|
85
|
+
prefix(hi) - prefix(lo-1); throws on out-of-range or lo > hi.
|
|
86
|
+
- `at(i)` -> number. The single element = prefix(i) - prefix(i-1).
|
|
87
|
+
- `set(i, value)` -> this. Set element i to value (absolute), via
|
|
88
|
+
update(i, value - at(i)); value must be finite.
|
|
89
|
+
- `length` getter; `clear()` -> this (zero-fill, keep capacity);
|
|
90
|
+
`forEach(fn)` visits (value, index, fenwick) in ascending index order.
|
|
91
|
+
- `Fenwick.build(values)` -> Fenwick. O(n) LINEAR bulk build (each cell adds
|
|
92
|
+
itself to its parent in one pass -- NOT n incremental updates); fails closed
|
|
93
|
+
on a non-array-like or any non-finite value.
|
|
94
|
+
|
|
95
|
+
- `SegmentTree` -- class. An associative range-query AND a point-update, BOTH
|
|
96
|
+
O(log n), over a SINGLE flat `Float64Array(2n)` (leaves at n..2n-1, `_t[0]`
|
|
97
|
+
unused). The fold (min / max / sum / gcd) is chosen ONCE at construction and
|
|
98
|
+
cached as a small-int combined by an inline switch on the hot path. The
|
|
99
|
+
iterative 2n layout is order-agnostic -- correct because all four folds are
|
|
100
|
+
commutative + associative (a non-commutative fold would need a pow2 layout).
|
|
101
|
+
Identity fills query accumulators + cleared / fresh leaves (sum -> 0, min ->
|
|
102
|
+
+Infinity, max -> -Infinity, gcd -> 0) but is never a legal INPUT: the value
|
|
103
|
+
door rejects user NaN / +-Infinity, and the gcd kind additionally rejects
|
|
104
|
+
negatives + non-integers (typeof-guarded before coercion).
|
|
105
|
+
- `new SegmentTree(length, kind)` -- length an integer in [1, 2^30-1] (HALF of
|
|
106
|
+
Fenwick's ceiling: the 2n array must keep `2n` a positive int32); kind is
|
|
107
|
+
'min' | 'max' | 'sum' | 'gcd'. Allocates one `Float64Array(2 * length)`.
|
|
108
|
+
- `query(lo, hi)` -> number. The fold over [lo, hi] INCLUSIVE both ends; throws
|
|
109
|
+
on out-of-range or lo > hi (matching Fenwick.rangeSum). lo == hi returns that
|
|
110
|
+
single leaf. A fresh / cleared tree queries to the identity.
|
|
111
|
+
- `update(i, value)` -> this. Set leaf i to value (ABSOLUTE), then fix ancestors;
|
|
112
|
+
value finite (nonnegative integer for the gcd kind), out-of-range i throws.
|
|
113
|
+
- `at(i)` -> number. The single leaf value. O(1). Out-of-range i throws.
|
|
114
|
+
- `length` / `kind` getters; `clear()` -> this (reset to identity, keep
|
|
115
|
+
capacity); `forEach(fn)` visits (value, index, tree) in ascending leaf order.
|
|
116
|
+
- `SegmentTree.build(values, kind)` -> SegmentTree. O(n) bottom-up bulk build
|
|
117
|
+
(seed leaves, then fold each internal node once deepest-first -- NOT n
|
|
118
|
+
incremental updates); fails closed on a non-array-like, any non-finite value,
|
|
119
|
+
or (gcd) any negative / non-integer.
|
|
120
|
+
|
|
121
|
+
- `SkipList` -- class. A pointer-free ordered map (key -> value) whose get / set /
|
|
122
|
+
delete / successor / predecessor are EXPECTED O(log n) via a probabilistic tower
|
|
123
|
+
of forward links stored as slot INDICES in a single flat `Uint32Array` of
|
|
124
|
+
`columns * (capacity + 1)` cells (`NIL = 0`, slot 0 the head sentinel) over a
|
|
125
|
+
PRIVATE free-list (NodePool) -- no heap object per op. Keys are finite numbers
|
|
126
|
+
(typeof-guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail closed);
|
|
127
|
+
values are finite numbers. EXPECTED, not worst-case (an unlucky seed can spike one
|
|
128
|
+
op; the witness prints the MAX single insert).
|
|
129
|
+
- `new SkipList(capacity, seed?)` -- capacity an integer in [1, 2^26-1] (slot
|
|
130
|
+
indices are Uint32, `NIL = 0` reserves slot 0); seed an unsigned 32-bit integer
|
|
131
|
+
(default fixed). Level columns sized to ceil(log2 cap)+1 up front (never grown).
|
|
132
|
+
- `get(key)` -> value | undefined. Absent -> undefined (no throw).
|
|
133
|
+
- `set(key, value)` -> this. Insert, or update the value in place if key exists
|
|
134
|
+
(no new node). Non-finite key/value throws; a full pool throws.
|
|
135
|
+
- `delete(key)` -> boolean. Idempotent: false if absent, true if removed.
|
|
136
|
+
- `successor(key)` -> key | undefined. Smallest key STRICTLY greater than key.
|
|
137
|
+
- `predecessor(key)` -> key | undefined. Largest key STRICTLY less than key.
|
|
138
|
+
- `rangeIter(lo, hi)` -> iterator of keys in [lo, hi] INCLUSIVE, ascending;
|
|
139
|
+
VERSION-STAMPED (mutation mid-iteration throws). Bounds may be +-Infinity;
|
|
140
|
+
NaN or lo > hi throws.
|
|
141
|
+
- `size` / `capacity` getters; `clear()` -> this (empty, keep capacity, reset the
|
|
142
|
+
PRNG to its initial seed); `forEach(fn)` visits (key, value, list) ascending.
|
|
143
|
+
|
|
63
144
|
Member exports (one tree-shakeable class each) are appended here as each member
|
|
64
145
|
ships.
|
|
65
146
|
|
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), and SkipList (pointer-free expected-O(log n) ordered map
|
|
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.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./LogN.js",
|
|
8
8
|
"module": "./LogN.js",
|