@zakkster/lite-logn 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +116 -1
- package/LogN.d.ts +103 -0
- package/LogN.js +890 -1
- package/README.md +96 -9
- package/llms.txt +85 -3
- package/package.json +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,120 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.7.0] - 2026-09-20
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **MinMaxHeap** -- the seventh member and the family's DOUBLE-ENDED priority queue
|
|
14
|
+
(DEPQ): a single array-embedded binary heap whose levels ALTERNATE min / max (Atkinson,
|
|
15
|
+
Sack, Santoro & Strothotte 1986). Even depth (the root is depth 0) is a MIN level, odd
|
|
16
|
+
depth a MAX level, so the global minimum sits at the root and the global maximum is the
|
|
17
|
+
LARGER of the root's up-to-two children. `peekMin` / `peekMax` / `peekMinKey` /
|
|
18
|
+
`peekMaxKey` are O(1); `push` / `popMin` / `popMax` are all WORST-case O(log n) from that
|
|
19
|
+
ONE heap (no second heap, no paired-heap correspondence). Surface: `push(id, key)` /
|
|
20
|
+
`popMin` / `popMax` / `peekMin` / `peekMax` / `peekMinKey` / `peekMaxKey` / `clear` /
|
|
21
|
+
`forEach` / `[Symbol.iterator]`, and `size` / `capacity` getters, plus a Floyd O(n)
|
|
22
|
+
`MinMaxHeap.build(ids, keys, capacity)` (deepest-first, level-aware sift-down). Keys are
|
|
23
|
+
finite numbers (typeof-guarded before coercion -- Symbol / BigInt / NaN / +-Infinity fail
|
|
24
|
+
closed with a `[lite-logn]` throw, key checked FIRST, then the id, then a full heap).
|
|
25
|
+
- **id + key payload, NON-addressable (the asymmetry vs BinaryHeap).** Two parallel
|
|
26
|
+
pointer-free typed-array columns (`_id` Uint32Array + `_key` Float64Array), the BinaryHeap
|
|
27
|
+
id+key idiom -- but with NO `_pos` reverse map, and therefore deliberately NO `changeKey` /
|
|
28
|
+
`remove`. The id is an OPAQUE Uint32 payload (NOT unique; the full [0, 2^32) domain, wider
|
|
29
|
+
than BinaryHeap's [0, capacity)). A DEPQ's job is the two extremes; addressability is the
|
|
30
|
+
separable concern BinaryHeap already carries. There is also NO `kind` argument / getter (a
|
|
31
|
+
DEPQ has both ends; a kind getter would be a lie).
|
|
32
|
+
- **Classic one-element-per-node min-max heap only.** The interval-heap DEPQ (two elements
|
|
33
|
+
per node) is a deliberately deferred alternative -- named, never silently omitted (see
|
|
34
|
+
`decisions/0009-minmaxheap.md` and "What this is not").
|
|
35
|
+
- **Level parity, computed zero-alloc.** Slot i is a MIN level iff
|
|
36
|
+
`((31 - Math.clz32(i + 1)) & 1) === 0`. The sifts are hole-punching (one write per level,
|
|
37
|
+
only local scalar temporaries): `push` compares to the parent to pick the own-level vs
|
|
38
|
+
other-level chain then bubbles by grandparents; `popMin` / `popMax` trickle down over the
|
|
39
|
+
up-to-six descendants (children + grandchildren), bound-checking every grandchild index
|
|
40
|
+
against the live size (the classic min-max off-by-one, verified at n = 1, 2, 3, 4).
|
|
41
|
+
|
|
42
|
+
### Verified
|
|
43
|
+
|
|
44
|
+
- **Witness (D-M5).** `MinMaxHeap.popMin` is the gated O(log n) witness op (the full-height
|
|
45
|
+
level-aware trickle-down). It inherits the FROZEN family R^2 floor 0.958 and calibrates its
|
|
46
|
+
OWN slope band by the shared ADR-0004 method: median-of-15 popMin fit-runs = 10.30 ns/level
|
|
47
|
+
(runs spanned 9.38..10.59, R^2 0.9897..0.9991), band = median x `[0.6, 1.4]` = `[6.18, 14.42]`.
|
|
48
|
+
A DEPQ whose push / popMin / popMax are ALL worst-case, so there is NO max-single-op line.
|
|
49
|
+
The O(n) foil is a linear min-scan-and-splice extract-min (R^2 ~ 0.77, OFF the line).
|
|
50
|
+
- **Zero-GC.** `node --expose-gc test/torture.mjs` -- 0 B/op on the popMin / popMax / mixed
|
|
51
|
+
push+popMin+popMax churn and the peek read lanes, gc major = 0, the deliberately-allocating
|
|
52
|
+
control lane still non-zero (teeth), arrayBuffers do not grow across fill/clear soak cycles.
|
|
53
|
+
`npm run test:perf` -- 0 scavenges + `grows === 0` on every MinMaxHeap scenario.
|
|
54
|
+
|
|
55
|
+
### Notes
|
|
56
|
+
|
|
57
|
+
- `LogN.js` gains `MMH_MAX_CAPACITY` (`0x7FFFFFFF`) + the `MinMaxHeap` class, appended after
|
|
58
|
+
Scapegoat; the prior six classes stay BYTE-IDENTICAL (only the `VERSION` const changes above
|
|
59
|
+
the append point). Version bumped to 0.7.0 across `package.json`, `LogN.js`, and `llms.txt`.
|
|
60
|
+
|
|
61
|
+
## [0.6.0] - 2026-09-20
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
|
|
65
|
+
- **Scapegoat** -- the sixth member and the family's DETERMINISTIC balanced BST, the
|
|
66
|
+
honest PAIR to Treap: a weight-balanced binary search tree that is ALSO an
|
|
67
|
+
order-statistic tree (an AUGMENTED ordered map key -> value). Where a treap randomizes
|
|
68
|
+
its shape to be balanced IN EXPECTATION, a scapegoat keeps a hard WORST-CASE height
|
|
69
|
+
bound -- so `get` is O(log n) WORST-case (never merely expected) -- and pays for it with
|
|
70
|
+
AMORTIZED O(log n) `set` / `delete`, where an occasional subtree rebuild absorbs the
|
|
71
|
+
imbalance. A subtree-size column `_size` (maintained in the same pass as every link
|
|
72
|
+
rewrite and every rebuild) adds O(log n) order statistics. Surface: `get` / `has` /
|
|
73
|
+
`set` (updates the value in place on an existing key) / `delete` (idempotent) /
|
|
74
|
+
`rank(x)` (count of keys STRICTLY less than x) / `select(k)` (the k-th smallest key,
|
|
75
|
+
0-based) / `successor` (strictly greater) / `predecessor` (strictly less) /
|
|
76
|
+
`rangeIter(lo, hi)` (a VERSION-STAMPED iterator over `[lo, hi]` inclusive, ascending;
|
|
77
|
+
`+-Infinity` bounds allowed, structural OR value mutation mid-iteration throws) /
|
|
78
|
+
`forEach` / `clear`, and `size` / `capacity` / `alpha` getters. Keys and values are
|
|
79
|
+
finite numbers (typeof-guarded before coercion -- Symbol / BigInt / NaN / +-Infinity
|
|
80
|
+
fail closed with a `[lite-logn]` throw).
|
|
81
|
+
- **NO priorities, NO RNG (fully deterministic).** Unlike Treap, Scapegoat draws no random
|
|
82
|
+
priority and uses no LCG / `Math.random` anywhere: the tree shape is a deterministic
|
|
83
|
+
function of the insert / delete order. `alpha` (the weight-balance factor) is validated
|
|
84
|
+
to the OPEN interval `(0.55, 0.75)` -- both ends throw -- and frozen at construction
|
|
85
|
+
(default `2/3`); the alpha-derived depth constant `_invAlpha = 1/alpha` is ctor-cached so
|
|
86
|
+
the hot insert path uses no per-op `Math.log` (the depth test is `_invAlpha^d > size`).
|
|
87
|
+
- **Zero-GC rebuild over preallocated scratch (the load-bearing design call).** No fresh
|
|
88
|
+
array per rebuild: ONE `_flat` (`Uint32Array(capacity)`) + ONE `_stack`
|
|
89
|
+
(`Uint32Array(capacity+1)`) are allocated at construction and reused every rebuild. An
|
|
90
|
+
ITERATIVE, Morris-free in-order flatten (via `_stack`) writes sorted slot indices into
|
|
91
|
+
`_flat`; a bounded log-depth balanced rebuild re-links `_left` / `_right` / `_size` on
|
|
92
|
+
the native call stack. Proven 0 B/op even under a rebuild-HEAVY ascending-insert trace by
|
|
93
|
+
the torture gate and a dedicated PerfGate scavenge-clean scenario
|
|
94
|
+
(decisions/0008-scapegoat.md).
|
|
95
|
+
- **Third bind of the shared NodePool.** Nodes are slot INDICES in five flat columns
|
|
96
|
+
(`_key` / `_value` Float64; `_left` / `_right` / `_size` Uint32, `NIL = 0`) over the SAME
|
|
97
|
+
private free-list (`NodePool`) SkipList and Treap ship -- design-parity, not a fork or a
|
|
98
|
+
runtime dep. The conservation invariant `activeSlots + freeListLength === capacity` holds
|
|
99
|
+
after every op, INCLUDING across rebuild storms. `SG_MAX_CAPACITY = 0x7FFFFFFF` (2^31 - 1:
|
|
100
|
+
slot indices + subtree counts fit a `Uint32`).
|
|
101
|
+
- **The documented asymmetry vs Treap: NO `split` / `merge`.** A scapegoat has no priority
|
|
102
|
+
heap to merge by, and an honest deterministic split/merge would be O(n) rebuilds
|
|
103
|
+
(forfeiting the sub-linear headline), so Scapegoat's surface is the ordered-map +
|
|
104
|
+
order-statistic core and split/merge are deliberately absent -- named on the public
|
|
105
|
+
surface (JSDoc + `llms.txt` + README + the `.d.ts`), not hidden.
|
|
106
|
+
- **Witness: `Scapegoat.get` gated + the amortized-trace assertion.** `get` (a
|
|
107
|
+
deterministic weight-balanced descent) is gated ON the O(log n) line in its own
|
|
108
|
+
calibrated band `SCAPEGOAT_GET_SLOPE_LO/HI = [2.41, 5.63]` (median-of-15 slope 4.02
|
|
109
|
+
ns/level * [0.6, 1.4], MEDIAN-centered per ADR-0004), inheriting the FROZEN shared R^2
|
|
110
|
+
floor 0.958; its O(n) linear-scan foil leaves the line. The rebuild spike lives on the
|
|
111
|
+
AMORTIZED `set` path and is NEVER gated as a per-op line; instead the amortized-trace
|
|
112
|
+
assertion proves the amortization -- the cumulative ascending-insert (rebuild-heavy)
|
|
113
|
+
cost/op tracks a LOG curve (last/first ratio ~1.5x, gated `< 4x`) where a rebuild-less
|
|
114
|
+
BST would blow to ~64x. The five prior members' bands are UNTOUCHED.
|
|
115
|
+
|
|
116
|
+
### Notes
|
|
117
|
+
|
|
118
|
+
- Append-only: `LogN.js` gains `SG_MAX_CAPACITY` + the `Scapegoat` class after Treap;
|
|
119
|
+
BinaryHeap / Fenwick / SegmentTree / SkipList / Treap are BYTE-IDENTICAL (only the
|
|
120
|
+
`VERSION` const changes). The repo-only benchmark admits Scapegoat as the 6th SUBJECT
|
|
121
|
+
(matrix 5x8=40 -> 6x8=48, a new `OLOGN_AMORTIZED` honesty class for `set` / `delete`).
|
|
122
|
+
|
|
9
123
|
## [0.5.0] - 2026-09-20
|
|
10
124
|
|
|
11
125
|
### Added
|
|
@@ -250,7 +364,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
250
364
|
[`decisions/0003-pack.md`](./decisions/0003-pack.md) (D-07: `files[]` ships the
|
|
251
365
|
six files only; `test/`, `benchmark/`, `decisions/`, `demo/` are repo-only).
|
|
252
366
|
|
|
253
|
-
[Unreleased]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.
|
|
367
|
+
[Unreleased]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.7.0...HEAD
|
|
368
|
+
[0.7.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.6.0...v0.7.0
|
|
254
369
|
[0.4.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.3.0...v0.4.0
|
|
255
370
|
[0.3.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.2.0...v0.3.0
|
|
256
371
|
[0.2.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.1.0...v0.2.0
|
package/LogN.d.ts
CHANGED
|
@@ -226,3 +226,106 @@ export class Treap {
|
|
|
226
226
|
* both. Non-Treap inputs, different arenas, or an overlapping range throw. */
|
|
227
227
|
static merge(a: Treap, b: Treap): Treap;
|
|
228
228
|
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A scapegoat tree: a DETERMINISTIC, weight-balanced BST that is also an order-statistic
|
|
232
|
+
* tree (an AUGMENTED ordered map key -> value) -- the honest pair to Treap. `get` is
|
|
233
|
+
* WORST-case O(log n) (a hard height bound, never merely expected); `set` / `delete` are
|
|
234
|
+
* AMORTIZED O(log n) (an occasional subtree rebuild absorbs the imbalance). A subtree-size
|
|
235
|
+
* column adds O(log n) rank / select. No priorities, no RNG: the shape is a deterministic
|
|
236
|
+
* function of the insert / delete order. Nodes are slot INDICES in flat typed-array columns
|
|
237
|
+
* over a private free-list, and every rebuild reuses ONE preallocated scratch buffer + index
|
|
238
|
+
* stack (no heap object, no fresh array per op). Keys and values are finite numbers (typeof-
|
|
239
|
+
* guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail closed). set on an existing
|
|
240
|
+
* key updates the value in place. Fixed capacity: a full pool throws. Every hot op allocates
|
|
241
|
+
* zero bytes. There is deliberately NO split / merge (the treap's arena-sharing surgery has no
|
|
242
|
+
* honest deterministic O(log n) analogue here) -- the documented asymmetry vs Treap.
|
|
243
|
+
*/
|
|
244
|
+
export class Scapegoat {
|
|
245
|
+
/** @param capacity exact max live entries; integer in [1, 2^31-1].
|
|
246
|
+
* @param alpha weight-balance factor in the OPEN interval (0.55, 0.75); default 2/3.
|
|
247
|
+
* Both ends throw. Frozen after construction. */
|
|
248
|
+
constructor(capacity: number, alpha?: number);
|
|
249
|
+
|
|
250
|
+
/** Live entry count. */
|
|
251
|
+
readonly size: number;
|
|
252
|
+
/** The fixed capacity this tree was sized for. */
|
|
253
|
+
readonly capacity: number;
|
|
254
|
+
/** The frozen weight-balance factor. */
|
|
255
|
+
readonly alpha: number;
|
|
256
|
+
|
|
257
|
+
/** The value under key, or undefined if absent (no throw). Non-finite key throws. */
|
|
258
|
+
get(key: number): number | undefined;
|
|
259
|
+
/** True iff key is currently stored. Non-finite key throws. */
|
|
260
|
+
has(key: number): boolean;
|
|
261
|
+
/** Insert key -> value, or update the value in place if key exists. Non-finite
|
|
262
|
+
* key/value throws; a full pool throws. */
|
|
263
|
+
set(key: number, value: number): this;
|
|
264
|
+
/** Remove key; true if it was present, false if absent (idempotent). Non-finite key throws. */
|
|
265
|
+
delete(key: number): boolean;
|
|
266
|
+
/** Count of stored keys strictly less than x (its rank), in [0, size]. Non-finite x throws. */
|
|
267
|
+
rank(x: number): number;
|
|
268
|
+
/** The k-th smallest key (0-based), or undefined if k is out of [0, size). Non-integer k throws. */
|
|
269
|
+
select(k: number): number | undefined;
|
|
270
|
+
/** The smallest key strictly greater than key, or undefined. Non-finite key throws. */
|
|
271
|
+
successor(key: number): number | undefined;
|
|
272
|
+
/** The largest key strictly less than key, or undefined. Non-finite key throws. */
|
|
273
|
+
predecessor(key: number): number | undefined;
|
|
274
|
+
/** A version-stamped iterator over keys in [lo, hi] inclusive, ascending. Bounds
|
|
275
|
+
* may be +-Infinity (unbounded ends); NaN or lo > hi throws; mutation during
|
|
276
|
+
* iteration throws. */
|
|
277
|
+
rangeIter(lo: number, hi: number): IterableIterator<number>;
|
|
278
|
+
/** Visit every (key, value) pair in ascending key order. */
|
|
279
|
+
forEach(fn: (key: number, value: number, tree: Scapegoat) => void): void;
|
|
280
|
+
/** Empty the tree, keeping capacity. */
|
|
281
|
+
clear(): this;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A min-max heap: a DOUBLE-ENDED priority queue (DEPQ) in ONE array-embedded binary heap
|
|
286
|
+
* whose levels ALTERNATE min / max (Atkinson et al. 1986). The minimum is the root; the
|
|
287
|
+
* maximum is the larger of the root's up-to-two children. peekMin / peekMax are O(1);
|
|
288
|
+
* push / popMin / popMax are O(log n) WORST-case. Two parallel typed-array columns (_key
|
|
289
|
+
* Float64, _id Uint32); the id is an OPAQUE Uint32 payload in [0, 2^32) (not unique, no
|
|
290
|
+
* reverse map), so there is deliberately NO changeKey / remove (the asymmetry vs
|
|
291
|
+
* BinaryHeap). Keys are finite numbers (typeof-guarded before coercion; Symbol / BigInt /
|
|
292
|
+
* NaN / +-Infinity fail closed). Fixed capacity: a full heap throws. Every hot op
|
|
293
|
+
* allocates zero bytes.
|
|
294
|
+
*/
|
|
295
|
+
export class MinMaxHeap {
|
|
296
|
+
/** @param capacity exact max live entries; integer in [1, 2^31-1]. */
|
|
297
|
+
constructor(capacity: number);
|
|
298
|
+
|
|
299
|
+
/** Live entry count. */
|
|
300
|
+
readonly size: number;
|
|
301
|
+
/** The fixed capacity this heap was sized for. */
|
|
302
|
+
readonly capacity: number;
|
|
303
|
+
|
|
304
|
+
/** Insert id with priority key. Throws on non-finite key, out-of-range id, or a full heap. */
|
|
305
|
+
push(id: number, key: number): void;
|
|
306
|
+
/** Remove and return the id at the minimum key, or undefined if empty. */
|
|
307
|
+
popMin(): number | undefined;
|
|
308
|
+
/** Remove and return the id at the maximum key, or undefined if empty. */
|
|
309
|
+
popMax(): number | undefined;
|
|
310
|
+
/** The id at the minimum key, or undefined if empty. */
|
|
311
|
+
peekMin(): number | undefined;
|
|
312
|
+
/** The id at the maximum key, or undefined if empty. */
|
|
313
|
+
peekMax(): number | undefined;
|
|
314
|
+
/** The minimum key, or undefined if empty. */
|
|
315
|
+
peekMinKey(): number | undefined;
|
|
316
|
+
/** The maximum key, or undefined if empty. */
|
|
317
|
+
peekMaxKey(): number | undefined;
|
|
318
|
+
/** Empty the heap, keeping capacity. */
|
|
319
|
+
clear(): void;
|
|
320
|
+
/** Visit every live (id, key) pair in unspecified (heap-array) order. */
|
|
321
|
+
forEach(fn: (id: number, key: number, heap: MinMaxHeap) => void): void;
|
|
322
|
+
/** Iterate live entity ids in unspecified (heap-array) order (NOT sorted). */
|
|
323
|
+
[Symbol.iterator](): IterableIterator<number>;
|
|
324
|
+
|
|
325
|
+
/** Floyd O(n) bulk build from parallel ids/keys arrays (level-aware sift-down). */
|
|
326
|
+
static build(
|
|
327
|
+
ids: ArrayLike<number>,
|
|
328
|
+
keys: ArrayLike<number>,
|
|
329
|
+
capacity: number,
|
|
330
|
+
): MinMaxHeap;
|
|
331
|
+
}
|