diff --git a/Data-Structures/Linked-List/SkipList.js b/Data-Structures/Linked-List/SkipList.js new file mode 100644 index 0000000000..d53991288c --- /dev/null +++ b/Data-Structures/Linked-List/SkipList.js @@ -0,0 +1,311 @@ +/** + * Skip List + * + * A skip list is a probabilistic data structure that maintains an ordered set + * of keys and supports search, insertion and deletion in expected O(log n) + * time. It is built from a stack of sorted linked lists where each higher + * level skips over more elements, acting as an "express lane" into the level + * below. Each newly inserted node is promoted to the next level with a fixed + * probability `p` (typically 1/2), giving an expected height of log_{1/p}(n). + * + * Compared to balanced binary search trees, skip lists are simpler to + * implement and require no rotations to maintain balance. + * + * Reference: https://en.wikipedia.org/wiki/Skip_list + */ + +const DEFAULT_MAX_LEVEL = 16 +const DEFAULT_P = 0.5 + +class SkipListNode { + /** + * @param {*} key - Ordered key stored at this node, or `null` for the head sentinel. + * @param {*} value - Arbitrary value associated with `key`. + * @param {number} level - Height of the node (number of forward pointers). + */ + constructor(key, value, level) { + this.key = key + this.value = value + this.forward = new Array(level + 1).fill(null) + } +} + +class SkipList { + /** + * Create an empty skip list. + * + * @param {object} [options] - Optional tuning parameters. + * @param {number} [options.maxLevel=16] - Maximum number of levels (>= 1). + * @param {number} [options.p=0.5] - Promotion probability in (0, 1). + * @param {() => number} [options.random=Math.random] - RNG returning a value in [0, 1); injectable for deterministic tests. + * + * @example + * const list = new SkipList() + * list.insert(3, 'three') + * list.search(3) // 'three' + */ + constructor(options = {}) { + const { + maxLevel = DEFAULT_MAX_LEVEL, + p = DEFAULT_P, + random = Math.random + } = options + + if (!Number.isInteger(maxLevel) || maxLevel < 1) { + throw new RangeError('maxLevel must be a positive integer') + } + if (typeof p !== 'number' || p <= 0 || p >= 1) { + throw new RangeError('p must be a number in the open interval (0, 1)') + } + if (typeof random !== 'function') { + throw new TypeError( + 'random must be a function returning a value in [0, 1)' + ) + } + + this.maxLevel = maxLevel + this.p = p + this.random = random + this.level = 0 + this.length = 0 + this.head = new SkipListNode(null, null, maxLevel) + } + + /** + * Pick a random level in [0, maxLevel] using geometric distribution. + * + * @returns {number} The chosen level for a new node. + * @private + */ + _randomLevel() { + let lvl = 0 + while (this.random() < this.p && lvl < this.maxLevel) { + lvl++ + } + return lvl + } + + /** + * Compare two keys using `<` and `>`. Numbers, strings and any type that + * supports the relational operators consistently can be used as keys. + * + * @param {*} a - Left-hand key. + * @param {*} b - Right-hand key. + * @returns {number} `-1` if `a < b`, `1` if `a > b`, `0` otherwise. + * @private + */ + _compare(a, b) { + if (a < b) return -1 + if (a > b) return 1 + return 0 + } + + /** + * Insert a key/value pair. If the key already exists, its value is updated + * in place and the size of the list does not change. + * + * @param {*} key - Key to insert; must be comparable with existing keys. + * @param {*} [value] - Value associated with the key (defaults to the key). + * @returns {SkipList} The list, for chaining. + * + * @example + * const list = new SkipList() + * list.insert(1, 'a').insert(2, 'b') + * list.size() // 2 + */ + insert(key, value = key) { + const update = new Array(this.maxLevel + 1).fill(this.head) + let current = this.head + + for (let i = this.level; i >= 0; i--) { + while ( + current.forward[i] !== null && + this._compare(current.forward[i].key, key) < 0 + ) { + current = current.forward[i] + } + update[i] = current + } + + current = current.forward[0] + + if (current !== null && this._compare(current.key, key) === 0) { + current.value = value + return this + } + + const lvl = this._randomLevel() + if (lvl > this.level) { + for (let i = this.level + 1; i <= lvl; i++) { + update[i] = this.head + } + this.level = lvl + } + + const node = new SkipListNode(key, value, lvl) + for (let i = 0; i <= lvl; i++) { + node.forward[i] = update[i].forward[i] + update[i].forward[i] = node + } + this.length++ + return this + } + + /** + * Look up the value associated with a key. + * + * @param {*} key - Key to search for. + * @returns {*} The associated value, or `undefined` if `key` is absent. + * + * @example + * const list = new SkipList() + * list.insert('apple', 1) + * list.search('apple') // 1 + * list.search('banana') // undefined + */ + search(key) { + let current = this.head + for (let i = this.level; i >= 0; i--) { + while ( + current.forward[i] !== null && + this._compare(current.forward[i].key, key) < 0 + ) { + current = current.forward[i] + } + } + current = current.forward[0] + if (current !== null && this._compare(current.key, key) === 0) { + return current.value + } + return undefined + } + + /** + * Check whether a key is present. + * + * @param {*} key - Key to test. + * @returns {boolean} `true` if the key is present. + * + * @example + * const list = new SkipList() + * list.insert(7) + * list.has(7) // true + */ + has(key) { + let current = this.head + for (let i = this.level; i >= 0; i--) { + while ( + current.forward[i] !== null && + this._compare(current.forward[i].key, key) < 0 + ) { + current = current.forward[i] + } + } + current = current.forward[0] + return current !== null && this._compare(current.key, key) === 0 + } + + /** + * Remove the entry with the given key. + * + * @param {*} key - Key to delete. + * @returns {boolean} `true` if a node was removed, `false` if the key was absent. + * + * @example + * const list = new SkipList() + * list.insert(1).insert(2) + * list.delete(1) // true + * list.delete(1) // false + */ + delete(key) { + const update = new Array(this.maxLevel + 1).fill(this.head) + let current = this.head + + for (let i = this.level; i >= 0; i--) { + while ( + current.forward[i] !== null && + this._compare(current.forward[i].key, key) < 0 + ) { + current = current.forward[i] + } + update[i] = current + } + + current = current.forward[0] + if (current === null || this._compare(current.key, key) !== 0) { + return false + } + + for (let i = 0; i <= this.level; i++) { + if (update[i].forward[i] !== current) break + update[i].forward[i] = current.forward[i] + } + + while (this.level > 0 && this.head.forward[this.level] === null) { + this.level-- + } + this.length-- + return true + } + + /** + * Number of distinct keys in the list. + * + * @returns {number} The current size. + * + * @example + * const list = new SkipList() + * list.insert(1).insert(2).insert(2) + * list.size() // 2 + */ + size() { + return this.length + } + + /** + * Whether the list contains no keys. + * + * @returns {boolean} `true` when empty. + */ + isEmpty() { + return this.length === 0 + } + + /** + * Yield `[key, value]` pairs in ascending key order. + * + * @returns {Generator<[*, *]>} Iterator over the entries of the list. + * + * @example + * const list = new SkipList() + * list.insert(2, 'b').insert(1, 'a') + * Array.from(list.entries()) // [[1, 'a'], [2, 'b']] + */ + *entries() { + let current = this.head.forward[0] + while (current !== null) { + yield [current.key, current.value] + current = current.forward[0] + } + } + + /** + * Default iterator yielding keys in ascending order. + * + * @returns {Generator<*>} Iterator over keys. + * + * @example + * const list = new SkipList() + * list.insert(3).insert(1).insert(2) + * [...list] // [1, 2, 3] + */ + *[Symbol.iterator]() { + let current = this.head.forward[0] + while (current !== null) { + yield current.key + current = current.forward[0] + } + } +} + +export { SkipList, SkipListNode } diff --git a/Data-Structures/Linked-List/test/SkipList.test.js b/Data-Structures/Linked-List/test/SkipList.test.js new file mode 100644 index 0000000000..475aa8283a --- /dev/null +++ b/Data-Structures/Linked-List/test/SkipList.test.js @@ -0,0 +1,141 @@ +import { SkipList } from '../SkipList' + +describe('SkipList', () => { + it('starts empty', () => { + const list = new SkipList() + expect(list.size()).toBe(0) + expect(list.isEmpty()).toBe(true) + expect(list.search(1)).toBeUndefined() + expect(list.has(1)).toBe(false) + }) + + it('inserts keys and tracks size', () => { + const list = new SkipList() + list.insert(3).insert(1).insert(2) + expect(list.size()).toBe(3) + expect(list.isEmpty()).toBe(false) + }) + + it('searches for inserted values', () => { + const list = new SkipList() + list.insert(1, 'one') + list.insert(2, 'two') + list.insert(3, 'three') + expect(list.search(1)).toBe('one') + expect(list.search(2)).toBe('two') + expect(list.search(3)).toBe('three') + expect(list.search(4)).toBeUndefined() + }) + + it('uses the key as default value when no value is given', () => { + const list = new SkipList() + list.insert(42) + expect(list.search(42)).toBe(42) + }) + + it('updates the value when the same key is reinserted', () => { + const list = new SkipList() + list.insert(1, 'a') + list.insert(1, 'b') + expect(list.size()).toBe(1) + expect(list.search(1)).toBe('b') + }) + + it('reports membership via has()', () => { + const list = new SkipList() + list.insert('apple') + list.insert('banana') + expect(list.has('apple')).toBe(true) + expect(list.has('banana')).toBe(true) + expect(list.has('cherry')).toBe(false) + }) + + it('deletes existing keys and leaves remaining ones intact', () => { + const list = new SkipList() + for (const k of [5, 1, 4, 2, 3]) list.insert(k) + expect(list.delete(3)).toBe(true) + expect(list.size()).toBe(4) + expect(list.has(3)).toBe(false) + expect([...list]).toEqual([1, 2, 4, 5]) + }) + + it('returns false when deleting an absent key', () => { + const list = new SkipList() + list.insert(1) + expect(list.delete(99)).toBe(false) + expect(list.size()).toBe(1) + }) + + it('iterates keys in ascending order regardless of insertion order', () => { + const list = new SkipList() + for (const k of [10, 3, 7, 1, 4, 9, 2, 8, 6, 5]) list.insert(k) + expect([...list]).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + }) + + it('yields [key, value] pairs via entries()', () => { + const list = new SkipList() + list.insert(2, 'b').insert(1, 'a').insert(3, 'c') + expect(Array.from(list.entries())).toEqual([ + [1, 'a'], + [2, 'b'], + [3, 'c'] + ]) + }) + + it('supports string keys with lexicographic ordering', () => { + const list = new SkipList() + for (const k of ['delta', 'alpha', 'charlie', 'bravo']) list.insert(k) + expect([...list]).toEqual(['alpha', 'bravo', 'charlie', 'delta']) + }) + + it('handles a randomized stress workload against a reference set', () => { + // Deterministic LCG so the test is reproducible. + let state = 0x1234abcd + const rand = () => { + state = (state * 1103515245 + 12345) & 0x7fffffff + return state / 0x80000000 + } + + const list = new SkipList({ random: rand }) + const reference = new Set() + + for (let i = 0; i < 500; i++) { + const key = Math.floor(rand() * 200) + const op = rand() + if (op < 0.6) { + list.insert(key) + reference.add(key) + } else { + list.delete(key) + reference.delete(key) + } + } + + const expected = [...reference].sort((a, b) => a - b) + expect([...list]).toEqual(expected) + expect(list.size()).toBe(reference.size) + for (const k of expected) expect(list.has(k)).toBe(true) + }) + + it('rejects invalid constructor options', () => { + expect(() => new SkipList({ maxLevel: 0 })).toThrow(RangeError) + expect(() => new SkipList({ maxLevel: 1.5 })).toThrow(RangeError) + expect(() => new SkipList({ p: 0 })).toThrow(RangeError) + expect(() => new SkipList({ p: 1 })).toThrow(RangeError) + expect(() => new SkipList({ random: 'nope' })).toThrow(TypeError) + }) + + it('respects a deterministic random source', () => { + // Force every node to the highest reachable level. + const list = new SkipList({ maxLevel: 4, p: 0.5, random: () => 0 }) + for (const k of [1, 2, 3, 4]) list.insert(k) + expect(list.level).toBe(4) + expect([...list]).toEqual([1, 2, 3, 4]) + + // Force every node to level 0 (no promotions). + const flat = new SkipList({ maxLevel: 4, p: 0.5, random: () => 0.99 }) + for (const k of [1, 2, 3, 4]) flat.insert(k) + expect(flat.level).toBe(0) + expect([...flat]).toEqual([1, 2, 3, 4]) + }) +})