undirected-graph-typed
Version:
Undirected Graph
1,331 lines (531 loc) • 21.3 kB
text/typescript
/**
* data-structure-typed
*
* @author Pablo Zeng
* @copyright Copyright (c) 2022 Pablo Zeng <zrwusa@gmail.com>
* @license MIT License
*/
import type { Comparator, EntryCallback } from '../../types';
import { ERR, raise } from '../../common';
import { IterableEntryBase } from '../base';
export class SkipListNode<K, V> {
key: K;
value: V;
forward: (SkipListNode<K, V> | undefined)[];
constructor(key: K, value: V, level: number) {
this.key = key;
this.value = value;
this.forward = new Array(level).fill(undefined);
}
}
export type SkipListOptions<K, V, R = [K, V]> = {
comparator?: Comparator<K>;
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<number, string>([[1, 'a']]);
* expect(() => sl.print()).not.toThrow();
*/
export class SkipList<K = any, V = any, R = [K, V]> extends IterableEntryBase<K, V | undefined> {
readonly #comparator: Comparator<K>;
readonly #isDefaultComparator: boolean;
constructor(
entries: Iterable<R> | Iterable<[K, V | undefined]> = [],
options: SkipListOptions<K, V, R> = {}
) {
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<K>();
this._head = new SkipListNode<K, V>(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<K>(): Comparator<K> {
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<K, V>;
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<K> {
return this.#comparator;
}
/**
* Check if empty
* @example
* // Check if empty
* const sl = new SkipList<number, string>();
* console.log(sl.isEmpty()); // true;
*/
isEmpty(): boolean {
return this._size === 0;
}
/**
* Remove all entries
* @example
* // Remove all entries
* const sl = new SkipList<number, string>([[1, 'a'], [2, 'b']]);
* sl.clear();
* console.log(sl.isEmpty()); // true;
*/
clear(): void {
this._head = new SkipListNode<K, V>(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<number, string>([[1, 'a'], [2, 'b']]);
* const copy = sl.clone();
* copy.delete(1);
* console.log(sl.has(1)); // true;
*/
clone(): this {
return new SkipList<K, V, R>(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<number, string>();
*
* 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<number, Product, Product>(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<number, string>([[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<string, number>();
*
* 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<number, string>([[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<number, string>([[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<number, string>([[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<number, string>([[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<number, string>([[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<number, string>([[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<number, string>([[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<number, string>([[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<K, V> | 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<number, string>([[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<number, string>([[1, 'a'], [2, 'b']]);
* const mapped = sl.map((v, k) => [k, v?.toUpperCase()] as [number, string]);
* console.log([...mapped.values()]); // ['A', 'B'];
*/
map<MK, MV>(
callback: EntryCallback<K, V | undefined, [MK, MV]>,
options?: SkipListOptions<MK, MV>
): SkipList<MK, MV> {
const out = new SkipList<MK, MV>([], 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<number, string>([[1, 'a'], [2, 'b'], [3, 'c']]);
* const result = sl.filter((v, k) => k > 1);
* console.log(result.size); // 2;
*/
filter(
callbackfn: EntryCallback<K, V | undefined, boolean>,
thisArg?: unknown
): this {
const out = new SkipList<K, V, R>([], {
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<K, V>[] {
const cmp = this.#comparator;
const update: SkipListNode<K, V>[] = 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<K, V> | 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;
}
}