Data Structures Explained with Python

Linked Lists Explained (Python)

A linked list is a linear data structure where elements are not stored at contiguous memory locations. Instead, the elements are linked using pointers. Each element, often called a node, consists of two parts: data and a reference (or link) to the next node in the sequence. The last node typically points to None or null, indicating the end of the list.

Characteristics of Linked Lists:

Types of Linked Lists:

  1. Singly Linked List: Each node points only to the next node in the list. Traversal is unidirectional.
  2. Doubly Linked List: Each node has two pointers: one to the next node and one to the previous node. This allows for bidirectional traversal.
  3. Circular Linked List: The last node points back to the first node (or head), forming a circle. Can be singly or doubly circular.

Singly Linked List Implementation in Python:

First, we define a Node class:


class Node:
    def __init__(self, data):
        self.data = data  # Assign data
        self.next = None  # Initialize next as null
        

Then, we define the LinkedList class:


class LinkedList:
    def __init__(self):
        self.head = None # Initialize head as None

    # Method to add a new node at the beginning
    def prepend(self, data):
        new_node = Node(data)
        new_node.next = self.head
        self.head = new_node

    # Method to add a new node at the end
    def append(self, data):
        new_node = Node(data)
        if not self.head:
            self.head = new_node
            return
        last_node = self.head
        while last_node.next:
            last_node = last_node.next
        last_node.next = new_node

    # Method to insert a new node after a given previous node
    def insert_after_node(self, prev_node_data, data):
        new_node = Node(data)
        current = self.head
        while current:
            if current.data == prev_node_data:
                new_node.next = current.next
                current.next = new_node
                return
            current = current.next
        print(f"Node with data {prev_node_data} not found.")

    # Method to delete a node by key
    def delete_node(self, key):
        current = self.head
        # If head node itself holds the key to be deleted
        if current and current.data == key:
            self.head = current.next
            current = None
            return
        
        prev = None
        # Search for the key to be deleted
        while current and current.data != key:
            prev = current
            current = current.next
        
        # If the key is not present
        if not current:
            print(f"Node with key {key} not found.")
            return
        
        # Unlink the node from linked list
        prev.next = current.next
        current = None

    # Method to print the linked list
    def print_list(self):
        current = self.head
        elements = []
        while current:
            elements.append(str(current.data))
            current = current.next
        print(" -> ".join(elements) if elements else "List is empty")

# Example Usage:
llist = LinkedList()
llist.append(1)
llist.append(2)
llist.append(3)
llist.prepend(0)
llist.print_list()  # Output: 0 -> 1 -> 2 -> 3

llist.insert_after_node(1, 1.5)
llist.print_list()  # Output: 0 -> 1 -> 1.5 -> 2 -> 3

llist.delete_node(2)
llist.print_list()  # Output: 0 -> 1 -> 1.5 -> 3

llist.delete_node(0) # Delete head
llist.print_list() # Output: 1 -> 1.5 -> 3

llist.delete_node(4) # Node not found
        

Advantages of Linked Lists:

Disadvantages of Linked Lists:

Linked lists are foundational for other data structures like stacks, queues, and are used in various applications such as implementing adjacency lists for graphs, hash table chaining, and managing dynamic memory allocation.