Skip to content
Merged
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
55 changes: 55 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -2943,6 +2946,58 @@ export class BinaryHeap<T> {
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;
}

/**
* 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<T> {
compare?: (a: T, b: T) => number;
p?: number;
maxLevel?: number;
seed?: number;
}
export class SkipList<T> {
constructor(options?: SkipListOptions<T>);
has(value: T): boolean;
insert(value: T): void;
remove(value: T): boolean;
values(): IterableIterator<T>;
}

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

const sl = new SkipList<number>({ 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()));

117 changes: 117 additions & 0 deletions src/data/skipList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
export interface SkipListOptions<T> {
compare?: (a: T, b: T) => number;
p?: number; // probability to raise level
maxLevel?: number;
seed?: number;
}

class SLNode<T> {
value: T | null;
next: Array<SLNode<T> | null>;
constructor(level: number, value: T | null) {
this.value = value;
this.next = new Array<SLNode<T> | 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<T> {
private head: SLNode<T>;
private level = 0;
private compare: (a: T, b: T) => number;
private p: number;
private maxLevel: number;
private state: number;

constructor(options: SkipListOptions<T> = {}) {
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<T>(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<SLNode<T>>(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<T>(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<SLNode<T>>(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<T> {
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);
}
}
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ describe('package entry point', () => {
| 'applyTreeDiff'
| 'UnionFind'
| 'BinaryHeap'
| 'BloomFilter'
| 'SegmentTree'
| 'SkipList'
>();

expectTypeOf<ExampleName<'search'>>().toEqualTypeOf<
Expand Down
17 changes: 17 additions & 0 deletions tests/skipList.test.ts
Original file line number Diff line number Diff line change
@@ -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<number>({ 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);
});
});