@zakkster/lite-logn 0.1.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 +44 -0
- package/LICENSE +21 -0
- package/LogN.d.ts +66 -0
- package/LogN.js +394 -0
- package/README.md +181 -0
- package/llms.txt +83 -0
- package/package.json +95 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@zakkster/lite-logn` are documented here. The format
|
|
4
|
+
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-09-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **BinaryHeap** -- the first member: an indexed binary min|max heap (addressable
|
|
14
|
+
priority queue) over three parallel typed arrays (`_key` Float64Array, `_id`
|
|
15
|
+
Uint32Array, `_pos` Int32Array reverse map). Surface: `push` / `pop` / `peek` /
|
|
16
|
+
`topKey` / `keyOf` / `has` / `changeKey` / `remove`, `size` / `capacity` /
|
|
17
|
+
`kind` getters, `clear`, `forEach` / `[Symbol.iterator]` (unspecified order),
|
|
18
|
+
and a static `BinaryHeap.build(kind, ids, keys, capacity)` Floyd O(n) bulk
|
|
19
|
+
build. changeKey / remove address elements by caller-supplied entity id via the
|
|
20
|
+
reverse-index map. push / pop / changeKey / remove are O(log n) with
|
|
21
|
+
hole-punching sift; peek / topKey / keyOf / has are O(1). Fixed-capacity
|
|
22
|
+
fail-closed (overflow, duplicate id, non-member changeKey, out-of-range id, and
|
|
23
|
+
non-finite key all throw `[lite-logn]`; never a silent drop). Zero allocation on
|
|
24
|
+
every hot path.
|
|
25
|
+
- **Scaffold release.** Stands up the repo for the O(log n) family, the sibling
|
|
26
|
+
of `@zakkster/lite-o1`. Ships the six `files[]` entries: `LogN.js` (header +
|
|
27
|
+
the `VERSION` const, no member yet), `LogN.d.ts`, `llms.txt`, `README.md`,
|
|
28
|
+
`CHANGELOG.md`, and `LICENSE`. Repo-only (not shipped): `GUIDE.md` and the
|
|
29
|
+
gate harnesses (`test/torture.mjs`, `test/witness.mjs`,
|
|
30
|
+
`test/perf/PerfGate.test.mjs`, `test/QaAudit.test.js`, `test/Bench.test.mjs`).
|
|
31
|
+
With zero members the harnesses run green and empty.
|
|
32
|
+
- **The witness harness stub.** The log-linear fit machinery
|
|
33
|
+
(`nsPerOp = intercept + slope*log2(n)`, least-squares `R^2` + slope) and an
|
|
34
|
+
O(n) foil are in place; the `R^2` floor + slope band are deliberately NOT
|
|
35
|
+
gated yet -- they are calibrated in the BinaryHeap session (decision D-02).
|
|
36
|
+
- **Design decisions on the record.** [`decisions/0001-nodepool.md`](./decisions/0001-nodepool.md)
|
|
37
|
+
(D-01: reconcile the pointer-free node pool against lite-o1's SlotPool, do not
|
|
38
|
+
fork), [`decisions/0002-spine.md`](./decisions/0002-spine.md) (D-03: the
|
|
39
|
+
ordered-collection spine reach -- offer where honest, force nowhere), and
|
|
40
|
+
[`decisions/0003-pack.md`](./decisions/0003-pack.md) (D-07: `files[]` ships the
|
|
41
|
+
six files only; `test/`, `benchmark/`, `decisions/`, `demo/` are repo-only).
|
|
42
|
+
|
|
43
|
+
[Unreleased]: https://github.com/PeshoVurtoleta/lite-logn/compare/v0.1.0...HEAD
|
|
44
|
+
[0.1.0]: https://github.com/PeshoVurtoleta/lite-logn/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/LogN.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zakkster/lite-logn -- ambient type surface.
|
|
3
|
+
*
|
|
4
|
+
* Hand-written to mirror EXACTLY the runtime exports of LogN.js. The three-place
|
|
5
|
+
* version sync (package.json / LogN.js VERSION / llms.txt) is enforced in review;
|
|
6
|
+
* at v0.1.0 (the scaffold release) the only export is `VERSION`. One ambient
|
|
7
|
+
* declaration block per member is appended here as each member ships. ASCII-only.
|
|
8
|
+
*
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Package version string. */
|
|
13
|
+
export const VERSION: string;
|
|
14
|
+
|
|
15
|
+
// --- member type blocks land here, append-only, one per shipped member -------
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An indexed binary heap (addressable priority queue): a min|max binary heap over
|
|
19
|
+
* three parallel typed arrays plus a reverse-index map, giving O(log n)
|
|
20
|
+
* changeKey / remove by caller-supplied entity id. Entity ids are integers in
|
|
21
|
+
* [0, capacity); keys are finite numbers. Every hot op allocates zero bytes.
|
|
22
|
+
*/
|
|
23
|
+
export class BinaryHeap {
|
|
24
|
+
/** @param capacity exact max live entries; integer in [1, 2^31-1].
|
|
25
|
+
* @param kind the frozen extreme this heap reports (default 'min'). */
|
|
26
|
+
constructor(capacity: number, kind?: 'min' | 'max');
|
|
27
|
+
|
|
28
|
+
/** Live entry count. */
|
|
29
|
+
readonly size: number;
|
|
30
|
+
/** The fixed capacity this heap was sized for. */
|
|
31
|
+
readonly capacity: number;
|
|
32
|
+
/** The frozen extreme this heap reports. */
|
|
33
|
+
readonly kind: 'min' | 'max';
|
|
34
|
+
|
|
35
|
+
/** Insert entity id with priority key. Throws on out-of-range/duplicate id,
|
|
36
|
+
* non-finite key, or a full heap. */
|
|
37
|
+
push(id: number, key: number): void;
|
|
38
|
+
/** Remove and return the extremum's entity id, or undefined if empty. */
|
|
39
|
+
pop(): number | undefined;
|
|
40
|
+
/** The extremum's entity id, or undefined if empty. */
|
|
41
|
+
peek(): number | undefined;
|
|
42
|
+
/** The extremum's key, or undefined if empty. */
|
|
43
|
+
topKey(): number | undefined;
|
|
44
|
+
/** The key associated with id, or undefined if absent. Out-of-range id throws. */
|
|
45
|
+
keyOf(id: number): number | undefined;
|
|
46
|
+
/** True iff id is currently in the heap. Out-of-range id throws. */
|
|
47
|
+
has(id: number): boolean;
|
|
48
|
+
/** Reprioritize a present entity, auto-directing the sift. Non-member throws. */
|
|
49
|
+
changeKey(id: number, newKey: number): void;
|
|
50
|
+
/** Remove entity id; true if it was present, false if absent (idempotent). */
|
|
51
|
+
remove(id: number): boolean;
|
|
52
|
+
/** Empty the heap. */
|
|
53
|
+
clear(): void;
|
|
54
|
+
/** Visit every live (id, key) pair in unspecified (heap-array) order. */
|
|
55
|
+
forEach(fn: (id: number, key: number, heap: BinaryHeap) => void): void;
|
|
56
|
+
/** Iterate live entity ids in unspecified (heap-array) order (NOT sorted). */
|
|
57
|
+
[Symbol.iterator](): IterableIterator<number>;
|
|
58
|
+
|
|
59
|
+
/** Floyd O(n) bulk build from parallel ids/keys arrays. */
|
|
60
|
+
static build(
|
|
61
|
+
kind: 'min' | 'max',
|
|
62
|
+
ids: ArrayLike<number>,
|
|
63
|
+
keys: ArrayLike<number>,
|
|
64
|
+
capacity: number,
|
|
65
|
+
): BinaryHeap;
|
|
66
|
+
}
|
package/LogN.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zakkster/lite-logn -- a tree-shakeable, zero-GC family of O(log n) data
|
|
3
|
+
* structures that doubles as a teachable textbook: each member solves a real
|
|
4
|
+
* problem AND proves its logarithm is real (the O(log n) Witness -- see
|
|
5
|
+
* test/witness.mjs).
|
|
6
|
+
*
|
|
7
|
+
* v0.1.0 ships its FIRST member: BinaryHeap, an INDEXED binary heap -- an
|
|
8
|
+
* addressable priority queue (a min|max binary heap over three parallel typed
|
|
9
|
+
* arrays plus a reverse-index map that makes changeKey / remove O(log n) by
|
|
10
|
+
* caller-supplied entity id). Members land append-only, leaving this header and
|
|
11
|
+
* the `VERSION` const the only prior lines that ever change. Planned roster:
|
|
12
|
+
* BinaryHeap (this release, array-embedded O(log n) push / pop min|max heap),
|
|
13
|
+
* Fenwick / BIT (O(log n) point-update AND prefix-sum via the
|
|
14
|
+
* `i & -i` walk), SegmentTree (O(log n) associative range-query + point-update),
|
|
15
|
+
* and SkipList (pointer-free expected-O(log n) ordered map). Members are
|
|
16
|
+
* independent (no shared mutable module state), so a bundler that imports one
|
|
17
|
+
* drops the others (`sideEffects: false`).
|
|
18
|
+
*
|
|
19
|
+
* The family delta: lite-o1 proves a FLAT ops/ms line on a log-x axis (the
|
|
20
|
+
* constant, slope ~ 0); lite-logn proves a STRAIGHT line on that same axis (one
|
|
21
|
+
* added level per doubling of n, slope > 0 within a per-member band). Every hot
|
|
22
|
+
* op each member ships is O(log n) worst-case (or expected/amortized where
|
|
23
|
+
* stated) and allocates ZERO bytes after construction. The witness harness
|
|
24
|
+
* (never imported here) fits `nsPerOp = intercept + slope * log2(n)` and shows
|
|
25
|
+
* the straight log line while an O(n) foil leaves it -- that line is the theorem
|
|
26
|
+
* made visible.
|
|
27
|
+
*
|
|
28
|
+
* @license MIT
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** Package version. One of the three version sites (package.json / VERSION / llms.txt). */
|
|
32
|
+
export const VERSION = '0.1.0';
|
|
33
|
+
|
|
34
|
+
// --- members land here, append-only, one tree-shakeable class each -----------
|
|
35
|
+
// BinaryHeap (v0.1.0 session) -- indexed O(log n) min|max heap (BELOW)
|
|
36
|
+
// Fenwick (v0.2.0) -- O(log n) point-update + prefix-sum
|
|
37
|
+
// SegmentTree (v0.3.0) -- O(log n) associative range-query + point-update
|
|
38
|
+
// SkipList (v0.4.0) -- pointer-free expected-O(log n) ordered map
|
|
39
|
+
|
|
40
|
+
/** Max heap capacity: slot indices 0..cap-1 must fit the Int32Array _pos map. */
|
|
41
|
+
const BH_MAX_CAPACITY = 0x7FFFFFFF; // 2^31 - 1
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* An INDEXED binary heap: an addressable priority queue over three parallel,
|
|
45
|
+
* pointer-free typed arrays plus a reverse-index map. A plain binary heap gives
|
|
46
|
+
* O(log n) push / pop but cannot find an arbitrary element to reprioritize; the
|
|
47
|
+
* `_pos` map (entity id -> current heap slot, sentinel -1 == absent) buys
|
|
48
|
+
* O(log n) `changeKey` / `remove` by a caller-supplied entity id. Every hot op
|
|
49
|
+
* (push / pop / peek / topKey / keyOf / has / changeKey / remove) allocates ZERO
|
|
50
|
+
* bytes after construction.
|
|
51
|
+
*
|
|
52
|
+
* Storage (allocated once, sized to capacity):
|
|
53
|
+
* - `_key` Float64Array -- the priority key at each heap SLOT (heap-ordered).
|
|
54
|
+
* - `_id` Uint32Array -- the entity id at each heap SLOT (moves with its key).
|
|
55
|
+
* - `_pos` Int32Array -- reverse map id -> slot; -1 means "not in heap".
|
|
56
|
+
* `_pos` is initialised to -1 (fill), NOT 0, because slot 0 is a valid position
|
|
57
|
+
* -- null is not zero. Children of slot i are 2i+1 / 2i+2, parent (i-1)>>1.
|
|
58
|
+
*
|
|
59
|
+
* The sift is HOLE-PUNCHING (not a 3-write swap): the moving element is cached in
|
|
60
|
+
* locals once, the hole walks down / up writing ONE slot per level (updating
|
|
61
|
+
* `_pos` for each shifted id), then the cached element drops into the final hole.
|
|
62
|
+
*
|
|
63
|
+
* ID contract: entity ids are integers in [0, capacity). Any out-of-range id is a
|
|
64
|
+
* hard `[lite-logn]` throw (a programming error). A key must be a finite number
|
|
65
|
+
* where required; NaN / non-finite / non-number fail closed rather than corrupt
|
|
66
|
+
* heap order. Fixed capacity: overflow throws, never silently drops.
|
|
67
|
+
*/
|
|
68
|
+
export class BinaryHeap {
|
|
69
|
+
/**
|
|
70
|
+
* @param {number} capacity exact max live entries; integer in [1, 2^31-1].
|
|
71
|
+
* @param {'min'|'max'} [kind] the frozen extreme this heap reports (default 'min').
|
|
72
|
+
*/
|
|
73
|
+
constructor(capacity, kind = 'min') {
|
|
74
|
+
// typeof guard BEFORE coercion (Number.isInteger is Symbol/BigInt-safe).
|
|
75
|
+
if (typeof capacity !== 'number' || !Number.isInteger(capacity) ||
|
|
76
|
+
capacity < 1 || capacity > BH_MAX_CAPACITY) {
|
|
77
|
+
throw new RangeError(
|
|
78
|
+
'[lite-logn] BinaryHeap capacity must be an integer in [1, 2^31-1], got ' +
|
|
79
|
+
String(capacity));
|
|
80
|
+
}
|
|
81
|
+
if (kind !== 'min' && kind !== 'max') {
|
|
82
|
+
throw new RangeError(
|
|
83
|
+
'[lite-logn] BinaryHeap kind must be "min" or "max", got ' + String(kind));
|
|
84
|
+
}
|
|
85
|
+
this._key = new Float64Array(capacity); // key at each heap slot
|
|
86
|
+
this._id = new Uint32Array(capacity); // entity id at each heap slot
|
|
87
|
+
this._pos = new Int32Array(capacity); // id -> slot; -1 == absent
|
|
88
|
+
this._pos.fill(-1); // slot 0 is valid: null is not zero
|
|
89
|
+
this._cap = capacity; // exact fixed capacity
|
|
90
|
+
this._n = 0; // live entries (heap size)
|
|
91
|
+
this._min = kind === 'min'; // ctor-frozen hot compare flag
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Live entry count. O(1). */
|
|
95
|
+
get size() { return this._n; }
|
|
96
|
+
|
|
97
|
+
/** The fixed capacity this heap was sized for. O(1). */
|
|
98
|
+
get capacity() { return this._cap; }
|
|
99
|
+
|
|
100
|
+
/** The frozen extreme this heap reports, 'min' or 'max'. O(1). */
|
|
101
|
+
get kind() { return this._min ? 'min' : 'max'; }
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Insert entity `id` with priority `key`. O(log n). Fails closed: an
|
|
105
|
+
* out-of-range id, an already-present id (no silent overwrite), a non-finite /
|
|
106
|
+
* non-number key, or a full heap each throw `[lite-logn]` as a no-op.
|
|
107
|
+
* @param {number} id integer in [0, capacity), not already present
|
|
108
|
+
* @param {number} key a finite number
|
|
109
|
+
*/
|
|
110
|
+
push(id, key) {
|
|
111
|
+
if (typeof id !== 'number' || !Number.isInteger(id) || id < 0 || id >= this._cap) {
|
|
112
|
+
return this._badId(id);
|
|
113
|
+
}
|
|
114
|
+
if (typeof key !== 'number' || !Number.isFinite(key)) return this._badKey(key);
|
|
115
|
+
if (this._pos[id] !== -1) return this._dup(id);
|
|
116
|
+
const n = this._n;
|
|
117
|
+
if (n === this._cap) return this._full();
|
|
118
|
+
this._n = n + 1;
|
|
119
|
+
this._siftUp(n, key, id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Remove and return the entity id at the extremum (min or max per kind), or
|
|
124
|
+
* `undefined` if empty (never throws on empty). O(log n).
|
|
125
|
+
* @returns {number|undefined}
|
|
126
|
+
*/
|
|
127
|
+
pop() {
|
|
128
|
+
const n = this._n;
|
|
129
|
+
if (n === 0) return undefined;
|
|
130
|
+
const top = this._id[0];
|
|
131
|
+
this._pos[top] = -1;
|
|
132
|
+
const last = n - 1;
|
|
133
|
+
this._n = last;
|
|
134
|
+
if (last > 0) this._siftDown(0, this._key[last], this._id[last]);
|
|
135
|
+
return top;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The entity id at the extremum, or `undefined` if empty. Read-only. O(1). */
|
|
139
|
+
peek() { return this._n === 0 ? undefined : this._id[0]; }
|
|
140
|
+
|
|
141
|
+
/** The key at the extremum (root), or `undefined` if empty. O(1). */
|
|
142
|
+
topKey() { return this._n === 0 ? undefined : this._key[0]; }
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The key currently associated with `id`, or `undefined` if id is not in the
|
|
146
|
+
* heap. O(1). An out-of-range id still throws.
|
|
147
|
+
* @param {number} id
|
|
148
|
+
* @returns {number|undefined}
|
|
149
|
+
*/
|
|
150
|
+
keyOf(id) {
|
|
151
|
+
if (typeof id !== 'number' || !Number.isInteger(id) || id < 0 || id >= this._cap) {
|
|
152
|
+
return this._badId(id);
|
|
153
|
+
}
|
|
154
|
+
const slot = this._pos[id];
|
|
155
|
+
return slot === -1 ? undefined : this._key[slot];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* True iff `id` is currently in the heap. O(1). An out-of-range id throws.
|
|
160
|
+
* @param {number} id
|
|
161
|
+
* @returns {boolean}
|
|
162
|
+
*/
|
|
163
|
+
has(id) {
|
|
164
|
+
if (typeof id !== 'number' || !Number.isInteger(id) || id < 0 || id >= this._cap) {
|
|
165
|
+
return this._badId(id);
|
|
166
|
+
}
|
|
167
|
+
return this._pos[id] !== -1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Reprioritize a present entity to `newKey`, auto-directing the sift. O(log n).
|
|
172
|
+
* Compares the new key against the current parent to pick the single direction
|
|
173
|
+
* that can violate order (up XOR down), then sifts that way with hole-punching.
|
|
174
|
+
* Fails closed: a non-member id throws `[lite-logn]` (never a silent no-op); an
|
|
175
|
+
* out-of-range id or non-finite key throws.
|
|
176
|
+
* @param {number} id integer in [0, capacity), currently present
|
|
177
|
+
* @param {number} newKey a finite number
|
|
178
|
+
*/
|
|
179
|
+
changeKey(id, newKey) {
|
|
180
|
+
if (typeof id !== 'number' || !Number.isInteger(id) || id < 0 || id >= this._cap) {
|
|
181
|
+
return this._badId(id);
|
|
182
|
+
}
|
|
183
|
+
if (typeof newKey !== 'number' || !Number.isFinite(newKey)) return this._badKey(newKey);
|
|
184
|
+
const slot = this._pos[id];
|
|
185
|
+
if (slot === -1) return this._notMember(id);
|
|
186
|
+
if (slot > 0) {
|
|
187
|
+
const pk = this._key[(slot - 1) >> 1];
|
|
188
|
+
if (this._min ? (newKey < pk) : (newKey > pk)) {
|
|
189
|
+
this._siftUp(slot, newKey, id);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
this._siftDown(slot, newKey, id);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Remove entity `id`. O(log n). Idempotent: returns false if id is absent (no
|
|
198
|
+
* throw), true if it was present and removed. The last entry fills the vacated
|
|
199
|
+
* slot and sifts the single direction that can violate order. An out-of-range
|
|
200
|
+
* id throws.
|
|
201
|
+
* @param {number} id
|
|
202
|
+
* @returns {boolean}
|
|
203
|
+
*/
|
|
204
|
+
remove(id) {
|
|
205
|
+
if (typeof id !== 'number' || !Number.isInteger(id) || id < 0 || id >= this._cap) {
|
|
206
|
+
return this._badId(id);
|
|
207
|
+
}
|
|
208
|
+
const slot = this._pos[id];
|
|
209
|
+
if (slot === -1) return false;
|
|
210
|
+
this._pos[id] = -1;
|
|
211
|
+
const last = this._n - 1;
|
|
212
|
+
this._n = last;
|
|
213
|
+
if (slot !== last) {
|
|
214
|
+
const mk = this._key[last];
|
|
215
|
+
const mid = this._id[last];
|
|
216
|
+
if (slot > 0) {
|
|
217
|
+
const pk = this._key[(slot - 1) >> 1];
|
|
218
|
+
if (this._min ? (mk < pk) : (mk > pk)) {
|
|
219
|
+
this._siftUp(slot, mk, mid);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
this._siftDown(slot, mk, mid);
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Empty the heap. O(capacity) cold path (resets size + the reverse map). */
|
|
229
|
+
clear() {
|
|
230
|
+
this._n = 0;
|
|
231
|
+
this._pos.fill(-1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Visit every live (id, key) pair in UNSPECIFIED (heap-array) order -- NOT
|
|
236
|
+
* sorted / pop order. O(n) cold scan, allocation-free (pass a hoisted callback).
|
|
237
|
+
* @param {(id:number, key:number, heap:BinaryHeap)=>void} fn
|
|
238
|
+
*/
|
|
239
|
+
forEach(fn) {
|
|
240
|
+
const id = this._id, key = this._key, n = this._n;
|
|
241
|
+
for (let i = 0; i < n; i++) fn(id[i], key[i], this);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Iterate live entity ids in UNSPECIFIED (heap-array) order -- NOT sorted. The
|
|
246
|
+
* one documented per-protocol allocator (a {value, done} per step); use forEach
|
|
247
|
+
* for the alloc-free scan.
|
|
248
|
+
*/
|
|
249
|
+
*[Symbol.iterator]() {
|
|
250
|
+
const id = this._id, n = this._n;
|
|
251
|
+
for (let i = 0; i < n; i++) yield id[i];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Floyd O(n) bulk build: load every (ids[i], keys[i]) pair then heapify
|
|
256
|
+
* bottom-up in O(n), rather than n individual O(log n) pushes. COLD path;
|
|
257
|
+
* fails closed on any violation (duplicate / out-of-range id, non-finite key,
|
|
258
|
+
* count > capacity) before the heap is usable.
|
|
259
|
+
* @param {'min'|'max'} kind
|
|
260
|
+
* @param {ArrayLike<number>} ids unique integers in [0, capacity)
|
|
261
|
+
* @param {ArrayLike<number>} keys finite numbers, keys.length === ids.length
|
|
262
|
+
* @param {number} capacity
|
|
263
|
+
* @returns {BinaryHeap}
|
|
264
|
+
*/
|
|
265
|
+
static build(kind, ids, keys, capacity) {
|
|
266
|
+
const heap = new BinaryHeap(capacity, kind);
|
|
267
|
+
if (ids == null || keys == null ||
|
|
268
|
+
typeof ids.length !== 'number' || typeof keys.length !== 'number') {
|
|
269
|
+
throw new TypeError('[lite-logn] BinaryHeap.build needs array-like ids and keys');
|
|
270
|
+
}
|
|
271
|
+
const count = ids.length;
|
|
272
|
+
if (keys.length !== count) {
|
|
273
|
+
throw new RangeError(
|
|
274
|
+
'[lite-logn] BinaryHeap.build ids/keys length mismatch (' +
|
|
275
|
+
count + ' vs ' + keys.length + ')');
|
|
276
|
+
}
|
|
277
|
+
if (count > capacity) {
|
|
278
|
+
throw new RangeError(
|
|
279
|
+
'[lite-logn] BinaryHeap.build count ' + count + ' exceeds capacity ' + capacity);
|
|
280
|
+
}
|
|
281
|
+
const K = heap._key, I = heap._id, P = heap._pos;
|
|
282
|
+
for (let i = 0; i < count; i++) {
|
|
283
|
+
const id = ids[i];
|
|
284
|
+
if (typeof id !== 'number' || !Number.isInteger(id) || id < 0 || id >= capacity) {
|
|
285
|
+
throw new RangeError(
|
|
286
|
+
'[lite-logn] BinaryHeap.build id must be an integer in [0, capacity), got ' +
|
|
287
|
+
String(id));
|
|
288
|
+
}
|
|
289
|
+
const key = keys[i];
|
|
290
|
+
if (typeof key !== 'number' || !Number.isFinite(key)) {
|
|
291
|
+
throw new TypeError(
|
|
292
|
+
'[lite-logn] BinaryHeap.build key must be a finite number, got ' + String(key));
|
|
293
|
+
}
|
|
294
|
+
if (P[id] !== -1) {
|
|
295
|
+
throw new RangeError('[lite-logn] BinaryHeap.build duplicate id ' + id);
|
|
296
|
+
}
|
|
297
|
+
K[i] = key;
|
|
298
|
+
I[i] = id;
|
|
299
|
+
P[id] = i;
|
|
300
|
+
}
|
|
301
|
+
heap._n = count;
|
|
302
|
+
// Floyd: sift down every internal node, deepest-first. O(n).
|
|
303
|
+
for (let i = (count >> 1) - 1; i >= 0; i--) {
|
|
304
|
+
heap._siftDown(i, K[i], I[i]);
|
|
305
|
+
}
|
|
306
|
+
return heap;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---- private hole-punching sift (hot bodies) ---------------------------
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Walk the hole UP from `hole`, pulling each larger (min) / smaller (max)
|
|
313
|
+
* parent down one level, then drop (key, id) into the settled hole. One write
|
|
314
|
+
* per level; `_pos` updated for every shifted id and for the placed id.
|
|
315
|
+
* @private
|
|
316
|
+
*/
|
|
317
|
+
_siftUp(hole, key, id) {
|
|
318
|
+
const K = this._key, I = this._id, P = this._pos, min = this._min;
|
|
319
|
+
while (hole > 0) {
|
|
320
|
+
const parent = (hole - 1) >> 1;
|
|
321
|
+
const pk = K[parent];
|
|
322
|
+
if (min ? (key >= pk) : (key <= pk)) break;
|
|
323
|
+
K[hole] = pk;
|
|
324
|
+
const pid = I[parent];
|
|
325
|
+
I[hole] = pid;
|
|
326
|
+
P[pid] = hole;
|
|
327
|
+
hole = parent;
|
|
328
|
+
}
|
|
329
|
+
K[hole] = key;
|
|
330
|
+
I[hole] = id;
|
|
331
|
+
P[id] = hole;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Walk the hole DOWN from `hole`, pulling the extreme child up one level, then
|
|
336
|
+
* drop (key, id) into the settled hole. One write per level; `_pos` updated for
|
|
337
|
+
* every shifted id and for the placed id. Bound is the current `_n`.
|
|
338
|
+
* @private
|
|
339
|
+
*/
|
|
340
|
+
_siftDown(hole, key, id) {
|
|
341
|
+
const K = this._key, I = this._id, P = this._pos, min = this._min;
|
|
342
|
+
const n = this._n;
|
|
343
|
+
const half = n >> 1; // nodes at index >= half are leaves
|
|
344
|
+
while (hole < half) {
|
|
345
|
+
let child = (hole << 1) + 1; // left child
|
|
346
|
+
const right = child + 1;
|
|
347
|
+
if (right < n && (min ? (K[right] < K[child]) : (K[right] > K[child]))) {
|
|
348
|
+
child = right;
|
|
349
|
+
}
|
|
350
|
+
const ck = K[child];
|
|
351
|
+
if (min ? (key <= ck) : (key >= ck)) break;
|
|
352
|
+
K[hole] = ck;
|
|
353
|
+
const cid = I[child];
|
|
354
|
+
I[hole] = cid;
|
|
355
|
+
P[cid] = hole;
|
|
356
|
+
hole = child;
|
|
357
|
+
}
|
|
358
|
+
K[hole] = key;
|
|
359
|
+
I[hole] = id;
|
|
360
|
+
P[id] = hole;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---- cold path only: throw builders (string concat off the hot body) ---
|
|
364
|
+
|
|
365
|
+
/** @private */
|
|
366
|
+
_badId(id) {
|
|
367
|
+
throw new RangeError(
|
|
368
|
+
'[lite-logn] BinaryHeap id must be an integer in [0, ' + this._cap + '), got ' +
|
|
369
|
+
String(id));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** @private */
|
|
373
|
+
_badKey(key) {
|
|
374
|
+
throw new TypeError(
|
|
375
|
+
'[lite-logn] BinaryHeap key must be a finite number, got ' + String(key));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** @private */
|
|
379
|
+
_dup(id) {
|
|
380
|
+
throw new RangeError(
|
|
381
|
+
'[lite-logn] BinaryHeap id ' + id + ' already present (no silent overwrite)');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** @private */
|
|
385
|
+
_full() {
|
|
386
|
+
throw new RangeError('[lite-logn] BinaryHeap full (capacity ' + this._cap + ')');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** @private */
|
|
390
|
+
_notMember(id) {
|
|
391
|
+
throw new RangeError(
|
|
392
|
+
'[lite-logn] BinaryHeap.changeKey: id ' + id + ' is not in the heap');
|
|
393
|
+
}
|
|
394
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# @zakkster/lite-logn
|
|
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.1.0 is the scaffold release; the planned roster is 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) -- 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
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@zakkster/lite-logn)
|
|
6
|
+
[](https://github.com/sponsors/PeshoVurtoleta)
|
|
7
|
+

|
|
8
|
+
[](https://bundlephobia.com/result?p=@zakkster/lite-logn)
|
|
9
|
+
[](https://www.npmjs.com/package/@zakkster/lite-logn)
|
|
10
|
+
[](https://www.npmjs.com/package/@zakkster/lite-logn)
|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
14
|
+
[](./LICENSE)
|
|
15
|
+
|
|
16
|
+
## The O(log n) toolkit the ecosystem was missing
|
|
17
|
+
|
|
18
|
+
Almost no JavaScript data-structure library ships the evidence that its Big-O claim survives contact with a real engine -- megamorphic call sites, GC pauses, cache misses, deopts. `lite-logn` is a curated, tree-shakeable family of the O(log n) structures that actually matter, each zero-GC, each written to teach the trick that buys the logarithm, and each shipped with a harness that DEMONSTRATES the straight log line rather than asserting it. The complexity class IS the product.
|
|
19
|
+
|
|
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
|
+
|
|
22
|
+
**v0.1.0 is the scaffold release.** It ships only the `VERSION` const -- there is no member yet. This first cut stands up the repo, the gates (torture / witness / perf), and the design decisions that outlive member 1 (see [`decisions/`](./decisions)). The planned roster below lands one member per session, each append-only so prior members stay byte-identical.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @zakkster/lite-logn
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { VERSION } from '@zakkster/lite-logn';
|
|
30
|
+
|
|
31
|
+
console.log(VERSION); // -> '0.1.0' (scaffold release; members land per session)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Once BinaryHeap ships (v0.1.0 member session), the quick-start becomes a heap push / pop whose every op is O(log n) worst-case and allocates zero bytes after construction, and `npm run witness` proves it holds the straight log line while a sorted-array-insert foil (O(n) shift) leaves it.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Table of contents
|
|
39
|
+
|
|
40
|
+
- [Why this exists](#why-this-exists)
|
|
41
|
+
- [What you get](#what-you-get)
|
|
42
|
+
- [The planned roster](#the-planned-roster)
|
|
43
|
+
- [The O(log n) Witness](#the-olog-n-witness)
|
|
44
|
+
- [API reference](#api-reference)
|
|
45
|
+
- [Constants](#constants)
|
|
46
|
+
- [Zero-GC design notes](#zero-gc-design-notes)
|
|
47
|
+
- [Testing](#testing)
|
|
48
|
+
- [What this is not](#what-this-is-not)
|
|
49
|
+
- [Ecosystem](#ecosystem)
|
|
50
|
+
- [License](#license)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Why this exists
|
|
55
|
+
|
|
56
|
+
A working programmer reaching for "keep the smallest element to hand" or "prefix sums that stay correct under updates" usually pays an O(n) cost hidden behind a friendly method name -- `Array.prototype.shift`, a full re-sum, a re-sort on every insert. The logarithm is the honest price of order, and it is cheap: one extra level of work per doubling of the data. But a naive O(log n) structure does `new Node` per insert, and that per-op allocation is a GC pause an engine will not honor -- it turns the clean logarithm into jitter.
|
|
57
|
+
|
|
58
|
+
lite-logn ships the O(log n) structures that matter with the allocation removed (array-embedded members are naturally node-free; pointer-based members use a pointer-free node pool) and the logarithm proven (the witness fits a straight log line and shows an O(n) foil leaving it). The complexity class is the product: you get the structure AND the evidence its bound is real on a real engine.
|
|
59
|
+
|
|
60
|
+
## What you get
|
|
61
|
+
|
|
62
|
+
- **Zero runtime dependencies.** ESM only, ASCII-only source, one PascalCase main file (`LogN.js`), `sideEffects: false`.
|
|
63
|
+
- **Zero allocation on every hot path.** Proven by `node --expose-gc test/torture.mjs` (`@zakkster/lite-leak` + `@zakkster/lite-gc-profiler`): 0 B/op, `gc major = 0`, leak tracker `size 0/0`.
|
|
64
|
+
- **A logarithm you can see.** `npm run witness` fits `nsPerOp = intercept + slope*log2(n)` across a geometric `n` sweep, gates the `R^2` floor + slope band, and shows an O(n) foil departing the line.
|
|
65
|
+
- **Tree-shakeable named exports.** Members share no mutable module state, so a bundler that imports one drops the others.
|
|
66
|
+
- **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
|
+
|
|
68
|
+
## The planned roster
|
|
69
|
+
|
|
70
|
+
One member per session, each landing append-only (prior members stay byte-identical). At v0.1.0 none are shipped yet -- this is the scaffold.
|
|
71
|
+
|
|
72
|
+
| Member | Version | Shape | Hot ops |
|
|
73
|
+
| --- | --- | --- | --- |
|
|
74
|
+
| **BinaryHeap** | 0.1.0 | array-embedded complete binary min-heap over a flat `Float64Array` | `push` / `pop` O(log n), `peek` O(1) |
|
|
75
|
+
| **Fenwick** (BIT) | 0.2.0 | flat array, lowest-set-bit walk (`i & -i`) | `update` / `prefix` / `rangeSum` O(log n) |
|
|
76
|
+
| **SegmentTree** | 0.3.0 | flat, array-embedded tree; associative fold chosen at construction | `rangeQuery` / `pointUpdate` O(log n) |
|
|
77
|
+
| **SkipList** | 0.4.0 | pointer-free over a shared node pool; expected O(log n) | `get` / `set` / `delete` / `successor` |
|
|
78
|
+
|
|
79
|
+
Later tiers (Treap / Scapegoat, OrderStatTree, IndexedHeap, SortedArray, MinMaxHeap, SplayTree, and presets) are queued in [`ROADMAP.md`](./ROADMAP.md).
|
|
80
|
+
|
|
81
|
+
## The O(log n) Witness
|
|
82
|
+
|
|
83
|
+
The family anchor. Time a fixed batch of the hot op at each `n` in a geometric sweep, fit `nsPerOp = intercept + slope * log2(n)` by least squares, and gate:
|
|
84
|
+
|
|
85
|
+
- `R^2 >= floor` (a straight line fits -- genuinely logarithmic), AND
|
|
86
|
+
- `slope` inside the member's band (the per-level cost, ns/level), AND
|
|
87
|
+
- the FOIL leaves the line (low `R^2` -- the O(n) default a working programmer reaches for, shown losing as `n` grows).
|
|
88
|
+
|
|
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 + slope band are calibrated in BinaryHeap and become the shared FAMILY gate. At v0.1.0 the witness harness is a stub: with zero members it runs green and empty (there is no member to fit yet).
|
|
90
|
+
|
|
91
|
+
## API reference
|
|
92
|
+
|
|
93
|
+
### Constants
|
|
94
|
+
|
|
95
|
+
| Export | Type | Value | Meaning |
|
|
96
|
+
| --- | --- | --- | --- |
|
|
97
|
+
| `VERSION` | `string` | `'0.1.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
|
+
|
|
99
|
+
### BinaryHeap
|
|
100
|
+
|
|
101
|
+
An **indexed binary heap** (an addressable priority queue): a min|max binary heap over three parallel, pointer-free typed arrays -- `_key` (`Float64Array`, the priority at each heap slot), `_id` (`Uint32Array`, the entity id at each slot), and `_pos` (`Int32Array`, the reverse map entity-id -> slot, sentinel `-1` == absent). A plain binary heap gives O(log n) `push` / `pop` but cannot find an arbitrary element to reprioritize; the reverse-index map buys O(log n) `changeKey` / `remove` by a caller-supplied entity id. Children of slot `i` are `2i+1` / `2i+2`. Entity ids are integers in `[0, capacity)`; keys are finite numbers. Every hot op allocates zero bytes after construction (hole-punching sift -- one write per level, no 3-write swap).
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
import { BinaryHeap } from '@zakkster/lite-logn';
|
|
105
|
+
|
|
106
|
+
const pq = new BinaryHeap(1024, 'min'); // capacity 1024, min-heap
|
|
107
|
+
pq.push(7, 5.0); // entity 7 at priority 5.0
|
|
108
|
+
pq.push(3, 2.5);
|
|
109
|
+
pq.push(9, 8.0);
|
|
110
|
+
pq.peek(); // -> 3 (id of the extremum)
|
|
111
|
+
pq.topKey(); // -> 2.5 (its key)
|
|
112
|
+
pq.changeKey(9, 1.0); // reprioritize entity 9 to the front
|
|
113
|
+
pq.pop(); // -> 9 (removes and returns the new extremum)
|
|
114
|
+
pq.remove(7); // -> true (addressable delete by id)
|
|
115
|
+
|
|
116
|
+
// Floyd O(n) bulk build from parallel arrays:
|
|
117
|
+
const heap = BinaryHeap.build('max', [0, 1, 2, 3], [4.0, 1.0, 9.0, 2.0], 16);
|
|
118
|
+
heap.pop(); // -> 2 (the id whose key 9.0 is the max)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
| Member | Signature | Complexity | Notes |
|
|
122
|
+
| --- | --- | --- | --- |
|
|
123
|
+
| constructor | `new BinaryHeap(capacity, kind = 'min')` | O(capacity) | `capacity` integer in `[1, 2^31-1]`; `kind` is `'min'` or `'max'`. Allocates the three typed arrays once; `_pos.fill(-1)`. |
|
|
124
|
+
| `push` | `push(id, key) -> void` | O(log n) | id in `[0, capacity)`, not already present; key finite. Throws on out-of-range/duplicate id, non-finite key, or full heap. |
|
|
125
|
+
| `pop` | `pop() -> number \| undefined` | O(log n) | Removes and returns the extremum's id; `undefined` if empty (no throw). |
|
|
126
|
+
| `peek` | `peek() -> number \| undefined` | O(1) | The extremum's id; `undefined` if empty. |
|
|
127
|
+
| `topKey` | `topKey() -> number \| undefined` | O(1) | The extremum's key; `undefined` if empty. |
|
|
128
|
+
| `keyOf` | `keyOf(id) -> number \| undefined` | O(1) | The key associated with id; `undefined` if absent. Out-of-range id throws. |
|
|
129
|
+
| `has` | `has(id) -> boolean` | O(1) | True iff id is resident. Out-of-range id throws. |
|
|
130
|
+
| `changeKey` | `changeKey(id, newKey) -> void` | O(log n) | Reprioritize a present entity (auto-direction sift); a non-member id throws. |
|
|
131
|
+
| `remove` | `remove(id) -> boolean` | O(log n) | Idempotent: `false` if absent, `true` if removed. |
|
|
132
|
+
| `clear` | `clear() -> void` | O(capacity) | Resets size and the reverse map. |
|
|
133
|
+
| `forEach` | `forEach(fn) -> void` | O(n) | Visits `(id, key)` in UNSPECIFIED (heap-array) order -- NOT sorted / pop order. |
|
|
134
|
+
| `[Symbol.iterator]` | `for (const id of heap)` | O(n) | Yields live ids in UNSPECIFIED order. |
|
|
135
|
+
| `size` / `capacity` / `kind` | getters | O(1) | Live count / fixed capacity / `'min'` \| `'max'`. |
|
|
136
|
+
| `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
|
+
|
|
138
|
+
Member signatures for later members are appended here as each ships.
|
|
139
|
+
|
|
140
|
+
## Zero-GC design notes
|
|
141
|
+
|
|
142
|
+
- **Array-embedded members allocate no nodes.** BinaryHeap, Fenwick, and SegmentTree live in flat typed arrays; there is no `new Node` per op, so there is nothing to collect. The parent / child / sibling relationships are index arithmetic (`2i+1`, `i & -i`), not pointers.
|
|
143
|
+
- **Pointer-based members use a pointer-free node pool.** SkipList and the later balanced-BST members allocate a slot INDEX from a free list over parallel `Uint32Array` link columns -- never a heap object. `NIL = 0`, slot 0 unused.
|
|
144
|
+
- **Fixed, preallocated capacity.** Overflow fails closed (a `[lite-logn]`-tagged throw), never a silent grow + amortized resize -- a resize would break the worst-case bound the witness proves.
|
|
145
|
+
|
|
146
|
+
| Op class | Allocation |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `BinaryHeap` push / pop / peek / topKey / keyOf / has / changeKey / remove | 0 B/op |
|
|
149
|
+
| `BinaryHeap` constructor / `build` / `clear` | O(capacity) typed arrays, once (cold) |
|
|
150
|
+
|
|
151
|
+
The allocation table is filled in per member as each lands, with the gated `R^2` / slope numbers from its witness run.
|
|
152
|
+
|
|
153
|
+
## Testing
|
|
154
|
+
|
|
155
|
+
`node:test` only, zero runtime deps. At v0.1.0 the harnesses are scaffolds that run green and empty (no member to exercise yet); each member session fills its tier.
|
|
156
|
+
|
|
157
|
+
- `npm test` -- per-member contract + boundary + fuzz-vs-oracle suites, plus the cross-member `QaAudit` block (VERSION trinity, author-spelling guard, ASCII-only source, the six-file pack).
|
|
158
|
+
- `npm run torture` -- `node --expose-gc test/torture.mjs`: 0 B/op on every hot path, `gc major = 0`, leak tracker `size 0/0`.
|
|
159
|
+
- `npm run witness` -- the O(log n) witness harness: log-linear `R^2` + slope fit, with an O(n) foil that must leave the line.
|
|
160
|
+
- `npm run test:perf` -- `@zakkster/lite-perf-gate` zero-alloc scenarios, each with a must-fail teeth case that proves the instrument has teeth.
|
|
161
|
+
- `npm run test:types` -- `tsc` over the ambient `LogN.d.ts` surface.
|
|
162
|
+
- `npm run verify` -- all of the above in sequence.
|
|
163
|
+
|
|
164
|
+
## What this is not
|
|
165
|
+
|
|
166
|
+
- **Not a bounded-integer priority queue.** If your priorities are small bounded integers, a heap's O(log n) is the wrong tool -- use `@zakkster/lite-o1`'s `BucketQueue` (Dial, O(1)) or `@zakkster/lite-scheduler`'s `FastBitScheduler`. lite-logn's heap is the GENERAL comparator PQ at O(log n).
|
|
167
|
+
- **Not an approximate-membership library.** Bloom / cuckoo / binary-fuse filters live in `@zakkster/lite-filter`. lite-logn owns exact ordered structures.
|
|
168
|
+
- **Not a cache.** `@zakkster/lite-lru` uses ordering internally for eviction but is a cache, not an ordered-collection library.
|
|
169
|
+
- **Not a grow-on-demand collection.** Capacity is fixed at construction and overflow fails closed.
|
|
170
|
+
|
|
171
|
+
## Ecosystem
|
|
172
|
+
|
|
173
|
+
Part of the `@zakkster/*` LiteLibrariesSuite of zero-GC, single-file ESM micro-libraries.
|
|
174
|
+
|
|
175
|
+
- [`@zakkster/lite-o1`](https://www.npmjs.com/package/@zakkster/lite-o1) -- the O(1) sibling and intended pair. lite-o1 holds the constant; lite-logn holds the logarithm. Keep the witnesses kin: a flat line vs a straight-log line.
|
|
176
|
+
- [`@zakkster/lite-filter`](https://www.npmjs.com/package/@zakkster/lite-filter) -- approximate / probabilistic membership.
|
|
177
|
+
- [`@zakkster/lite-lru`](https://www.npmjs.com/package/@zakkster/lite-lru) -- zero-GC cache family.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
|
package/llms.txt
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @zakkster/lite-logn
|
|
2
|
+
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
License: MIT (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
|
|
5
|
+
Runtime dependencies: none. ESM only. ASCII-only source. sideEffects: false.
|
|
6
|
+
Node: >= 18.
|
|
7
|
+
|
|
8
|
+
## What it is
|
|
9
|
+
|
|
10
|
+
A tree-shakeable, zero-GC family of O(log n) data structures that PROVES its
|
|
11
|
+
logarithm instead of asserting it. The complexity class is the product: every
|
|
12
|
+
hot op is O(log n) worst-case (or expected / amortized where stated) and
|
|
13
|
+
allocates zero bytes after construction, and a shipped witness harness fits
|
|
14
|
+
`nsPerOp = intercept + slope * log2(n)` and demonstrates the straight log line
|
|
15
|
+
against a built-in O(n) foil that leaves it.
|
|
16
|
+
|
|
17
|
+
It is the O(log n) sibling of @zakkster/lite-o1: lite-o1 holds the constant (a
|
|
18
|
+
FLAT ops/ms line on a log-x axis), lite-logn holds the logarithm (a STRAIGHT
|
|
19
|
+
line on that same axis, one added level per doubling of n).
|
|
20
|
+
|
|
21
|
+
v0.1.0 ships its first member, BinaryHeap. The planned roster (one member per
|
|
22
|
+
session, each landing append-only):
|
|
23
|
+
|
|
24
|
+
- BinaryHeap (v0.1.0) -- an INDEXED binary heap (addressable priority queue): a
|
|
25
|
+
min|max binary heap over three parallel typed arrays (`_key` Float64Array,
|
|
26
|
+
`_id` Uint32Array, `_pos` Int32Array reverse map) with children at 2i+1 / 2i+2.
|
|
27
|
+
push / pop / changeKey / remove are O(log n); peek / topKey / keyOf / has are
|
|
28
|
+
O(1); nothing allocated per op. The reverse-index map makes changeKey / remove
|
|
29
|
+
addressable by a caller-supplied entity id. The cleanest witness in the family
|
|
30
|
+
and the one that calibrates the shared R^2 floor + slope band gate (pop).
|
|
31
|
+
- Fenwick / BIT (v0.2.0) -- BOTH point-update AND prefix-sum in O(log n) over a
|
|
32
|
+
single flat array via the lowest-set-bit walk (`i & -i`); rangeSum is two
|
|
33
|
+
prefix queries. Two straight log lines.
|
|
34
|
+
- SegmentTree (v0.3.0) -- general associative range-query (min / max / sum / gcd)
|
|
35
|
+
+ point-update over a flat, array-embedded tree, O(log n) worst-case; the fold
|
|
36
|
+
is chosen once at construction.
|
|
37
|
+
- SkipList (v0.4.0) -- the ordered map (get / set / delete / successor /
|
|
38
|
+
rangeIter), pointer-free over a shared node pool (parallel Uint32Array `next`
|
|
39
|
+
columns), expected O(log n), deterministic seed, tail reported.
|
|
40
|
+
|
|
41
|
+
## Exports (from the single main file LogN.js)
|
|
42
|
+
|
|
43
|
+
- `VERSION` -- string. The package version; one of the three version sites
|
|
44
|
+
(package.json / LogN.js VERSION const / this file's `Version:` header).
|
|
45
|
+
- `BinaryHeap` -- class. An indexed binary heap (addressable priority queue).
|
|
46
|
+
- `new BinaryHeap(capacity, kind = 'min')` -- capacity is an integer in
|
|
47
|
+
[1, 2^31-1]; kind is 'min' or 'max'. Allocates the three typed arrays once.
|
|
48
|
+
- `push(id, key)` -> void. id integer in [0, capacity), not already present;
|
|
49
|
+
key finite. Throws on out-of-range/duplicate id, non-finite key, full heap.
|
|
50
|
+
- `pop()` -> id | undefined. Removes the extremum; undefined if empty (no throw).
|
|
51
|
+
- `peek()` -> id | undefined. `topKey()` -> key | undefined. Read-only.
|
|
52
|
+
- `keyOf(id)` -> key | undefined. `has(id)` -> boolean. Out-of-range id throws.
|
|
53
|
+
- `changeKey(id, newKey)` -> void. Reprioritize a present entity (auto-direction
|
|
54
|
+
sift); a non-member id throws (never a silent no-op).
|
|
55
|
+
- `remove(id)` -> boolean. Idempotent: false if absent, true if removed.
|
|
56
|
+
- `size` / `capacity` / `kind` getters; `clear()`; `forEach(fn)` and
|
|
57
|
+
`[Symbol.iterator]()` yield live ids in UNSPECIFIED (heap-array) order -- NOT
|
|
58
|
+
sorted / pop order.
|
|
59
|
+
- `BinaryHeap.build(kind, ids, keys, capacity)` -> BinaryHeap. Floyd O(n) bulk
|
|
60
|
+
build from parallel arrays; fails closed on duplicate/out-of-range id,
|
|
61
|
+
non-finite key, or count > capacity.
|
|
62
|
+
|
|
63
|
+
Member exports (one tree-shakeable class each) are appended here as each member
|
|
64
|
+
ships.
|
|
65
|
+
|
|
66
|
+
## Design bounds
|
|
67
|
+
|
|
68
|
+
- Zero runtime deps; node:test only; ASCII-only source.
|
|
69
|
+
- Single PascalCase main file LogN.js; sideEffects: false; tree-shakeable named
|
|
70
|
+
exports, no barrel.
|
|
71
|
+
- Fixed, preallocated capacity (node / element count, not bytes). Fail closed on
|
|
72
|
+
every unverified state; `null` is not zero.
|
|
73
|
+
- typeof-guard FIRST, before any coercion, at the door of every mutating op.
|
|
74
|
+
- Zero allocation on every steady-state hot path (0 B/op), proven by
|
|
75
|
+
`node --expose-gc test/torture.mjs`.
|
|
76
|
+
|
|
77
|
+
## Cross-package boundaries
|
|
78
|
+
|
|
79
|
+
- @zakkster/lite-o1 -- the O(1) sibling and intended pair. Any BOUNDED-INTEGER
|
|
80
|
+
priority queue is O(1) and belongs there (Dial's bucket queue); lite-logn's
|
|
81
|
+
heap is the GENERAL comparator PQ at O(log n).
|
|
82
|
+
- @zakkster/lite-filter -- owns approximate / probabilistic membership;
|
|
83
|
+
lite-logn owns exact ordered structures. No overlap.
|
package/package.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zakkster/lite-logn",
|
|
3
|
+
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Zero-dependency, zero-GC family of O(log n) data structures that proves its logarithm: BinaryHeap (array-embedded O(log n) push/pop min-heap), Fenwick/BIT (O(log n) point-update AND prefix-sum via the i & -i walk), SegmentTree (O(log n) associative range-query + point-update), and SkipList (pointer-free expected-O(log n) ordered map) -- 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
|
+
"type": "module",
|
|
7
|
+
"main": "./LogN.js",
|
|
8
|
+
"module": "./LogN.js",
|
|
9
|
+
"types": "./LogN.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./LogN.d.ts",
|
|
13
|
+
"node": "./LogN.js",
|
|
14
|
+
"import": "./LogN.js",
|
|
15
|
+
"default": "./LogN.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"LogN.js",
|
|
20
|
+
"LogN.d.ts",
|
|
21
|
+
"llms.txt",
|
|
22
|
+
"README.md",
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test test/*.test.js test/Bench.test.mjs",
|
|
28
|
+
"test:types": "tsc -p test/types/tsconfig.json",
|
|
29
|
+
"torture": "node --expose-gc test/torture.mjs",
|
|
30
|
+
"witness": "node test/witness.mjs",
|
|
31
|
+
"test:perf": "node --expose-gc --max-semi-space-size=4 --test test/perf/PerfGate.test.mjs",
|
|
32
|
+
"bench": "node benchmark/Bench.mjs",
|
|
33
|
+
"bench:report": "node benchmark/Bench.mjs && node benchmark/Report.mjs",
|
|
34
|
+
"verify": "npm test && npm run test:types && npm run torture && npm run witness && npm run test:perf"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"binary-heap",
|
|
38
|
+
"heap",
|
|
39
|
+
"priority-queue",
|
|
40
|
+
"d-ary-heap",
|
|
41
|
+
"fenwick",
|
|
42
|
+
"bit",
|
|
43
|
+
"binary-indexed-tree",
|
|
44
|
+
"prefix-sum",
|
|
45
|
+
"segment-tree",
|
|
46
|
+
"range-query",
|
|
47
|
+
"range-sum",
|
|
48
|
+
"skip-list",
|
|
49
|
+
"ordered-set",
|
|
50
|
+
"balanced-bst",
|
|
51
|
+
"bst",
|
|
52
|
+
"log-n",
|
|
53
|
+
"logarithmic",
|
|
54
|
+
"order-statistics",
|
|
55
|
+
"rank-select",
|
|
56
|
+
"data-structures",
|
|
57
|
+
"zero-gc",
|
|
58
|
+
"gc",
|
|
59
|
+
"typed-array",
|
|
60
|
+
"soa",
|
|
61
|
+
"tree-shakeable",
|
|
62
|
+
"witness",
|
|
63
|
+
"performance",
|
|
64
|
+
"lightweight",
|
|
65
|
+
"zero-dependency"
|
|
66
|
+
],
|
|
67
|
+
"license": "MIT",
|
|
68
|
+
"publishConfig": {
|
|
69
|
+
"access": "public"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@zakkster/lite-gc-profiler": "^1.16.0",
|
|
73
|
+
"@zakkster/lite-leak": "^1.10.0",
|
|
74
|
+
"@zakkster/lite-perf-gate": "^1.4.2",
|
|
75
|
+
"esbuild": "^0.28.2",
|
|
76
|
+
"typescript": "^7.0.2"
|
|
77
|
+
},
|
|
78
|
+
"homepage": "https://github.com/PeshoVurtoleta/lite-logn#readme",
|
|
79
|
+
"repository": {
|
|
80
|
+
"type": "git",
|
|
81
|
+
"url": "git+https://github.com/PeshoVurtoleta/lite-logn.git"
|
|
82
|
+
},
|
|
83
|
+
"bugs": {
|
|
84
|
+
"url": "https://github.com/PeshoVurtoleta/lite-logn/issues",
|
|
85
|
+
"email": "shinikchiev@yahoo.com"
|
|
86
|
+
},
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": ">=18"
|
|
89
|
+
},
|
|
90
|
+
"funding": {
|
|
91
|
+
"type": "github",
|
|
92
|
+
"url": "https://github.com/sponsors/PeshoVurtoleta"
|
|
93
|
+
},
|
|
94
|
+
"sideEffects": false
|
|
95
|
+
}
|