Skip List
Skip List
A Skip List is a probabilistic data structure that allows for fast search, insert, and delete operations. It extends a sorted linked list with multiple layers of express lanes, enabling average-time complexity comparable to balanced binary search trees, but with a much simpler implementation.
Introduction
Skip List was invented by William Pugh in 1989. Instead of balancing tree nodes, it uses randomization to maintain balance: each node in the list has a randomly assigned "height" (number of layers), and faster "express lanes" skip over many intermediate nodes.
Skip List provides average-case performance for search, insert, and delete operations without requiring complex tree rebalancing, making it significantly easier to implement correctly than balanced BSTs.
How It Works
Think of a skip list as a highway system:
- Level 0: The base road with every stop (the regular sorted linked list)
- Higher levels: Express lanes that skip over many intermediate stops
- Each element randomly chooses its height when inserted
Skip List Example:
Level 3: HEAD ------------------------> +inf
Level 2: HEAD --------> 30 ---------> +inf
Level 1: HEAD ----> 10 ----> 30 ----> +inf
Level 0: HEAD -> 5 -> 10 -> 20 -> 30 -> 40 -> +inf
Implementation
import random
class SkipListNode:
def __init__(self, key, level):
self.key = key
self.forward = [None] * (level + 1)
class SkipList:
MAX_LEVEL = 16
def __init__(self):
self.header = SkipListNode(float('inf'), SkipList.MAX_LEVEL)
self.level = 0
def random_level(self):
level = 0
while random.random() < 0.5 and level < self.MAX_LEVEL:
level += 1
return level
def search(self, key):
current = self.header
for i in range(self.level, -1, -1):
while current.forward[i] and current.forward[i].key < key:
current = current.forward[i]
current = current.forward[0]
if current and current.key == key:
return current
return None
def insert(self, key):
update = [None] * (self.MAX_LEVEL + 1)
current = self.header
for i in range(self.level, -1, -1):
while current.forward[i] and current.forward[i].key < key:
current = current.forward[i]
update[i] = current
current = current.forward[0]
if current is None or current.key != key:
new_level = self.random_level()
if new_level > self.level:
for i in range(self.level + 1, new_level + 1):
update[i] = self.header
self.level = new_level
new_node = SkipListNode(key, new_level)
for i in range(new_level + 1):
new_node.forward[i] = update[i].forward[i]
update[i].forward[i] = new_node
def delete(self, key):
update = [None] * (self.MAX_LEVEL + 1)
current = self.header
for i in range(self.level, -1, -1):
while current.forward[i] and current.forward[i].key < key:
current = current.forward[i]
update[i] = current
current = current.forward[0]
if current and current.key == key:
for i in range(self.level + 1):
if update[i].forward[i] != current:
break
update[i].forward[i] = current.forward[i]
while self.level > 0 and self.header.forward[self.level] is None:
self.level -= 1
def __str__(self):
result = []
for i in range(self.level + 1):
level_str = f"Level {i}: "
current = self.header.forward[i]
while current:
level_str += f"{current.key} -> "
current = current.forward[i]
level_str += "None"
result.append(level_str)
return "\n".join(result)
Complexity Analysis
| Operation | Average Case | Worst Case |
|---|---|---|
| Search | ||
| Insert | ||
| Delete | ||
| Space |
The probability of worst-case behavior is astronomically small for reasonable values of .
Why Randomization Works
Each node's level is chosen randomly with probability at each level. The expected number of nodes at level is . This creates a structure where:
- The height of the list is with high probability
- Each level contains approximately half the nodes of the level below
- The number of pointers to follow is on average
Practical Applications
- Redis Sorted Sets: Redis uses skip lists for ordered data types
- LevelDB / RocksDB: Used in memtable implementations
- Lucene: Used for maintaining sorted indices
- In-memory databases: As an alternative to B-trees
Advantages Over Balanced BSTs
- Simpler implementation: No need for complex rebalancing operations
- Concurrent-friendly: Lock-free implementations are simpler than BSTs
- More cache-friendly: Sequential memory access patterns in lower levels
- Predictable performance: No worst-case tree degeneration
Disadvantages
- is average-case, not guaranteed (though worst case is extremely rare)
- Requires more memory per node than a simple linked list
- Slower than well-tuned binary heaps for priority queue use cases
Done with this topic? Mark it as complete to track your progress.