@zakkster/lite-logn 0.1.0 → 0.3.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 +88 -1
- package/LogN.d.ts +66 -0
- package/LogN.js +550 -10
- package/README.md +111 -19
- package/llms.txt +59 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,91 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.0] - 2026-09-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **SegmentTree** -- the third member: an associative range-query AND a
|
|
14
|
+
point-update, BOTH O(log n), over a SINGLE flat `Float64Array(2 * length)`
|
|
15
|
+
(leaves at `n .. 2n-1`, `_t[0]` unused) via iterative bottom-up walks -- no
|
|
16
|
+
nodes, no pointers, no recursion on the hot path. The fold is chosen ONCE at
|
|
17
|
+
construction (`'min'` / `'max'` / `'sum'` / `'gcd'`) and cached as a small-int
|
|
18
|
+
`_k` combined by an INLINE switch in the hot body (no function ref, no closure,
|
|
19
|
+
no megamorphic call site). Surface: `query(lo, hi)` (INCLUSIVE both ends,
|
|
20
|
+
matching `Fenwick.rangeSum`) / `update(i, value)` (ABSOLUTE leaf set + ancestor
|
|
21
|
+
fix) / `at(i)` (O(1) leaf read), `length` / `kind` getters, `clear`, `forEach`,
|
|
22
|
+
and a static `SegmentTree.build(values, kind)` O(n) bottom-up bulk build (seed
|
|
23
|
+
leaves, then fold each internal node once deepest-first -- not n incremental
|
|
24
|
+
updates). The fold identity fills query accumulators and cleared / fresh leaves
|
|
25
|
+
(`sum -> 0`, `min -> +Infinity`, `max -> -Infinity`, `gcd -> 0`); it is a legal
|
|
26
|
+
RESULT but never a legal INPUT -- the value door rejects user `NaN` /
|
|
27
|
+
`+-Infinity` (and, for the `gcd` kind, negatives + non-integers), typeof-guarded
|
|
28
|
+
before coercion, with a `[lite-logn]` throw. `SEGTREE_MAX = 2^30 - 1` (HALF of
|
|
29
|
+
Fenwick's ceiling: the `2n` layout must keep `2n` a positive int32). `query` /
|
|
30
|
+
`update` / `at` allocate zero bytes after construction. Verified: torture 0 B/op
|
|
31
|
+
on every hot lane (+ a 32 B/op control lane proving the instrument has teeth),
|
|
32
|
+
leak `size 0/0`, `gc major = 0`.
|
|
33
|
+
- **Witness: two more straight log lines.** `test/witness.mjs` gains SegmentTree's
|
|
34
|
+
`update` and `query` entries. Measured on this machine (shared R^2 floor 0.958):
|
|
35
|
+
update R^2 ~ 0.99, slope ~ 3.2 ns/level (band `[2.29, 5.35]`, median 3.83 x
|
|
36
|
+
[0.6, 1.4]); query R^2 ~ 0.99, slope ~ 7 ns/level (band `[4.30, 10.04]`,
|
|
37
|
+
median 7.17 x [0.6, 1.4]). The gated sweep is pinned to EXACT powers of two in
|
|
38
|
+
`[2^10, 2^16]` (a segment-tree op touches a node per level spread across the
|
|
39
|
+
`2n` array, so above ~2^16 the tree leaves the steady cache band; exact powers
|
|
40
|
+
keep the range decomposition a regular node count). Both O(n) foils leave the
|
|
41
|
+
line: the whole-tree rebuild (O(n) per update, R^2 ~ 0.85) and the scan-fold
|
|
42
|
+
(O(n) per query, R^2 ~ 0.75), each below the floor.
|
|
43
|
+
- **ADR.** [`decisions/0005-segtree.md`](./decisions/0005-segtree.md) (D-05):
|
|
44
|
+
scope is point-update + range-query ONLY (no lazy propagation, no caller-supplied
|
|
45
|
+
fold) for v0.3.0; the fold is injected via a ctor-cached `_k` inline switch, not
|
|
46
|
+
a function ref; and the iterative `2n` layout is order-agnostic, so it is correct
|
|
47
|
+
ONLY for commutative + associative folds -- a future non-commutative fold is
|
|
48
|
+
routed to a pow2 layout instead.
|
|
49
|
+
|
|
50
|
+
### Unchanged
|
|
51
|
+
|
|
52
|
+
- **BinaryHeap and Fenwick are byte-identical.** The v0.1.0 and v0.2.0 member class
|
|
53
|
+
bodies are untouched; only the file header roster, the `VERSION` const, and the
|
|
54
|
+
appended SegmentTree block changed in `LogN.js`.
|
|
55
|
+
|
|
56
|
+
## [0.2.0] - 2026-09-17
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- **Fenwick** (Binary Indexed Tree) -- the second member: BOTH point-update AND
|
|
61
|
+
prefix-sum in O(log n) over a single flat `Float64Array`, via the lowest-set-bit
|
|
62
|
+
walk (`i & -i`). Surface: `update(i, delta)` / `prefix(i)` / `rangeSum(lo, hi)` /
|
|
63
|
+
`at(i)` / `set(i, value)`, a `length` getter, `clear`, `forEach`, and a static
|
|
64
|
+
`Fenwick.build(values)` O(n) LINEAR bulk build (each cell adds itself to its
|
|
65
|
+
parent in one forward pass -- not n incremental updates). Public indices are
|
|
66
|
+
0-based in `[0, length)`; internally 1-based (`_t[0]` the unused identity
|
|
67
|
+
sentinel). `prefix(-1) === 0` is the empty-prefix base case; `rangeSum` and `at`
|
|
68
|
+
are pairs of inlined prefix walks. Values are finite numbers (negatives
|
|
69
|
+
allowed); NaN / +-Infinity / non-number fail closed (typeof-guarded before
|
|
70
|
+
coercion) with a `[lite-logn]` throw. `update` / `prefix` / `rangeSum` / `at` /
|
|
71
|
+
`set` allocate zero bytes after construction. `FENWICK_MAX = 2^31 - 1` (the
|
|
72
|
+
`i & -i` walk relies on signed-int32 two's complement, so indices stay in that
|
|
73
|
+
range). Verified: torture 0 B/op on every hot lane (+ a 32 B/op control lane
|
|
74
|
+
proving the instrument has teeth), leak `size 0/0`, `gc major = 0`.
|
|
75
|
+
- **Witness: two straight log lines.** `test/witness.mjs` gains Fenwick's `update`
|
|
76
|
+
and `prefix` entries. Measured on this machine (shared R^2 floor 0.958): update
|
|
77
|
+
R^2 ~ 0.98-0.99, slope ~ 2.9-3.0 ns/level (band `[1.84, 4.30]`, median 3.07 x
|
|
78
|
+
[0.6, 1.4]); prefix R^2 ~ 0.97, slope ~ 2.6-2.7 ns/level (band `[1.76, 4.10]`,
|
|
79
|
+
median 2.93 x [0.6, 1.4]). Both O(n) foils leave the line: the prefix-array
|
|
80
|
+
rebuild (O(n) per update) and the naive re-sum (O(n) per query) each fit at
|
|
81
|
+
R^2 ~ 0.76, below the floor.
|
|
82
|
+
- **ADR.** [`decisions/0004-witness-band.md`](./decisions/0004-witness-band.md)
|
|
83
|
+
(D-08): the R^2 floor (0.958) is frozen family-wide; each member calibrates its
|
|
84
|
+
OWN per-op slope band = median-of-15 fit-runs x [0.6, 1.4] (the same procedure
|
|
85
|
+
that set BinaryHeap's band). A cheaper op having a lower slope is expected, not
|
|
86
|
+
a regression.
|
|
87
|
+
|
|
88
|
+
### Unchanged
|
|
89
|
+
|
|
90
|
+
- **BinaryHeap is byte-identical.** The v0.1.0 member's class body is untouched;
|
|
91
|
+
only the file header roster, the `VERSION` const, and the appended Fenwick block
|
|
92
|
+
changed in `LogN.js`.
|
|
93
|
+
|
|
9
94
|
## [0.1.0] - 2026-09-17
|
|
10
95
|
|
|
11
96
|
### Added
|
|
@@ -40,5 +125,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
40
125
|
[`decisions/0003-pack.md`](./decisions/0003-pack.md) (D-07: `files[]` ships the
|
|
41
126
|
six files only; `test/`, `benchmark/`, `decisions/`, `demo/` are repo-only).
|
|
42
127
|
|
|
43
|
-
[Unreleased]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.
|
|
128
|
+
[Unreleased]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.3.0...HEAD
|
|
129
|
+
[0.3.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.2.0...v0.3.0
|
|
130
|
+
[0.2.0]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.1.0...v0.2.0
|
|
44
131
|
[0.1.0]: https://github.com/PeshoVurtoleta/lite-logn/releases/tag/v0.1.0
|
package/LogN.d.ts
CHANGED
|
@@ -64,3 +64,69 @@ export class BinaryHeap {
|
|
|
64
64
|
capacity: number,
|
|
65
65
|
): BinaryHeap;
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A Fenwick tree (Binary Indexed Tree): BOTH point-update AND prefix-sum in
|
|
70
|
+
* O(log n) over a single flat Float64Array via the lowest-set-bit walk (i & -i).
|
|
71
|
+
* Public indices are 0-based in [0, length); internally 1-based (_t[0] the unused
|
|
72
|
+
* identity sentinel). Values are finite numbers (negatives allowed); NaN /
|
|
73
|
+
* Infinity / non-number fail closed. Every hot op allocates zero bytes.
|
|
74
|
+
*/
|
|
75
|
+
export class Fenwick {
|
|
76
|
+
/** @param length exact element count; integer in [1, 2^31-1]. */
|
|
77
|
+
constructor(length: number);
|
|
78
|
+
|
|
79
|
+
/** Element count this tree was sized for. */
|
|
80
|
+
readonly length: number;
|
|
81
|
+
|
|
82
|
+
/** Add delta at 0-based index i. O(log n). Non-finite delta / out-of-range i throws. */
|
|
83
|
+
update(i: number, delta: number): this;
|
|
84
|
+
/** Sum of [0, i] inclusive (prefix(-1) === 0). O(log n). Out-of-range i throws. */
|
|
85
|
+
prefix(i: number): number;
|
|
86
|
+
/** Sum of [lo, hi] inclusive = prefix(hi) - prefix(lo-1). O(log n). lo > hi throws. */
|
|
87
|
+
rangeSum(lo: number, hi: number): number;
|
|
88
|
+
/** The single element at i = prefix(i) - prefix(i-1). O(log n). Out-of-range i throws. */
|
|
89
|
+
at(i: number): number;
|
|
90
|
+
/** Set the element at i to value (absolute). O(log n). Non-finite value throws. */
|
|
91
|
+
set(i: number, value: number): this;
|
|
92
|
+
/** Zero every element in place, keeping capacity. */
|
|
93
|
+
clear(): this;
|
|
94
|
+
/** Visit every element as (value, index, fenwick) in ascending index order. */
|
|
95
|
+
forEach(fn: (value: number, index: number, fenwick: Fenwick) => void): void;
|
|
96
|
+
|
|
97
|
+
/** O(n) linear bulk build from a finite-number array-like. */
|
|
98
|
+
static build(values: ArrayLike<number>): Fenwick;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A segment tree: an associative range-query AND a point-update, BOTH O(log n),
|
|
103
|
+
* over a single flat Float64Array(2n) (leaves at n..2n-1, _t[0] unused). The fold
|
|
104
|
+
* (min / max / sum / gcd) is chosen once at construction and cached. query(lo, hi)
|
|
105
|
+
* is INCLUSIVE both ends; update(i, value) sets an ABSOLUTE leaf value. Values are
|
|
106
|
+
* finite numbers (nonnegative integers for the gcd kind); NaN / +-Infinity / out-
|
|
107
|
+
* of-domain values fail closed. Every hot op allocates zero bytes.
|
|
108
|
+
*/
|
|
109
|
+
export class SegmentTree {
|
|
110
|
+
/** @param length exact element count; integer in [1, 2^30-1].
|
|
111
|
+
* @param kind the frozen associative fold. */
|
|
112
|
+
constructor(length: number, kind: 'min' | 'max' | 'sum' | 'gcd');
|
|
113
|
+
|
|
114
|
+
/** Element count this tree was sized for. */
|
|
115
|
+
readonly length: number;
|
|
116
|
+
/** The frozen associative fold. */
|
|
117
|
+
readonly kind: 'min' | 'max' | 'sum' | 'gcd';
|
|
118
|
+
|
|
119
|
+
/** Folded value over [lo, hi] inclusive both ends. O(log n). Throws on OOB or lo > hi. */
|
|
120
|
+
query(lo: number, hi: number): number;
|
|
121
|
+
/** Set leaf i to value (absolute), fixing ancestors. O(log n). Non-finite / OOB throws. */
|
|
122
|
+
update(i: number, value: number): this;
|
|
123
|
+
/** The single element at leaf i. O(1). Out-of-range i throws. */
|
|
124
|
+
at(i: number): number;
|
|
125
|
+
/** Reset every element to the fold identity, keeping capacity. */
|
|
126
|
+
clear(): this;
|
|
127
|
+
/** Visit every element as (value, index, tree) in ascending leaf order. */
|
|
128
|
+
forEach(fn: (value: number, index: number, tree: SegmentTree) => void): void;
|
|
129
|
+
|
|
130
|
+
/** O(n) bottom-up bulk build from a finite-number array-like and a fold kind. */
|
|
131
|
+
static build(values: ArrayLike<number>, kind: 'min' | 'max' | 'sum' | 'gcd'): SegmentTree;
|
|
132
|
+
}
|
package/LogN.js
CHANGED
|
@@ -4,15 +4,21 @@
|
|
|
4
4
|
* problem AND proves its logarithm is real (the O(log n) Witness -- see
|
|
5
5
|
* test/witness.mjs).
|
|
6
6
|
*
|
|
7
|
-
* v0.1.0
|
|
7
|
+
* v0.1.0 shipped the FIRST member: BinaryHeap, an INDEXED binary heap -- an
|
|
8
8
|
* addressable priority queue (a min|max binary heap over three parallel typed
|
|
9
9
|
* arrays plus a reverse-index map that makes changeKey / remove O(log n) by
|
|
10
|
-
* caller-supplied entity id).
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* caller-supplied entity id). v0.2.0 adds the SECOND member: Fenwick (BIT), a
|
|
11
|
+
* flat-array structure whose point-update AND prefix-sum are BOTH O(log n) via
|
|
12
|
+
* the lowest-set-bit walk (`i & -i`). v0.3.0 adds the THIRD member: SegmentTree,
|
|
13
|
+
* a flat `Float64Array(2n)` (leaves at n..2n-1) whose range-query AND point-update
|
|
14
|
+
* are BOTH O(log n) via iterative bottom-up walks, with the associative fold
|
|
15
|
+
* (min / max / sum / gcd) chosen ONCE at construction. Members land append-only,
|
|
16
|
+
* leaving this header and the `VERSION` const the only prior lines that ever
|
|
17
|
+
* change. Roster: BinaryHeap (v0.1.0, array-embedded O(log n) push / pop min|max
|
|
18
|
+
* heap), Fenwick / BIT (v0.2.0, O(log n) point-update AND prefix-sum via the
|
|
19
|
+
* `i & -i` walk), SegmentTree (v0.3.0, O(log n) associative range-query +
|
|
20
|
+
* point-update over a flat 2n array, fold chosen at construction), and
|
|
21
|
+
* SkipList (planned, pointer-free expected-O(log n) ordered map). Members are
|
|
16
22
|
* independent (no shared mutable module state), so a bundler that imports one
|
|
17
23
|
* drops the others (`sideEffects: false`).
|
|
18
24
|
*
|
|
@@ -29,12 +35,12 @@
|
|
|
29
35
|
*/
|
|
30
36
|
|
|
31
37
|
/** Package version. One of the three version sites (package.json / VERSION / llms.txt). */
|
|
32
|
-
export const VERSION = '0.
|
|
38
|
+
export const VERSION = '0.3.0';
|
|
33
39
|
|
|
34
40
|
// --- members land here, append-only, one tree-shakeable class each -----------
|
|
35
41
|
// BinaryHeap (v0.1.0 session) -- indexed O(log n) min|max heap (BELOW)
|
|
36
|
-
// Fenwick (v0.2.0)
|
|
37
|
-
// SegmentTree (v0.3.0)
|
|
42
|
+
// Fenwick (v0.2.0 session) -- O(log n) point-update + prefix-sum (BELOW)
|
|
43
|
+
// SegmentTree (v0.3.0 session) -- O(log n) associative range-query + point-update (BELOW)
|
|
38
44
|
// SkipList (v0.4.0) -- pointer-free expected-O(log n) ordered map
|
|
39
45
|
|
|
40
46
|
/** Max heap capacity: slot indices 0..cap-1 must fit the Int32Array _pos map. */
|
|
@@ -392,3 +398,537 @@ export class BinaryHeap {
|
|
|
392
398
|
'[lite-logn] BinaryHeap.changeKey: id ' + id + ' is not in the heap');
|
|
393
399
|
}
|
|
394
400
|
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Max Fenwick length: internal 1-based indices `k` run in [1, length] and the
|
|
404
|
+
* whole trick is the two's-complement lowest-set-bit `k & -k`. JavaScript
|
|
405
|
+
* bitwise operators coerce to a SIGNED 32-bit integer, so `k & -k` is only the
|
|
406
|
+
* lowest set bit while `k` fits a positive int32 -- i.e. `k <= 2^31 - 1`. Since
|
|
407
|
+
* the largest `k` the walk ever reaches equals `length`, `length` itself must
|
|
408
|
+
* stay in that range. Same bound as BinaryHeap's capacity, for the same reason:
|
|
409
|
+
* the index arithmetic, not the byte count, is the hard ceiling.
|
|
410
|
+
*/
|
|
411
|
+
const FENWICK_MAX = 0x7FFFFFFF; // 2^31 - 1
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* A Fenwick tree (Binary Indexed Tree): BOTH point-update AND prefix-sum in
|
|
415
|
+
* O(log n) over a SINGLE flat `Float64Array`, using nothing but the lowest-set-
|
|
416
|
+
* bit walk (`i & -i`). The member whose Big-O is most delightfully non-obvious
|
|
417
|
+
* -- "how can update AND query both be logarithmic on a plain array?" is exactly
|
|
418
|
+
* the claim the witness answers, with TWO straight log lines (one per op). No
|
|
419
|
+
* nodes, no pointers; nothing is allocated per op after construction.
|
|
420
|
+
*
|
|
421
|
+
* Index base: PUBLIC indices are 0-based in `[0, length)`. Internally the tree is
|
|
422
|
+
* 1-based -- each public `i` maps to `i + 1` in the backing `_t` Float64Array, so
|
|
423
|
+
* `_t[0]` is the unused identity sentinel and is NEVER read as data (null is not
|
|
424
|
+
* zero: slot 0 does not mean "the element at 0" -- it means "no cell"). Element
|
|
425
|
+
* `i` lives, spread across a logarithmic set of cells, in `_t[1 .. length]`.
|
|
426
|
+
*
|
|
427
|
+
* The two walks are the whole structure:
|
|
428
|
+
* - `update(i, delta)` climbs: from `k = i + 1`, repeatedly `k += k & -k` (add
|
|
429
|
+
* the lowest set bit) until `k > length`, touching ONE cell per level.
|
|
430
|
+
* - `prefix(i)` descends: from `k = i + 1`, repeatedly `k -= k & -k` (strip the
|
|
431
|
+
* lowest set bit) until `k == 0`, summing ONE cell per level.
|
|
432
|
+
* Both take at most `log2(length)` steps -- the logarithm the witness proves.
|
|
433
|
+
*
|
|
434
|
+
* Value type: `Float64Array`; deltas and values may be ANY finite number,
|
|
435
|
+
* including negatives. NaN / +-Infinity / non-number fail closed (typeof-guarded
|
|
436
|
+
* BEFORE coercion) rather than corrupt a running sum. Honesty note on precision:
|
|
437
|
+
* sums are IEEE-754 double addition, so a very large corpus of very different
|
|
438
|
+
* magnitudes accumulates the usual floating-point rounding error -- the bound is
|
|
439
|
+
* exact in step count, not in the last ULP of the sum. For exact integer sums,
|
|
440
|
+
* keep values within the 2^53 safe-integer range.
|
|
441
|
+
*
|
|
442
|
+
* Fixed capacity: `length` is frozen at construction; there is no grow. Every
|
|
443
|
+
* out-of-range index and every non-finite value is a hard `[lite-logn]` throw.
|
|
444
|
+
*/
|
|
445
|
+
export class Fenwick {
|
|
446
|
+
/**
|
|
447
|
+
* @param {number} length exact element count; integer in [1, 2^31-1].
|
|
448
|
+
*/
|
|
449
|
+
constructor(length) {
|
|
450
|
+
// typeof guard BEFORE coercion (Number.isInteger is Symbol/BigInt-safe).
|
|
451
|
+
if (typeof length !== 'number' || !Number.isInteger(length) ||
|
|
452
|
+
length < 1 || length > FENWICK_MAX) {
|
|
453
|
+
throw new RangeError(
|
|
454
|
+
'[lite-logn] Fenwick length must be an integer in [1, 2^31-1], got ' +
|
|
455
|
+
String(length));
|
|
456
|
+
}
|
|
457
|
+
this._n = length; // element count (fixed)
|
|
458
|
+
this._t = new Float64Array(length + 1); // 1-based; _t[0] is the unused sentinel
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Element count this tree was sized for. O(1). */
|
|
462
|
+
get length() { return this._n; }
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Add `delta` to the element at 0-based index `i`. O(log n): climb from
|
|
466
|
+
* `k = i + 1` by the lowest set bit, one `_t` touch per level. Fails closed:
|
|
467
|
+
* a non-number / non-finite delta (typeof-guarded first) or an out-of-range
|
|
468
|
+
* index each throw `[lite-logn]` as a no-op.
|
|
469
|
+
* @param {number} i integer in [0, length)
|
|
470
|
+
* @param {number} delta a finite number (may be negative)
|
|
471
|
+
* @returns {this}
|
|
472
|
+
*/
|
|
473
|
+
update(i, delta) {
|
|
474
|
+
if (typeof delta !== 'number' || !Number.isFinite(delta)) return this._badDelta(delta);
|
|
475
|
+
if (typeof i !== 'number' || !Number.isInteger(i) || i < 0 || i >= this._n) {
|
|
476
|
+
return this._badIndex(i);
|
|
477
|
+
}
|
|
478
|
+
const t = this._t, n = this._n;
|
|
479
|
+
for (let k = i + 1; k <= n; k += k & -k) t[k] += delta; // <= log2(n) steps
|
|
480
|
+
return this;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Sum of elements in `[0, i]` INCLUSIVE. O(log n): descend from `k = i + 1`
|
|
485
|
+
* by the lowest set bit, one `_t` read per level. `prefix(-1) === 0` is the
|
|
486
|
+
* clean base case (the empty prefix). An out-of-range index throws; the valid
|
|
487
|
+
* domain is `[-1, length)`.
|
|
488
|
+
* @param {number} i integer in [-1, length)
|
|
489
|
+
* @returns {number}
|
|
490
|
+
*/
|
|
491
|
+
prefix(i) {
|
|
492
|
+
if (typeof i !== 'number' || !Number.isInteger(i) || i < -1 || i >= this._n) {
|
|
493
|
+
return this._badPrefixIndex(i);
|
|
494
|
+
}
|
|
495
|
+
const t = this._t;
|
|
496
|
+
let s = 0;
|
|
497
|
+
for (let k = i + 1; k > 0; k -= k & -k) s += t[k]; // <= log2(n) steps
|
|
498
|
+
return s;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Sum of elements in `[lo, hi]` INCLUSIVE on both ends = `prefix(hi) -
|
|
503
|
+
* prefix(lo - 1)`, inlined as two lowest-set-bit walks (no intermediate
|
|
504
|
+
* object, no double validation). O(log n). Fails closed: out-of-range `lo` or
|
|
505
|
+
* `hi`, or `lo > hi`, each throw `[lite-logn]`.
|
|
506
|
+
* @param {number} lo integer in [0, length)
|
|
507
|
+
* @param {number} hi integer in [lo, length)
|
|
508
|
+
* @returns {number}
|
|
509
|
+
*/
|
|
510
|
+
rangeSum(lo, hi) {
|
|
511
|
+
if (typeof lo !== 'number' || !Number.isInteger(lo) || lo < 0 || lo >= this._n) {
|
|
512
|
+
return this._badRange(lo, hi);
|
|
513
|
+
}
|
|
514
|
+
if (typeof hi !== 'number' || !Number.isInteger(hi) || hi < 0 || hi >= this._n) {
|
|
515
|
+
return this._badRange(lo, hi);
|
|
516
|
+
}
|
|
517
|
+
if (lo > hi) return this._badRange(lo, hi);
|
|
518
|
+
const t = this._t;
|
|
519
|
+
let s = 0;
|
|
520
|
+
for (let k = hi + 1; k > 0; k -= k & -k) s += t[k]; // prefix(hi)
|
|
521
|
+
for (let k = lo; k > 0; k -= k & -k) s -= t[k]; // - prefix(lo-1)
|
|
522
|
+
return s;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* The single element at 0-based index `i` = `prefix(i) - prefix(i - 1)`,
|
|
527
|
+
* inlined as two lowest-set-bit walks. O(log n), zero allocation. An
|
|
528
|
+
* out-of-range index throws `[lite-logn]`.
|
|
529
|
+
* @param {number} i integer in [0, length)
|
|
530
|
+
* @returns {number}
|
|
531
|
+
*/
|
|
532
|
+
at(i) {
|
|
533
|
+
if (typeof i !== 'number' || !Number.isInteger(i) || i < 0 || i >= this._n) {
|
|
534
|
+
return this._badIndex(i);
|
|
535
|
+
}
|
|
536
|
+
const t = this._t;
|
|
537
|
+
let s = 0;
|
|
538
|
+
for (let k = i + 1; k > 0; k -= k & -k) s += t[k]; // prefix(i)
|
|
539
|
+
for (let k = i; k > 0; k -= k & -k) s -= t[k]; // - prefix(i-1)
|
|
540
|
+
return s;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Set the element at 0-based index `i` to `value` (absolute), via
|
|
545
|
+
* `update(i, value - at(i))`, inlined so the read and the climb share one
|
|
546
|
+
* validation and allocate nothing. O(log n). Fails closed: a non-finite
|
|
547
|
+
* value (typeof-guarded first) or out-of-range index throws `[lite-logn]`.
|
|
548
|
+
* @param {number} i integer in [0, length)
|
|
549
|
+
* @param {number} value a finite number
|
|
550
|
+
* @returns {this}
|
|
551
|
+
*/
|
|
552
|
+
set(i, value) {
|
|
553
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return this._badValue(value);
|
|
554
|
+
if (typeof i !== 'number' || !Number.isInteger(i) || i < 0 || i >= this._n) {
|
|
555
|
+
return this._badIndex(i);
|
|
556
|
+
}
|
|
557
|
+
const t = this._t, n = this._n;
|
|
558
|
+
let cur = 0; // = at(i)
|
|
559
|
+
for (let k = i + 1; k > 0; k -= k & -k) cur += t[k];
|
|
560
|
+
for (let k = i; k > 0; k -= k & -k) cur -= t[k];
|
|
561
|
+
const delta = value - cur;
|
|
562
|
+
for (let k = i + 1; k <= n; k += k & -k) t[k] += delta; // update(i, delta)
|
|
563
|
+
return this;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** Zero every element in place, keeping the fixed capacity. O(n) cold path. */
|
|
567
|
+
clear() {
|
|
568
|
+
this._t.fill(0);
|
|
569
|
+
return this;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Visit every element as `(value, index, fenwick)` for index in `[0, length)`,
|
|
574
|
+
* in ascending index order. O(n log n) COLD scan (each element is an `at`
|
|
575
|
+
* walk); allocation-free in the loop body (pass a hoisted callback).
|
|
576
|
+
* @param {(value:number, index:number, fenwick:Fenwick)=>void} fn
|
|
577
|
+
*/
|
|
578
|
+
forEach(fn) {
|
|
579
|
+
const t = this._t, n = this._n;
|
|
580
|
+
for (let i = 0; i < n; i++) {
|
|
581
|
+
let s = 0;
|
|
582
|
+
for (let k = i + 1; k > 0; k -= k & -k) s += t[k];
|
|
583
|
+
for (let k = i; k > 0; k -= k & -k) s -= t[k];
|
|
584
|
+
fn(s, i, this);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* O(n) LINEAR bulk build from `values` -- the SECOND teachable trick. Load
|
|
590
|
+
* each value into its own cell, then in ONE forward pass let each cell add
|
|
591
|
+
* itself to its parent (`_t[j] += _t[i]` where `j = i + (i & -i)`). This is
|
|
592
|
+
* O(n), NOT n incremental O(log n) updates. COLD path; fails closed on a
|
|
593
|
+
* non-array-like `values` or any non-finite entry before the tree is usable.
|
|
594
|
+
* @param {ArrayLike<number>} values finite numbers; length in [1, 2^31-1]
|
|
595
|
+
* @returns {Fenwick}
|
|
596
|
+
*/
|
|
597
|
+
static build(values) {
|
|
598
|
+
if (values == null || typeof values.length !== 'number') {
|
|
599
|
+
throw new TypeError('[lite-logn] Fenwick.build needs an array-like of finite numbers');
|
|
600
|
+
}
|
|
601
|
+
const length = values.length;
|
|
602
|
+
const f = new Fenwick(length); // validates length in [1, 2^31-1]
|
|
603
|
+
const t = f._t;
|
|
604
|
+
for (let i = 0; i < length; i++) {
|
|
605
|
+
const v = values[i];
|
|
606
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
607
|
+
throw new TypeError(
|
|
608
|
+
'[lite-logn] Fenwick.build value must be a finite number, got ' + String(v));
|
|
609
|
+
}
|
|
610
|
+
t[i + 1] = v; // seed each cell with its own value
|
|
611
|
+
}
|
|
612
|
+
// Linear propagation: each 1-based cell i pushes its running sum to its
|
|
613
|
+
// parent j = i + (i & -i). One pass, O(n) -- the non-obvious build trick.
|
|
614
|
+
for (let i = 1; i <= length; i++) {
|
|
615
|
+
const j = i + (i & -i);
|
|
616
|
+
if (j <= length) t[j] += t[i];
|
|
617
|
+
}
|
|
618
|
+
return f;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ---- cold path only: throw builders (string concat off the hot body) ---
|
|
622
|
+
|
|
623
|
+
/** @private */
|
|
624
|
+
_badIndex(i) {
|
|
625
|
+
throw new RangeError(
|
|
626
|
+
'[lite-logn] Fenwick index must be an integer in [0, ' + this._n + '), got ' +
|
|
627
|
+
String(i));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** @private */
|
|
631
|
+
_badPrefixIndex(i) {
|
|
632
|
+
throw new RangeError(
|
|
633
|
+
'[lite-logn] Fenwick prefix index must be an integer in [-1, ' + this._n + '), got ' +
|
|
634
|
+
String(i));
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** @private */
|
|
638
|
+
_badRange(lo, hi) {
|
|
639
|
+
throw new RangeError(
|
|
640
|
+
'[lite-logn] Fenwick rangeSum needs integers 0 <= lo <= hi < ' + this._n +
|
|
641
|
+
', got lo=' + String(lo) + ' hi=' + String(hi));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** @private */
|
|
645
|
+
_badDelta(delta) {
|
|
646
|
+
throw new TypeError(
|
|
647
|
+
'[lite-logn] Fenwick delta must be a finite number, got ' + String(delta));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** @private */
|
|
651
|
+
_badValue(value) {
|
|
652
|
+
throw new TypeError(
|
|
653
|
+
'[lite-logn] Fenwick value must be a finite number, got ' + String(value));
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Max SegmentTree length: the whole structure lives in ONE `Float64Array(2 *
|
|
659
|
+
* length)` with leaves at indices `n .. 2n-1` and internal node `p`'s children
|
|
660
|
+
* at `2p` / `2p+1`. Both the leaf index (`n + i`, up to `2n - 1`) and the child
|
|
661
|
+
* index (`p << 1`, up to `2n - 2`) are computed with the signed-int32 `<<` / `+`
|
|
662
|
+
* operators, so the largest index the walks ever reach -- `2n - 1` -- must stay a
|
|
663
|
+
* POSITIVE int32 (`<= 2^31 - 1`). That caps `length` at `2^30 - 1`: HALF of
|
|
664
|
+
* BinaryHeap's / Fenwick's ceiling, because SegmentTree's backing array is 2n
|
|
665
|
+
* wide (a node per leaf plus a node per internal cell) where theirs are n wide.
|
|
666
|
+
* The index arithmetic, not the byte count, is the hard ceiling.
|
|
667
|
+
*/
|
|
668
|
+
const SEGTREE_MAX = 0x3FFFFFFF; // 2^30 - 1 (so 2n stays a positive int32)
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Euclidean GCD over nonnegative integers-in-doubles. Off the value door the
|
|
672
|
+
* inputs are already validated nonnegative finite integers (the `gcd` kind
|
|
673
|
+
* constrains its domain -- see SegmentTree's value door), and the identity 0
|
|
674
|
+
* makes this associative + commutative: `gcd(0, x) === x`, `gcd(x, 0) === x`.
|
|
675
|
+
* A plain module function (monomorphic, allocation-free) -- it is the arithmetic
|
|
676
|
+
* of the fold, NOT the fold dispatch (which is the ctor-cached `_k` inline
|
|
677
|
+
* switch). `%` is exact for integers within the 2^53 safe range.
|
|
678
|
+
* @param {number} a nonnegative finite integer
|
|
679
|
+
* @param {number} b nonnegative finite integer
|
|
680
|
+
* @returns {number} gcd(a, b), with gcd(0, 0) === 0
|
|
681
|
+
*/
|
|
682
|
+
function segGcd(a, b) {
|
|
683
|
+
while (b !== 0) { const r = a % b; a = b; b = r; }
|
|
684
|
+
return a;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* A SEGMENT TREE: an associative range-query AND a point-update, BOTH O(log n),
|
|
689
|
+
* over a SINGLE flat `Float64Array(2n)` -- no nodes, no pointers, no recursion on
|
|
690
|
+
* the hot path. The complement to Fenwick: Fenwick's `rangeSum` works only
|
|
691
|
+
* because subtraction inverts addition, so it is a SUM machine; SegmentTree folds
|
|
692
|
+
* ANY associative + commutative operation over a range -- min / max / sum / gcd --
|
|
693
|
+
* because it stores a fold of each subtree at its internal node rather than a
|
|
694
|
+
* prefix. The fold is chosen ONCE at construction and cached as a small-int `_k`
|
|
695
|
+
* combined by an INLINE switch in the hot body (no function ref, no closure, no
|
|
696
|
+
* megamorphic call site).
|
|
697
|
+
*
|
|
698
|
+
* Layout (the iterative "2n" trick):
|
|
699
|
+
* - `_t` is a `Float64Array(2 * length)`; `_t[0]` is UNUSED (null is not zero:
|
|
700
|
+
* index 0 is never read as data).
|
|
701
|
+
* - Leaves are `_t[n + i]` for public index `i` in `[0, n)`.
|
|
702
|
+
* - Internal node `p` (in `[1, n)`) holds the fold of its subtree; its children
|
|
703
|
+
* are `_t[2p]` and `_t[2p + 1]`, so `_t[1]` is the fold of the whole array.
|
|
704
|
+
*
|
|
705
|
+
* The two hot walks:
|
|
706
|
+
* - `update(i, value)` sets leaf `_t[n + i] = value`, then climbs to the root
|
|
707
|
+
* recomputing each ancestor `_t[p] = fold(_t[2p], _t[2p+1])` -- one write per
|
|
708
|
+
* level, `<= log2(n)` levels.
|
|
709
|
+
* - `query(lo, hi)` walks the two boundary indices UP the tree
|
|
710
|
+
* (`l = n + lo`, `r = n + hi + 1`), folding in each node that lies fully
|
|
711
|
+
* inside `[lo, hi]` as the boundaries ascend -- `<= 2 * log2(n)` folds.
|
|
712
|
+
*
|
|
713
|
+
* RISK (recorded in decisions/0005-segtree.md, D-05): the iterative 2n layout is
|
|
714
|
+
* ORDER-AGNOSTIC -- `query` mixes left- and right-boundary contributions into one
|
|
715
|
+
* accumulator, so it is correct ONLY because min / max / sum / gcd are all
|
|
716
|
+
* COMMUTATIVE as well as associative. A future NON-commutative fold (matrix
|
|
717
|
+
* product, string concat) must NOT reuse this layout; it belongs on a pow2
|
|
718
|
+
* layout with separate left/right accumulators combined in order.
|
|
719
|
+
*
|
|
720
|
+
* Identity (the trap): the fold's identity fills query accumulators and cleared /
|
|
721
|
+
* fresh leaves -- sum -> 0, min -> +Infinity, max -> -Infinity, gcd -> 0. Identity
|
|
722
|
+
* is a legal RESULT (a cleared min tree queries to +Infinity) but NEVER a legal
|
|
723
|
+
* INPUT: the value door still rejects user NaN / +-Infinity (and, for the `gcd`
|
|
724
|
+
* kind, any negative or non-integer value), typeof-guarded BEFORE coercion. Fixed
|
|
725
|
+
* capacity: `length` is frozen at construction; every out-of-range index and
|
|
726
|
+
* every non-finite (or out-of-domain gcd) value is a hard `[lite-logn]` throw.
|
|
727
|
+
*/
|
|
728
|
+
export class SegmentTree {
|
|
729
|
+
/**
|
|
730
|
+
* @param {number} length exact element count; integer in [1, 2^30-1].
|
|
731
|
+
* @param {'min'|'max'|'sum'|'gcd'} kind the frozen associative fold.
|
|
732
|
+
*/
|
|
733
|
+
constructor(length, kind) {
|
|
734
|
+
// typeof guard BEFORE coercion (Number.isInteger is Symbol/BigInt-safe).
|
|
735
|
+
if (typeof length !== 'number' || !Number.isInteger(length) ||
|
|
736
|
+
length < 1 || length > SEGTREE_MAX) {
|
|
737
|
+
throw new RangeError(
|
|
738
|
+
'[lite-logn] SegmentTree length must be an integer in [1, 2^30-1], got ' +
|
|
739
|
+
String(length));
|
|
740
|
+
}
|
|
741
|
+
const k = kind === 'min' ? 0 : kind === 'max' ? 1 : kind === 'sum' ? 2 :
|
|
742
|
+
kind === 'gcd' ? 3 : -1;
|
|
743
|
+
if (k === -1) {
|
|
744
|
+
throw new RangeError(
|
|
745
|
+
'[lite-logn] SegmentTree kind must be "min", "max", "sum" or "gcd", got ' +
|
|
746
|
+
String(kind));
|
|
747
|
+
}
|
|
748
|
+
this._n = length; // element count (fixed)
|
|
749
|
+
this._k = k; // ctor-frozen fold: 0 min 1 max 2 sum 3 gcd
|
|
750
|
+
this._idv = k === 0 ? Infinity : k === 1 ? -Infinity : 0; // fold identity
|
|
751
|
+
this._t = new Float64Array(2 * length); // _t[0] unused; leaves at n..2n-1
|
|
752
|
+
if (this._idv !== 0) this._t.fill(this._idv); // sum/gcd identity is 0 already
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** Element count this tree was sized for. O(1). */
|
|
756
|
+
get length() { return this._n; }
|
|
757
|
+
|
|
758
|
+
/** The frozen associative fold, 'min' | 'max' | 'sum' | 'gcd'. O(1). */
|
|
759
|
+
get kind() {
|
|
760
|
+
const k = this._k;
|
|
761
|
+
return k === 0 ? 'min' : k === 1 ? 'max' : k === 2 ? 'sum' : 'gcd';
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* The folded value over `[lo, hi]` INCLUSIVE on both ends. O(log n): walk the
|
|
766
|
+
* two boundaries up the tree, folding each node that lies fully inside the
|
|
767
|
+
* range into a single accumulator (started at the fold identity). Fails closed:
|
|
768
|
+
* out-of-range `lo` or `hi`, or `lo > hi`, each throw `[lite-logn]` (matching
|
|
769
|
+
* Fenwick.rangeSum). A one-element range `lo == hi` returns that leaf's value.
|
|
770
|
+
* @param {number} lo integer in [0, length)
|
|
771
|
+
* @param {number} hi integer in [lo, length)
|
|
772
|
+
* @returns {number} the fold over `[lo, hi]` (always folds at least one leaf)
|
|
773
|
+
*/
|
|
774
|
+
query(lo, hi) {
|
|
775
|
+
const n = this._n;
|
|
776
|
+
if (typeof lo !== 'number' || !Number.isInteger(lo) || lo < 0 || lo >= n) {
|
|
777
|
+
return this._badRange(lo, hi);
|
|
778
|
+
}
|
|
779
|
+
if (typeof hi !== 'number' || !Number.isInteger(hi) || hi < 0 || hi >= n) {
|
|
780
|
+
return this._badRange(lo, hi);
|
|
781
|
+
}
|
|
782
|
+
if (lo > hi) return this._badRange(lo, hi);
|
|
783
|
+
const t = this._t, k = this._k;
|
|
784
|
+
let res = this._idv;
|
|
785
|
+
// Order-agnostic fold (correct because the fold is commutative -- D-05).
|
|
786
|
+
for (let l = n + lo, r = n + hi + 1; l < r; l >>= 1, r >>= 1) {
|
|
787
|
+
if (l & 1) {
|
|
788
|
+
const v = t[l++];
|
|
789
|
+
res = k === 0 ? (v < res ? v : res) : k === 1 ? (v > res ? v : res) :
|
|
790
|
+
k === 2 ? res + v : segGcd(res, v);
|
|
791
|
+
}
|
|
792
|
+
if (r & 1) {
|
|
793
|
+
const v = t[--r];
|
|
794
|
+
res = k === 0 ? (v < res ? v : res) : k === 1 ? (v > res ? v : res) :
|
|
795
|
+
k === 2 ? res + v : segGcd(res, v);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return res;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Set the element at 0-based leaf `i` to `value` (ABSOLUTE), then fix every
|
|
803
|
+
* ancestor by recomputing its fold. O(log n): one leaf write plus one write
|
|
804
|
+
* per level up to the root. Fails closed: a non-finite value (typeof-guarded
|
|
805
|
+
* first), a gcd-kind value that is negative or non-integer, or an out-of-range
|
|
806
|
+
* index each throw `[lite-logn]` as a no-op.
|
|
807
|
+
* @param {number} i integer in [0, length)
|
|
808
|
+
* @param {number} value a finite number (nonnegative integer for the gcd kind)
|
|
809
|
+
* @returns {this}
|
|
810
|
+
*/
|
|
811
|
+
update(i, value) {
|
|
812
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return this._badValue(value);
|
|
813
|
+
if (this._k === 3 && (!Number.isInteger(value) || value < 0)) return this._badGcdValue(value);
|
|
814
|
+
if (typeof i !== 'number' || !Number.isInteger(i) || i < 0 || i >= this._n) {
|
|
815
|
+
return this._badIndex(i);
|
|
816
|
+
}
|
|
817
|
+
const t = this._t, k = this._k;
|
|
818
|
+
let p = this._n + i;
|
|
819
|
+
t[p] = value;
|
|
820
|
+
for (p >>= 1; p >= 1; p >>= 1) {
|
|
821
|
+
const c = p << 1; // left child; right is c + 1
|
|
822
|
+
const a = t[c], b = t[c + 1];
|
|
823
|
+
t[p] = k === 0 ? (a < b ? a : b) : k === 1 ? (a > b ? a : b) :
|
|
824
|
+
k === 2 ? a + b : segGcd(a, b);
|
|
825
|
+
}
|
|
826
|
+
return this;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* The single element at 0-based leaf `i` (the stored leaf value). O(1). An
|
|
831
|
+
* out-of-range index throws `[lite-logn]`.
|
|
832
|
+
* @param {number} i integer in [0, length)
|
|
833
|
+
* @returns {number}
|
|
834
|
+
*/
|
|
835
|
+
at(i) {
|
|
836
|
+
if (typeof i !== 'number' || !Number.isInteger(i) || i < 0 || i >= this._n) {
|
|
837
|
+
return this._badIndex(i);
|
|
838
|
+
}
|
|
839
|
+
return this._t[this._n + i];
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Reset every element to the fold identity in place, keeping the fixed
|
|
844
|
+
* capacity. O(n) cold path. Because `fold(identity, identity) === identity`,
|
|
845
|
+
* filling the WHOLE backing array (leaves and internal nodes alike) with the
|
|
846
|
+
* identity leaves a fully-consistent tree -- a query returns the identity.
|
|
847
|
+
* @returns {this}
|
|
848
|
+
*/
|
|
849
|
+
clear() {
|
|
850
|
+
this._t.fill(this._idv);
|
|
851
|
+
return this;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Visit every element as `(value, index, tree)` for index in `[0, length)`, in
|
|
856
|
+
* ASCENDING leaf order. O(n) COLD scan, allocation-free in the loop body (pass
|
|
857
|
+
* a hoisted callback).
|
|
858
|
+
* @param {(value:number, index:number, tree:SegmentTree)=>void} fn
|
|
859
|
+
*/
|
|
860
|
+
forEach(fn) {
|
|
861
|
+
const t = this._t, n = this._n;
|
|
862
|
+
for (let i = 0; i < n; i++) fn(t[n + i], i, this);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* O(n) bottom-up bulk build from `values` -- NOT n individual O(log n) updates.
|
|
867
|
+
* Seed each leaf `_t[n + i] = values[i]`, then fold every internal node once,
|
|
868
|
+
* deepest-first (`p` from `n - 1` down to `1`): `_t[p] = fold(_t[2p],
|
|
869
|
+
* _t[2p+1])`. COLD path; fails closed on a non-array-like `values`, any
|
|
870
|
+
* non-finite entry, or (gcd kind) any negative / non-integer entry before the
|
|
871
|
+
* tree is usable. `length` and `kind` are validated by the delegated ctor.
|
|
872
|
+
* @param {ArrayLike<number>} values finite numbers; length in [1, 2^30-1]
|
|
873
|
+
* @param {'min'|'max'|'sum'|'gcd'} kind the frozen associative fold
|
|
874
|
+
* @returns {SegmentTree}
|
|
875
|
+
*/
|
|
876
|
+
static build(values, kind) {
|
|
877
|
+
if (values == null || typeof values.length !== 'number') {
|
|
878
|
+
throw new TypeError('[lite-logn] SegmentTree.build needs an array-like of finite numbers');
|
|
879
|
+
}
|
|
880
|
+
const length = values.length;
|
|
881
|
+
const st = new SegmentTree(length, kind); // validates length in [1, 2^30-1] + kind
|
|
882
|
+
const t = st._t, n = st._n, k = st._k;
|
|
883
|
+
const gcdKind = k === 3;
|
|
884
|
+
for (let i = 0; i < length; i++) {
|
|
885
|
+
const v = values[i];
|
|
886
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) {
|
|
887
|
+
throw new TypeError(
|
|
888
|
+
'[lite-logn] SegmentTree.build value must be a finite number, got ' + String(v));
|
|
889
|
+
}
|
|
890
|
+
if (gcdKind && (!Number.isInteger(v) || v < 0)) {
|
|
891
|
+
throw new RangeError(
|
|
892
|
+
'[lite-logn] SegmentTree.build gcd value must be a nonnegative integer, got ' +
|
|
893
|
+
String(v));
|
|
894
|
+
}
|
|
895
|
+
t[n + i] = v; // seed the leaf
|
|
896
|
+
}
|
|
897
|
+
// Fold every internal node once, deepest-first -- O(n), not n * O(log n).
|
|
898
|
+
for (let p = n - 1; p >= 1; p--) {
|
|
899
|
+
const c = p << 1;
|
|
900
|
+
const a = t[c], b = t[c + 1];
|
|
901
|
+
t[p] = k === 0 ? (a < b ? a : b) : k === 1 ? (a > b ? a : b) :
|
|
902
|
+
k === 2 ? a + b : segGcd(a, b);
|
|
903
|
+
}
|
|
904
|
+
return st;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// ---- cold path only: throw builders (string concat off the hot body) ---
|
|
908
|
+
|
|
909
|
+
/** @private */
|
|
910
|
+
_badIndex(i) {
|
|
911
|
+
throw new RangeError(
|
|
912
|
+
'[lite-logn] SegmentTree index must be an integer in [0, ' + this._n + '), got ' +
|
|
913
|
+
String(i));
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** @private */
|
|
917
|
+
_badRange(lo, hi) {
|
|
918
|
+
throw new RangeError(
|
|
919
|
+
'[lite-logn] SegmentTree query needs integers 0 <= lo <= hi < ' + this._n +
|
|
920
|
+
', got lo=' + String(lo) + ' hi=' + String(hi));
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** @private */
|
|
924
|
+
_badValue(value) {
|
|
925
|
+
throw new TypeError(
|
|
926
|
+
'[lite-logn] SegmentTree value must be a finite number, got ' + String(value));
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** @private */
|
|
930
|
+
_badGcdValue(value) {
|
|
931
|
+
throw new RangeError(
|
|
932
|
+
'[lite-logn] SegmentTree gcd value must be a nonnegative integer, got ' + String(value));
|
|
933
|
+
}
|
|
934
|
+
}
|
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.3.0 ships three 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), and 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) is planned -- 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.3.0 ships three members: BinaryHeap, Fenwick and SegmentTree.** 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,13 @@ 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)
|
|
46
61
|
- [Zero-GC design notes](#zero-gc-design-notes)
|
|
47
62
|
- [Testing](#testing)
|
|
48
63
|
- [What this is not](#what-this-is-not)
|
|
@@ -65,16 +80,16 @@ lite-logn ships the O(log n) structures that matter with the allocation removed
|
|
|
65
80
|
- **Tree-shakeable named exports.** Members share no mutable module state, so a bundler that imports one drops the others.
|
|
66
81
|
- **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
82
|
|
|
68
|
-
## The
|
|
83
|
+
## The roster
|
|
69
84
|
|
|
70
|
-
One member per session, each landing append-only (prior members stay byte-identical). At v0.
|
|
85
|
+
One member per session, each landing append-only (prior members stay byte-identical). At v0.3.0, BinaryHeap, Fenwick and SegmentTree are shipped; SkipList is planned.
|
|
71
86
|
|
|
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 shared node pool; expected O(log n) | `get` / `set` / `delete` / `successor` |
|
|
87
|
+
| Member | Version | Status | Shape | Hot ops |
|
|
88
|
+
| --- | --- | --- | --- | --- |
|
|
89
|
+
| **BinaryHeap** | 0.1.0 | shipped | array-embedded complete binary min|max heap over a flat `Float64Array` | `push` / `pop` O(log n), `peek` O(1) |
|
|
90
|
+
| **Fenwick** (BIT) | 0.2.0 | shipped | flat `Float64Array`, lowest-set-bit walk (`i & -i`) | `update` / `prefix` / `rangeSum` / `at` / `set` O(log n) |
|
|
91
|
+
| **SegmentTree** | 0.3.0 | shipped | single flat `Float64Array(2n)` (leaves n..2n-1); associative fold (min/max/sum/gcd) chosen at construction | `query` / `update` O(log n), `at` O(1) |
|
|
92
|
+
| **SkipList** | 0.4.0 | planned | pointer-free over a shared node pool; expected O(log n) | `get` / `set` / `delete` / `successor` |
|
|
78
93
|
|
|
79
94
|
Later tiers (Treap / Scapegoat, OrderStatTree, IndexedHeap, SortedArray, MinMaxHeap, SplayTree, and presets) are queued in [`ROADMAP.md`](./ROADMAP.md).
|
|
80
95
|
|
|
@@ -86,7 +101,7 @@ The family anchor. Time a fixed batch of the hot op at each `n` in a geometric s
|
|
|
86
101
|
- `slope` inside the member's band (the per-level cost, ns/level), AND
|
|
87
102
|
- the FOIL leaves the line (low `R^2` -- the O(n) default a working programmer reaches for, shown losing as `n` grows).
|
|
88
103
|
|
|
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
|
|
104
|
+
For amortized / randomized members the witness also prints the MAX single-op time -- the honesty hook: a rebuild spike or a degenerate tail shows as a tall bar even when the mean still fits the line. The `R^2` floor (0.958) is frozen family-wide in BinaryHeap; each member then calibrates its OWN per-op slope band (median-of-15 fit-runs x `[0.6, 1.4]`), because a cheaper op honestly has a lower per-level slope (see [`decisions/0004-witness-band.md`](./decisions/0004-witness-band.md)). At v0.3.0 the witness gates five 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), and 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]`) all ON the line. SegmentTree's gated sweep is pinned to EXACT powers of two in `[2^10, 2^16]` (a segment-tree op touches a node per level spread across the `2n` array, so above ~2^16 the tree leaves the steady cache band, and exact powers keep the range decomposition a regular node count). Each op's O(n) foil fits well below the floor: the sorted-array insert (BinaryHeap) foil runs R^2 ~ 0.77-0.84, the Fenwick foils (prefix-array rebuild, naive re-sum) hold steadier at R^2 ~ 0.75-0.76, and SegmentTree's foils (whole-tree rebuild per update, scan-fold per query) fit at R^2 ~ 0.75-0.85 -- all foil families sit comfortably under the 0.958 floor.
|
|
90
105
|
|
|
91
106
|
## API reference
|
|
92
107
|
|
|
@@ -94,7 +109,7 @@ For amortized / randomized members the witness also prints the MAX single-op tim
|
|
|
94
109
|
|
|
95
110
|
| Export | Type | Value | Meaning |
|
|
96
111
|
| --- | --- | --- | --- |
|
|
97
|
-
| `VERSION` | `string` | `'0.
|
|
112
|
+
| `VERSION` | `string` | `'0.3.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
113
|
|
|
99
114
|
### BinaryHeap
|
|
100
115
|
|
|
@@ -135,6 +150,77 @@ heap.pop(); // -> 2 (the id whose key 9.0 is the max)
|
|
|
135
150
|
| `size` / `capacity` / `kind` | getters | O(1) | Live count / fixed capacity / `'min'` \| `'max'`. |
|
|
136
151
|
| `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
152
|
|
|
153
|
+
### Fenwick
|
|
154
|
+
|
|
155
|
+
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.
|
|
156
|
+
|
|
157
|
+
```js
|
|
158
|
+
import { Fenwick } from '@zakkster/lite-logn';
|
|
159
|
+
|
|
160
|
+
const f = new Fenwick(1000);
|
|
161
|
+
f.update(10, 5); // add 5 at index 10
|
|
162
|
+
f.update(20, 3); // add 3 at index 20
|
|
163
|
+
f.prefix(15); // -> 5 (sum of [0..15] inclusive)
|
|
164
|
+
f.prefix(-1); // -> 0 (the empty-prefix base case)
|
|
165
|
+
f.rangeSum(10, 20); // -> 8 (sum of [10..20] inclusive)
|
|
166
|
+
f.at(10); // -> 5 (single element = prefix(10) - prefix(9))
|
|
167
|
+
f.set(10, 100); // set index 10 to 100 (absolute)
|
|
168
|
+
f.prefix(20); // -> 103
|
|
169
|
+
|
|
170
|
+
// O(n) LINEAR bulk build (not n incremental updates):
|
|
171
|
+
const g = Fenwick.build([1, 2, 3, 4, 5]);
|
|
172
|
+
g.rangeSum(1, 3); // -> 9
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| Member | Signature | Complexity | Notes |
|
|
176
|
+
| --- | --- | --- | --- |
|
|
177
|
+
| constructor | `new Fenwick(length)` | O(length) | `length` integer in `[1, 2^31-1]`. Allocates one `Float64Array(length + 1)`, zero-initialized. |
|
|
178
|
+
| `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. |
|
|
179
|
+
| `prefix` | `prefix(i) -> number` | O(log n) | Sum of `[0, i]` INCLUSIVE (descend by `i & -i`). `prefix(-1) === 0`; valid domain `[-1, length)`. |
|
|
180
|
+
| `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`. |
|
|
181
|
+
| `at` | `at(i) -> number` | O(log n) | The single element = `prefix(i) - prefix(i-1)`. Out-of-range `i` throws. |
|
|
182
|
+
| `set` | `set(i, value) -> this` | O(log n) | Set element `i` to `value` (absolute), via `update(i, value - at(i))`. `value` finite. |
|
|
183
|
+
| `clear` | `clear() -> this` | O(length) | Zeros every element in place, keeping capacity. |
|
|
184
|
+
| `forEach` | `forEach(fn) -> void` | O(n log n) | Visits `(value, index, fenwick)` in ascending index order (each element is an `at` walk). |
|
|
185
|
+
| `length` | getter | O(1) | Element count this tree was sized for. |
|
|
186
|
+
| `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. |
|
|
187
|
+
|
|
188
|
+
### SegmentTree
|
|
189
|
+
|
|
190
|
+
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.
|
|
191
|
+
|
|
192
|
+
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.
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
import { SegmentTree } from '@zakkster/lite-logn';
|
|
196
|
+
|
|
197
|
+
const st = new SegmentTree(1000, 'min'); // 1000 slots, all +Infinity (min identity)
|
|
198
|
+
st.update(10, 5); // set index 10 to 5 (absolute)
|
|
199
|
+
st.update(20, 3); // set index 20 to 3
|
|
200
|
+
st.query(0, 999); // -> 3 (min over [0..999] inclusive)
|
|
201
|
+
st.query(10, 10); // -> 5 (a one-element range = the leaf)
|
|
202
|
+
st.at(20); // -> 3 (the single leaf value, O(1))
|
|
203
|
+
|
|
204
|
+
// A different fold, chosen at construction:
|
|
205
|
+
const sum = SegmentTree.build([1, 2, 3, 4, 5], 'sum'); // O(n) bottom-up bulk build
|
|
206
|
+
sum.query(1, 3); // -> 9 (2 + 3 + 4)
|
|
207
|
+
const g = SegmentTree.build([12, 18, 24], 'gcd');
|
|
208
|
+
g.query(0, 2); // -> 6
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
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)).
|
|
212
|
+
|
|
213
|
+
| Member | Signature | Complexity | Notes |
|
|
214
|
+
| --- | --- | --- | --- |
|
|
215
|
+
| 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)`. |
|
|
216
|
+
| `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. |
|
|
217
|
+
| `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. |
|
|
218
|
+
| `at` | `at(i) -> number` | O(1) | The single leaf value. Out-of-range `i` throws. |
|
|
219
|
+
| `clear` | `clear() -> this` | O(n) | Resets every element to the fold identity, keeping capacity. |
|
|
220
|
+
| `forEach` | `forEach(fn) -> void` | O(n) | Visits `(value, index, tree)` in ascending leaf order. |
|
|
221
|
+
| `length` / `kind` | getters | O(1) | Element count / the frozen fold `'min'` \| `'max'` \| `'sum'` \| `'gcd'`. |
|
|
222
|
+
| `SegmentTree.build` | `build(values, kind) -> SegmentTree` | O(n) | Bottom-up bulk build (seed leaves, then fold each internal node once deepest-first -- NOT n incremental updates); fails closed on a non-array-like, any non-finite value, or (gcd) any negative / non-integer. |
|
|
223
|
+
|
|
138
224
|
Member signatures for later members are appended here as each ships.
|
|
139
225
|
|
|
140
226
|
## Zero-GC design notes
|
|
@@ -147,8 +233,14 @@ Member signatures for later members are appended here as each ships.
|
|
|
147
233
|
| --- | --- |
|
|
148
234
|
| `BinaryHeap` push / pop / peek / topKey / keyOf / has / changeKey / remove | 0 B/op |
|
|
149
235
|
| `BinaryHeap` constructor / `build` / `clear` | O(capacity) typed arrays, once (cold) |
|
|
150
|
-
|
|
151
|
-
|
|
236
|
+
| `Fenwick` update / prefix / rangeSum / at / set | 0 B/op |
|
|
237
|
+
| `Fenwick` constructor / `build` / `clear` | O(length) typed array, once (cold) |
|
|
238
|
+
| `Fenwick` forEach | 0 B/op in the loop body (pass a hoisted callback) |
|
|
239
|
+
| `SegmentTree` query / update / at | 0 B/op |
|
|
240
|
+
| `SegmentTree` constructor / `build` / `clear` | O(length) typed array (`2n` cells), once (cold) |
|
|
241
|
+
| `SegmentTree` forEach | 0 B/op in the loop body (pass a hoisted callback) |
|
|
242
|
+
|
|
243
|
+
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]`). The allocation table is extended per member as each lands.
|
|
152
244
|
|
|
153
245
|
## Testing
|
|
154
246
|
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-logn
|
|
2
2
|
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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. The
|
|
22
|
+
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,11 +29,15 @@ 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
|
|
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.
|
|
37
41
|
- SkipList (v0.4.0) -- the ordered map (get / set / delete / successor /
|
|
38
42
|
rangeIter), pointer-free over a shared node pool (parallel Uint32Array `next`
|
|
39
43
|
columns), expected O(log n), deterministic seed, tail reported.
|
|
@@ -60,6 +64,54 @@ session, each landing append-only):
|
|
|
60
64
|
build from parallel arrays; fails closed on duplicate/out-of-range id,
|
|
61
65
|
non-finite key, or count > capacity.
|
|
62
66
|
|
|
67
|
+
- `Fenwick` -- class. A Fenwick tree (Binary Indexed Tree): BOTH point-update AND
|
|
68
|
+
prefix-sum in O(log n) over a single flat `Float64Array` via the lowest-set-bit
|
|
69
|
+
walk (`i & -i`). Public indices 0-based in [0, length); internally 1-based
|
|
70
|
+
(`_t[0]` the unused identity sentinel -- null is not zero). Values are finite
|
|
71
|
+
numbers (negatives allowed); NaN / +-Infinity / non-number fail closed.
|
|
72
|
+
- `new Fenwick(length)` -- length is an integer in [1, 2^31-1]. Allocates one
|
|
73
|
+
`Float64Array(length + 1)`, zero-initialized.
|
|
74
|
+
- `update(i, delta)` -> this. Add delta at 0-based index i (climb by `i & -i`);
|
|
75
|
+
delta must be a finite number (typeof-guarded first); out-of-range i throws.
|
|
76
|
+
- `prefix(i)` -> number. Sum of [0, i] INCLUSIVE (descend by `i & -i`).
|
|
77
|
+
`prefix(-1) === 0` is the clean base case; valid domain [-1, length).
|
|
78
|
+
- `rangeSum(lo, hi)` -> number. Sum of [lo, hi] INCLUSIVE both ends =
|
|
79
|
+
prefix(hi) - prefix(lo-1); throws on out-of-range or lo > hi.
|
|
80
|
+
- `at(i)` -> number. The single element = prefix(i) - prefix(i-1).
|
|
81
|
+
- `set(i, value)` -> this. Set element i to value (absolute), via
|
|
82
|
+
update(i, value - at(i)); value must be finite.
|
|
83
|
+
- `length` getter; `clear()` -> this (zero-fill, keep capacity);
|
|
84
|
+
`forEach(fn)` visits (value, index, fenwick) in ascending index order.
|
|
85
|
+
- `Fenwick.build(values)` -> Fenwick. O(n) LINEAR bulk build (each cell adds
|
|
86
|
+
itself to its parent in one pass -- NOT n incremental updates); fails closed
|
|
87
|
+
on a non-array-like or any non-finite value.
|
|
88
|
+
|
|
89
|
+
- `SegmentTree` -- class. An associative range-query AND a point-update, BOTH
|
|
90
|
+
O(log n), over a SINGLE flat `Float64Array(2n)` (leaves at n..2n-1, `_t[0]`
|
|
91
|
+
unused). The fold (min / max / sum / gcd) is chosen ONCE at construction and
|
|
92
|
+
cached as a small-int combined by an inline switch on the hot path. The
|
|
93
|
+
iterative 2n layout is order-agnostic -- correct because all four folds are
|
|
94
|
+
commutative + associative (a non-commutative fold would need a pow2 layout).
|
|
95
|
+
Identity fills query accumulators + cleared / fresh leaves (sum -> 0, min ->
|
|
96
|
+
+Infinity, max -> -Infinity, gcd -> 0) but is never a legal INPUT: the value
|
|
97
|
+
door rejects user NaN / +-Infinity, and the gcd kind additionally rejects
|
|
98
|
+
negatives + non-integers (typeof-guarded before coercion).
|
|
99
|
+
- `new SegmentTree(length, kind)` -- length an integer in [1, 2^30-1] (HALF of
|
|
100
|
+
Fenwick's ceiling: the 2n array must keep `2n` a positive int32); kind is
|
|
101
|
+
'min' | 'max' | 'sum' | 'gcd'. Allocates one `Float64Array(2 * length)`.
|
|
102
|
+
- `query(lo, hi)` -> number. The fold over [lo, hi] INCLUSIVE both ends; throws
|
|
103
|
+
on out-of-range or lo > hi (matching Fenwick.rangeSum). lo == hi returns that
|
|
104
|
+
single leaf. A fresh / cleared tree queries to the identity.
|
|
105
|
+
- `update(i, value)` -> this. Set leaf i to value (ABSOLUTE), then fix ancestors;
|
|
106
|
+
value finite (nonnegative integer for the gcd kind), out-of-range i throws.
|
|
107
|
+
- `at(i)` -> number. The single leaf value. O(1). Out-of-range i throws.
|
|
108
|
+
- `length` / `kind` getters; `clear()` -> this (reset to identity, keep
|
|
109
|
+
capacity); `forEach(fn)` visits (value, index, tree) in ascending leaf order.
|
|
110
|
+
- `SegmentTree.build(values, kind)` -> SegmentTree. O(n) bottom-up bulk build
|
|
111
|
+
(seed leaves, then fold each internal node once deepest-first -- NOT n
|
|
112
|
+
incremental updates); fails closed on a non-array-like, any non-finite value,
|
|
113
|
+
or (gcd) any negative / non-integer.
|
|
114
|
+
|
|
63
115
|
Member exports (one tree-shakeable class each) are appended here as each member
|
|
64
116
|
ships.
|
|
65
117
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-logn",
|
|
3
3
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
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) -- planned -- 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",
|