Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ export const examples: {
readonly diffTree: 'examples/treeDiff.ts';
readonly applyTreeDiff: 'examples/treeDiff.ts';
readonly UnionFind: 'examples/graph.ts';
readonly BinaryHeap: 'examples/binaryHeap.ts';
readonly BloomFilter: 'examples/bloomFilter.ts';
readonly SegmentTree: 'examples/segmentTree.ts';
};
readonly performance: {
readonly debounce: 'examples/requestDedup.ts';
Expand Down Expand Up @@ -2929,6 +2932,52 @@ export function groupBy<T>(
key: keyof T | ((item: T) => string)
): Record<string, T[]>;

/**
* Binary heap (priority queue) with custom comparator.
* Use for: A*/Dijkstra, schedulers, real-time queues.
* Import: data/binaryHeap.ts
*/
export class BinaryHeap<T> {
constructor(compare: (a: T, b: T) => number, items?: Iterable<T>);
readonly size: number;
peek(): T | undefined;
push(value: T): void;
pop(): T | undefined;
}

/**
* Bloom filter (probabilistic set with no false negatives).
* Use for: quick membership checks, caching fronts, anti-spam.
* Import: data/bloomFilter.ts
*/
export interface BloomFilterOptions {
size: number;
hashes: number;
seed?: number;
}
export class BloomFilter {
constructor(options: BloomFilterOptions);
add(value: string | number | Uint8Array): void;
has(value: string | number | Uint8Array): boolean;
static fromCapacity(capacity: number, errorRate?: number, seed?: number): BloomFilter;
}

/**
* Segment tree for range queries with point updates.
* Use for: range sums/min/max and similar associative operations.
* Import: data/segmentTree.ts
*/
export interface SegmentTreeOptions<T> {
values: ReadonlyArray<T>;
combine?: (a: T, b: T) => T;
identity?: T;
}
export class SegmentTree<T = number> {
constructor(options: SegmentTreeOptions<T>);
update(index: number, value: T): void;
query(left: number, right: number): T;
}

/**
* Disjoint Set Union (Union-Find) with path compression and union by size.
* Use for: connectivity queries, Kruskal MST, clustering.
Expand Down
8 changes: 8 additions & 0 deletions examples/binaryHeap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { BinaryHeap } from '../src/index.js';

const heap = new BinaryHeap<number>((a, b) => a - b, [5, 1, 4]);
heap.push(3);
console.log('peek', heap.peek());
console.log('pop', heap.pop());
console.log('pop', heap.pop());

12 changes: 12 additions & 0 deletions examples/bloomFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { BloomFilter } from '../src/index.js';

// Create a Bloom filter for ~1000 items with ~1% false positive rate
const bf = BloomFilter.fromCapacity(1000, 0.01, 42);

bf.add('apple');
bf.add('banana');
bf.add('cherry');

console.log('has apple?', bf.has('apple'));
console.log('has grape?', bf.has('grape'));

16 changes: 16 additions & 0 deletions examples/segmentTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SegmentTree } from '../src/index.js';

// Range sum segment tree
const st = new SegmentTree<number>({ values: [1, 3, 5, 7, 9, 11] });
console.log('sum(0..2)=', st.query(0, 2));
st.update(3, 10);
console.log('sum(2..5)=', st.query(2, 5));

// Range min tree
const minTree = new SegmentTree<number>({
values: [5, 2, 7, 3, 9],
combine: (a, b) => Math.min(a, b),
identity: Number.POSITIVE_INFINITY,
});
console.log('min(1..3)=', minTree.query(1, 3));

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
{
"name": "bundle",
"path": "dist/index.js",
"limit": "41 KB"
"limit": "42 KB"
}
]
}
81 changes: 81 additions & 0 deletions src/data/binaryHeap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Minimal binary heap (priority queue) with custom comparator.
* Useful for: A* / Dijkstra open sets, schedulers, simulation queues.
*/
export class BinaryHeap<T> {
private data: T[] = [];
constructor(private compare: (a: T, b: T) => number, items?: Iterable<T>) {
if (items) {
for (const it of items) this.data.push(it);
this.heapify();
}
}

get size(): number {
return this.data.length;
}

peek(): T | undefined {
return this.data[0];
}

push(value: T): void {
this.data.push(value);
this.bubbleUp(this.data.length - 1);
}

pop(): T | undefined {
const n = this.data.length;
if (n === 0) return undefined;
const top = this.data[0];
const last = this.data.pop() as T;
if (n > 1) {
this.data[0] = last;
this.bubbleDown(0);
}
return top;
}

private heapify(): void {
for (let i = Math.floor(this.data.length / 2) - 1; i >= 0; i -= 1) {
this.bubbleDown(i);
}
}

private bubbleUp(i: number): void {
while (i > 0) {
const p = (i - 1) >> 1;
if (this.compare(this.data[i], this.data[p]) < 0) {
this.swap(i, p);
i = p;
} else {
break;
}
}
}

private bubbleDown(i: number): void {
const n = this.data.length;
let idx = i;
let moved = true;
while (moved) {
moved = false;
const l = idx * 2 + 1;
const r = l + 1;
let smallest = idx;
if (l < n && this.compare(this.data[l], this.data[smallest]) < 0) smallest = l;
if (r < n && this.compare(this.data[r], this.data[smallest]) < 0) smallest = r;
if (smallest !== idx) {
this.swap(idx, smallest);
idx = smallest;
moved = true;
}
}
}

private swap(i: number, j: number): void {
const tmp = this.data[i];
this.data[i] = this.data[j];
this.data[j] = tmp;
}
}
110 changes: 110 additions & 0 deletions src/data/bloomFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Bloom filter with double hashing (Kirsch–Mitzenmacher optimisation).
* Useful for: probabilistic membership checks with no false negatives.
*/
export interface BloomFilterOptions {
/** Total number of bits in the filter (m). */
size: number;
/** Number of hash functions (k). */
hashes: number;
/** Optional seed for hashing. */
seed?: number;
}

export class BloomFilter {
private bits: Uint8Array;
private m: number;
private k: number;
private seed: number;

constructor(options: BloomFilterOptions) {
const { size, hashes, seed = 0x9e3779b1 } = options;
if (size <= 0 || !Number.isFinite(size)) throw new Error('Invalid bloom size');
if (hashes <= 0 || !Number.isFinite(hashes)) throw new Error('Invalid hash count');
this.m = size | 0;
this.k = hashes | 0;
this.seed = seed | 0;
this.bits = new Uint8Array(Math.ceil(this.m / 8));
}

/** Adds a value to the filter. */
add(value: string | number | Uint8Array): void {
const { h1, h2 } = this.doubleHash(value);
for (let i = 0; i < this.k; i += 1) {
const idx = this.indexFor(h1, h2, i);
this.setBit(idx);
}
}

/** Checks if a value may be in the set (no false negatives). */
has(value: string | number | Uint8Array): boolean {
const { h1, h2 } = this.doubleHash(value);
for (let i = 0; i < this.k; i += 1) {
const idx = this.indexFor(h1, h2, i);
if (!this.getBit(idx)) return false;
}
return true;
}

/** Creates a Bloom filter sized for the given capacity and error rate. */
static fromCapacity(capacity: number, errorRate = 0.01, seed?: number): BloomFilter {
if (capacity <= 0) throw new Error('Capacity must be > 0');
if (!(errorRate > 0 && errorRate < 1)) throw new Error('Error rate must be in (0,1)');
const ln2 = Math.log(2);
const m = Math.ceil(-(capacity * Math.log(errorRate)) / (ln2 * ln2));
const k = Math.max(1, Math.round((m / capacity) * ln2));
return new BloomFilter({ size: m, hashes: k, seed });
}

// ---- internals ----
private indexFor(h1: number, h2: number, i: number): number {
// (h1 + i*h2) % m with unsigned wrapping
const x = (h1 + Math.imul(i, h2)) >>> 0;
return x % this.m;
}

private setBit(idx: number): void {
const byte = idx >> 3;
const mask = 1 << (idx & 7);
this.bits[byte] |= mask;
}

private getBit(idx: number): boolean {
const byte = idx >> 3;
const mask = 1 << (idx & 7);
return (this.bits[byte] & mask) !== 0;
}

private doubleHash(value: string | number | Uint8Array): { h1: number; h2: number } {
const bytes = toBytes(value);
// Two 32-bit hashes derived from FNV-1a mixed with seed
const h1 = fnv1a(bytes, this.seed);
const h2 = fnv1a(bytes, h1 ^ 0x85ebca6b);
// Ensure non-zero step to avoid repeating same position
return { h1, h2: (h2 | 1) >>> 0 };
}
}

function toBytes(value: string | number | Uint8Array): Uint8Array {
if (typeof value === 'string') {
return new TextEncoder().encode(value);
}
if (typeof value === 'number') {
const v = new DataView(new ArrayBuffer(8));
v.setFloat64(0, value, true);
return new Uint8Array(v.buffer);
}
return value;
}

// FNV-1a 32-bit
function fnv1a(data: Uint8Array, seed = 0): number {
let hash = (0x811c9dc5 ^ seed) >>> 0;
for (let i = 0; i < data.length; i += 1) {
hash ^= data[i];
hash = Math.imul(hash, 0x01000193);
}
return hash >>> 0;
}

export const __internals = { fnv1a };
67 changes: 67 additions & 0 deletions src/data/segmentTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Segment tree for range queries with point updates.
* Default operation is sum; can be customised with an associative combine.
*/
export interface SegmentTreeOptions<T> {
values: ReadonlyArray<T>;
combine?: (a: T, b: T) => T;
identity?: T;
}

export class SegmentTree<T = number> {
private n: number;
private tree: T[];
private combine: (a: T, b: T) => T;
private identity: T;

constructor(options: SegmentTreeOptions<T>) {
const { values, combine, identity } = options;
this.n = values.length;
const defaultCombine = ((a: number, b: number) => a + b) as unknown as (a: T, b: T) => T;
this.combine = combine ?? defaultCombine;
this.identity = (identity as T) ?? ((0 as unknown as T));
const size = 1 << (Math.ceil(Math.log2(Math.max(1, this.n))) + 1);
this.tree = new Array<T>(size).fill(this.identity);
if (this.n > 0) this.build(values, 1, 0, this.n - 1);
}

update(index: number, value: T): void {
if (index < 0 || index >= this.n) throw new Error('Index out of bounds');
this.updateRec(1, 0, this.n - 1, index, value);
}

query(left: number, right: number): T {
if (left > right) return this.identity;
left = Math.max(0, left);
right = Math.min(this.n - 1, right);
return this.queryRec(1, 0, this.n - 1, left, right);
}

private build(values: ReadonlyArray<T>, node: number, l: number, r: number) {
if (l === r) {
this.tree[node] = values[l];
return;
}
const mid = (l + r) >> 1;
this.build(values, node * 2, l, mid);
this.build(values, node * 2 + 1, mid + 1, r);
this.tree[node] = this.combine(this.tree[node * 2], this.tree[node * 2 + 1]);
}

private updateRec(node: number, l: number, r: number, idx: number, val: T) {
if (l === r) { this.tree[node] = val; return; }
const mid = (l + r) >> 1;
if (idx <= mid) this.updateRec(node * 2, l, mid, idx, val);
else this.updateRec(node * 2 + 1, mid + 1, r, idx, val);
this.tree[node] = this.combine(this.tree[node * 2], this.tree[node * 2 + 1]);
}

private queryRec(node: number, l: number, r: number, ql: number, qr: number): T {
if (ql <= l && r <= qr) return this.tree[node];
const mid = (l + r) >> 1;
let res = this.identity;
if (ql <= mid) res = this.combine(res, this.queryRec(node * 2, l, mid, ql, qr));
if (qr > mid) res = this.combine(res, this.queryRec(node * 2 + 1, mid + 1, r, ql, qr));
return res;
}
}
Loading