Implementing Core Linked List Operations: Removal, Design, and Reversal in Python
Working with singly linked lists requires a firm grasp of pointer manipulation and edge-case handling. The following sections break down three classic problems: removing nodes by value, building a custom linked list with index-based operations, and reversing a list in-place.
Eliminating Nodes with a Specific Value
When tasked with deleting all nodes that hold a target value, the head itself may need removal. Since standard deletion relies on updating the previous node’s next reference, a sentinel node placed before the head simplifies the logic. Traverse the list using a ptr starting at the sentinel. If ptr.next carries the target value, bypass it; otherwise advance ptr. At the end, return sentinel.next as the new head.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def removeElements(self, head: ListNode, val: int) -> ListNode:
sentinel = ListNode(next=head)
ptr = sentinel
while ptr.next:
if ptr.next.val == val:
ptr.next = ptr.next.next
else:
ptr = ptr.next
return sentinel.next
Building a Customizable Singly Linked List
Designing a linked list class involves maintaining a sentinel head and a size counter to support get, addAtHead, addAtTail, addAtIndex, and deleteAtIndex. All index-based operations must validate bounds before traversing.
Data Structure Setup
A nested Node class stores a value and a reference. The MyLinkedList constructor creates a sentinel node and initializes _size to zero.
class MyLinkedList:
class Node:
__slots__ = ('val', 'next')
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def __init__(self):
self._sentinel = self.Node()
self._size = 0
Retrieval by Index
Return -1 for negative indices or indices ≥ _size. Start from the first real node (_sentinel.next) and advance index times.
def get(self, index: int) -> int:
if index < 0 or index >= self._size:
return -1
current = self._sentinel.next
for _ in range(index):
current = current.next
return current.val
Insertion at Head and Tail
For head insertion, link the new node right after the sentinel and increment _size. For tail insertion, walk to the last node, then append.
def addAtHead(self, val: int) -> None:
self._sentinel.next = self.Node(val, self._sentinel.next)
self._size += 1
def addAtTail(self, val: int) -> None:
cur = self._sentinel.next
if not cur: # list is empty
self.addAtHead(val)
return
while cur.next:
cur = cur.next
cur.next = self.Node(val)
self._size += 1
Insertion at a Specific Position
If index equals _size, the node goes to the tail; if index exceeds _size or is negative, do nothing. Otherwise, start from the sentinel and move index steps, then link the new node.
def addAtIndex(self, index: int, val: int) -> None:
if index > self._size or index < 0:
return
prev = self._sentinel
for _ in range(index):
prev = prev.next
prev.next = self.Node(val, prev.next)
self._size += 1
Deletion by Index
Guard against invalid indices. Position a pointer before the target and bypass it.
def deleteAtIndex(self, index: int) -> None:
if index < 0 or index >= self._size:
return
prev = self._sentinel
for _ in range(index):
prev = prev.next
prev.next = prev.next.next
self._size -= 1
Reversing a Linked List In-Place
The iterative two-pointer approach reverses the next pointers one by one. Initialize prev = None and curr = head. Inside a loop, cache curr.next, point curr.next to prev, shift both pointers forward (prev = curr, curr = saved). When the loop ends, prev becomes the new head.
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
prev = None
curr = head
while curr:
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
return prev
This approach uses O(1) extra space and runs in O(n) time. A recursive alternative can also be defined, but the iterative version remains straightforward and widely used.