Linked Lists

Linked lists can be thought of as arrays with a different implementation.

Each value in the list is allocated a separate space in memory, with a pointer to the next value. This way, the data structure doesn’t require contiguous memory allocation.

A linked list is by default a dynamic data structure, and compared to a static array it uses more memory to store data, but wastes less memory in unused memory allocation.

Because inserting data at the head or tail of a linked list is a constant time operation, it is commonly used to represent stacks and queues.

Complexity Implications

Getting a value using a linked list is not as efficient as for an array. When using an array it is easy to determine where a value is (index can be calculated because of contiguous memory allocation), but for a linked list, the list must be traversed to find a value.

Common operations

Insertion complexity can vary depending on implementation and place of entry. Inserting at head or tail of linked list is O(1) Space Time. If the reference to where the insert should happen is known, it is also O(1), but if not the linked list needs to be traversed to reach that point, making it O(n) time.

Common Uses

Example Implementation

class Node:
    def __init__(self, data, next=None):
        self.data = data
        self.next = next
    
    def __repr__(self):
        return self.data
        
class LinkedList:
  
    def __init__(self, nodes=None):
      
        self.head = None
        
        if nodes:
            node = Node(data=nodes.pop(0))
            self.head = node
            for element in nodes:
                node.next = Node(data=element)
                node = node.next

		def add_first(self, node):
      	node.next = self.head
        self.head = node
        
    def add_last(self, node):
      	if not self.head:
          	self.head = node
	          return
        else:
          	for current_node in self:
              	pass
             current_node.next = node
    
    def insert_after(self, target_node_data, new_node):
      	
        if not self.head:
          	raise Exception("List is empty")
            
        for node in self:
          	if node.data == target_node_data:
              	new_node.next = node.next
                node.next = new_node
                return
        raise Exception(f"Node with data '{target_node_data}' not found")
        
    def insert_before(self, target_node_data, new_node):
      
        if not self.head:
          	raise Exception("List is empty")
            
        if self.head.data == target_node_data:
          	return self.add_first(new_node)
            
        previous_node = self.head
        for node in self:
          	if node.data == target_node_data:
              	previous_node.next = new_node
                new_node.next = node
                return
            previous_node = node
        raise Exception(f"Node with data '{target_node_data}' not found")   
    
    def remove_node(self, target_node_data):
      
				if not self.head:
          	raise Exception("List is empty")
            
        if self.head.data == target_node_data:
          	self.head = self.head.next
            return
        
        previous_node = self.head
        for node in self:
          	if node.data == target_node_data:
              	previous_node.next = node.next
                return
            previous_node = node
        raise Exception(f"Node with data '{target_node_data}' not found")
    
	  def __iter__(self):
      	node = self.head
        while node:
          	yield node
            node = node.next
    
    def __repr__(self):
      
        node = self.head
        nodes = []
        
        while node:
            nodes.append(node.data)
            node = node.next
        nodes.append('None')
        
        return '->'.join(nodes)
      

Example Use

Merge two sorted linked lists and return it as a sorted list. The list should be made by splicing together the nodes of the first two lists.

Input: l1 = [1,2,4], l2 = [1,3,4]
Output: [1,1,2,3,4,4]
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

def mergeTwoLists(l1: ListNode, l2: ListNode) -> ListNode:

  	head = ListNode()
    tail = head
    
    while l1 and l2:
        if l1.val <= l2.val:
        	  tail.next = l1
            l1 = l1.next
        elif l2.val < l1.val:
            tail.next = l2
            l2 = l2.next

        tail = tail.next

     if l1: tail.next = l1
     if l2: tail.next = l2

     return head.next

Head being defined and unused till the end is also called a dummy node, and is a common strategy to return the linked list from the start after traversing through.

Advantages and Disadvantages

Advantages

Disadvantages

Additional Information

There are variations of linked lists: