/** * data-structure-typed * * @author Pablo Zeng * @copyright Copyright (c) 2022 Pablo Zeng * @license MIT License */ import type { Comparator, EntryCallback } from '../../types'; import { ERR, raise } from '../../common'; import { IterableEntryBase } from '../base'; export class SkipListNode { key: K; value: V; forward: (SkipListNode | undefined)[]; constructor(key: K, value: V, level: number) { this.key = key; this.value = value; this.forward = new Array(level).fill(undefined); } } export type SkipListOptions = { comparator?: Comparator; toEntryFn?: (rawElement: R) => [K, V]; maxLevel?: number; probability?: number; }; export type SkipListRangeOptions = { lowInclusive?: boolean; highInclusive?: boolean; }; /** * SkipList — a probabilistic sorted key-value container. * * API mirrors TreeMap: users can swap `TreeMap` ↔ `SkipList` with zero code changes. * Reference: Java ConcurrentSkipListMap (NavigableMap interface). * * @example * // Display skip list * const sl = new SkipList([[1, 'a']]); * expect(() => sl.print()).not.toThrow(); */ export class SkipList extends IterableEntryBase { readonly #comparator: Comparator; readonly #isDefaultComparator: boolean; constructor( entries: Iterable | Iterable<[K, V | undefined]> = [], options: SkipListOptions = {} ) { super(); const { comparator, toEntryFn, maxLevel, probability } = options; if (typeof maxLevel === 'number' && maxLevel > 0) this._maxLevel = maxLevel; if (typeof probability === 'number' && probability > 0 && probability < 1) this._probability = probability; this.#isDefaultComparator = comparator === undefined; this.#comparator = comparator ?? SkipList.createDefaultComparator(); this._head = new SkipListNode(undefined as K, undefined as V, this._maxLevel); for (const item of entries) { let k: K; let v: V | undefined; if (toEntryFn) { [k, v] = toEntryFn(item as R); } else { if (!Array.isArray(item) || item.length < 2) { raise(TypeError, ERR.invalidEntry('SkipList')); } [k, v] = item as [K, V]; } this.set(k, v as V); } } /** * Creates a default comparator supporting number, string, Date, and bigint. */ static createDefaultComparator(): Comparator { return (a: K, b: K): number => { if (typeof a === 'number' && typeof b === 'number') { if (Number.isNaN(a) || Number.isNaN(b)) raise(TypeError, ERR.invalidNaN('SkipList')); return a - b; } if (typeof a === 'string' && typeof b === 'string') { return a < b ? -1 : a > b ? 1 : 0; } if (a instanceof Date && b instanceof Date) { const ta = a.getTime(), tb = b.getTime(); if (Number.isNaN(ta) || Number.isNaN(tb)) raise(TypeError, ERR.invalidDate('SkipList')); return ta - tb; } if (typeof a === 'bigint' && typeof b === 'bigint') { return a < b ? -1 : a > b ? 1 : 0; } raise(TypeError, ERR.comparatorRequired('SkipList')); }; } // ─── Internal state ────────────────────────────────────────── protected _head: SkipListNode; protected _level: number = 0; protected _size: number = 0; protected _maxLevel: number = 16; protected _probability: number = 0.5; // ─── Size & lifecycle ──────────────────────────────────────── get size(): number { return this._size; } get maxLevel(): number { return this._maxLevel; } get probability(): number { return this._probability; } get comparator(): Comparator { return this.#comparator; } /** * Check if empty * @example * // Check if empty * const sl = new SkipList(); * console.log(sl.isEmpty()); // true; */ isEmpty(): boolean { return this._size === 0; } /** * Remove all entries * @example * // Remove all entries * const sl = new SkipList([[1, 'a'], [2, 'b']]); * sl.clear(); * console.log(sl.isEmpty()); // true; */ clear(): void { this._head = new SkipListNode(undefined as K, undefined as V, this._maxLevel); this._level = 0; this._size = 0; } /** * Create independent copy * @example * // Create independent copy * const sl = new SkipList([[1, 'a'], [2, 'b']]); * const copy = sl.clone(); * copy.delete(1); * console.log(sl.has(1)); // true; */ clone(): this { return new SkipList(this as Iterable<[K, V | undefined]>, { comparator: this.#isDefaultComparator ? undefined : this.#comparator, maxLevel: this._maxLevel, probability: this._probability }) as this; } // ─── Core CRUD ─────────────────────────────────────────────── /** * Insert or update a key-value pair. Returns `this` for chaining. * Unique keys only — if key exists, value is updated in place. * @example * // In-memory sorted key-value store * const store = new SkipList(); * * store.set(3, 'three'); * store.set(1, 'one'); * store.set(5, 'five'); * store.set(2, 'two'); * * console.log(store.get(3)); // 'three'; * console.log(store.get(1)); // 'one'; * console.log(store.get(5)); // 'five'; * * // Update existing key * store.set(3, 'THREE'); * console.log(store.get(3)); // 'THREE'; */ set(key: K, value: V): this { const cmp = this.#comparator; const update = this._findUpdate(key); // If key already exists, update value in place const existing = update[0].forward[0]; if (existing && cmp(existing.key, key) === 0) { existing.value = value; return this; } const newLevel = this._randomLevel(); const newNode = new SkipListNode(key, value, newLevel); if (newLevel > this._level) { for (let i = this._level; i < newLevel; i++) { update[i] = this._head; } this._level = newLevel; } for (let i = 0; i < newLevel; i++) { newNode.forward[i] = update[i].forward[i]; update[i].forward[i] = newNode; } this._size++; return this; } /** * Get the value for a key, or `undefined` if not found. * Overrides base O(n) with O(log n) skip-list search. * @example * // Building a sorted index * type Product = { id: number; name: string; price: number }; * const products: Product[] = [ * { id: 1, name: 'Widget', price: 25 }, * { id: 2, name: 'Gadget', price: 50 }, * { id: 3, name: 'Doohickey', price: 15 } * ]; * * const index = new SkipList(products, { * toEntryFn: (p: Product) => [p.price, p] * }); * * // Iterate in sorted order by price * const names = [...index.values()].map(p => p!.name); * console.log(names); // ['Doohickey', 'Widget', 'Gadget']; * * // Range search: products between $20 and $60 * const range = index.rangeSearch([20, 60]); * console.log(range.map(([, p]) => p!.name)); // ['Widget', 'Gadget']; */ override get(key: K): V | undefined { const node = this._findNode(key); return node ? node.value : undefined; } /** * Check if a key exists. * Overrides base O(n) with O(log n) skip-list search. * @example * // Check key existence * const sl = new SkipList([[1, 'a'], [3, 'c'], [5, 'e']]); * console.log(sl.has(3)); // true; * console.log(sl.has(4)); // false; */ override has(key: K): boolean { return this._findNode(key) !== undefined; } /** * Delete a key. Returns `true` if the key was found and removed. * @example * // Fast lookup with deletion * const cache = new SkipList(); * * cache.set('alpha', 1); * cache.set('beta', 2); * cache.set('gamma', 3); * * console.log(cache.has('beta')); // true; * cache.delete('beta'); * console.log(cache.has('beta')); // false; * console.log(cache.size); // 2; */ delete(key: K): boolean { const cmp = this.#comparator; const update = this._findUpdate(key); const target = update[0].forward[0]; if (!target || cmp(target.key, key) !== 0) return false; for (let i = 0; i < this._level; i++) { if (update[i].forward[i] !== target) break; update[i].forward[i] = target.forward[i]; } while (this._level > 0 && !this._head.forward[this._level - 1]) { this._level--; } this._size--; return true; } // ─── Navigation ────────────────────────────────────────────── /** * Returns the first (smallest key) entry, or `undefined` if empty. * @example * // Access the minimum entry * const sl = new SkipList([[5, 'e'], [1, 'a'], [3, 'c']]); * console.log(sl.first()); // [1, 'a']; */ first(): [K, V | undefined] | undefined { const node = this._head.forward[0]; return node ? [node.key, node.value] : undefined; } /** * Returns the last (largest key) entry, or `undefined` if empty. * @example * // Access the maximum entry * const sl = new SkipList([[5, 'e'], [1, 'a'], [3, 'c']]); * console.log(sl.last()); // [5, 'e']; */ last(): [K, V | undefined] | undefined { let current = this._head; for (let i = this._level - 1; i >= 0; i--) { while (current.forward[i]) { current = current.forward[i]!; } } return current === this._head ? undefined : [current.key, current.value]; } /** * Remove and return the first (smallest key) entry. * @example * // Remove and return smallest * const sl = new SkipList([[1, 'a'], [2, 'b'], [3, 'c']]); * console.log(sl.pollFirst()); // [1, 'a']; * console.log(sl.size); // 2; */ pollFirst(): [K, V | undefined] | undefined { const entry = this.first(); if (!entry) return undefined; this.delete(entry[0]); return entry; } /** * Remove and return the last (largest key) entry. * @example * // Remove and return largest * const sl = new SkipList([[1, 'a'], [2, 'b'], [3, 'c']]); * console.log(sl.pollLast()); // [3, 'c']; * console.log(sl.size); // 2; */ pollLast(): [K, V | undefined] | undefined { const entry = this.last(); if (!entry) return undefined; this.delete(entry[0]); return entry; } /** * Least entry ≥ key, or `undefined`. * @example * // Least entry ≥ key * const sl = new SkipList([[10, 'a'], [20, 'b'], [30, 'c']]); * console.log(sl.ceiling(15)); // [20, 'b']; * console.log(sl.ceiling(20)); // [20, 'b']; */ ceiling(key: K): [K, V | undefined] | undefined { const cmp = this.#comparator; let current = this._head; for (let i = this._level - 1; i >= 0; i--) { while (current.forward[i] && cmp(current.forward[i]!.key, key) < 0) { current = current.forward[i]!; } } const node = current.forward[0]; return node ? [node.key, node.value] : undefined; } /** * Greatest entry ≤ key, or `undefined`. * @example * // Greatest entry ≤ key * const sl = new SkipList([[10, 'a'], [20, 'b'], [30, 'c']]); * console.log(sl.floor(25)); // [20, 'b']; * console.log(sl.floor(5)); // undefined; */ floor(key: K): [K, V | undefined] | undefined { const cmp = this.#comparator; let current = this._head; for (let i = this._level - 1; i >= 0; i--) { while (current.forward[i] && cmp(current.forward[i]!.key, key) <= 0) { current = current.forward[i]!; } } const result = current === this._head ? undefined : current; // Check if we're exactly at or before key if (result && cmp(result.key, key) <= 0) return [result.key, result.value]; return undefined; } /** * Least entry strictly > key, or `undefined`. * @example * // Strictly greater entry * const sl = new SkipList([[10, 'a'], [20, 'b'], [30, 'c']]); * console.log(sl.higher(15)); // [20, 'b']; * console.log(sl.higher(30)); // undefined; */ higher(key: K): [K, V | undefined] | undefined { const cmp = this.#comparator; let current = this._head; for (let i = this._level - 1; i >= 0; i--) { while (current.forward[i] && cmp(current.forward[i]!.key, key) <= 0) { current = current.forward[i]!; } } const node = current.forward[0]; return node ? [node.key, node.value] : undefined; } /** * Greatest entry strictly < key, or `undefined`. * @example * // Strictly less entry * const sl = new SkipList([[10, 'a'], [20, 'b'], [30, 'c']]); * console.log(sl.lower(25)); // [20, 'b']; * console.log(sl.lower(10)); // undefined; */ lower(key: K): [K, V | undefined] | undefined { const cmp = this.#comparator; let current = this._head; let result: SkipListNode | undefined; for (let i = this._level - 1; i >= 0; i--) { while (current.forward[i] && cmp(current.forward[i]!.key, key) < 0) { current = current.forward[i]!; } if (current !== this._head && cmp(current.key, key) < 0) { result = current; } } return result ? [result.key, result.value] : undefined; } /** * Returns entries within the given key range. * @example * // Find entries in a range * const sl = new SkipList([[1, 'a'], [2, 'b'], [3, 'c'], [4, 'd'], [5, 'e']]); * const result = sl.rangeSearch([2, 4]); * console.log(result); // [[2, 'b'], [3, 'c'], [4, 'd']]; */ rangeSearch(range: [K, K], options: SkipListRangeOptions = {}): Array<[K, V | undefined]> { const { lowInclusive = true, highInclusive = true } = options; const [low, high] = range; const cmp = this.#comparator; const out: Array<[K, V | undefined]> = []; // Start from the first node >= low let current = this._head; for (let i = this._level - 1; i >= 0; i--) { while (current.forward[i] && cmp(current.forward[i]!.key, low) < 0) { current = current.forward[i]!; } } current = current.forward[0]!; while (current) { const cmpHigh = cmp(current.key, high); if (cmpHigh > 0) break; if (cmpHigh === 0 && !highInclusive) break; const cmpLow = cmp(current.key, low); if (cmpLow > 0 || (cmpLow === 0 && lowInclusive)) { out.push([current.key, current.value]); } current = current.forward[0]!; } return out; } // ─── Functional (overrides) ────────────────────────────────── /** * Creates a new SkipList with entries transformed by callback. * @example * // Transform entries * const sl = new SkipList([[1, 'a'], [2, 'b']]); * const mapped = sl.map((v, k) => [k, v?.toUpperCase()] as [number, string]); * console.log([...mapped.values()]); // ['A', 'B']; */ map( callback: EntryCallback, options?: SkipListOptions ): SkipList { const out = new SkipList([], options ?? {}); let i = 0; for (const [k, v] of this) { const [nk, nv] = callback(v, k, i++, this); out.set(nk, nv); } return out; } /** * Creates a new SkipList with entries that pass the predicate. * @example * // Filter entries * const sl = new SkipList([[1, 'a'], [2, 'b'], [3, 'c']]); * const result = sl.filter((v, k) => k > 1); * console.log(result.size); // 2; */ filter( callbackfn: EntryCallback, thisArg?: unknown ): this { const out = new SkipList([], { comparator: this.#isDefaultComparator ? undefined : this.#comparator, maxLevel: this._maxLevel, probability: this._probability }); let i = 0; for (const [k, v] of this) { const ok = callbackfn.call(thisArg, v, k, i++, this); if (ok) out.set(k, v as V); } return out as this; } // ─── Iterator (required by IterableEntryBase) ──────────────── protected _getIterator(): IterableIterator<[K, V | undefined]> { const head = this._head; return (function* () { let node = head.forward[0]; while (node) { yield [node.key, node.value] as [K, V | undefined]; node = node.forward[0]; } })(); } // ─── Internal helpers ──────────────────────────────────────── /** * Finds the update array (predecessors at each level) for a given key. */ protected _findUpdate(key: K): SkipListNode[] { const cmp = this.#comparator; const update: SkipListNode[] = new Array(this._maxLevel).fill(this._head); let current = this._head; for (let i = this._level - 1; i >= 0; i--) { while (current.forward[i] && cmp(current.forward[i]!.key, key) < 0) { current = current.forward[i]!; } update[i] = current; } return update; } /** * Finds the node for a given key, or undefined. */ protected _findNode(key: K): SkipListNode | undefined { const cmp = this.#comparator; let current = this._head; for (let i = this._level - 1; i >= 0; i--) { while (current.forward[i] && cmp(current.forward[i]!.key, key) < 0) { current = current.forward[i]!; } } const candidate = current.forward[0]; if (candidate && cmp(candidate.key, key) === 0) return candidate; return undefined; } protected _randomLevel(): number { let level = 1; while (Math.random() < this._probability && level < this._maxLevel) { level++; } return level; } }