Introduction
In our previous exploration, we covered a basic Skip List implementation with a single skip step of 2. While functional, this basic version doesn't fully leverage the power of skip lists. A true skip list employs multiple skip levels, allowing for even faster lookups by skipping over more elements. This article demonstrates how to enhance our skip list with multiple index levels, creating a more sophisticated and efficient data structure. Redefining Node Structure
The fundamental change begins with the node definition. Since we need to support multiple skip levels, storing a single index reference won't suffice. We replace the single index reference with a list that can hold multiple index pointers: ``` class SkipNode: def init(self, data, prev=None, next_node=None, indices=None): self.data = data self.prev = prev self.next_node = next_node self.indices = indices if indices is not None else []
This pattern demonstrates Python's conditional expression syntax, which differs from Java's ternary operator. The format `value1 if condition else value2` achieves the same result as `condition ? value1 : value2` in Java. Insertion Operation
-------------------
The insertion logic remains largely unchanged from our basic implementation. We locate the appropriate position and insert the new node. The key difference lies in how we handle the index reconstruction: ```
def add_node(new_node):
if start.next_node is None:
start.next_node = new_node
new_node.next_node = end
new_node.prev = start
end.prev = new_node
return
current = start.next_node
while current.next_node is not None or current == end:
if current.data > new_node.data or current == end:
predecessor = current.prev
predecessor.next_node = new_node
current.prev = new_node
new_node.prev = predecessor
new_node.next_node = current
break
current = current.next_node
rebuild_indices_advanced()
Multi-Level Index Reconstruction
Our enhanced index rebuilding method creates two skip levels: one at every 2nd node and another at every 4th node: ``` def rebuild_indices_advanced(): primary_step = 2 secondary_step = 4
primary_index_tracker = start.next_node
secondary_index_tracker = start.next_node
current = start.next_node
while current.next_node is not None:
current.indices = []
if primary_step == 0:
primary_step = 2
primary_index_tracker.indices.append(current)
primary_index_tracker = current
if secondary_step == 0:
secondary_step = 4
secondary_index_tracker.indices.append(current)
secondary_index_tracker = current
current = current.next_node
primary_step -= 1
secondary_step -= 1
Optimized Search Algorithm
--------------------------
The search operation now leverages our multi-level indices. We begin with the highest-level index and progressively move to lower levels when our jump overshoots the target: ```
def find_value(target):
current = start.next_node
comparison_count = 0
while current.next_node is not None:
comparison_count += 1
if target == current.data:
print(f"Value found after {comparison_count} comparisons")
return
elif target < current.data:
print(f"Value not found after {comparison_count} comparisons")
return
if current.indices:
for idx in range(len(current.indices) - 1, -1, -1):
if target > current.indices[idx].data:
current = current.indices[idx]
break
else:
current = current.next_node
else:
current = current.next_node
print(f"Value not found after {comparison_count} comparisons")
Visualization Helper
For debugging and understanding the structure, here's a method to display the skip list with its indices: ``` def display_structure(): result = [] current = start.next_node
while current.next_node is not None:
if current.indices:
entry = {
"data": current.data,
"indices": [node.data for node in current.indices]
}
else:
entry = {"data": current.data, "indices": current.indices}
result.append(entry)
current = current.next_node
for item in result:
print(item)
Dynamic Step Calculation
------------------------
To create a truly adaptive skip list, we can calculate skip levels dynamically based on the total number of elements. First, we maintain a counter for the list size: ```
def add_node(new_node):
global element_count
element_count += 1
if start.next_node is None:
# Insertion logic continues...
Using the global keyword is essential when modifying a top-level varible from within a function, similar to how static variables work in other languages. Now we implement the dynamic index reconstruction: ```
def rebuild_indices_dynamic():
size = element_count
step_mapping = {}
step_values = []
# Calculate logarithmic skip levels
while int((size / 2)) > 1:
size = int((size / 2))
key = f"level_{size}"
step_mapping[key] = start.next_node
step_values.append(size)
step_backup = list(step_values)
current = start.next_node
while current.next_node is not None:
current.indices = []
for i in range(len(step_values)):
step_values[i] -= 1
if step_values[i] == 0:
step_values[i] = step_backup[i]
index_key = f"level_{step_backup[i]}"
index_node = step_mapping[index_key]
index_node.indices.append(current)
step_mapping[index_key] = current
current = current.next_node
Performance Analysis
--------------------
With our multi-level skip list, we observe significant improvements in search performance. In testing scenarios, we've reduced the number of comparisons needed to locate elements. While the actual performance varies based on data distribution, this implementation handles worst-case scenarios reasonably well. Future Improvements
-------------------
Several optimization opportunities remain for this implementation: - Implementing lazy deletion instead of rebuilding indices after every removal - Caching index structures to avoid complete reconstruction - Exploring altenrative step calculation algorithms - Adding concurrent access support for multi-threaded environments The skip list remains an elegant data structure balancing simplicity with performance, making it valuable for ordered collections requiring efficient lookups.