Algorithms Series — Linked List

Bar Dadon
CodeX
Published in
6 min readAug 7, 2023
Photo by JJ Ying on Unsplash

Linked List

A linked list is one of the simplest data structures there is. It is an array of nodes that are connected via pointers.

Each node in the list contains a value and a pointer to the next node in the list. This behavior allows the linked list to behave as a dynamic array, where the size is not fixed and can be modified even after its creation(unlike traditional static arrays).

The three basic operations a linked list must support are searching, insertion and deletion.

Running times

Insertion:

  • Insertion at the beginning: O(1)
  • Insertion at the end: O(n) (in worst case, when we have to traverse to the end to insert a new node)
  • Insertion at a specific position: O(n) (in worst case, when we have to traverse to the specific position to insert a new node)

Deletion:

  • Deletion at the beginning: O(1)
  • Deletion at the end: O(n) (in worst case, when we have to traverse to the end to delete a node)
  • Deletion at a specific position: O(n) (in worst case, when we have to traverse to the specific position to delete a node)

Search: O(n) (in worst case, when we have to traverse through all the elements in the list to find a specific element)

Implementing a Linked List

1. Creating a linked list

First, let’s create the Node class to represent nodes in the list. As mentioned, each node has a value and a pointer to the next node.

class Node:
def __init__(self, value, next = None) -> None:
self.value = value
self.next = next

Next, the LinkedList class. I will also add the __repr__ method so we can print the items of the list.

class LinkedList:
def __init__(self, head: Node = None):
"""
# Consturctor #1 - Empty Linked List
"""
self.head = head

def __repr__(self):
"""
Print Linked List
"""
items = ''
current_node = self.head
while current_node != None:
items += str(current_node.value) + '-->'
current_node = current_node.next
return f"LinkedList({items[:-3]})"

Let’s see how we can create a linked list:

# Creating a linked list
head = Node(2)
head.next = Node(1)
head.next.next = Node(4)
head.next.next.next = Node(3)
head.next.next.next.next = Node(5)
head.next.next.next.next.next = Node(6)

linked_list = LinkedList(head)

# print list
print(linked_list)

# Output:
# LinkedList(2-->1-->4-->3-->5-->6)

Next, we can create a second constructor for the class to allow for easier creation of linked lists. Let’s say we want to allow users to create linked lists by passing a list of values. We can do it like this:

class LinkedList:
def __init__(self, head: Node = None):
"""
# Consturctor #1 - Empty Linked List
"""
self.head = head

def __repr__(self):
"""
Print Linked List
"""
items = ''
current_node = self.head
while current_node != None:
items += str(current_node.value) + '-->'
current_node = current_node.next
return f"LinkedList({items[:-3]})"

@classmethod
def generate_list(cls, *args):
"""
Constructor #2 - Generate Linked List from list of values
"""
head = Node(args[0])
current_node = head
for value in args[1:]:
current_node.next = Node(value)
current_node = current_node.next

return cls(head = head)

Now we can create a linked list much more easily:

linked_list = LinkedList.generate_list(2,1,4,3,5,6)
print(linked_list)

# Output
# LinkedList(2-->1-->4-->3-->5-->6)

2. Searching

Searching a value in a linked list involves traversing through the list until we reach None value(end of list) or we find the node with the value we searched for.

class Node:
def __init__(self, value, next = None) -> None:
self.value = value
self.next = next

class LinkedList:
def __init__(self, head: Node = None):
"""
# Consturctor #1 - Empty Linked List
"""
self.head = head

@classmethod
def generate_list(cls, *args):
"""
Constructor #2 - Generate Linked List from list of values
"""
head = Node(args[0])
current_node = head
for value in args[1:]:
current_node.next = Node(value)
current_node = current_node.next

return cls(head = head)

def __repr__(self):
"""
Print Linked List
"""
items = ''
current_node = self.head
while current_node != None:
items += str(current_node.value) + '-->'
current_node = current_node.next
return f"LinkedList({items[:-3]})"

def index(self, index):
"""
Seach a value in Linked List. Return -1 if not found
Description:
- Iterate through the linked list
- If value is found, return the value
- If value is not found, return -1
"""
current_node = self.head
while current_node != None:
if index == 0:
return current_node.value
else:
index -= 1
current_node = current_node.next
return -1

Let’s see an example:

linked_list = LinkedList.generate_list(2,1,4,3,5,6)
print(linked_list.index(2))

# Output:
# 4

2. Insert

