974 Skip List
Tutorial
The Problem
Implement a skip list — a probabilistic sorted data structure providing O(log n) average search, insert, and delete. Use an arena-based approach: nodes are stored in a Vec and referenced by index instead of raw pointers. Each node has a forward: Vec<usize> of index references to nodes at each level. Use a deterministic xorshift PRNG for reproducible tests.
🎯 Learning Outcomes
Vec<SkipListNode> where node 0 is the header sentinelrandom_level() using a geometric distribution: keep incrementing level while PRNG output < P=0.5, up to MAX_LEVELsearch(value) -> bool by descending from max level and advancing forward[level] while value is too smallinsert(value) using the same top-down search with an update array tracking the predecessor at each levelCode Example
#![allow(clippy::all)]
// 974: Skip List (Simplified)
// Probabilistic sorted structure: O(log n) average search/insert
// Uses raw indices instead of pointers (safer than raw pointers in Rust)
use std::collections::VecDeque;
const MAX_LEVEL: usize = 8;
const P: f64 = 0.5;
// Simple deterministic PRNG for reproducible tests (xorshift)
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Rng(seed)
}
fn next_f64(&mut self) -> f64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
(self.0 & 0xFFFF) as f64 / 0x10000 as f64
}
fn random_level(&mut self) -> usize {
let mut level = 1;
while level < MAX_LEVEL && self.next_f64() < P {
level += 1;
}
level
}
}
// Arena-based skip list: nodes stored in Vec, referenced by index
// 0 = header sentinel
struct SkipListNode {
value: i64,
forward: Vec<usize>, // indices into node arena; 0 = None (header is sentinel)
}
pub struct SkipList {
nodes: Vec<SkipListNode>,
level: usize,
rng: Rng,
}
impl SkipList {
pub fn new() -> Self {
let header = SkipListNode {
value: i64::MIN,
forward: vec![0; MAX_LEVEL], // 0 = nil (self-loop = nil for header)
};
SkipList {
nodes: vec![header],
level: 0,
rng: Rng::new(12345),
}
}
fn is_nil(&self, idx: usize) -> bool {
idx == 0 && self.nodes[0].forward[0] == 0 || (idx == 0 && self.level == 0)
}
pub fn search(&self, target: i64) -> bool {
let mut current = 0usize; // start at header
for i in (0..self.level).rev() {
loop {
let next = self.nodes[current].forward[i];
if next == 0 {
break;
}
if self.nodes[next].value < target {
current = next;
} else {
break;
}
}
}
let next = self.nodes[current].forward[0];
next != 0 && self.nodes[next].value == target
}
pub fn insert(&mut self, value: i64) {
let mut update = vec![0usize; MAX_LEVEL];
let mut current = 0usize;
for i in (0..self.level).rev() {
loop {
let next = self.nodes[current].forward[i];
if next == 0 || self.nodes[next].value >= value {
break;
}
current = next;
}
update[i] = current;
}
let new_level = self.rng.random_level();
if new_level > self.level {
for i in self.level..new_level {
update[i] = 0; // header
}
self.level = new_level;
}
let new_idx = self.nodes.len();
let mut new_node = SkipListNode {
value,
forward: vec![0; new_level],
};
for i in 0..new_level {
new_node.forward[i] = self.nodes[update[i]].forward[i];
self.nodes[update[i]].forward[i] = new_idx;
}
self.nodes.push(new_node);
}
pub fn to_vec(&self) -> Vec<i64> {
let mut result = vec![];
let mut current = self.nodes[0].forward[0];
while current != 0 {
result.push(self.nodes[current].value);
current = self.nodes[current].forward[0];
}
result
}
}
impl Default for SkipList {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sorted_order() {
let mut sl = SkipList::new();
for v in [5, 3, 7, 1, 9, 4, 6, 2, 8] {
sl.insert(v);
}
assert_eq!(sl.to_vec(), vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
#[test]
fn test_search_found() {
let mut sl = SkipList::new();
for v in [5, 3, 7, 1, 9] {
sl.insert(v);
}
assert!(sl.search(5));
assert!(sl.search(1));
assert!(sl.search(9));
}
#[test]
fn test_search_not_found() {
let mut sl = SkipList::new();
for v in [5, 3, 7, 1, 9] {
sl.insert(v);
}
assert!(!sl.search(0));
assert!(!sl.search(10));
assert!(!sl.search(4));
}
#[test]
fn test_empty() {
let sl = SkipList::new();
assert_eq!(sl.to_vec(), Vec::<i64>::new());
assert!(!sl.search(1));
}
#[test]
fn test_single() {
let mut sl = SkipList::new();
sl.insert(42);
assert_eq!(sl.to_vec(), vec![42]);
assert!(sl.search(42));
assert!(!sl.search(43));
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Arena | Vec<SkipListNode> | DynArray.t or Array.t ref |
| Null pointer | Index 0 (header sentinel) | Same convention |
| Raw pointers | Not needed — indices only | Not needed with arena |
| PRNG | Inline xorshift (deterministic) | Random.float (stdlib) |
| Level randomization | Geometric distribution, P=0.5 | Same |
Skip lists provide similar O(log n) average performance to balanced BSTs but are simpler to implement. They are used in Redis (sorted sets), LevelDB, and various in-memory databases.
OCaml Approach
type node = {
value: int;
mutable forward: int array; (* indices into node array *)
}
type t = {
nodes: node DynArray.t; (* resizable array *)
mutable level: int;
rng: unit -> float;
}
let search sl v =
let current = ref 0 in
for lvl = sl.level - 1 downto 0 do
while (DynArray.get sl.nodes (DynArray.get sl.nodes !current).forward.(lvl)).value < v do
current := (DynArray.get sl.nodes !current).forward.(lvl)
done
done;
let next = (DynArray.get sl.nodes !current).forward.(0) in
next <> 0 && (DynArray.get sl.nodes next).value = v
OCaml's approach is structurally identical — arena-based with mutable forward arrays. The key difference is that OCaml uses DynArray (Batteries) or Array.t ref for the arena, while Rust uses a Vec.
Full Source
#![allow(clippy::all)]
// 974: Skip List (Simplified)
// Probabilistic sorted structure: O(log n) average search/insert
// Uses raw indices instead of pointers (safer than raw pointers in Rust)
use std::collections::VecDeque;
const MAX_LEVEL: usize = 8;
const P: f64 = 0.5;
// Simple deterministic PRNG for reproducible tests (xorshift)
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Rng(seed)
}
fn next_f64(&mut self) -> f64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
(self.0 & 0xFFFF) as f64 / 0x10000 as f64
}
fn random_level(&mut self) -> usize {
let mut level = 1;
while level < MAX_LEVEL && self.next_f64() < P {
level += 1;
}
level
}
}
// Arena-based skip list: nodes stored in Vec, referenced by index
// 0 = header sentinel
struct SkipListNode {
value: i64,
forward: Vec<usize>, // indices into node arena; 0 = None (header is sentinel)
}
pub struct SkipList {
nodes: Vec<SkipListNode>,
level: usize,
rng: Rng,
}
impl SkipList {
pub fn new() -> Self {
let header = SkipListNode {
value: i64::MIN,
forward: vec![0; MAX_LEVEL], // 0 = nil (self-loop = nil for header)
};
SkipList {
nodes: vec![header],
level: 0,
rng: Rng::new(12345),
}
}
fn is_nil(&self, idx: usize) -> bool {
idx == 0 && self.nodes[0].forward[0] == 0 || (idx == 0 && self.level == 0)
}
pub fn search(&self, target: i64) -> bool {
let mut current = 0usize; // start at header
for i in (0..self.level).rev() {
loop {
let next = self.nodes[current].forward[i];
if next == 0 {
break;
}
if self.nodes[next].value < target {
current = next;
} else {
break;
}
}
}
let next = self.nodes[current].forward[0];
next != 0 && self.nodes[next].value == target
}
pub fn insert(&mut self, value: i64) {
let mut update = vec![0usize; MAX_LEVEL];
let mut current = 0usize;
for i in (0..self.level).rev() {
loop {
let next = self.nodes[current].forward[i];
if next == 0 || self.nodes[next].value >= value {
break;
}
current = next;
}
update[i] = current;
}
let new_level = self.rng.random_level();
if new_level > self.level {
for i in self.level..new_level {
update[i] = 0; // header
}
self.level = new_level;
}
let new_idx = self.nodes.len();
let mut new_node = SkipListNode {
value,
forward: vec![0; new_level],
};
for i in 0..new_level {
new_node.forward[i] = self.nodes[update[i]].forward[i];
self.nodes[update[i]].forward[i] = new_idx;
}
self.nodes.push(new_node);
}
pub fn to_vec(&self) -> Vec<i64> {
let mut result = vec![];
let mut current = self.nodes[0].forward[0];
while current != 0 {
result.push(self.nodes[current].value);
current = self.nodes[current].forward[0];
}
result
}
}
impl Default for SkipList {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sorted_order() {
let mut sl = SkipList::new();
for v in [5, 3, 7, 1, 9, 4, 6, 2, 8] {
sl.insert(v);
}
assert_eq!(sl.to_vec(), vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
#[test]
fn test_search_found() {
let mut sl = SkipList::new();
for v in [5, 3, 7, 1, 9] {
sl.insert(v);
}
assert!(sl.search(5));
assert!(sl.search(1));
assert!(sl.search(9));
}
#[test]
fn test_search_not_found() {
let mut sl = SkipList::new();
for v in [5, 3, 7, 1, 9] {
sl.insert(v);
}
assert!(!sl.search(0));
assert!(!sl.search(10));
assert!(!sl.search(4));
}
#[test]
fn test_empty() {
let sl = SkipList::new();
assert_eq!(sl.to_vec(), Vec::<i64>::new());
assert!(!sl.search(1));
}
#[test]
fn test_single() {
let mut sl = SkipList::new();
sl.insert(42);
assert_eq!(sl.to_vec(), vec![42]);
assert!(sl.search(42));
assert!(!sl.search(43));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sorted_order() {
let mut sl = SkipList::new();
for v in [5, 3, 7, 1, 9, 4, 6, 2, 8] {
sl.insert(v);
}
assert_eq!(sl.to_vec(), vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
#[test]
fn test_search_found() {
let mut sl = SkipList::new();
for v in [5, 3, 7, 1, 9] {
sl.insert(v);
}
assert!(sl.search(5));
assert!(sl.search(1));
assert!(sl.search(9));
}
#[test]
fn test_search_not_found() {
let mut sl = SkipList::new();
for v in [5, 3, 7, 1, 9] {
sl.insert(v);
}
assert!(!sl.search(0));
assert!(!sl.search(10));
assert!(!sl.search(4));
}
#[test]
fn test_empty() {
let sl = SkipList::new();
assert_eq!(sl.to_vec(), Vec::<i64>::new());
assert!(!sl.search(1));
}
#[test]
fn test_single() {
let mut sl = SkipList::new();
sl.insert(42);
assert_eq!(sl.to_vec(), vec![42]);
assert!(sl.search(42));
assert!(!sl.search(43));
}
}
Deep Comparison
Skip List — Comparison
Core Insight
A skip list is a sorted linked list with multiple levels of "skip" pointers. Inserting at a random height (O(log n) average) gives probabilistic O(log n) search. OCaml naturally models pointers as 'a node option (nullable reference); Rust avoids unsafe raw pointers by using an arena (Vec<Node>) and integer indices — safe, cache-friendly, and avoids lifetime complexity.
OCaml Approach
mutable forward: 'a node option array — array of optional node pointers per levelObj.magic () for uninitiated header value (unsafe but practical)ref cells for traversal: let current = ref sl.headerwhile !continue_ do ... done pattern for conditional loopsRandom.float 1.0 for probabilistic level generationupdate.(i).forward.(i) <- Some new_nodeRust Approach
Vec<SkipListNode> with usize index references (no raw pointers)0 = header sentinel (always exists); forward[i] == 0 means nilRng (xorshift) for deterministic testingfor i in (0..self.level).rev() — iterate levels top to bottomself.nodes[idx] for node access by indexunsafe code needed — arena pattern solves the "many mutable pointers" problemComparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Node pointers | 'a node option | usize index into arena |
| Null/nil | None | Index 0 (header) |
| Arena | n/a (GC heap) | Vec<SkipListNode> |
| Mutation | mutable fields | &mut self |
| Traversal ptr | let current = ref header | let mut current = 0usize |
| RNG | Random.float 1.0 | Custom xorshift Rng |
| Unsafe | Obj.magic for init | None required |
| Nil forward | None check | next == 0 check |
Exercises
delete(value) -> bool using the same update array as insert.range_query(l, r) -> Vec<i64> that returns all values in [l, r] in sorted order.rank(value) -> Option<usize> that returns the position of value in sorted order.SkipList<T: Ord> rather than SkipList with i64.BTreeSet for 100,000 random insertions and lookups.