Skip to content

Commit 712771f

Browse files
✨ feat: First draft.
1 parent 99033bd commit 712771f

28 files changed

+9045
-8
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[@aureooms/js-skip-list](https://aureooms.github.io/js-skip-list)
1+
:fast_forward: [@aureooms/js-skip-list](https://aureooms.github.io/js-skip-list)
22
==
33

44
Skip list data structure for JavaScript.

package.json

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"transform-remove-console",
2828
{
2929
"exclude": [
30+
"debug",
3031
"log",
3132
"error",
3233
"warn"
@@ -77,6 +78,10 @@
7778
},
7879
"dependencies": {},
7980
"devDependencies": {
81+
"@aureooms/js-cardinality": "^2.0.0",
82+
"@aureooms/js-compare": "^1.4.8",
83+
"@aureooms/js-itertools": "^4.1.0",
84+
"@aureooms/js-random": "^2.0.0",
8085
"@babel/cli": "7.11.6",
8186
"@babel/core": "7.11.6",
8287
"@babel/preset-env": "7.11.5",
@@ -101,7 +106,15 @@
101106
"lib"
102107
],
103108
"homepage": "https://aureooms.github.io/js-skip-list",
104-
"keywords": ["data", "list", "randomized", "skip", "skip-list", "skiplist", "structures"],
109+
"keywords": [
110+
"data",
111+
"list",
112+
"randomized",
113+
"skip",
114+
"skip-list",
115+
"skiplist",
116+
"structures"
117+
],
105118
"license": "AGPL-3.0",
106119
"main": "lib/index.js",
107120
"prettier": {
@@ -133,12 +146,7 @@
133146
"unicorn"
134147
],
135148
"rules": {
136-
"unicorn/filename-case": [
137-
"error",
138-
{
139-
"case": "camelCase"
140-
}
141-
]
149+
"unicorn/filename-case": "off"
142150
}
143151
}
144152
}

src/Node.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export default function Node(
2+
key = undefined,
3+
down = null,
4+
left = null,
5+
right = null,
6+
up = null,
7+
) {
8+
this.key = key;
9+
this.right = right;
10+
this.down = down;
11+
this.left = left;
12+
this.up = up;
13+
}

src/SkipList.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import assert from 'assert';
2+
import Node from './Node';
3+
import keys from './keys';
4+
import iter from './iter';
5+
import downMost from './downMost';
6+
import searchTopMost from './searchTopMost';
7+
import bottomMostPredecessor from './bottomMostPredecessor';
8+
import deleteFromTopMost from './deleteFromTopMost';
9+
import insertFromBottomMostPredecessor from './insertFromBottomMostPredecessor';
10+
import makeQuasiRandom from './makeQuasiRandom';
11+
import makeBottomLevel from './makeBottomLevel';
12+
13+
/**
14+
* @param {Number} p Promotion probability in (0,1).
15+
* @param {Function} compare
16+
*/
17+
export default function SkipList(p, compare, head = new Node()) {
18+
this.head = head; // Sentinel node
19+
this.p = p;
20+
this.compare = compare;
21+
}
22+
23+
SkipList.prototype.add = function (key) {
24+
const node = bottomMostPredecessor(this.compare, this.head, key);
25+
const pred = insertFromBottomMostPredecessor(this.p, node, key);
26+
if (pred.left === null && pred.up === null) this.head = pred;
27+
};
28+
29+
SkipList.prototype.get = function (key) {
30+
const node = searchTopMost(this.compare, this.head, key);
31+
return node === null ? null : node.key;
32+
};
33+
34+
SkipList.prototype.has = function (key) {
35+
return searchTopMost(this.compare, this.head, key) !== null;
36+
};
37+
38+
SkipList.prototype.remove = function (key) {
39+
const node = searchTopMost(this.compare, this.head, key);
40+
if (node === null) return false;
41+
42+
deleteFromTopMost(node);
43+
return true;
44+
};
45+
46+
SkipList.prototype._predecessor = function (key) {
47+
return bottomMostPredecessor(this.compare, this.head, key);
48+
};
49+
50+
SkipList.prototype.levelOne = function () {
51+
return downMost(this.head);
52+
};
53+
54+
SkipList.prototype.levels = function () {
55+
assert(this.head !== null);
56+
let k = 0;
57+
let current = this.head;
58+
do {
59+
++k;
60+
current = current.down;
61+
} while (current !== null);
62+
63+
return k;
64+
};
65+
66+
SkipList.prototype.keys = function () {
67+
const level = this.levelOne();
68+
return keys(level);
69+
};
70+
71+
SkipList.prototype.values = function () {
72+
return this.keys();
73+
};
74+
75+
SkipList.prototype[Symbol.iterator] = function () {
76+
return this.keys();
77+
};
78+
79+
SkipList.prototype.range = function* (left, right) {
80+
const pred = this._predecessor(left);
81+
for (const node of iter(pred)) {
82+
if (this.compare(node.key, right) >= 0) break;
83+
yield node.key;
84+
}
85+
};
86+
87+
SkipList.from = (compare, iterable, p = 1 / 2) => {
88+
if (p === 1 / 2) {
89+
const bottomLevelHead = makeBottomLevel(compare, iterable);
90+
const head = makeQuasiRandom(p, bottomLevelHead);
91+
return new SkipList(p, compare, head);
92+
}
93+
94+
const list = new SkipList(p, compare);
95+
for (const key of iterable) list.add(key);
96+
return list;
97+
};

src/bottomMostPredecessor.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import assert from 'assert';
2+
3+
const bottomMostPredecessor = (compare, node, key) => {
4+
// eslint-disable-next-line no-constant-condition
5+
while (true) {
6+
assert(node !== null);
7+
// Predecessor search on current level
8+
while (node.right !== null && compare(key, node.right.key) > 0)
9+
node = node.right;
10+
11+
// Continue on bottom level or abandon
12+
if (node.down === null) return node;
13+
node = node.down;
14+
}
15+
};
16+
17+
export default bottomMostPredecessor;

src/debug.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import iter from './iter';
2+
import iterDown from './iterDown';
3+
import {enumerate} from '@aureooms/js-itertools';
4+
import {count} from '@aureooms/js-cardinality';
5+
6+
export default function* debug(skiplist) {
7+
for (const [k, sentinel] of enumerate(iterDown(skiplist.head))) {
8+
yield [k, count(iter(sentinel))];
9+
}
10+
}

src/deleteFromTopMost.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import assert from 'assert';
2+
3+
const deleteFromTopMost = (node) => {
4+
assert(node !== null);
5+
assert(node.up === null); // Node is from top-most level
6+
do {
7+
assert(node.left !== null); // Node is not sentinel
8+
node.left.right = node.right;
9+
if (node.right !== null) node.right.left = node.left;
10+
node = node.down;
11+
} while (node !== null);
12+
};
13+
14+
export default deleteFromTopMost;

src/downMost.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import assert from 'assert';
2+
3+
const downMost = (node) => {
4+
assert(node !== null);
5+
while (node.down !== null) node = node.down;
6+
return node;
7+
};
8+
9+
export default downMost;

src/heads.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
2+
// "The Math.random() function returns a floating-point, pseudo-random number in
3+
// the range 0 to less than 1 (inclusive of 0, but not 1)"
4+
const heads = (bias = 1 / 2) => Math.random() < bias;
5+
6+
export default heads;

src/index.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Node from './Node';
2+
import SkipList from './SkipList';
3+
import bottomMostPredecessor from './bottomMostPredecessor';
4+
import debug from './debug';
5+
import deleteFromTopMost from './deleteFromTopMost';
6+
import downMost from './downMost';
7+
import heads from './heads';
8+
import insertFromBottomMostPredecessor from './insertFromBottomMostPredecessor';
9+
import iter from './iter';
10+
import iterDown from './iterDown';
11+
import keys from './keys';
12+
import makeBottomLevel from './makeBottomLevel';
13+
import makeDeterministic from './makeDeterministic';
14+
import makeQuasiRandom from './makeQuasiRandom';
15+
import node from './node';
16+
import predecessorOnPreviousLevel from './predecessorOnPreviousLevel';
17+
import searchTopMost from './searchTopMost';
18+
19+
export default SkipList;
20+
21+
export {
22+
Node,
23+
SkipList,
24+
bottomMostPredecessor,
25+
debug,
26+
deleteFromTopMost,
27+
downMost,
28+
heads,
29+
insertFromBottomMostPredecessor,
30+
iter,
31+
iterDown,
32+
keys,
33+
makeBottomLevel,
34+
makeDeterministic,
35+
makeQuasiRandom,
36+
node,
37+
predecessorOnPreviousLevel,
38+
searchTopMost,
39+
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import assert from 'assert';
2+
3+
import node from './node';
4+
import heads from './heads';
5+
import predecessorOnPreviousLevel from './predecessorOnPreviousLevel';
6+
7+
const insertFromBottomMostPredecessor = (p, pred, key) => {
8+
assert(pred !== null);
9+
let newNode = node(key, null, pred, pred.right);
10+
while (heads(p)) {
11+
pred = predecessorOnPreviousLevel(pred);
12+
assert(pred !== null);
13+
newNode = node(key, newNode, pred, pred.right);
14+
}
15+
16+
return pred;
17+
};
18+
19+
export default insertFromBottomMostPredecessor;

src/iter.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import assert from 'assert';
2+
3+
export default function* iter(head) {
4+
assert(head !== null);
5+
while (head.right !== null) {
6+
head = head.right;
7+
yield head;
8+
}
9+
}

src/iterDown.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default function* iterDown(head) {
2+
while (head !== null) {
3+
yield head;
4+
head = head.down;
5+
}
6+
}

src/keys.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import iter from './iter';
2+
3+
export default function* keys(level) {
4+
for (const node of iter(level)) yield node.key;
5+
}

src/makeBottomLevel.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Node from './Node';
2+
3+
const makeBottomLevel = (compare, iterable) => {
4+
const sorted = [...iterable].sort(compare);
5+
const bottomLevelHead = new Node();
6+
let current = bottomLevelHead;
7+
for (const key of sorted) {
8+
current = new Node(key, null, current);
9+
current.left.right = current;
10+
}
11+
12+
return bottomLevelHead;
13+
};
14+
15+
export default makeBottomLevel;

src/makeDeterministic.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import assert from 'assert';
2+
import Node from './Node';
3+
4+
/**
5+
* Make a balanced Skip-list with p = 1/2.
6+
*/
7+
const makeDeterministic = (p, head) => {
8+
assert(p === 1 / 2);
9+
10+
// Together, the if and while conditions guarantee that `makeQuasiRandom`
11+
// does not do anything if the number of elements is <= 1
12+
// TODO Could use some other constant?
13+
if (head.right === null) return head;
14+
15+
while (head.right.right !== null) {
16+
const newHead = new Node(head.key, head);
17+
18+
let current = head;
19+
let next = newHead;
20+
while (current.right !== null) {
21+
current = current.right;
22+
if (current.right === null) break; // Skip odd indexed nodes
23+
current = current.right;
24+
next = new Node(current.key, current, next);
25+
next.left.right = next;
26+
current.up = next;
27+
}
28+
29+
head.up = newHead;
30+
head = newHead;
31+
assert(head.right !== null);
32+
}
33+
34+
return head;
35+
};
36+
37+
export default makeDeterministic;

0 commit comments

Comments
 (0)