Similar logic as searching. Traverse through the list until we reach the desired index location. Generate a Node and insert it at that location in the list.

Then we need to update the links in the list. The new node will need to point to the next node in the list. And the previous node will need to point to the new node.

class Node:
def __init__(self, value, next = None) -> None:
self.value = value
self.next = next

class LinkedList:
def __init__(self, head: Node = None):
"""
# Consturctor #1 - Empty Linked List
"""
self.head = head

@classmethod
def generate_list(cls, *args):
"""
Constructor #2 - Generate Linked List from list of values
"""
head = Node(args[0])
current_node = head
for value in args[1:]:
current_node.next = Node(value)
current_node = current_node.next

return cls(head = head)

def __repr__(self):
"""
Print Linked List
"""
items = ''
current_node = self.head
while current_node != None:
items += str(current_node.value) + '-->'
current_node = current_node.next
return f"LinkedList({items[:-3]})"

def index(self, index):
"""
Seach a value in Linked List. Return -1 if not found
Description:
- Iterate through the linked list
- If value is found, return the value
- If value is not found, return -1
"""
current_node = self.head
while current_node != None:
if index == 0:
return current_node.value
else:
index -= 1
current_node = current_node.next
return -1

def insert(self, value, index):
"""
Insert a value at a given index
"""
if len(self) < index:
print("Index not in Range")
return None

current_node = self.head
while current_node != None:
if index == 1:
next = current_node.next
current_node.next = Node(value)
current_node.next.next = next
return
else:
index -= 1
current_node = current_node.next

def __len__(self):
"""
Return the length of the linked list
"""
current_node = self.head
counter = 0
while current_node != None:
counter += 1
current_node = current_node.next
return counter

Let’s see an example:

# Creating a linked list
linked_list = LinkedList.generate_list(2,1,4,3,5,6)

# Inserting a value
linked_list.insert(value=20, index=2)

# Print list
print(linked_list)

# Output:
# LinkedList(2-->1-->20-->4-->3-->5-->6)

3. Delete

To delete a node we need to find the node and then delete it. Then we need to update the pointers of the previous and next nodes to point to each other.

class Node:
def __init__(self, value, next = None) -> None:
self.value = value
self.next = next

class LinkedList:
def __init__(self, head: Node = None):
"""
# Consturctor #1 - Empty Linked List
"""
self.head = head

@classmethod
def generate_list(cls, *args):
"""
Constructor #2 - Generate Linked List from list of values
"""
head = Node(args[0])
current_node = head
for value in args[1:]:
current_node.next = Node(value)
current_node = current_node.next

return cls(head = head)

def __repr__(self):
"""
Print Linked List
"""
items = ''
current_node = self.head
while current_node != None:
items += str(current_node.value) + '-->'
current_node = current_node.next
return f"LinkedList({items[:-3]})"

def index(self, index):
"""
Seach a value in Linked List. Return -1 if not found
Description:
- Iterate through the linked list
- If value is found, return the value
- If value is not found, return -1
"""
current_node = self.head
while current_node != None:
if index == 0:
return current_node.value
else:
index -= 1
current_node = current_node.next
return -1

def insert(self, value, index):
"""
Insert a value at a given index
"""
if len(self) < index:
print("Index not in Range")
return None

current_node = self.head
while current_node != None:
if index == 1:
next = current_node.next
current_node.next = Node(value)
current_node.next.next = next
return
else:
index -= 1
current_node = current_node.next

def delete(self, index):
"""
Delete a node at specifc index
"""
current_node = self.head
while current_node != None:
if index == 1:
current_node.next = current_node.next.next
break
else:
index -= 1
current_node = current_node.next

def __len__(self):
"""
Return the length of the linked list
"""
current_node = self.head
counter = 0
while current_node != None:
counter += 1
current_node = current_node.next
return counter

Let’s see an example:

# Creating a linked list
linked_list = LinkedList.generate_list(2,1,4,3,5,6)

# Delete the node at index 2
linked_list.delete(index=2)

# Print list
print(linked_list)

# Output:
# LinkedList(2-->1-->3-->5-->6)

Note that this implementation is the basic form of a linked list, we can and should customize it to suit our needs. Also, Python has two built-in data structures that can behave as a linked list, they are Lists and collections.deque. In most cases, we would be better of using these two data structures.

If there’s no need for special customization, we should opt to use these built-in data structures first. Linked lists are more useful in languages like C that have static arrays. However, knowing how to implement this data structure can be useful in particular situations and will add to your understanding of how other data structures are built and behave.

--

--