Slide 1: Node Class Implementation
A linked list fundamentally begins with the Node class, representing individual elements in the list. Each node contains two essential components: the data value and a reference to the next node, forming the building blocks of our linked list structure.
class Node:
def __init__(self, data):
self.data = data # Store the actual data
self.next = None # Reference to the next node
def __str__(self):
return f"Node(data={self.data})"
# Example usage
node1 = Node(5)
node2 = Node(10)
node1.next = node2
print(node1) # Output: Node(data=5)
print(node1.next) # Output: Node(data=10)Slide 2: LinkedList Class Foundation
The LinkedList class serves as a wrapper around our nodes, providing a clean interface for list operations. It maintains a reference to the head node and tracks the list's size, establishing the foundation for more complex operations.
class LinkedList:
def __init__(self):
self.head = None # Initialize empty list
self.size = 0 # Track list size
def is_empty(self):
return self.head is None
def __len__(self):
return self.size
def __str__(self):
if self.is_empty():
return "[]"
current = self.head
result = []
while current:
result.append(str(current.data))
current = current.next
return "[" + " -> ".join(result) + "]"Slide 3: Insertion Operations
Inserting elements into a linked list requires careful pointer manipulation. We implement three primary insertion methods: insert_front for beginning insertion, insert_end for appending, and insert_at for arbitrary position insertion.
def insert_front(self, data):
new_node = Node(data)
new_node.next = self.head
self.head = new_node
self.size += 1
def insert_end(self, data):
if self.is_empty():
self.head = Node(data)
else:
current = self.head
while current.next:
current = current.next
current.next = Node(data)
self.size += 1
def insert_at(self, data, position):
if position < 0 or position > self.size:
raise IndexError("Invalid position")
if position == 0:
self.insert_front(data)
return
current = self.head
for _ in range(position - 1):
current = current.next
new_node = Node(data)
new_node.next = current.next
current.next = new_node
self.size += 1Slide 4: Deletion Operations
Deletion operations in linked lists require proper handling of node references to maintain list integrity. We implement methods for removing elements from the front, end, and specific positions while managing edge cases.
def delete_front(self):
if self.is_empty():
raise IndexError("Delete from empty list")
data = self.head.data
self.head = self.head.next
self.size -= 1
return data
def delete_end(self):
if self.is_empty():
raise IndexError("Delete from empty list")
if self.head.next is None:
data = self.head.data
self.head = None
self.size -= 1
return data
current = self.head
while current.next.next:
current = current.next
data = current.next.data
current.next = None
self.size -= 1
return data
def delete_at(self, position):
if position < 0 or position >= self.size:
raise IndexError("Invalid position")
if position == 0:
return self.delete_front()
current = self.head
for _ in range(position - 1):
current = current.next
data = current.next.data
current.next = current.next.next
self.size -= 1
return dataSlide 5: Search and Access Operations
Efficient search and access operations are crucial for linked list functionality. We implement methods to find elements by value and position, with time complexity analysis showing linear search characteristics.
def find(self, value):
current = self.head
position = 0
while current:
if current.data == value:
return position
current = current.next
position += 1
return -1
def get_at(self, position):
if position < 0 or position >= self.size:
raise IndexError("Invalid position")
current = self.head
for _ in range(position):
current = current.next
return current.data
def contains(self, value):
return self.find(value) != -1Slide 6: Traversal and List Manipulation
Linked list traversal forms the basis for many operations like reversing, detecting cycles, and finding middle elements. We implement essential traversal-based operations using iterative and recursive approaches for optimal performance.
def reverse(self):
previous = None
current = self.head
while current:
next_node = current.next
current.next = previous
previous = current
current = next_node
self.head = previous
def find_middle(self):
if self.is_empty():
return None
slow = fast = self.head
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
return slow.data
def has_cycle(self):
if self.is_empty():
return False
slow = fast = self.head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return FalseSlide 7: List Merging and Sorting
Understanding list merging and sorting operations is crucial for handling complex data manipulations. We implement merge sort for linked lists, demonstrating efficient sorting with O(nlogn)O(n \log n)O(nlogn) time complexity.
def merge_sorted_lists(list1, list2):
dummy = Node(0)
current = dummy
while list1 and list2:
if list1.data <= list2.data:
current.next = list1
list1 = list1.next
else:
current.next = list2
list2 = list2.next
current = current.next
current.next = list1 or list2
return dummy.next
def merge_sort(self):
if not self.head or not self.head.next:
return self.head
# Find middle
middle = self.get_middle_node()
next_to_middle = middle.next
middle.next = None
# Recursive sort
left = LinkedList()
left.head = self.head
right = LinkedList()
right.head = next_to_middle
left.merge_sort()
right.merge_sort()
# Merge sorted halves
self.head = merge_sorted_lists(left.head, right.head)Slide 8: Advanced List Operations
Complex list operations like finding intersections, removing duplicates, and creating circular lists demonstrate advanced pointer manipulation and algorithm design principles in linked list implementations.
def remove_duplicates(self):
if self.is_empty():
return
seen = set()
current = self.head
seen.add(current.data)
while current.next:
if current.next.data in seen:
current.next = current.next.next
self.size -= 1
else:
seen.add(current.next.data)
current = current.next
def find_intersection(self, other_list):
if self.is_empty() or other_list.is_empty():
return None
ptr1 = self.head
ptr2 = other_list.head
while ptr1 != ptr2:
ptr1 = ptr1.next if ptr1 else other_list.head
ptr2 = ptr2.next if ptr2 else self.head
return ptr1
def make_circular(self, position):
if position >= self.size:
return False
current = self.head
target = None
for i in range(self.size):
if i == position:
target = current
if not current.next:
current.next = target
break
current = current.next
return TrueSlide 9: Memory Efficient Implementation
Memory management is crucial in linked list implementations. This optimized version uses slots for memory efficiency and implements memory-conscious methods for large-scale data handling scenarios.
class OptimizedNode:
__slots__ = ['data', 'next']
def __init__(self, data):
self.data = data
self.next = None
class MemoryEfficientList:
__slots__ = ['head', 'size']
def __init__(self):
self.head = None
self.size = 0
def memory_usage(self):
import sys
current = self.head
total_bytes = sys.getsizeof(self)
while current:
total_bytes += sys.getsizeof(current)
current = current.next
return total_bytes
# Example usage and memory comparison
regular_list = [i for i in range(1000)]
efficient_list = MemoryEfficientList()
for i in range(1000):
efficient_list.insert_front(i)
print(f"Regular List Memory: {sys.getsizeof(regular_list)} bytes")
print(f"Efficient List Memory: {efficient_list.memory_usage()} bytes")Slide 10: Real-world Application - LRU Cache Implementation
Implementing a Least Recently Used (LRU) cache demonstrates a practical application of linked lists in system design. This implementation combines hash maps with doubly linked lists for O(1) access time.
class LRUNode:
def __init__(self, key, value):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = {}
self.head = LRUNode(0, 0) # Dummy head
self.tail = LRUNode(0, 0) # Dummy tail
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def _add(self, node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def get(self, key):
if key in self.cache:
node = self.cache[key]
self._remove(node)
self._add(node)
return node.value
return -1
def put(self, key, value):
if key in self.cache:
self._remove(self.cache[key])
node = LRUNode(key, value)
self._add(node)
self.cache[key] = node
if len(self.cache) > self.capacity:
lru = self.tail.prev
self._remove(lru)
del self.cache[lru.key]
# Usage example
cache = LRUCache(2)
cache.put(1, 1)
cache.put(2, 2)
print(cache.get(1)) # returns 1
cache.put(3, 3) # evicts key 2
print(cache.get(2)) # returns -1 (not found)Slide 11: Performance Analysis and Big-O Complexity
Understanding the time and space complexity of linked list operations is crucial for efficient implementation. Here we implement a performance testing framework with mathematical analysis of complexity bounds.
def analyze_performance():
"""
Time Complexity Analysis:
Access: O(n)
Search: O(n)
Insertion: O(1) at head, O(n) at tail
Deletion: O(1) at head, O(n) at tail
Space Complexity: O(n)
"""
import time
import random
def measure_operation(func, size):
start_time = time.time()
func(size)
return time.time() - start_time
def test_insertion(size):
lst = LinkedList()
for _ in range(size):
lst.insert_front(random.randint(1, 1000))
sizes = [1000, 5000, 10000, 50000]
results = {}
for size in sizes:
results[size] = measure_operation(test_insertion, size)
return results
# Run analysis
performance_results = analyze_performance()
for size, time_taken in performance_results.items():
print(f"Size {size}: {time_taken:.4f} seconds")Slide 12: Real-world Application - Transaction Log System
A transaction log system demonstrates linked lists' practical application in financial systems. This implementation handles transaction recording, rollback capabilities, and maintains temporal ordering of operations.
class Transaction:
def __init__(self, tx_id, amount, timestamp):
self.tx_id = tx_id
self.amount = amount
self.timestamp = timestamp
self.next = None
self.prev = None
class TransactionLog:
def __init__(self):
self.head = None
self.tail = None
self.tx_count = 0
self.total_amount = 0
def add_transaction(self, amount):
import time
tx_id = f"TX{self.tx_count + 1}"
tx = Transaction(tx_id, amount, time.time())
if not self.head:
self.head = self.tail = tx
else:
tx.prev = self.tail
self.tail.next = tx
self.tail = tx
self.tx_count += 1
self.total_amount += amount
return tx_id
def rollback_transaction(self, tx_id):
current = self.head
while current:
if current.tx_id == tx_id:
self.total_amount -= current.amount
if current.prev:
current.prev.next = current.next
else:
self.head = current.next
if current.next:
current.next.prev = current.prev
else:
self.tail = current.prev
self.tx_count -= 1
return True
current = current.next
return False
# Example usage
log = TransactionLog()
tx1 = log.add_transaction(100.00)
tx2 = log.add_transaction(50.50)
tx3 = log.add_transaction(-25.25)
print(f"Total transactions: {log.tx_count}")
print(f"Total amount: ${log.total_amount:.2f}")
log.rollback_transaction(tx2)
print(f"After rollback - Total amount: ${log.total_amount:.2f}")Slide 13: Advanced List Algorithms - Skip List Implementation
Skip lists enhance linked list performance by maintaining multiple layers of connections, providing O(logn)O(\log n)O(logn) average case complexity for search operations, making them competitive with balanced trees.
import random
class SkipNode:
def __init__(self, value, level):
self.value = value
self.forward = [None] * (level + 1)
class SkipList:
def __init__(self, max_level=16, p=0.5):
self.max_level = max_level
self.p = p
self.level = 0
self.header = SkipNode(-float('inf'), max_level)
def random_level(self):
level = 0
while random.random() < self.p and level < self.max_level:
level += 1
return level
def insert(self, value):
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].value < value:
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.header
self.level = level
new_node = SkipNode(value, level)
for i in range(level + 1):
new_node.forward[i] = update[i].forward[i]
update[i].forward[i] = new_node
def search(self, value):
current = self.header
for i in range(self.level, -1, -1):
while current.forward[i] and current.forward[i].value < value:
current = current.forward[i]
current = current.forward[0]
return current and current.value == value
# Performance demonstration
skip_list = SkipList()
values = [3, 6, 9, 2, 1, 7, 8, 4, 5]
for value in values:
skip_list.insert(value)
print(f"Search for 6: {skip_list.search(6)}")
print(f"Search for 10: {skip_list.search(10)}")Slide 14: Additional Resources
- "Skip Lists: A Probabilistic Alternative to Balanced Trees" - https://arxiv.org/abs/0905.2975
- "Optimizing Linked List Performance Through Transactional Memory" - https://arxiv.org/abs/1607.04719
- "A Comparative Analysis of Tree and Linked List Data Structures" - https://arxiv.org/abs/1509.05053
- "Memory-Efficient Data Structures: An Algorithmic Perspective" - https://arxiv.org/abs/1808.09574
- "Concurrent Lock-Free Linked Lists: Design and Analysis" - https://arxiv.org/abs/1711.04530