@zakkster/lite-logn 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/LogN.d.ts +54 -0
- package/LogN.js +514 -1
- package/README.md +54 -9
- package/llms.txt +52 -3
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,68 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.0] - 2026-09-20
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Scapegoat** -- the sixth member and the family's DETERMINISTIC balanced BST, the
|
|
14
|
+
honest PAIR to Treap: a weight-balanced binary search tree that is ALSO an
|
|
15
|
+
order-statistic tree (an AUGMENTED ordered map key -> value). Where a treap randomizes
|
|
16
|
+
its shape to be balanced IN EXPECTATION, a scapegoat keeps a hard WORST-CASE height
|
|
17
|
+
bound -- so `get` is O(log n) WORST-case (never merely expected) -- and pays for it with
|
|
18
|
+
AMORTIZED O(log n) `set` / `delete`, where an occasional subtree rebuild absorbs the
|
|
19
|
+
imbalance. A subtree-size column `_size` (maintained in the same pass as every link
|
|
20
|
+
rewrite and every rebuild) adds O(log n) order statistics. Surface: `get` / `has` /
|
|
21
|
+
`set` (updates the value in place on an existing key) / `delete` (idempotent) /
|
|
22
|
+
`rank(x)` (count of keys STRICTLY less than x) / `select(k)` (the k-th smallest key,
|
|
23
|
+
0-based) / `successor` (strictly greater) / `predecessor` (strictly less) /
|
|
24
|
+
`rangeIter(lo, hi)` (a VERSION-STAMPED iterator over `[lo, hi]` inclusive, ascending;
|
|
25
|
+
`+-Infinity` bounds allowed, structural OR value mutation mid-iteration throws) /
|
|
26
|
+
`forEach` / `clear`, and `size` / `capacity` / `alpha` getters. Keys and values are
|
|
27
|
+
finite numbers (typeof-guarded before coercion -- Symbol / BigInt / NaN / +-Infinity
|
|
28
|
+
fail closed with a `[lite-logn]` throw).
|
|
29
|
+
- **NO priorities, NO RNG (fully deterministic).** Unlike Treap, Scapegoat draws no random
|
|
30
|
+
priority and uses no LCG / `Math.random` anywhere: the tree shape is a deterministic
|
|
31
|
+
function of the insert / delete order. `alpha` (the weight-balance factor) is validated
|
|
32
|
+
to the OPEN interval `(0.55, 0.75)` -- both ends throw -- and frozen at construction
|
|
33
|
+
(default `2/3`); the alpha-derived depth constant `_invAlpha = 1/alpha` is ctor-cached so
|
|
34
|
+
the hot insert path uses no per-op `Math.log` (the depth test is `_invAlpha^d > size`).
|
|
35
|
+
- **Zero-GC rebuild over preallocated scratch (the load-bearing design call).** No fresh
|
|
36
|
+
array per rebuild: ONE `_flat` (`Uint32Array(capacity)`) + ONE `_stack`
|
|
37
|
+
(`Uint32Array(capacity+1)`) are allocated at construction and reused every rebuild. An
|
|
38
|
+
ITERATIVE, Morris-free in-order flatten (via `_stack`) writes sorted slot indices into
|
|
39
|
+
`_flat`; a bounded log-depth balanced rebuild re-links `_left` / `_right` / `_size` on
|
|
40
|
+
the native call stack. Proven 0 B/op even under a rebuild-HEAVY ascending-insert trace by
|
|
41
|
+
the torture gate and a dedicated PerfGate scavenge-clean scenario
|
|
42
|
+
(decisions/0008-scapegoat.md).
|
|
43
|
+
- **Third bind of the shared NodePool.** Nodes are slot INDICES in five flat columns
|
|
44
|
+
(`_key` / `_value` Float64; `_left` / `_right` / `_size` Uint32, `NIL = 0`) over the SAME
|
|
45
|
+
private free-list (`NodePool`) SkipList and Treap ship -- design-parity, not a fork or a
|
|
46
|
+
runtime dep. The conservation invariant `activeSlots + freeListLength === capacity` holds
|
|
47
|
+
after every op, INCLUDING across rebuild storms. `SG_MAX_CAPACITY = 0x7FFFFFFF` (2^31 - 1:
|
|
48
|
+
slot indices + subtree counts fit a `Uint32`).
|
|
49
|
+
- **The documented asymmetry vs Treap: NO `split` / `merge`.** A scapegoat has no priority
|
|
50
|
+
heap to merge by, and an honest deterministic split/merge would be O(n) rebuilds
|
|
51
|
+
(forfeiting the sub-linear headline), so Scapegoat's surface is the ordered-map +
|
|
52
|
+
order-statistic core and split/merge are deliberately absent -- named on the public
|
|
53
|
+
surface (JSDoc + `llms.txt` + README + the `.d.ts`), not hidden.
|
|
54
|
+
- **Witness: `Scapegoat.get` gated + the amortized-trace assertion.** `get` (a
|
|
55
|
+
deterministic weight-balanced descent) is gated ON the O(log n) line in its own
|
|
56
|
+
calibrated band `SCAPEGOAT_GET_SLOPE_LO/HI = [2.41, 5.63]` (median-of-15 slope 4.02
|
|
57
|
+
ns/level * [0.6, 1.4], MEDIAN-centered per ADR-0004), inheriting the FROZEN shared R^2
|
|
58
|
+
floor 0.958; its O(n) linear-scan foil leaves the line. The rebuild spike lives on the
|
|
59
|
+
AMORTIZED `set` path and is NEVER gated as a per-op line; instead the amortized-trace
|
|
60
|
+
assertion proves the amortization -- the cumulative ascending-insert (rebuild-heavy)
|
|
61
|
+
cost/op tracks a LOG curve (last/first ratio ~1.5x, gated `< 4x`) where a rebuild-less
|
|
62
|
+
BST would blow to ~64x. The five prior members' bands are UNTOUCHED.
|
|
63
|
+
|
|
64
|
+
### Notes
|
|
65
|
+
|
|
66
|
+
- Append-only: `LogN.js` gains `SG_MAX_CAPACITY` + the `Scapegoat` class after Treap;
|
|
67
|
+
BinaryHeap / Fenwick / SegmentTree / SkipList / Treap are BYTE-IDENTICAL (only the
|
|
68
|
+
`VERSION` const changes). The repo-only benchmark admits Scapegoat as the 6th SUBJECT
|
|
69
|
+
(matrix 5x8=40 -> 6x8=48, a new `OLOGN_AMORTIZED` honesty class for `set` / `delete`).
|
|
70
|
+
|
|
9
71
|
## [0.5.0] - 2026-09-20
|
|
10
72
|
|
|
11
73
|
### Added
|
package/LogN.d.ts
CHANGED
|
@@ -226,3 +226,57 @@ 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
|
+
}
|
package/LogN.js
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
/** Package version. One of the three version sites (package.json / VERSION / llms.txt). */
|
|
42
|
-
export const VERSION = '0.
|
|
42
|
+
export const VERSION = '0.6.0';
|
|
43
43
|
|
|
44
44
|
// --- members land here, append-only, one tree-shakeable class each -----------
|
|
45
45
|
// BinaryHeap (v0.1.0 session) -- indexed O(log n) min|max heap (BELOW)
|
|
@@ -1983,3 +1983,516 @@ export class Treap {
|
|
|
1983
1983
|
throw new RangeError('[lite-logn] Treap full (capacity ' + this._cap + ')');
|
|
1984
1984
|
}
|
|
1985
1985
|
}
|
|
1986
|
+
|
|
1987
|
+
// Scapegoat (v0.6.0 session) -- a DETERMINISTIC weight-balanced augmented ordered map (BELOW).
|
|
1988
|
+
|
|
1989
|
+
/**
|
|
1990
|
+
* Max Scapegoat capacity: `0x7FFFFFFF` (2^31 - 1). Every node is addressed by a slot
|
|
1991
|
+
* INDEX stored in `Uint32Array` link columns (`_left` / `_right`), each subtree count
|
|
1992
|
+
* lives in a `Uint32Array` (`_size`), and the rebuild scratch (`_flat`) plus the
|
|
1993
|
+
* flatten index-stack (`_stack`) are `Uint32Array` slot buffers; an index and a count
|
|
1994
|
+
* must both fit an unsigned 32-bit word. `NIL = 0` reserves slot 0 as the empty-subtree
|
|
1995
|
+
* sentinel, so live slots run [1, capacity]. The index / count arithmetic (Uint32 slot
|
|
1996
|
+
* indices + `NIL = 0` + Uint32 subtree sizes), not the byte count, is the hard ceiling
|
|
1997
|
+
* -- the same "the arithmetic caps it" reasoning as the array-embedded members.
|
|
1998
|
+
*/
|
|
1999
|
+
const SG_MAX_CAPACITY = 0x7FFFFFFF; // 2^31 - 1
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* A SCAPEGOAT TREE: a DETERMINISTIC, weight-balanced BINARY SEARCH TREE that is ALSO an
|
|
2003
|
+
* order-statistic tree (an AUGMENTED ordered map key -> value). It is the honest PAIR to
|
|
2004
|
+
* Treap: where a treap randomizes its shape to be balanced IN EXPECTATION, a scapegoat
|
|
2005
|
+
* keeps a hard WORST-CASE height bound (`get` is O(log n) worst-case, never merely
|
|
2006
|
+
* expected) by paying for it with AMORTIZED O(log n) `set` / `delete` -- an occasional
|
|
2007
|
+
* subtree rebuild absorbs the imbalance. No priorities, no RNG anywhere: the tree shape
|
|
2008
|
+
* is a deterministic function of the insert / delete order. The trick that keeps it
|
|
2009
|
+
* zero-GC is the SkipList / Treap one: nodes are slot INDICES in flat typed-array columns
|
|
2010
|
+
* over the same private free-list (NodePool), never heap objects; AND the rebuild reuses
|
|
2011
|
+
* ONE preallocated scratch buffer (`_flat`) + ONE preallocated index-stack (`_stack`),
|
|
2012
|
+
* so even a rebuild-heavy trace allocates ZERO bytes after construction.
|
|
2013
|
+
*
|
|
2014
|
+
* Two invariants held at once:
|
|
2015
|
+
* - BST order on `_key` (an in-order walk is ascending by key); and
|
|
2016
|
+
* - alpha-WEIGHT-BALANCE: after every mutation the height stays <= log_{1/alpha}(n) + 1
|
|
2017
|
+
* (h_alpha), enforced by the dual trigger below. The augmentation is a third
|
|
2018
|
+
* invariant: `_size[x]` is the number of nodes in x's subtree, maintained in the SAME
|
|
2019
|
+
* pass as every link rewrite, so `rank` (keys < x) and `select` (k-th smallest key)
|
|
2020
|
+
* are O(log n) via subtree counts.
|
|
2021
|
+
*
|
|
2022
|
+
* The DUAL trigger (alpha frozen at construction, `alpha` in the OPEN interval
|
|
2023
|
+
* (0.55, 0.75); default 2/3):
|
|
2024
|
+
* - `set`: descend recording the path, link the new leaf at depth d, then if the node
|
|
2025
|
+
* is "too deep" (d > h_alpha(size)) walk the recorded path back up to the SCAPEGOAT
|
|
2026
|
+
* -- the lowest ancestor whose child subtree exceeds `alpha` of its own size -- and
|
|
2027
|
+
* rebuild THAT subtree perfectly balanced. `_maxCount` tracks the high-water size.
|
|
2028
|
+
* - `delete`: remove the node (standard BST delete, `_size` fixed on the unwind), then
|
|
2029
|
+
* when `size < alpha * _maxCount` rebuild the WHOLE tree and reset `_maxCount = size`.
|
|
2030
|
+
* The depth test uses NO per-op `Math.log`: since `_invAlpha = 1/alpha` is ctor-cached,
|
|
2031
|
+
* `d > h_alpha(n)` (= `d > floor(_invLog * log2(n))`, `_invLog = 1/log2(1/alpha)`) is
|
|
2032
|
+
* tested EXACTLY as `_invAlpha^d > n` (for integer d the strict `>` matches the floor),
|
|
2033
|
+
* accumulated with one float multiply per path level -- only on the `set` path.
|
|
2034
|
+
*
|
|
2035
|
+
* ZERO-GC rebuild (the load-bearing design call, decisions/0008-scapegoat.md): NO fresh
|
|
2036
|
+
* array per rebuild. `_flatten` walks the target subtree in-order ITERATIVELY (Morris-
|
|
2037
|
+
* free) using the preallocated `_stack` index-column, writing sorted slot indices into
|
|
2038
|
+
* the preallocated `_flat` buffer; `_buildBalanced` reads that sorted range and re-links
|
|
2039
|
+
* `_left` / `_right` / `_size` via bounded native recursion whose depth is O(log
|
|
2040
|
+
* subtree) <= ~31 (it produces a perfectly balanced subtree), so it runs on the native
|
|
2041
|
+
* call stack, never the GC heap. Both are 0 B/op -- proven by the torture gate's rebuild-
|
|
2042
|
+
* heavy ascending-insert lane. RECURSION: `delete` and `forEach` also recurse to a depth
|
|
2043
|
+
* equal to the tree height, which is O(log n) worst-case here (the weight balance bounds
|
|
2044
|
+
* it) -- strictly safer than Treap's expected bound, disclosed here + in the ADR, on the
|
|
2045
|
+
* native stack, so still 0 B/op.
|
|
2046
|
+
*
|
|
2047
|
+
* Keys and values are FINITE numbers (typeof-guarded BEFORE coercion -- Symbol / BigInt /
|
|
2048
|
+
* NaN / +-Infinity fail closed with a `[lite-logn]` throw). `set` on an EXISTING key
|
|
2049
|
+
* updates its value in place (no new node, no rebuild). A missing / empty query returns
|
|
2050
|
+
* `undefined` (never throws). Fixed capacity: a full pool throws, never silently drops.
|
|
2051
|
+
* `rangeIter` is a VERSION-STAMPED iterator -- any structural OR value mutation mid-
|
|
2052
|
+
* iteration throws `[lite-logn]` rather than yield stale data.
|
|
2053
|
+
*
|
|
2054
|
+
* Unlike Treap there is NO `split` / `merge`: those are the treap's arena-sharing set
|
|
2055
|
+
* surgery (they rewire a randomized heap in place); a scapegoat has no priority heap to
|
|
2056
|
+
* merge by, and an honest deterministic split/merge would be O(n) rebuilds, forfeiting
|
|
2057
|
+
* the sub-linear headline -- so the surface is deliberately the ordered-map + order-
|
|
2058
|
+
* statistic core (get / has / set / delete / rank / select / successor / predecessor /
|
|
2059
|
+
* rangeIter / forEach / clear), documented in the ADR as the asymmetry vs Treap.
|
|
2060
|
+
*/
|
|
2061
|
+
export class Scapegoat {
|
|
2062
|
+
/**
|
|
2063
|
+
* @param {number} capacity exact max live entries; integer in [1, 2^31-1].
|
|
2064
|
+
* @param {number} [alpha] weight-balance factor in the OPEN interval (0.55, 0.75)
|
|
2065
|
+
* (both ends throw); default 2/3. Frozen after construction.
|
|
2066
|
+
*/
|
|
2067
|
+
constructor(capacity, alpha = 2 / 3) {
|
|
2068
|
+
// typeof guard BEFORE coercion (Number.isInteger is Symbol/BigInt-safe).
|
|
2069
|
+
if (typeof capacity !== 'number' || !Number.isInteger(capacity) ||
|
|
2070
|
+
capacity < 1 || capacity > SG_MAX_CAPACITY) {
|
|
2071
|
+
throw new RangeError(
|
|
2072
|
+
'[lite-logn] Scapegoat capacity must be an integer in [1, 2^31-1], got ' +
|
|
2073
|
+
String(capacity));
|
|
2074
|
+
}
|
|
2075
|
+
// typeof guard BEFORE the range check; the interval is OPEN (both 0.55 and 0.75 throw).
|
|
2076
|
+
if (typeof alpha !== 'number' || !Number.isFinite(alpha) || alpha <= 0.55 || alpha >= 0.75) {
|
|
2077
|
+
throw new RangeError(
|
|
2078
|
+
'[lite-logn] Scapegoat alpha must be a number in the open interval (0.55, 0.75), got ' +
|
|
2079
|
+
String(alpha));
|
|
2080
|
+
}
|
|
2081
|
+
this._cap = capacity; // max live entries
|
|
2082
|
+
this._key = new Float64Array(capacity + 1); // key at each slot
|
|
2083
|
+
this._value = new Float64Array(capacity + 1); // value at each slot
|
|
2084
|
+
this._left = new Uint32Array(capacity + 1); // left child slot; NIL = 0
|
|
2085
|
+
this._right = new Uint32Array(capacity + 1); // right child slot; NIL = 0
|
|
2086
|
+
this._size = new Uint32Array(capacity + 1); // subtree node count; _size[0] = 0
|
|
2087
|
+
this._pool = new NodePool(capacity); // free-list over slots [1, capacity]
|
|
2088
|
+
this._flat = new Uint32Array(capacity); // rebuild scratch: sorted slot indices
|
|
2089
|
+
this._stack = new Uint32Array(capacity + 1); // flatten / descent index-stack (reused)
|
|
2090
|
+
this._root = 0; // NIL == empty tree
|
|
2091
|
+
this._maxCount = 0; // high-water size since the last full rebuild
|
|
2092
|
+
this._version = 0; // iterator invalidation stamp
|
|
2093
|
+
this._alpha = alpha; // ctor-frozen weight-balance factor
|
|
2094
|
+
this._invAlpha = 1 / alpha; // ctor-cached: no per-op Math.log
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
/** Live entry count. O(1) (the root subtree count). */
|
|
2098
|
+
get size() { return this._root === 0 ? 0 : this._size[this._root]; }
|
|
2099
|
+
|
|
2100
|
+
/** The fixed capacity this tree was sized for. O(1). */
|
|
2101
|
+
get capacity() { return this._cap; }
|
|
2102
|
+
|
|
2103
|
+
/** The frozen weight-balance factor. O(1). */
|
|
2104
|
+
get alpha() { return this._alpha; }
|
|
2105
|
+
|
|
2106
|
+
/**
|
|
2107
|
+
* The value stored under `key`, or `undefined` if absent (never throws on a missing /
|
|
2108
|
+
* empty query). O(log n) WORST-case: a plain BST descent over a weight-balanced tree.
|
|
2109
|
+
* Fails closed on a non-number / non-finite key (typeof-guarded first).
|
|
2110
|
+
* @param {number} key a finite number
|
|
2111
|
+
* @returns {number|undefined}
|
|
2112
|
+
*/
|
|
2113
|
+
get(key) {
|
|
2114
|
+
if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
|
|
2115
|
+
const L = this._left, R = this._right, K = this._key;
|
|
2116
|
+
let t = this._root;
|
|
2117
|
+
while (t !== 0) {
|
|
2118
|
+
if (key < K[t]) t = L[t];
|
|
2119
|
+
else if (key > K[t]) t = R[t];
|
|
2120
|
+
else return this._value[t];
|
|
2121
|
+
}
|
|
2122
|
+
return undefined;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
/**
|
|
2126
|
+
* True iff `key` is currently in the tree. O(log n) worst-case. Fails closed on a
|
|
2127
|
+
* non-finite key (typeof-guarded first).
|
|
2128
|
+
* @param {number} key a finite number
|
|
2129
|
+
* @returns {boolean}
|
|
2130
|
+
*/
|
|
2131
|
+
has(key) {
|
|
2132
|
+
if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
|
|
2133
|
+
const L = this._left, R = this._right, K = this._key;
|
|
2134
|
+
let t = this._root;
|
|
2135
|
+
while (t !== 0) {
|
|
2136
|
+
if (key < K[t]) t = L[t];
|
|
2137
|
+
else if (key > K[t]) t = R[t];
|
|
2138
|
+
else return true;
|
|
2139
|
+
}
|
|
2140
|
+
return false;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
/**
|
|
2144
|
+
* Insert `key -> value`, or UPDATE the value in place if `key` already exists (no new
|
|
2145
|
+
* node, no rebuild). AMORTIZED O(log n): a descent recording the path, then (on
|
|
2146
|
+
* insert) an amortized-cheap weight-balance check that occasionally rebuilds the
|
|
2147
|
+
* scapegoat subtree. Fails closed: a non-finite key or value (typeof-guarded first),
|
|
2148
|
+
* or a full pool, each throw `[lite-logn]` as a no-op.
|
|
2149
|
+
* @param {number} key a finite number
|
|
2150
|
+
* @param {number} value a finite number
|
|
2151
|
+
* @returns {this}
|
|
2152
|
+
*/
|
|
2153
|
+
set(key, value) {
|
|
2154
|
+
if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
|
|
2155
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return this._badValue(value);
|
|
2156
|
+
const K = this._key, L = this._left, R = this._right, S = this._size, stk = this._stack;
|
|
2157
|
+
let t = this._root, sp = 0;
|
|
2158
|
+
while (t !== 0) { // descend, recording the path; update in place if present
|
|
2159
|
+
stk[sp++] = t;
|
|
2160
|
+
if (key < K[t]) t = L[t];
|
|
2161
|
+
else if (key > K[t]) t = R[t];
|
|
2162
|
+
else { this._value[t] = value; this._version = (this._version + 1) | 0; return this; }
|
|
2163
|
+
}
|
|
2164
|
+
const slot = this._pool.alloc();
|
|
2165
|
+
if (slot === 0) return this._full();
|
|
2166
|
+
K[slot] = key; this._value[slot] = value; L[slot] = 0; R[slot] = 0; S[slot] = 1;
|
|
2167
|
+
if (sp === 0) this._root = slot; // first node
|
|
2168
|
+
else { const p = stk[sp - 1]; if (key < K[p]) L[p] = slot; else R[p] = slot; }
|
|
2169
|
+
for (let i = 0; i < sp; i++) S[stk[i]]++; // every ancestor gained one node
|
|
2170
|
+
const newSize = S[this._root]; // == old size + 1
|
|
2171
|
+
if (newSize > this._maxCount) this._maxCount = newSize;
|
|
2172
|
+
this._version = (this._version + 1) | 0;
|
|
2173
|
+
// Depth test with NO Math.log: the new node sits at depth d == sp; it is too deep
|
|
2174
|
+
// iff d > h_alpha(newSize) == floor(_invLog * log2(newSize)), tested EXACTLY as
|
|
2175
|
+
// _invAlpha^d > newSize (integer d, so strict > matches the floor). One float
|
|
2176
|
+
// multiply per level -- only on this insert path, never on get.
|
|
2177
|
+
let bound = 1;
|
|
2178
|
+
for (let i = 0; i < sp; i++) bound *= this._invAlpha;
|
|
2179
|
+
if (bound > newSize) {
|
|
2180
|
+
// Walk the recorded path up to the SCAPEGOAT: the lowest ancestor whose
|
|
2181
|
+
// path-child subtree exceeds alpha of its own (post-insert) size.
|
|
2182
|
+
let g = -1;
|
|
2183
|
+
for (let i = sp - 1; i >= 0; i--) {
|
|
2184
|
+
const node = stk[i];
|
|
2185
|
+
const child = i === sp - 1 ? slot : stk[i + 1];
|
|
2186
|
+
if (S[child] > this._alpha * S[node]) { g = i; break; }
|
|
2187
|
+
}
|
|
2188
|
+
if (g === -1) {
|
|
2189
|
+
this._rebuildSubtree(this._root, 0, false); // defensive: rebuild whole tree
|
|
2190
|
+
} else {
|
|
2191
|
+
const node = stk[g];
|
|
2192
|
+
const parent = g > 0 ? stk[g - 1] : 0;
|
|
2193
|
+
const wasLeft = parent !== 0 && L[parent] === node;
|
|
2194
|
+
this._rebuildSubtree(node, parent, wasLeft);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
return this;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
/**
|
|
2201
|
+
* Remove `key`. AMORTIZED O(log n). Idempotent: returns `false` if `key` is absent (no
|
|
2202
|
+
* throw), `true` if it was present and removed. A standard BST delete fixes `_size` on
|
|
2203
|
+
* the unwind; when the tree has shrunk below `alpha * _maxCount` the WHOLE tree is
|
|
2204
|
+
* rebuilt perfectly balanced and `_maxCount` reset. Fails closed on a non-finite key.
|
|
2205
|
+
* @param {number} key a finite number
|
|
2206
|
+
* @returns {boolean}
|
|
2207
|
+
*/
|
|
2208
|
+
delete(key) {
|
|
2209
|
+
if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
|
|
2210
|
+
const L = this._left, R = this._right, K = this._key;
|
|
2211
|
+
let t = this._root, found = false;
|
|
2212
|
+
while (t !== 0) {
|
|
2213
|
+
if (key < K[t]) t = L[t];
|
|
2214
|
+
else if (key > K[t]) t = R[t];
|
|
2215
|
+
else { found = true; break; }
|
|
2216
|
+
}
|
|
2217
|
+
if (!found) return false; // absent (no throw)
|
|
2218
|
+
this._root = this._delete(this._root, key);
|
|
2219
|
+
this._version = (this._version + 1) | 0;
|
|
2220
|
+
const newSize = this._root === 0 ? 0 : this._size[this._root];
|
|
2221
|
+
if (newSize < this._alpha * this._maxCount) {
|
|
2222
|
+
this._rebuildSubtree(this._root, 0, false); // global rebuild
|
|
2223
|
+
this._maxCount = newSize;
|
|
2224
|
+
}
|
|
2225
|
+
return true;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
/**
|
|
2229
|
+
* The number of stored keys STRICTLY LESS than `x` (its rank / position). O(log n) via
|
|
2230
|
+
* subtree counts. `x` need not be present; `rank` of the smallest key is 0, of a key
|
|
2231
|
+
* past the max is `size`. Fails closed on a non-finite `x`.
|
|
2232
|
+
* @param {number} x a finite number
|
|
2233
|
+
* @returns {number} count of keys < x, in [0, size]
|
|
2234
|
+
*/
|
|
2235
|
+
rank(x) {
|
|
2236
|
+
if (typeof x !== 'number' || !Number.isFinite(x)) return this._badKey(x);
|
|
2237
|
+
const L = this._left, R = this._right, K = this._key, S = this._size;
|
|
2238
|
+
let t = this._root, r = 0;
|
|
2239
|
+
while (t !== 0) {
|
|
2240
|
+
if (x <= K[t]) t = L[t]; // t (and its right) are >= x
|
|
2241
|
+
else { r += S[L[t]] + 1; t = R[t]; } // t's left subtree + t precede x
|
|
2242
|
+
}
|
|
2243
|
+
return r;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
/**
|
|
2247
|
+
* The k-th smallest KEY (0-based order statistic), or `undefined` if `k` is out of
|
|
2248
|
+
* range [0, size). O(log n) via subtree counts. Fails closed on a non-integer `k`
|
|
2249
|
+
* (typeof-guarded first); an in-type out-of-range `k` returns `undefined`.
|
|
2250
|
+
* @param {number} k integer in [0, size)
|
|
2251
|
+
* @returns {number|undefined} the k-th smallest key
|
|
2252
|
+
*/
|
|
2253
|
+
select(k) {
|
|
2254
|
+
if (typeof k !== 'number' || !Number.isInteger(k)) return this._badRank(k);
|
|
2255
|
+
const sz = this._root === 0 ? 0 : this._size[this._root];
|
|
2256
|
+
if (k < 0 || k >= sz) return undefined;
|
|
2257
|
+
const L = this._left, R = this._right, K = this._key, S = this._size;
|
|
2258
|
+
let t = this._root;
|
|
2259
|
+
for (;;) {
|
|
2260
|
+
const ls = S[L[t]];
|
|
2261
|
+
if (k < ls) t = L[t];
|
|
2262
|
+
else if (k > ls) { k -= ls + 1; t = R[t]; }
|
|
2263
|
+
else return K[t];
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
/**
|
|
2268
|
+
* The smallest key STRICTLY greater than `key`, or `undefined` if none. O(log n).
|
|
2269
|
+
* `key` itself need not be present. Fails closed on a non-finite key.
|
|
2270
|
+
* @param {number} key a finite number
|
|
2271
|
+
* @returns {number|undefined}
|
|
2272
|
+
*/
|
|
2273
|
+
successor(key) {
|
|
2274
|
+
if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
|
|
2275
|
+
const L = this._left, R = this._right, K = this._key;
|
|
2276
|
+
let t = this._root, best;
|
|
2277
|
+
while (t !== 0) {
|
|
2278
|
+
if (K[t] > key) { best = K[t]; t = L[t]; }
|
|
2279
|
+
else t = R[t];
|
|
2280
|
+
}
|
|
2281
|
+
return best;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
/**
|
|
2285
|
+
* The largest key STRICTLY less than `key`, or `undefined` if none. O(log n). `key`
|
|
2286
|
+
* itself need not be present. Fails closed on a non-finite key.
|
|
2287
|
+
* @param {number} key a finite number
|
|
2288
|
+
* @returns {number|undefined}
|
|
2289
|
+
*/
|
|
2290
|
+
predecessor(key) {
|
|
2291
|
+
if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
|
|
2292
|
+
const L = this._left, R = this._right, K = this._key;
|
|
2293
|
+
let t = this._root, best;
|
|
2294
|
+
while (t !== 0) {
|
|
2295
|
+
if (K[t] < key) { best = K[t]; t = R[t]; }
|
|
2296
|
+
else t = L[t];
|
|
2297
|
+
}
|
|
2298
|
+
return best;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
/**
|
|
2302
|
+
* A VERSION-STAMPED iterator over the keys in `[lo, hi]` INCLUSIVE, ascending. Bounds
|
|
2303
|
+
* may be any number INCLUDING +-Infinity (an unbounded end); `NaN` fails closed, as
|
|
2304
|
+
* does `lo > hi`. The generator captures the tree's version and throws `[lite-logn]`
|
|
2305
|
+
* if any STRUCTURAL or VALUE mutation happens mid-iteration, rather than yield stale
|
|
2306
|
+
* data. It walks by repeated `successor` (each step a fresh O(log n) descent, so NO
|
|
2307
|
+
* scratch stack is allocated); the one documented per-protocol allocator is the
|
|
2308
|
+
* {value, done} per step.
|
|
2309
|
+
* @param {number} lo lower bound (inclusive); may be -Infinity
|
|
2310
|
+
* @param {number} hi upper bound (inclusive); may be +Infinity
|
|
2311
|
+
* @returns {IterableIterator<number>} the keys in [lo, hi], ascending
|
|
2312
|
+
*/
|
|
2313
|
+
rangeIter(lo, hi) {
|
|
2314
|
+
if (typeof lo !== 'number' || Number.isNaN(lo)) return this._badBound(lo);
|
|
2315
|
+
if (typeof hi !== 'number' || Number.isNaN(hi)) return this._badBound(hi);
|
|
2316
|
+
if (lo > hi) return this._badRange(lo, hi);
|
|
2317
|
+
return this._rangeGen(lo, hi);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/** @private version-stamped range generator (see rangeIter). */
|
|
2321
|
+
*_rangeGen(lo, hi) {
|
|
2322
|
+
const ver = this._version;
|
|
2323
|
+
let cur = this._ceil(lo); // smallest key >= lo, or undefined
|
|
2324
|
+
while (cur !== undefined && cur <= hi) {
|
|
2325
|
+
if (this._version !== ver) {
|
|
2326
|
+
throw new Error('[lite-logn] Scapegoat mutated during iteration');
|
|
2327
|
+
}
|
|
2328
|
+
yield cur;
|
|
2329
|
+
cur = this.successor(cur);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
/**
|
|
2334
|
+
* Visit every live `(key, value)` pair in ASCENDING key order. O(n) cold in-order walk
|
|
2335
|
+
* (recursion depth = tree height, O(log n)), allocation-free in the loop body (pass a
|
|
2336
|
+
* hoisted callback). Unlike rangeIter this is NOT version-stamped -- mutating from
|
|
2337
|
+
* within the callback is the caller's responsibility (matching the other members).
|
|
2338
|
+
* @param {(key:number, value:number, tree:Scapegoat)=>void} fn
|
|
2339
|
+
*/
|
|
2340
|
+
forEach(fn) {
|
|
2341
|
+
this._forEach(this._root, fn);
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
/** @private recursive in-order walk. */
|
|
2345
|
+
_forEach(t, fn) {
|
|
2346
|
+
if (t === 0) return;
|
|
2347
|
+
this._forEach(this._left[t], fn);
|
|
2348
|
+
fn(this._key[t], this._value[t], this);
|
|
2349
|
+
this._forEach(this._right[t], fn);
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
/**
|
|
2353
|
+
* Empty the tree, keeping the fixed capacity. O(capacity) cold path: returns every
|
|
2354
|
+
* node to the pool and points the root at NIL. @returns {this}
|
|
2355
|
+
*/
|
|
2356
|
+
clear() {
|
|
2357
|
+
this._pool.clear();
|
|
2358
|
+
this._root = 0;
|
|
2359
|
+
this._maxCount = 0;
|
|
2360
|
+
this._version = (this._version + 1) | 0;
|
|
2361
|
+
return this;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
// ---- private structure (rebuild + recursive delete; hot bodies elsewhere) ----
|
|
2365
|
+
|
|
2366
|
+
/**
|
|
2367
|
+
* @private In-order flatten of subtree `root` into `_flat` using the preallocated
|
|
2368
|
+
* `_stack` index-column (Morris-free, ITERATIVE -- so a degenerate pre-rebuild chain
|
|
2369
|
+
* cannot overflow the native stack). Returns the node count written. 0 B/op.
|
|
2370
|
+
*/
|
|
2371
|
+
_flatten(root) {
|
|
2372
|
+
const L = this._left, R = this._right, stk = this._stack, flat = this._flat;
|
|
2373
|
+
let node = root, sp = 0, c = 0;
|
|
2374
|
+
while (node !== 0 || sp > 0) {
|
|
2375
|
+
while (node !== 0) { stk[sp++] = node; node = L[node]; }
|
|
2376
|
+
node = stk[--sp];
|
|
2377
|
+
flat[c++] = node;
|
|
2378
|
+
node = R[node];
|
|
2379
|
+
}
|
|
2380
|
+
return c;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
/**
|
|
2384
|
+
* @private Build a perfectly balanced BST from the sorted slot range `_flat[lo..hi]`,
|
|
2385
|
+
* re-linking `_left` / `_right` / `_size`. Returns the subtree root (NIL if empty).
|
|
2386
|
+
* Bounded native recursion: depth is O(log(hi-lo+1)) <= ~31 (it produces a balanced
|
|
2387
|
+
* subtree), so it runs on the native call stack, never the GC heap. 0 B/op.
|
|
2388
|
+
*/
|
|
2389
|
+
_buildBalanced(lo, hi) {
|
|
2390
|
+
if (lo > hi) return 0;
|
|
2391
|
+
const mid = (lo + hi) >> 1;
|
|
2392
|
+
const s = this._flat[mid];
|
|
2393
|
+
const l = this._buildBalanced(lo, mid - 1);
|
|
2394
|
+
const r = this._buildBalanced(mid + 1, hi);
|
|
2395
|
+
this._left[s] = l;
|
|
2396
|
+
this._right[s] = r;
|
|
2397
|
+
this._size[s] = this._size[l] + this._size[r] + 1; // _size[0] is a permanent 0
|
|
2398
|
+
return s;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
/**
|
|
2402
|
+
* @private Rebuild subtree `root` perfectly balanced and re-link it under `parent`
|
|
2403
|
+
* (or the tree root when `parent === 0`). `wasLeft` records which child link to
|
|
2404
|
+
* rewrite. Reuses the preallocated `_flat` + `_stack` scratch: 0 B/op.
|
|
2405
|
+
*/
|
|
2406
|
+
_rebuildSubtree(root, parent, wasLeft) {
|
|
2407
|
+
const count = this._flatten(root);
|
|
2408
|
+
const nr = this._buildBalanced(0, count - 1);
|
|
2409
|
+
if (parent === 0) this._root = nr;
|
|
2410
|
+
else if (wasLeft) this._left[parent] = nr;
|
|
2411
|
+
else this._right[parent] = nr;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
/** @private recursive BST delete of `key` from subtree `t`; frees the removed slot,
|
|
2415
|
+
* fixing `_size` on the unwind. Depth = tree height = O(log n). */
|
|
2416
|
+
_delete(t, key) {
|
|
2417
|
+
const L = this._left, R = this._right, S = this._size, K = this._key;
|
|
2418
|
+
if (key < K[t]) {
|
|
2419
|
+
L[t] = this._delete(L[t], key);
|
|
2420
|
+
S[t] = S[L[t]] + S[R[t]] + 1;
|
|
2421
|
+
return t;
|
|
2422
|
+
}
|
|
2423
|
+
if (key > K[t]) {
|
|
2424
|
+
R[t] = this._delete(R[t], key);
|
|
2425
|
+
S[t] = S[L[t]] + S[R[t]] + 1;
|
|
2426
|
+
return t;
|
|
2427
|
+
}
|
|
2428
|
+
// found t
|
|
2429
|
+
const l = L[t], r = R[t];
|
|
2430
|
+
if (l === 0) { this._pool.free(t); return r; }
|
|
2431
|
+
if (r === 0) { this._pool.free(t); return l; }
|
|
2432
|
+
// two children: copy the in-order successor (min of the right subtree) into t,
|
|
2433
|
+
// then delete that successor from the right subtree (frees ITS slot).
|
|
2434
|
+
let m = r; while (L[m] !== 0) m = L[m];
|
|
2435
|
+
K[t] = K[m]; this._value[t] = this._value[m];
|
|
2436
|
+
R[t] = this._deleteMin(R[t]);
|
|
2437
|
+
S[t] = S[L[t]] + S[R[t]] + 1;
|
|
2438
|
+
return t;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
/** @private remove the minimum of subtree `t`, freeing its slot; return the new root. */
|
|
2442
|
+
_deleteMin(t) {
|
|
2443
|
+
const L = this._left, R = this._right, S = this._size;
|
|
2444
|
+
if (L[t] === 0) { const r = R[t]; this._pool.free(t); return r; }
|
|
2445
|
+
L[t] = this._deleteMin(L[t]);
|
|
2446
|
+
S[t] = S[L[t]] + S[R[t]] + 1;
|
|
2447
|
+
return t;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
/** @private smallest key >= `lo`, or undefined (the range-iter start). */
|
|
2451
|
+
_ceil(lo) {
|
|
2452
|
+
const L = this._left, R = this._right, K = this._key;
|
|
2453
|
+
let t = this._root, best;
|
|
2454
|
+
while (t !== 0) {
|
|
2455
|
+
if (K[t] >= lo) { best = K[t]; t = L[t]; }
|
|
2456
|
+
else t = R[t];
|
|
2457
|
+
}
|
|
2458
|
+
return best;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// ---- cold path only: throw builders (string concat off the hot body) ----
|
|
2462
|
+
|
|
2463
|
+
/** @private */
|
|
2464
|
+
_badKey(key) {
|
|
2465
|
+
throw new TypeError(
|
|
2466
|
+
'[lite-logn] Scapegoat key must be a finite number, got ' + String(key));
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
/** @private */
|
|
2470
|
+
_badValue(value) {
|
|
2471
|
+
throw new TypeError(
|
|
2472
|
+
'[lite-logn] Scapegoat value must be a finite number, got ' + String(value));
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
/** @private */
|
|
2476
|
+
_badRank(k) {
|
|
2477
|
+
throw new TypeError(
|
|
2478
|
+
'[lite-logn] Scapegoat select index must be an integer, got ' + String(k));
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
/** @private */
|
|
2482
|
+
_badBound(b) {
|
|
2483
|
+
throw new TypeError(
|
|
2484
|
+
'[lite-logn] Scapegoat rangeIter bound must be a number (not NaN), got ' + String(b));
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
/** @private */
|
|
2488
|
+
_badRange(lo, hi) {
|
|
2489
|
+
throw new RangeError(
|
|
2490
|
+
'[lite-logn] Scapegoat rangeIter needs lo <= hi, got lo=' + String(lo) +
|
|
2491
|
+
' hi=' + String(hi));
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
/** @private */
|
|
2495
|
+
_full() {
|
|
2496
|
+
throw new RangeError('[lite-logn] Scapegoat full (capacity ' + this._cap + ')');
|
|
2497
|
+
}
|
|
2498
|
+
}
|
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.6.0 ships six members: BinaryHeap (array-embedded O(log n) push / pop min|max heap), Fenwick / BIT (O(log n) point-update AND prefix-sum via the `i & -i` walk), SegmentTree (O(log n) associative range-query -- min / max / sum / gcd -- plus point-update over a flat 2n array), SkipList (pointer-free expected-O(log n) ordered map over a private free-list node pool), Treap (a randomized-balanced augmented ordered map with O(log n) rank / select / split / merge), and Scapegoat (a DETERMINISTIC weight-balanced augmented ordered map: worst-case-O(log n) get, amortized-O(log n) set / delete, zero-GC rebuild) -- each zero-GC, each shipped with a log-linear Witness that fits `nsPerOp = intercept + slope*log2(n)` and shows the straight log line while an O(n) foil leaves it.
|
|
4
4
|
|
|
5
5
|
[](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.6.0 ships six members: BinaryHeap, Fenwick, SegmentTree, SkipList, Treap and Scapegoat.** Members land one per session, each append-only so prior members stay byte-identical. The planned roster below fills in per release.
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
npm install @zakkster/lite-logn
|
|
@@ -61,6 +61,7 @@ Every hot op allocates zero bytes after construction, and `npm run witness` prov
|
|
|
61
61
|
- [SegmentTree](#segmenttree)
|
|
62
62
|
- [SkipList](#skiplist)
|
|
63
63
|
- [Treap](#treap)
|
|
64
|
+
- [Scapegoat](#scapegoat)
|
|
64
65
|
- [Zero-GC design notes](#zero-gc-design-notes)
|
|
65
66
|
- [Testing](#testing)
|
|
66
67
|
- [What this is not](#what-this-is-not)
|
|
@@ -85,7 +86,7 @@ lite-logn ships the O(log n) structures that matter with the allocation removed
|
|
|
85
86
|
|
|
86
87
|
## The roster
|
|
87
88
|
|
|
88
|
-
One member per session, each landing append-only (prior members stay byte-identical). At v0.
|
|
89
|
+
One member per session, each landing append-only (prior members stay byte-identical). At v0.6.0, BinaryHeap, Fenwick, SegmentTree, SkipList, Treap and Scapegoat are shipped.
|
|
89
90
|
|
|
90
91
|
| Member | Version | Status | Shape | Hot ops |
|
|
91
92
|
| --- | --- | --- | --- | --- |
|
|
@@ -94,8 +95,9 @@ One member per session, each landing append-only (prior members stay byte-identi
|
|
|
94
95
|
| **SegmentTree** | 0.3.0 | shipped | single flat `Float64Array(2n)` (leaves n..2n-1); associative fold (min/max/sum/gcd) chosen at construction | `query` / `update` O(log n), `at` O(1) |
|
|
95
96
|
| **SkipList** | 0.4.0 | shipped | pointer-free over a private free-list node pool; expected O(log n) | `get` / `set` / `delete` / `successor` / `predecessor` |
|
|
96
97
|
| **Treap** | 0.5.0 | shipped | randomized-balanced augmented BST over the same node pool; expected O(log n) | `get` / `has` / `set` / `delete` / `rank` / `select` / `successor` / `predecessor` / `forEach` / `rangeIter` / `split` + `merge` |
|
|
98
|
+
| **Scapegoat** | 0.6.0 | shipped | DETERMINISTIC weight-balanced augmented BST over the same node pool; worst-case O(log n) get, amortized O(log n) set/delete (zero-GC rebuild) | `get` / `has` / `set` / `delete` / `rank` / `select` / `successor` / `predecessor` / `forEach` / `rangeIter` (NO split/merge) |
|
|
97
99
|
|
|
98
|
-
Later tiers (
|
|
100
|
+
Later tiers (OrderStatTree, IndexedHeap, SortedArray, MinMaxHeap, SplayTree, and presets) are queued in [`ROADMAP.md`](./ROADMAP.md).
|
|
99
101
|
|
|
100
102
|
## The O(log n) Witness
|
|
101
103
|
|
|
@@ -105,7 +107,7 @@ The family anchor. Time a fixed batch of the hot op at each `n` in a geometric s
|
|
|
105
107
|
- `slope` inside the member's band (the per-level cost, ns/level), AND
|
|
106
108
|
- the FOIL leaves the line (low `R^2` -- the O(n) default a working programmer reaches for, shown losing as `n` grows).
|
|
107
109
|
|
|
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.
|
|
110
|
+
For amortized / randomized members the witness also prints the MAX single-op time -- the honesty hook: a rebuild spike or a degenerate tail shows as a tall bar even when the mean still fits the line. The `R^2` floor (0.958) is frozen family-wide in BinaryHeap; each member then calibrates its OWN per-op slope band (median-of-15 fit-runs x `[0.6, 1.4]`), because a cheaper op honestly has a lower per-level slope (see [`decisions/0004-witness-band.md`](./decisions/0004-witness-band.md)). At v0.6.0 the witness gates nine ops: BinaryHeap `pop` (R^2 ~ 0.99, slope ~ 8-10 ns/level), Fenwick `update` (R^2 ~ 0.98-0.99, slope ~ 2.9-3.0 ns/level) and `prefix` (R^2 ~ 0.97, slope ~ 2.6-2.7 ns/level), SegmentTree `update` (R^2 ~ 0.99, slope ~ 3.2 ns/level, band `[2.29, 5.35]`) and `query` (R^2 ~ 0.99, slope ~ 7 ns/level, band `[4.30, 10.04]`), SkipList `get` (R^2 ~ 0.97-0.99, slope ~ 9 ns/level, band `[5.27, 12.30]`) and `set` (R^2 ~ 0.97-0.99, slope ~ 14 ns/level, band `[8.36, 19.50]`), Treap `get` (R^2 ~ 0.99, slope ~ 4 ns/level, band `[2.55, 5.95]`), and Scapegoat `get` (R^2 ~ 0.99, slope ~ 4 ns/level, band `[2.41, 5.63]`) all ON the line. Scapegoat is DETERMINISTIC, so its `get` is WORST-case (not expected) O(log n); its rebuild spike lives on the AMORTIZED `set` path and is proven not by a per-op line but by an amortized-trace assertion -- the cumulative ascending-insert (rebuild-heavy) cost/op tracks a LOG curve (last/first ratio ~1.5x over `[2^11, 2^17]`, gated `< 4x`) where a rebuild-less BST would degenerate to an O(n)-amortized chain and blow the ratio to ~64x. Treap's descent touches one node per level, so its per-level slope is lower than SkipList's tower search -- expected, which is why only the R^2 floor is shared and each op calibrates its own band. SkipList's two ops are gated over DIFFERENT sweeps -- each measured where its logarithm is visible, not where the cache wall is: `get` (a clean search with no per-op randomness) over `[2^11, 2^17]` for dynamic range; `set` (a heavier insert+delete churn whose per-insert tower height is random) over the smaller, fully cache-resident `[2^9, 2^14]` so the fit sees the structural level count, not DRAM latency. Because SkipList is EXPECTED (not worst-case) O(log n), the witness also prints the MAX single insert over a realistic randomized build trace -- the unlucky-tower tail a mean hides. Each op's O(n) foil fits well below the floor: the sorted-array insert (BinaryHeap / SkipList) foil runs R^2 ~ 0.77-0.87, the Fenwick foils (prefix-array rebuild, naive re-sum) and SkipList's linear-scan search foil hold at R^2 ~ 0.75-0.82, and SegmentTree's foils (whole-tree rebuild per update, scan-fold per query) fit at R^2 ~ 0.72-0.85 -- all foil families sit comfortably under the 0.958 floor.
|
|
109
111
|
|
|
110
112
|
## Benchmarks
|
|
111
113
|
|
|
@@ -145,6 +147,9 @@ Each op fits `nsPerOp = intercept + slope*log2(n)`. ON-LINE = `R^2 >= 0.958` (th
|
|
|
145
147
|
| `SkipList.get` | 0.988 | 8.0 | `[5.27, 12.30]` | ON | linear scan | 0.79 | off |
|
|
146
148
|
| `SkipList.set` | 0.985 | 12.1 | `[8.36, 19.50]` | ON | sorted-array insert | 0.77 | off |
|
|
147
149
|
| `Treap.get` | 0.988 | 4.0 | `[2.55, 5.95]` | ON | linear scan | 0.79 | off |
|
|
150
|
+
| `Scapegoat.get` | 0.988 | 3.8 | `[2.41, 5.63]` | ON | linear scan | 0.79 | off |
|
|
151
|
+
|
|
152
|
+
**Scapegoat amortized-trace (the rebuild honesty).** Scapegoat's `get` is WORST-case O(log n) (a deterministic weight-balance height bound), so it carries no expected-op MAX-single-op disclosure. The rebuild spike lives on the AMORTIZED `set` path; D1 proves the amortization not with a per-op line but with an amortized-trace assertion -- the cumulative ascending-insert (rebuild-heavy) cost/op tracks a LOG curve (last/first ratio `~1.5x` over `[2^11, 2^17]`, gated `< 4x`) where a rebuild-less BST would blow to `~64x`.
|
|
148
153
|
|
|
149
154
|
**SkipList counter-foil (the order tax).** A native `Map` is O(1) at get/set (`~27 ns/op`, FLATTER than any log line) but ORDER-BLIND: it cannot answer `successor` / `predecessor` / `rangeIter`. The log factor SkipList pays buys exactly the ordered queries Map cannot. SkipList is EXPECTED O(log n), so D1 also DISCLOSES its MAX single insert (an unlucky tall tower over a randomized build: `~18-130 us`, not gated).
|
|
150
155
|
|
|
@@ -178,8 +183,8 @@ All seven gated op-rows report **0 B/op** across the `n = 1e3..1e6` sweep, with
|
|
|
178
183
|
|
|
179
184
|
- **D2 amortized cost** -- cumulative ns/op stays bounded over a `~1M`-op mixed trace (drift `< 1.0` here: the trace speeds up as the JIT warms, never degrades).
|
|
180
185
|
- **D4 cache (PROXY, labelled)** -- dense `forEach` iteration vs random single-element lookup; the random/dense gap is `~1.9x` (SegmentTree) to `~2.9x` (SkipList). No native perf counters.
|
|
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).
|
|
186
|
+
- **D7 scalability** -- numeric substrates: string + object keys read `n/a`. Load factors `0.3/0.5/0.7/0.9`; insertion order (sorted / random / adversarial-reverse) applies to the comparison-ordered BinaryHeap + SkipList + Treap + Scapegoat, `n/a` for the index-addressed Fenwick + SegmentTree.
|
|
187
|
+
- **D8 workloads** -- churn (all members) + an ordered scan (`successor` + `rangeIter`, SkipList + Treap + Scapegoat; `n/a` elsewhere).
|
|
183
188
|
|
|
184
189
|
## API reference
|
|
185
190
|
|
|
@@ -187,7 +192,7 @@ All seven gated op-rows report **0 B/op** across the `n = 1e3..1e6` sweep, with
|
|
|
187
192
|
|
|
188
193
|
| Export | Type | Value | Meaning |
|
|
189
194
|
| --- | --- | --- | --- |
|
|
190
|
-
| `VERSION` | `string` | `'0.
|
|
195
|
+
| `VERSION` | `string` | `'0.6.0'` | The package version. One of the three version sites (package.json / `LogN.js` `VERSION` const / `llms.txt`), kept in lockstep and enforced in review. |
|
|
191
196
|
|
|
192
197
|
### BinaryHeap
|
|
193
198
|
|
|
@@ -372,6 +377,42 @@ const whole = Treap.merge(lo, hi);// fuse back (all lo keys < all hi keys); both
|
|
|
372
377
|
| `merge` (static) | `Treap.merge(a, b) -> Treap` | expected O(log n) | Fuse two arena-sharing treaps where every key of `a` < every key of `b`; CONSUMES both. Non-Treap inputs, cross-arena treaps, or an overlapping range throw. |
|
|
373
378
|
| `size` / `capacity` | getters | O(1) | Live entry count / fixed capacity. |
|
|
374
379
|
|
|
380
|
+
### Scapegoat
|
|
381
|
+
|
|
382
|
+
A **scapegoat tree**: a **DETERMINISTIC**, weight-balanced **binary search tree** that is also an **order-statistic tree** -- an AUGMENTED ordered map (key -> value) -- the honest **pair to Treap**. Where a treap randomizes its shape to be balanced *in expectation*, a scapegoat keeps a hard **worst-case height bound** (`height <= log_{1/alpha}(n) + 1`), so `get` is **worst-case O(log n)** (never merely expected). It pays for that with **amortized O(log n)** `set` / `delete`: after a mutation makes the tree too deep (or, on delete, too sparse), an occasional **subtree rebuild** restores balance in bulk. A subtree-size column (maintained in the same pass as every link rewrite and every rebuild) adds `rank(x)` / `select(k)`, O(log n). **No priorities, no RNG anywhere** -- the shape is a deterministic function of the insert / delete order. Nodes are slot **indices** in five flat columns (`_key` / `_value` `Float64`; `_left` / `_right` / `_size` `Uint32`, `NIL = 0`) over the SAME private free-list (`NodePool`) SkipList and Treap use. Keys and values are finite numbers (typeof-guarded before coercion; Symbol / BigInt / NaN / +-Infinity fail closed); `set` on an existing key updates the value in place. Every hot op allocates zero bytes after construction.
|
|
383
|
+
|
|
384
|
+
Zero-GC rebuild (the load-bearing design call): there is **no fresh array per rebuild**. One `_flat` (`Uint32Array(capacity)`) + one `_stack` (`Uint32Array(capacity+1)`) are allocated at construction and reused every rebuild -- an ITERATIVE, Morris-free in-order flatten (via `_stack`) writes sorted slot indices into `_flat`, and a bounded log-depth balanced rebuild re-links `_left` / `_right` / `_size` on the native call stack. Proven 0 B/op even under a rebuild-HEAVY ascending-insert trace (see [`decisions/0008-scapegoat.md`](./decisions/0008-scapegoat.md)). Unlike Treap there is **no `split` / `merge`**: a scapegoat has no priority heap to merge by, and an honest deterministic split/merge would be O(n) rebuilds -- the documented asymmetry vs Treap. `alpha` (the weight-balance factor) is validated to the OPEN interval `(0.55, 0.75)` -- both ends throw -- and frozen at construction (default `2/3`).
|
|
385
|
+
|
|
386
|
+
```js
|
|
387
|
+
import { Scapegoat } from '@zakkster/lite-logn';
|
|
388
|
+
|
|
389
|
+
const sg = new Scapegoat(1000); // capacity 1000, alpha = 2/3 (deterministic, no seed)
|
|
390
|
+
sg.set(50, 500); // insert key 50 -> value 500
|
|
391
|
+
sg.set(20, 200);
|
|
392
|
+
sg.set(80, 800);
|
|
393
|
+
sg.get(20); // -> 200 -- worst-case O(log n)
|
|
394
|
+
sg.rank(50); // -> 1 (one key, 20, is strictly less than 50)
|
|
395
|
+
sg.select(0); // -> 20 (the smallest key)
|
|
396
|
+
sg.successor(20); // -> 50 (smallest key strictly greater)
|
|
397
|
+
[...sg.rangeIter(20, 80)]; // -> [20, 50, 80] (inclusive, ascending)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
| Member | Signature | Complexity | Notes |
|
|
401
|
+
| --- | --- | --- | --- |
|
|
402
|
+
| constructor | `new Scapegoat(capacity, alpha?)` | O(capacity) | `capacity` integer in `[1, 2^31-1]` (slot indices + subtree counts are `Uint32`, `NIL = 0` reserves slot 0); `alpha` in the OPEN interval `(0.55, 0.75)` (both ends throw), frozen at construction (default `2/3`). Allocates five columns + private pool + two rebuild scratch buffers once. |
|
|
403
|
+
| `get` | `get(key) -> number \| undefined` | worst-case O(log n) | The value under `key`, or `undefined` if absent (no throw). Non-finite key throws. |
|
|
404
|
+
| `has` | `has(key) -> boolean` | worst-case O(log n) | True iff `key` is stored. Non-finite key throws. |
|
|
405
|
+
| `set` | `set(key, value) -> this` | amortized O(log n) | Insert `key -> value`, or update the value in place if `key` exists (no rebuild). Non-finite key/value throws; a full pool throws. |
|
|
406
|
+
| `delete` | `delete(key) -> boolean` | amortized O(log n) | Idempotent: `false` if absent, `true` if removed. Non-finite key throws. |
|
|
407
|
+
| `rank` | `rank(x) -> number` | worst-case O(log n) | Count of stored keys STRICTLY less than `x`, in `[0, size]`. `x` need not be present. Non-finite `x` throws. |
|
|
408
|
+
| `select` | `select(k) -> number \| undefined` | worst-case O(log n) | The k-th smallest key (0-based), or `undefined` if `k` is out of `[0, size)`. Non-integer `k` throws. |
|
|
409
|
+
| `successor` | `successor(key) -> number \| undefined` | worst-case O(log n) | The smallest key STRICTLY greater than `key`, or `undefined`. |
|
|
410
|
+
| `predecessor` | `predecessor(key) -> number \| undefined` | worst-case O(log n) | The largest key STRICTLY less than `key`, or `undefined`. |
|
|
411
|
+
| `rangeIter` | `rangeIter(lo, hi) -> IterableIterator<number>` | O(k log n) | Version-stamped iterator over keys in `[lo, hi]` INCLUSIVE, ascending. Bounds may be `+-Infinity`; `NaN` or `lo > hi` throws; a structural OR value mutation mid-iteration throws. |
|
|
412
|
+
| `forEach` | `forEach(fn) -> void` | O(n) | Visits `(key, value, tree)` in ascending key order. |
|
|
413
|
+
| `clear` | `clear() -> this` | O(capacity) | Empties the tree, keeps capacity. |
|
|
414
|
+
| `size` / `capacity` / `alpha` | getters | O(1) | Live entry count / fixed capacity / frozen weight-balance factor. |
|
|
415
|
+
|
|
375
416
|
Member signatures for later members are appended here as each ships.
|
|
376
417
|
|
|
377
418
|
## Zero-GC design notes
|
|
@@ -399,8 +440,12 @@ Member signatures for later members are appended here as each ships.
|
|
|
399
440
|
| `Treap` forEach | 0 B/op in the loop body (recursive in-order walk, hoisted callback) |
|
|
400
441
|
| `Treap` rangeIter | one iterator + `{value, done}` per step (the documented per-protocol allocator; transient, not retained) |
|
|
401
442
|
| `Treap` split / merge | 0 B/op beyond the returned Treap view(s); rewire in place, share the arena, consume the input(s) |
|
|
443
|
+
| `Scapegoat` get / has / set / delete / rank / select / successor / predecessor | 0 B/op (nodes are slot indices; the rebuild reuses the preallocated `_flat` + `_stack` scratch and the native call stack, so even a rebuild storm is 0 B/op) |
|
|
444
|
+
| `Scapegoat` constructor / `clear` | O(capacity) five columns + pool + two rebuild scratch buffers, once (cold) |
|
|
445
|
+
| `Scapegoat` forEach | 0 B/op in the loop body (recursive in-order walk, hoisted callback) |
|
|
446
|
+
| `Scapegoat` rangeIter | one iterator + `{value, done}` per step (the documented per-protocol allocator; transient, not retained) |
|
|
402
447
|
|
|
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.
|
|
448
|
+
Gated witness numbers (this machine, shared R^2 floor 0.958): BinaryHeap `pop` R^2 ~ 0.99, slope ~ 8-10 ns/level; Fenwick `update` R^2 ~ 0.98-0.99, slope ~ 2.9-3.0 ns/level (band `[1.84, 4.30]`); Fenwick `prefix` R^2 ~ 0.97, slope ~ 2.6-2.7 ns/level (band `[1.76, 4.10]`); SegmentTree `update` R^2 ~ 0.99, slope ~ 3.2 ns/level (band `[2.29, 5.35]`); SegmentTree `query` R^2 ~ 0.99, slope ~ 7 ns/level (band `[4.30, 10.04]`); SkipList `get` R^2 ~ 0.97-0.99, slope ~ 9 ns/level (band `[5.27, 12.30]`, sweep `[2^11, 2^17]`); SkipList `set` R^2 ~ 0.97-0.99, slope ~ 14 ns/level (band `[8.36, 19.50]`, cache-resident sweep `[2^9, 2^14]`); Treap `get` R^2 ~ 0.99, slope ~ 4 ns/level (band `[2.55, 5.95]`, sweep `[2^11, 2^17]`); Scapegoat `get` R^2 ~ 0.99, slope ~ 3.8-4.0 ns/level (band `[2.41, 5.63]`, sweep `[2^11, 2^17]`) -- WORST-case (deterministic), with the AMORTIZED `set` rebuild spike proven by the amortized-trace assertion (ratio `< 4x`), not a per-op line. The allocation table is extended per member as each lands.
|
|
404
449
|
|
|
405
450
|
## Testing
|
|
406
451
|
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-logn
|
|
2
2
|
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
License: MIT (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
|
|
5
5
|
Runtime dependencies: none. ESM only. ASCII-only source. sideEffects: false.
|
|
6
6
|
Node: >= 18.
|
|
@@ -19,8 +19,8 @@ FLAT ops/ms line on a log-x axis), lite-logn holds the logarithm (a STRAIGHT
|
|
|
19
19
|
line on that same axis, one added level per doubling of n).
|
|
20
20
|
|
|
21
21
|
v0.1.0 shipped BinaryHeap; v0.2.0 adds Fenwick; v0.3.0 adds SegmentTree; v0.4.0
|
|
22
|
-
adds SkipList; v0.5.0 adds Treap. The roster (one member per
|
|
23
|
-
append-only):
|
|
22
|
+
adds SkipList; v0.5.0 adds Treap; v0.6.0 adds Scapegoat. The roster (one member per
|
|
23
|
+
session, each landing append-only):
|
|
24
24
|
|
|
25
25
|
- BinaryHeap (v0.1.0) -- an INDEXED binary heap (addressable priority queue): a
|
|
26
26
|
min|max binary heap over three parallel typed arrays (`_key` Float64Array,
|
|
@@ -59,6 +59,22 @@ append-only):
|
|
|
59
59
|
value in place. split / merge REWIRE in place (O(log n)) so the two treaps SHARE a
|
|
60
60
|
backing arena and CONSUME their inputs. EXPECTED, not worst-case: the MAX single
|
|
61
61
|
insert (rotation chain) is DISCLOSED, never gated. Zero allocation on every hot op.
|
|
62
|
+
- Scapegoat (v0.6.0) -- a DETERMINISTIC, weight-balanced AUGMENTED ordered map (key ->
|
|
63
|
+
value) that is also an order-statistic tree -- the honest PAIR to Treap. get is
|
|
64
|
+
WORST-case O(log n) (a hard height bound <= log_{1/alpha}(n) + 1, never merely
|
|
65
|
+
expected); set / delete are AMORTIZED O(log n) (an occasional subtree rebuild absorbs
|
|
66
|
+
the imbalance). A subtree-size column adds rank(x) / select(k), O(log n). NO priorities,
|
|
67
|
+
NO RNG anywhere: the shape is a deterministic function of the insert / delete order.
|
|
68
|
+
Nodes are slot INDICES in flat typed-array columns (_key / _value / _left / _right /
|
|
69
|
+
_size) over the SAME private free-list (NodePool); the ZERO-GC rebuild reuses ONE
|
|
70
|
+
preallocated scratch buffer (_flat) + ONE preallocated index-stack (_stack) -- an
|
|
71
|
+
iterative Morris-free flatten + a bounded log-depth balanced rebuild on the native
|
|
72
|
+
stack -- so even a rebuild-heavy trace is 0 B/op. alpha is frozen at construction in the
|
|
73
|
+
OPEN interval (0.55, 0.75) (both ends throw); default 2/3. Unlike Treap there is NO
|
|
74
|
+
split / merge (no priority heap to merge by; an honest deterministic split/merge would
|
|
75
|
+
be O(n) rebuilds) -- the documented asymmetry vs Treap. set updates an existing key's
|
|
76
|
+
value in place. The amortized-trace witness shows cumulative insert cost/op tracks log n
|
|
77
|
+
despite the rebuild spikes. Zero allocation on every hot op.
|
|
62
78
|
|
|
63
79
|
## Exports (from the single main file LogN.js)
|
|
64
80
|
|
|
@@ -189,6 +205,39 @@ append-only):
|
|
|
189
205
|
< every key of b; O(log n) EXPECTED, CONSUMES both. Non-Treap inputs, treaps from
|
|
190
206
|
different arenas, or an overlapping key range each throw.
|
|
191
207
|
|
|
208
|
+
- `Scapegoat` -- class. A DETERMINISTIC, weight-balanced AUGMENTED ordered map (key ->
|
|
209
|
+
value) that is also an order-statistic tree -- the honest PAIR to Treap. A BST on
|
|
210
|
+
`_key` kept alpha-weight-balanced (get WORST-case O(log n); set / delete AMORTIZED
|
|
211
|
+
O(log n) via an occasional subtree rebuild); a `_size` subtree-count column adds O(log n)
|
|
212
|
+
order statistics. NO priorities, NO RNG. Nodes are slot INDICES in flat typed-array
|
|
213
|
+
columns (`_key` / `_value` Float64, `_left` / `_right` / `_size` Uint32, `NIL = 0`) over
|
|
214
|
+
a PRIVATE free-list (NodePool); the zero-GC rebuild reuses ONE preallocated `_flat`
|
|
215
|
+
scratch + ONE `_stack` index-stack (iterative Morris-free flatten + bounded log-depth
|
|
216
|
+
balanced rebuild on the native stack) -- no heap object, no fresh array, 0 B/op even
|
|
217
|
+
under a rebuild storm. Keys and values are finite numbers (typeof-guarded before
|
|
218
|
+
coercion; Symbol / BigInt / NaN / +-Infinity fail closed).
|
|
219
|
+
- `new Scapegoat(capacity, alpha?)` -- capacity an integer in [1, 2^31-1] (slot indices
|
|
220
|
+
+ subtree counts are Uint32, `NIL = 0` reserves slot 0); alpha the weight-balance
|
|
221
|
+
factor in the OPEN interval (0.55, 0.75) (both ends throw), frozen at construction;
|
|
222
|
+
default 2/3. Allocates five columns + a free-list + two rebuild scratch buffers once.
|
|
223
|
+
- `get(key)` -> value | undefined. Absent -> undefined (no throw).
|
|
224
|
+
- `has(key)` -> boolean.
|
|
225
|
+
- `set(key, value)` -> this. Insert, or update the value in place if key exists (no new
|
|
226
|
+
node, no rebuild). Non-finite key/value throws; a full pool throws.
|
|
227
|
+
- `delete(key)` -> boolean. Idempotent: false if absent, true if removed.
|
|
228
|
+
- `rank(x)` -> number. Count of stored keys STRICTLY LESS than x, in [0, size].
|
|
229
|
+
- `select(k)` -> key | undefined. The k-th smallest key (0-based); undefined if k is out
|
|
230
|
+
of [0, size). Non-integer k throws.
|
|
231
|
+
- `successor(key)` -> key | undefined. Smallest key STRICTLY greater than key.
|
|
232
|
+
- `predecessor(key)` -> key | undefined. Largest key STRICTLY less than key.
|
|
233
|
+
- `rangeIter(lo, hi)` -> iterator of keys in [lo, hi] INCLUSIVE, ascending; VERSION-
|
|
234
|
+
STAMPED (structural OR value mutation mid-iteration throws). Bounds may be +-Infinity;
|
|
235
|
+
NaN or lo > hi throws.
|
|
236
|
+
- `size` / `capacity` / `alpha` getters; `clear()` -> this (empty, keep capacity);
|
|
237
|
+
`forEach(fn)` visits (key, value, tree) ascending.
|
|
238
|
+
- There is deliberately NO `split` / `merge` (the documented asymmetry vs Treap: no
|
|
239
|
+
priority heap to merge by; an honest deterministic split/merge would be O(n) rebuilds).
|
|
240
|
+
|
|
192
241
|
Member exports (one tree-shakeable class each) are appended here as each member
|
|
193
242
|
ships.
|
|
194
243
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-logn",
|
|
3
3
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"description": "Zero-dependency, zero-GC family of O(log n) data structures that proves its logarithm: BinaryHeap (array-embedded O(log n) push/pop min-heap), Fenwick/BIT (O(log n) point-update AND prefix-sum via the i & -i walk), SegmentTree (O(log n) associative range-query + point-update), SkipList (pointer-free expected-O(log n) ordered map over a private free-list node pool),
|
|
4
|
+
"version": "0.6.0",
|
|
5
|
+
"description": "Zero-dependency, zero-GC family of O(log n) data structures that proves its logarithm: BinaryHeap (array-embedded O(log n) push/pop min-heap), Fenwick/BIT (O(log n) point-update AND prefix-sum via the i & -i walk), SegmentTree (O(log n) associative range-query + point-update), SkipList (pointer-free expected-O(log n) ordered map over a private free-list node pool), Treap (randomized-balanced augmented ordered map with O(log n) rank/select/split/merge), and Scapegoat (DETERMINISTIC weight-balanced augmented ordered map: worst-case-O(log n) get, amortized-O(log n) set/delete, zero-GC rebuild) with a log-linear O(log n) Witness harness that fits nsPerOp = intercept + slope*log2(n) and shows the straight log line while an O(n) foil leaves it. Tree-shakeable named exports.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./LogN.js",
|
|
8
8
|
"module": "./LogN.js",
|
|
@@ -49,6 +49,9 @@
|
|
|
49
49
|
"range-sum",
|
|
50
50
|
"skip-list",
|
|
51
51
|
"treap",
|
|
52
|
+
"scapegoat",
|
|
53
|
+
"scapegoat-tree",
|
|
54
|
+
"weight-balanced",
|
|
52
55
|
"ordered-set",
|
|
53
56
|
"ordered-map",
|
|
54
57
|
"balanced-bst",
|