Skip Lists in Data Structures - CSU083 | Shoolini University

Skip Lists

Skip Lists

A Skip List is a probabilistic data structure that enhances the efficiency of search operations in a linked list by using multiple layers of linked lists with varying densities. It is an alternative to balanced trees for fast search, insertion, and deletion.

1. Prerequisites

2. What is a Skip List?

A Skip List augments a sorted linked list with additional layers of linked lists, each skipping over a fraction of the elements in the layer below. These additional layers speed up search operations by reducing the number of steps required to locate an element.

2.1 Structure of a Skip List

2.2 Operations in Skip Lists

3. Why Does This Algorithm Exist?

4. When Should You Use It?

5. How Does It Compare to Alternatives?

5.1 Strengths

5.2 Weaknesses

6. Basic Implementation (Python)

The following code defines a Skip List with insert, search, and display functions.


import random

class Node:
    def __init__(self, key, level):
        self.key = key
        self.forward = [None] * (level + 1)

class SkipList:
    def __init__(self, max_level=4, p=0.5):
        self.max_level = max_level
        self.p = p
        self.head = Node(-1, self.max_level)
        self.level = 0

    def random_level(self):
        lvl = 0
        while random.random() < self.p and lvl < self.max_level:
            lvl += 1
        return lvl

    def insert(self, key):
        update = [None] * (self.max_level + 1)
        current = self.head

        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
        
        level = self.random_level()
        if level > self.level:
            for i in range(self.level + 1, level + 1):
                update[i] = self.head
            self.level = level
        
        new_node = Node(key, level)
        for i in range(level + 1):
            new_node.forward[i] = update[i].forward[i]
            update[i].forward[i] = new_node

    def search(self, key):
        current = self.head
        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]
        return current and current.key == key

    def display(self):
        for i in range(self.level + 1):
            current = self.head.forward[i]
            print(f"Level {i}:", end=" ")
            while current:
                print(current.key, end=" -> ")
                current = current.forward[i]
            print("None")

# Example usage
skip_list = SkipList()
elements = [3, 6, 7, 9, 12, 19]
for el in elements:
    skip_list.insert(el)

skip_list.display()
print("Search 7:", skip_list.search(7))
print("Search 15:", skip_list.search(15))

7. Dry Run (Step-by-Step Execution)

This section manually tracks how variables change during execution. We insert values 3, 6, and 7 and observe how the Skip List evolves.

7.1 Step 1: Insert(3)

List after insertion:


Level 0: 3 -> None

7.2 Step 2: Insert(6)

List after insertion:


Level 1: 6 -> None
Level 0: 3 -> 6 -> None

7.3 Step 3: Insert(7)

List after insertion:


Level 1: 6 -> None
Level 0: 3 -> 6 -> 7 -> None

8. Variable Tracking Table

The table below tracks key variables during execution.

Step Operation Current Node Level Traversed Inserted At
1 Insert(3) Head → None 0 0
2 Insert(6) Head → 3 → None 1 → 0 1
3 Insert(7) Head → 3 → 6 → None 1 → 0 0

9. Quick Recap

10. Time Complexity Analysis

Skip Lists allow fast search, insertion, and deletion by utilizing multiple layers of linked lists. The time complexity varies based on how nodes are distributed across levels.

10.1 Worst-Case Complexity

10.2 Best-Case Complexity

10.3 Average-Case Complexity

Operation Best Case Average Case Worst Case
Search O(1) O(log n) O(n)
Insert O(1) O(log n) O(n)
Delete O(1) O(log n) O(n)

11. Space Complexity Analysis

Skip Lists consume more memory compared to linked lists due to multiple pointers at different levels.

11.1 Memory Usage Derivation

11.2 Space Complexity

Scenario Space Complexity
Worst Case O(n log n)
Best Case O(n)
Average Case O(n)

12. Trade-Offs

Skip Lists offer an elegant balance between speed and memory usage, but they come with trade-offs:

12.1 Strengths

12.2 Weaknesses

13. Common Optimizations

Optimizing Skip Lists focuses on improving search efficiency, reducing memory usage, and enhancing parallel execution.

13.1 Optimized Probability Factor (P)

13.2 Dynamic Level Adjustment

13.3 Memory-Efficient Node Representation

13.4 Parallel Skip Lists

14. Variants of Skip Lists

Different versions of Skip Lists modify the standard structure for various use cases.

14.1 Deterministic Skip List

14.2 Indexable Skip List

14.3 Self-Adaptive Skip List

14.4 Layered Skip List

15. Iterative vs. Recursive Implementations

Skip Lists can be implemented using either an iterative or recursive approach, each with different efficiency trade-offs.

15.1 Iterative Implementation

15.2 Recursive Implementation

15.3 Efficiency Comparison

Aspect Iterative Recursive
Performance O(log n) O(log n)
Memory Usage Lower Higher (due to recursion stack)
Readability More complex Simpler
Scalability Better for large lists May hit recursion depth limits

16. Choosing the Right Optimization

17. Common Edge Cases & Pitfalls

Understanding edge cases is critical to ensuring robustness in Skip List implementations.

17.1 Edge Cases

17.2 Pitfalls & Failure Points

