diff --git a/docs/index.d.ts b/docs/index.d.ts index c71c69e..07f3d73 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -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'; @@ -2929,6 +2931,36 @@ export function groupBy( key: keyof T | ((item: T) => string) ): Record; +/** + * Binary heap (priority queue) with custom comparator. + * Use for: A*/Dijkstra, schedulers, real-time queues. + * Import: data/binaryHeap.ts + */ +export class BinaryHeap { + constructor(compare: (a: T, b: T) => number, items?: Iterable); + 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. diff --git a/examples/binaryHeap.ts b/examples/binaryHeap.ts new file mode 100644 index 0000000..4247e6b --- /dev/null +++ b/examples/binaryHeap.ts @@ -0,0 +1,8 @@ +import { BinaryHeap } from '../src/index.js'; + +const heap = new BinaryHeap((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()); + diff --git a/examples/bloomFilter.ts b/examples/bloomFilter.ts new file mode 100644 index 0000000..9253858 --- /dev/null +++ b/examples/bloomFilter.ts @@ -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')); + diff --git a/package.json b/package.json index 7bf057b..d54ba37 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ { "name": "bundle", "path": "dist/index.js", - "limit": "41 KB" + "limit": "42 KB" } ] } diff --git a/src/data/binaryHeap.ts b/src/data/binaryHeap.ts new file mode 100644 index 0000000..4b9d6a5 --- /dev/null +++ b/src/data/binaryHeap.ts @@ -0,0 +1,81 @@ +/** + * Minimal binary heap (priority queue) with custom comparator. + * Useful for: A* / Dijkstra open sets, schedulers, simulation queues. + */ +export class BinaryHeap { + private data: T[] = []; + constructor(private compare: (a: T, b: T) => number, items?: Iterable) { + 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; + } +} diff --git a/src/data/bloomFilter.ts b/src/data/bloomFilter.ts new file mode 100644 index 0000000..89f0e72 --- /dev/null +++ b/src/data/bloomFilter.ts @@ -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 }; diff --git a/src/index.ts b/src/index.ts index ab88224..ac731d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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', @@ -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, diff --git a/tests/binaryHeap.test.ts b/tests/binaryHeap.test.ts new file mode 100644 index 0000000..d087a8b --- /dev/null +++ b/tests/binaryHeap.test.ts @@ -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((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((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(); + }); +}); + diff --git a/tests/bloomFilter.test.ts b/tests/bloomFilter.test.ts new file mode 100644 index 0000000..18e2f9d --- /dev/null +++ b/tests/bloomFilter.test.ts @@ -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); + }); +}); + diff --git a/tests/index.test.ts b/tests/index.test.ts index cb04ce9..f697317 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -124,6 +124,8 @@ describe('package entry point', () => { | 'diffTree' | 'applyTreeDiff' | 'UnionFind' + | 'BinaryHeap' + | 'BloomFilter' >(); expectTypeOf>().toEqualTypeOf<