diff --git a/docs/index.d.ts b/docs/index.d.ts index 80b4851..eb369b6 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -98,6 +98,9 @@ export const examples: { 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 SkipList: 'examples/skipList.ts'; }; readonly performance: { readonly debounce: 'examples/requestDedup.ts'; @@ -2943,6 +2946,58 @@ export class BinaryHeap { 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 { + values: ReadonlyArray; + combine?: (a: T, b: T) => T; + identity?: T; +} +export class SegmentTree { + constructor(options: SegmentTreeOptions); + update(index: number, value: T): void; + query(left: number, right: number): T; +} + +/** + * Skip list (probabilistic ordered set) with seeded RNG. + * Use for: ordered sets/maps with expected O(log n) ops. + * Import: data/skipList.ts + */ +export interface SkipListOptions { + compare?: (a: T, b: T) => number; + p?: number; + maxLevel?: number; + seed?: number; +} +export class SkipList { + constructor(options?: SkipListOptions); + has(value: T): boolean; + insert(value: T): void; + remove(value: T): boolean; + values(): IterableIterator; +} + /** * Disjoint Set Union (Union-Find) with path compression and union by size. * Use for: connectivity queries, Kruskal MST, clustering. diff --git a/examples/skipList.ts b/examples/skipList.ts new file mode 100644 index 0000000..b583434 --- /dev/null +++ b/examples/skipList.ts @@ -0,0 +1,7 @@ +import { SkipList } from '../src/index.js'; + +const sl = new SkipList({ seed: 2024 }); +[5, 1, 9, 3, 7].forEach((x) => sl.insert(x)); +console.log('has 3?', sl.has(3)); +console.log('values:', Array.from(sl.values())); + diff --git a/src/data/skipList.ts b/src/data/skipList.ts new file mode 100644 index 0000000..8f74358 --- /dev/null +++ b/src/data/skipList.ts @@ -0,0 +1,117 @@ +export interface SkipListOptions { + compare?: (a: T, b: T) => number; + p?: number; // probability to raise level + maxLevel?: number; + seed?: number; +} + +class SLNode { + value: T | null; + next: Array | null>; + constructor(level: number, value: T | null) { + this.value = value; + this.next = new Array | null>(level + 1).fill(null); + } +} + +/** + * Deterministic skip list with seeded RNG (Xorshift32). + * Supports insert, has, and remove in expected O(log n). + */ +export class SkipList { + private head: SLNode; + private level = 0; + private compare: (a: T, b: T) => number; + private p: number; + private maxLevel: number; + private state: number; + + constructor(options: SkipListOptions = {}) { + const defaultCompare = ((a: T, b: T) => { + const av = a as unknown as number | string; + const bv = b as unknown as number | string; + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + return av < bv ? -1 : av > bv ? 1 : 0; + }) as (a: T, b: T) => number; + this.compare = options.compare ?? defaultCompare; + this.p = options.p ?? 0.5; + this.maxLevel = options.maxLevel ?? 16; + this.state = options.seed ?? 0x12345678; + this.head = new SLNode(this.maxLevel, null); + } + + has(value: T): boolean { + let x = this.head; + for (let i = this.level; i >= 0; i -= 1) { + let nxt = x.next[i]; + while (nxt && this.compare(nxt.value as T, value) < 0) { x = nxt; nxt = x.next[i]; } + } + const y = x.next[0]; + return !!(y && this.compare(y.value as T, value) === 0); + } + + insert(value: T): void { + const update = new Array>(this.maxLevel + 1); + let x = this.head; + for (let i = this.level; i >= 0; i -= 1) { + let nxt = x.next[i]; + while (nxt && this.compare(nxt.value as T, value) < 0) { x = nxt; nxt = x.next[i]; } + update[i] = x; + } + const y = x.next[0]; + if (y && this.compare(y.value as T, value) === 0) { + // replace value + y.value = value; + return; + } + const lvl = this.randomLevel(); + if (lvl > this.level) { + for (let i = this.level + 1; i <= lvl; i += 1) update[i] = this.head; + this.level = lvl; + } + const node = new SLNode(lvl, value); + for (let i = 0; i <= lvl; i += 1) { + node.next[i] = update[i].next[i]; + update[i].next[i] = node; + } + } + + remove(value: T): boolean { + const update = new Array>(this.maxLevel + 1); + let x = this.head; + for (let i = this.level; i >= 0; i -= 1) { + let nxt = x.next[i]; + while (nxt && this.compare(nxt.value as T, value) < 0) { x = nxt; nxt = x.next[i]; } + update[i] = x; + } + const y = x.next[0]; + if (!y || this.compare(y.value as T, value) !== 0) return false; + for (let i = 0; i <= this.level; i += 1) { + if (update[i].next[i] !== y) break; + update[i].next[i] = y.next[i]; + } + while (this.level > 0 && !this.head.next[this.level]) this.level -= 1; + return true; + } + + // Iterate in-order + *values(): IterableIterator { + let x = this.head.next[0]; + while (x) { yield x.value as T; x = x.next[0]; } + } + + private randomLevel(): number { + let lvl = 0; + while (lvl < this.maxLevel && this.random() < this.p) lvl += 1; + return lvl; + } + + private random(): number { + // Xorshift32 + let x = this.state | 0; + x ^= x << 13; x ^= x >>> 17; x ^= x << 5; + this.state = x | 0; + // map to [0,1) + return ((x >>> 0) / 0xffffffff); + } +} diff --git a/src/index.ts b/src/index.ts index 8b54c15..15f1618 100644 --- a/src/index.ts +++ b/src/index.ts @@ -96,6 +96,9 @@ export const examples = { applyTreeDiff: 'examples/treeDiff.ts', UnionFind: 'examples/graph.ts', BinaryHeap: 'examples/binaryHeap.ts', + BloomFilter: 'examples/bloomFilter.ts', + SegmentTree: 'examples/segmentTree.ts', + SkipList: 'examples/skipList.ts', }, performance: { debounce: 'examples/requestDedup.ts', @@ -1033,6 +1036,14 @@ export { UnionFind } from './data/unionFind.js'; * Example file: examples/binaryHeap.ts */ export { BinaryHeap } from './data/binaryHeap.js'; +// BloomFilter and SegmentTree exports land via their feature branches +// and are available on main after merge. +/** + * Skip list (probabilistic ordered set) with seeded RNG. + * + * Example file: examples/skipList.ts + */ +export { SkipList } from './data/skipList.js'; export type { TreeNode, diff --git a/tests/index.test.ts b/tests/index.test.ts index f66dd3f..05e198e 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -125,6 +125,9 @@ describe('package entry point', () => { | 'applyTreeDiff' | 'UnionFind' | 'BinaryHeap' + | 'BloomFilter' + | 'SegmentTree' + | 'SkipList' >(); expectTypeOf>().toEqualTypeOf< diff --git a/tests/skipList.test.ts b/tests/skipList.test.ts new file mode 100644 index 0000000..1f8e3ef --- /dev/null +++ b/tests/skipList.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +import { SkipList } from '../src/index.js'; + +describe('SkipList', () => { + it('inserts, finds, removes, and iterates in order', () => { + const sl = new SkipList({ seed: 1337 }); + const data = [5, 1, 9, 3, 7]; + data.forEach((x) => sl.insert(x)); + expect(sl.has(3)).toBe(true); + expect(sl.has(4)).toBe(false); + expect(Array.from(sl.values())).toEqual([1, 3, 5, 7, 9]); + expect(sl.remove(5)).toBe(true); + expect(Array.from(sl.values())).toEqual([1, 3, 7, 9]); + expect(sl.remove(5)).toBe(false); + }); +}); +