18. Writing Test Cases

Below are test cases to validate the correctness of a Skip List implementation.


import unittest

class TestSkipList(unittest.TestCase):

    def setUp(self):
        self.skip_list = SkipList()

    def test_insert_and_search(self):
        self.skip_list.insert(10)
        self.assertTrue(self.skip_list.search(10))  # Ensure inserted element is found

    def test_search_non_existent_element(self):
        self.assertFalse(self.skip_list.search(42))  # Searching for non-existent element

    def test_insert_duplicates(self):
        self.skip_list.insert(20)
        self.skip_list.insert(20)
        count = 0
        node = self.skip_list.head.forward[0]
        while node:
            if node.key == 20:
                count += 1
            node = node.forward[0]
        self.assertEqual(count, 1)  # Ensure duplicate was not inserted

    def test_delete_existing_element(self):
        self.skip_list.insert(30)
        self.skip_list.insert(40)
        self.assertTrue(self.skip_list.search(30))
        self.skip_list.delete(30)
        self.assertFalse(self.skip_list.search(30))  # Ensure element is deleted

    def test_delete_non_existent_element(self):
        self.skip_list.delete(99)  # Deleting non-existent element should not cause error

    def test_delete_last_element(self):
        self.skip_list.insert(50)
        self.skip_list.delete(50)
        self.assertFalse(self.skip_list.search(50))  # Ensure list is empty after deletion

    def test_large_number_of_insertions(self):
        for i in range(1000):
            self.skip_list.insert(i)
        for i in range(1000):
            self.assertTrue(self.skip_list.search(i))  # Ensure all elements are present

19. Real-World Failure Scenarios

Skip Lists are used in applications like databases, caching systems, and networking protocols. Failures in real-world scenarios can arise due to poor implementation choices.

19.1 Inefficient Memory Usage in Large-Scale Applications

19.2 Race Conditions in Concurrent Environments

19.3 Degraded Performance Due to Poor Level Balancing

20. Ensuring Robustness

21. Real-World Applications

Skip Lists are utilized in various domains where dynamic data structures are needed.

21.1 Database Indexing

21.2 Memory Caching (Redis)

21.3 Distributed Systems (Google Bigtable, Apache Cassandra)

21.4 Network Routing Protocols

21.5 AI & Machine Learning

22. Open-Source Implementations

Several open-source projects implement Skip Lists for different use cases:

23. Practical Project: Implementing a Simple In-Memory Key-Value Store

This project demonstrates how a Skip List can be used as an in-memory key-value store.


import random

class Node:
    def __init__(self, key, value, level):
        self.key = key
        self.value = value
        self.forward = [None] * (level + 1)

class SkipListKVStore:
    def __init__(self, max_level=4, p=0.5):
        self.max_level = max_level
        self.p = p
        self.head = Node(-1, None, self.max_level)
        self.level = 0

    def random_level(self):
        lvl = 0
        while random.random() < self.p and lvl < self.max_level:
            lvl += 1
        return lvl

    def put(self, key, value):
        update = [None] * (self.max_level + 1)
        current = self.head

        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
        
        level = self.random_level()
        if level > self.level:
            for i in range(self.level + 1, level + 1):
                update[i] = self.head
            self.level = level
        
        new_node = Node(key, value, level)
        for i in range(level + 1):
            new_node.forward[i] = update[i].forward[i]
            update[i].forward[i] = new_node

    def get(self, key):
        current = self.head
        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]
        return current.value if current and current.key == key else None

    def delete(self, key):
        update = [None] * (self.max_level + 1)
        current = self.head

        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
        
        target = current.forward[0]
        if target and target.key == key:
            for i in range(self.level + 1):
                if update[i].forward[i] != target:
                    break
                update[i].forward[i] = target.forward[i]

    def display(self):
        for i in range(self.level + 1):
            current = self.head.forward[i]
            print(f"Level {i}:", end=" ")
            while current:
                print(f"({current.key}: {current.value})", end=" -> ")
                current = current.forward[i]
            print("None")

# Example usage
store = SkipListKVStore()
store.put("user1", "Alice")
store.put("user2", "Bob")
store.put("user3", "Charlie")

store.display()

print("Get user2:", store.get("user2"))  # Output: Bob
store.delete("user2")
print("Get user2 after deletion:", store.get("user2"))  # Output: None

24. Project Breakdown

25. Real-World Application of This Project

26. Competitive Programming Assignments

Practice solving these problems using Skip Lists to gain confidence in applying them under time constraints.

26.1 Basic Problems

26.2 Intermediate Problems

26.3 Advanced Problems

Assignment: Solve at least 10 of these problems and implement an optimized Skip List within a strict time limit (e.g., 30 minutes).

27. System Design Problem: Implement a High-Performance Leaderboard

Use a Skip List to design a scalable leaderboard system where users can be ranked efficiently.

27.1 System Requirements

27.2 Implementation Strategy

Assignment: Implement the leaderboard using a Skip List and compare performance with a balanced tree implementation.

28. Practicing Under Time Constraints

Assignment: Implement, test, and optimize a Skip List within a competitive programming setting (e.g., Codeforces, LeetCode, or HackerRank).