Skip List
In the landscape of data structures, few concepts balance elegance, simplicity, and performance as effectively as the Skip List. Introduced by William Pugh in 1989, this probabilistic data structure offers an alternative to balanced binary search trees (such as AVL or Red-Black trees) for maintaining ordered data. While balanced trees guarantee logarithmic time complexity through rigorous rebalancing operations, skip lists achieve similar average-case performance through a much simpler mechanism: multi-level indexing on a sorted linked list.
This article explores the internal mechanics of skip lists, detailing how they handle search, insertion, and deletion, and why they have become a cornerstone in high-performance systems like Redis and LevelDB.
Structural Anatomy
At its core, a skip list is a layered collection of sorted linked lists. The structure can be visualized as a series of "express lanes" built atop a "local road."
- Level 0 (The Base Layer): This is the foundation of the structure and contains every element in the list, sorted in ascending order. It functions as a standard singly linked list.
- Higher Levels: Each subsequent level (Level 1, Level 2, etc.) is a sparse subset of the level below it. These levels act as skip pointers, allowing the algorithm to jump over large chunks of data.
- Nodes: Unlike a standard linked list node, a skip list node contains an array of pointers. The length of this array corresponds to the node's height. A node at height $h$ has pointers for levels $0$ through $h-1$.
- Head Node: A special head node typically exists at the maximum possible height. It does not store data but serves as the entry point for all operations.
The key to the skip list's efficiency is the randomized height of each node. When a new element is inserted, its height is determined by a random process, often modeled as a coin flip. With probability $p$ (typically $0.5$), the node is promoted to the next level. This randomness ensures that, on average, the number of nodes at each level decreases by half, creating a logarithmic distribution of pointers.
The Search Algorithm
Searching in a skip list is a process of elimination that moves from the highest level down to the base. The goal is to traverse right as far as possible without overshooting the target value, then drop down a level to refine the search.
The algorithm proceeds as follows:
- Start at the Top: Begin at the head node of the highest active level.
- Traverse Right: Move to the next node in the current level as long as its value is strictly less than the target.
- Descend: If the next node’s value is greater than or equal to the target, or if you reach the end of the list, move down to the next lower level.
- Repeat: Continue this process until you reach Level 0.
- Final Check: At Level 0, check if the next node’s value matches the target.
Example:
Imagine searching for the value 17 in a skip list containing [10, 15, 20, 25].
- At Level 3, you start at the head. The next node might be
10. Since $10 < 17$, you move right. The next node is20. Since $20 > 17$, you cannot move further right. You drop down to Level 2. - At Level 2, you are now at
10. The next node is15. Since $15 < 17$, you move right. The next node is20. Since $20 > 17$, you drop down to Level 1. - At Level 1, you are at
15. The next node is17. Since $17$ is not less than $17$, you drop down to Level 0. - At Level 0, the next node is
17. You have found your target.
This "right then down" strategy ensures that the search path is short, typically proportional to $\log n$.
Insertion and Deletion
Insertion and deletion in a skip list are more complex than in a standard linked list because they require updating pointers across multiple levels.
Insertion
To insert a new value, you must first determine where it fits in the existing structure. This involves a search process similar to the one described above, but with a crucial addition: you must record the predecessor node at every level. This is often stored in an update array.
- Find Position: Traverse the list from the top level down, recording the last node with a value less than the new value at each level.
- Determine Height: Randomly generate the height of the new node using the probability $p$.
- Adjust Structure: If the new node’s height exceeds the current maximum height of the list, the list’s height is increased, and the head node’s pointers are extended.
- Splice In: For each level from $0$ up to the new node’s height, the new node is inserted between the recorded predecessor and its successor.
new_node.next[i] = predecessor.next[i]predecessor.next[i] = new_node
Deletion
Deletion follows a similar path. You search for the target node while recording the predecessors at each level. Once the node is found at Level 0, you iterate through the levels up to the node’s height. At each level, you bypass the target node by pointing the predecessor’s pointer directly to the target’s successor. If the topmost levels become empty after deletion, the list’s height is reduced to save memory and optimize future searches.
Complexity Analysis
The performance of a skip list is defined by its probabilistic guarantees.
- Time Complexity:
- Average Case: $O(\log n)$ for search, insert, and delete. The expected number of levels is logarithmic with respect to the number of elements.
- Worst Case: $O(n)$. In the unlikely event that all nodes are assigned the maximum height, the structure degenerates into a single-level linked list. However, the probability of this occurring is exponentially small.
- Space Complexity:
- Average Case: $O(n)$. Each node has an average height of $\frac{1}{1-p}$. With $p=0.5$, the average node has 2 pointers, resulting in linear space overhead.
- Worst Case: $O(n \cdot \log n)$, if all nodes reach the maximum height.
Why Choose Skip Lists?
Despite the existence of sophisticated balanced trees, skip lists have gained significant popularity in modern software engineering for several reasons:
- Simplicity of Implementation: Balanced trees require complex rotation logic (left/right rotations) to maintain balance. Skip lists rely on simple pointer manipulation and random number generation. This makes them easier to code, debug, and maintain.
- Concurrency Friendliness: Skip lists are naturally suited for concurrent environments. Because updates are localized to specific nodes and levels, fine-grained locking can be applied. In contrast, tree rotations can affect a large portion of the tree, requiring coarser locks or complex synchronization.
- Range Queries: Since the base level is a sorted linked list, range queries (e.g., "get all elements between X and Y") are efficient. You simply find the start node and traverse the list until you reach the end of the range.
- Probabilistic Guarantees: While not deterministic, the probabilistic nature of skip lists provides consistent performance in practice. The "average case" is almost always the "real case."
Real-World Applications
Skip lists are not just academic curiosities; they are integral to several high-profile systems:
- Redis: The Sorted Set (
ZSET) data structure in Redis uses a skip list (combined with a hash table) to provide $O(\log n)$ operations for adding, removing, and retrieving elements by rank or score. - LevelDB: The MemTable in LevelDB, a key-value store developed by Google, uses a skip list to maintain in-memory data before it is flushed to disk.
- Concurrent Data Structures: Many libraries for concurrent programming use skip lists as the underlying structure for ordered maps and sets due to their lock-friendly design.
Conclusion
The skip list represents a powerful trade-off: it sacrifices the strict worst-case guarantees of balanced trees in exchange for implementation simplicity and concurrency advantages. By leveraging randomness to create a hierarchical index, it achieves logarithmic performance with a codebase that is significantly easier to manage. For developers building high-performance, concurrent systems, the skip list remains an indispensable tool in the data structure toolkit.