Master skip lists — what they are, how probabilistic layering achieves O(log n) expected search, complete implementation with insert/search/delete, why Redis and LevelDB use skip lists instead of balanced trees, and comparison with AVL trees and hash maps.
What Is a Skip List?
A skip list is a layered linked list data structure that uses randomization to achieve O(log n) expected time for search, insert, and delete operations — the same as a balanced BST — but with a much simpler implementation.
The key idea: multiple layers of linked lists, where each higher layer is a "fast lane" with fewer elements, allowing searches to skip over large sections of the list.
| Level 3 | 1 ──────────────────────────────── 9 |
| Level 2 | 1 ─────────── 5 ───────────────── 9 |
| Level 1 | 1 ─── 3 ────── 5 ─── 7 ────────── 9 |
| Level 0 | 1 ─ 2 ─ 3 ─ 4 ─ 5 ─ 6 ─ 7 ─ 8 ─ 9 |
| Start at Level 3: 1 | 9 (9>7, drop down) |
| Level 2: 1 | 5→9 (9>7, from 5 drop down) |
| Level 1: 5 | 7 FOUND! |
Instead of searching all 9 elements, only 5 comparisons needed.
Complete Skip List Implementation
import random
class SkipNode:
def __init__(self, key, value, level):
self.key = key
self.value = value
self.forward = [None] * level # Forward pointers for each level
class SkipList:
"""
Skip list with O(log n) expected search, insert, delete.
Uses probability 0.5 for level promotion.
"""
MAX_LEVEL = 16 # Maximum height (supports up to 2^16 = 65536 elements)
P = 0.5 # Probability of promoting to next level
def __init__(self):
# Sentinel head node with -infinity key at all levels
self.head = SkipNode(float('-inf'), None, self.MAX_LEVEL)
self.level = 1 # Current maximum level in use
def _random_level(self) -> int:
"""Determine level for new node."""
level = 1
while random.random() < self.P and level < self.MAX_LEVEL:
level += 1
return level
def search(self, key) -> any:
"""
Search for key. Returns value if found, None otherwise.
Time: O(log n) expected.
"""
current = self.head
# Start from highest level, work down
for i in range(self.level - 1, -1, -1):
while current.forward[i] and current.forward[i].key < key:
current = current.forward[i] # Move right on this level
# Check if we found the key at level 0
current = current.forward[0]
if current and current.key == key:
return current.value
return None
def insert(self, key, value) -> None:
"""
Insert key-value pair.
Time: O(log n) expected.
"""
# Find insertion point at each level
update = [None] * self.MAX_LEVEL
current = self.head
for i in range(self.level - 1, -1, -1):
while current.forward[i] and current.forward[i].key < key:
current = current.forward[i]
update[i] = current # Last node at level i before insertion point
current = current.forward[0]
# If key already exists, update value
if current and current.key == key:
current.value = value
return
# Create new node
new_level = self._random_level()
# Expand list level if needed
if new_level > self.level:
for i in range(self.level, new_level):
update[i] = self.head
self.level = new_level
new_node = SkipNode(key, value, new_level)
# Splice new node into each level
for i in range(new_level):
new_node.forward[i] = update[i].forward[i]
update[i].forward[i] = new_node
def delete(self, key) -> bool:
"""
Delete key from skip list.
Returns True if deleted, False if not found.
Time: O(log n) expected.
"""
update = [None] * self.MAX_LEVEL
current = self.head
for i in range(self.level - 1, -1, -1):
while current.forward[i] and current.forward[i].key < key:
current = current.forward[i]
update[i] = current
current = current.forward[0]
if not current or current.key != key:
return False # Key not found
# Remove node from each level it appears in
for i in range(self.level):
if update[i].forward[i] != current:
break
update[i].forward[i] = current.forward[i]
# Reduce list level if top levels are now empty
while self.level > 1 and self.head.forward[self.level - 1] is None:
self.level -= 1
return True
def range_query(self, start_key, end_key) -> list:
"""
Return all (key, value) pairs where start_key <= key <= end_key.
This is where skip lists shine over hash maps — O(log n + k).
"""
result = []
current = self.head
# Find the first node >= start_key
for i in range(self.level - 1, -1, -1):
while current.forward[i] and current.forward[i].key < start_key:
current = current.forward[i]
current = current.forward[0]
while current and current.key <= end_key:
result.append((current.key, current.value))
current = current.forward[0]
return result
def display(self) -> None:
"""Print the skip list structure."""
for i in range(self.level - 1, -1, -1):
current = self.head.forward[i]
keys = []
while current:
keys.append(str(current.key))
current = current.forward[i]
print(f"Level {i}: {' → '.join(keys)}")
Testing the Implementation
sl = SkipList()
# Insert
for key, val in [(3, "c"), (1, "a"), (7, "g"), (5, "e"), (2, "b"), (4, "d"), (6, "f")]:
sl.insert(key, val)
sl.display()
# Level 0: 1 → 2 → 3 → 4 → 5 → 6 → 7
# Search
print(sl.search(5)) # "e"
print(sl.search(9)) # None
# Range query
print(sl.range_query(2, 5)) # [(2,'b'), (3,'c'), (4,'d'), (5,'e')]
# Delete
sl.delete(3)
print(sl.search(3)) # None
sl.display()
# Level 0: 1 → 2 → 4 → 5 → 6 → 7
Complexity Analysis
| Operation | Expected | Worst Case | Notes |
|---|
| Search | O(log n) | O(n) | Worst case extremely rare |
| Insert | O(log n) | O(n) | |
| Delete | O(log n) | O(n) | |
| Range query | O(log n + k) | — | k = results count |
| Space | O(n) | O(n log n) | Expected O(n) with p=0.5 |
Why O(log n) expected? With p=0.5, the expected number of nodes at level k is n/2^k. Total work examining nodes at all levels for a search is O(log n) in expectation.
Skip List vs Balanced BST
| Feature | Skip List | AVL / Red-Black Tree |
|---|
| Search | O(log n) expected | O(log n) guaranteed |
| Insert | O(log n) expected | O(log n) guaranteed |
| Delete | O(log n) expected | O(log n) guaranteed |
| Implementation | Simple (linked list + random) | Complex (rotations) |
| Range queries | Natural O(log n + k) | O(log n + k) |
| Concurrent access | Easier to make lock-free | Harder |
| Worst case | O(n) (with probability ~0) | O(log n) |
Skip list wins on: Implementation simplicity, concurrent access, range queries BST wins on: Guaranteed (not expected) O(log n), deterministic behavior
Real-World Usage
Redis Sorted Sets
Redis uses skip lists (not balanced BSTs) to implement sorted sets:
ZADD leaderboard 100 "Alice"
ZADD leaderboard 200 "Bob"
ZADD leaderboard 150 "Charlie"
ZRANGE leaderboard 0 -1 WITHSCORES
# Uses skip list range query: O(log n + k)
ZRANGEBYSCORE leaderboard 100 200
# Score range query: O(log n + k)
Redis chose skip lists over trees because:
- Simpler code (easier to maintain)
- Efficient range queries
- Easier to make lock-free for concurrent access
LevelDB / RocksDB
Google's LevelDB uses skip lists for its in-memory write buffer (memtable).
Summary
A skip list is a multi-layered linked list that achieves O(log n) expected operations through probabilistic promotion of elements to higher levels.
- Core operations: Search, Insert, Delete — all O(log n) expected
- Advantage over hash maps: Maintains sorted order, supports range queries
- Advantage over balanced BSTs: Simpler to implement and make concurrent
- Used by: Redis sorted sets, LevelDB memtable, CockroachDB
The skip list is one of the most elegant examples of how randomization can replace complex deterministic algorithms with simple, efficient ones.
*Next: Bloom Filter*