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
32 changes: 32 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ 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 performance: {
readonly debounce: 'examples/requestDedup.ts';
Expand Down Expand Up @@ -2929,6 +2931,36 @@ 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;
}

/**
* 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'));

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 };
14 changes: 14 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ export const examples = {
diffTree: 'examples/treeDiff.ts',
applyTreeDiff: 'examples/treeDiff.ts',
UnionFind: 'examples/graph.ts',
BinaryHeap: 'examples/binaryHeap.ts',
BloomFilter: 'examples/bloomFilter.ts',
},
performance: {
debounce: 'examples/requestDedup.ts',
Expand Down Expand Up @@ -1026,6 +1028,18 @@ export { diffTree, applyTreeDiff } from './data/treeDiff.js';
* Example file: examples/graph.ts
*/
export { UnionFind } from './data/unionFind.js';
/**
* Binary heap (priority queue) with custom comparator.
*
* Example file: examples/binaryHeap.ts
*/
export { BinaryHeap } from './data/binaryHeap.js';
/**
* Bloom filter (probabilistic membership, no false negatives).
*
* Example file: examples/bloomFilter.ts
*/
export { BloomFilter } from './data/bloomFilter.js';

export type {
TreeNode,
Expand Down
26 changes: 26 additions & 0 deletions tests/binaryHeap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { describe, it, expect } from 'vitest';
import { BinaryHeap } from '../src/index.js';

describe('BinaryHeap', () => {
it('orders items by comparator', () => {
const heap = new BinaryHeap<number>((a, b) => a - b, [5, 1, 3, 4, 2]);
expect(heap.size).toBe(5);
expect(heap.peek()).toBe(1);
const popped: number[] = [];
while (heap.size) popped.push(heap.pop()!);
expect(popped).toEqual([1, 2, 3, 4, 5]);
});

it('supports pushing and popping', () => {
const heap = new BinaryHeap<number>((a, b) => a - b);
heap.push(10);
heap.push(5);
heap.push(7);
expect(heap.peek()).toBe(5);
expect(heap.pop()).toBe(5);
expect(heap.pop()).toBe(7);
expect(heap.pop()).toBe(10);
expect(heap.pop()).toBeUndefined();
});
});

18 changes: 18 additions & 0 deletions tests/bloomFilter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, it, expect } from 'vitest';
import { BloomFilter } from '../src/index.js';

describe('BloomFilter', () => {
it('adds items and reports membership without false negatives', () => {
const bf = BloomFilter.fromCapacity(1000, 0.01, 123);
const items = Array.from({ length: 200 }, (_, i) => `key-${i}`);
for (const x of items) bf.add(x);
for (const x of items) expect(bf.has(x)).toBe(true);

// Most not-added items should be reported as false; avoid strict assertions
const probes = Array.from({ length: 50 }, (_, i) => `other-${i + 10000}`);
const maybes = probes.map((p) => bf.has(p));
// At least some must be false
expect(maybes.some((v) => v === false)).toBe(true);
});
});

2 changes: 2 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ describe('package entry point', () => {
| 'diffTree'
| 'applyTreeDiff'
| 'UnionFind'
| 'BinaryHeap'
| 'BloomFilter'
>();

expectTypeOf<ExampleName<'search'>>().toEqualTypeOf<
Expand Down