Data Structures: Linked Lists

Posted on in programming

cover image for article

Linked lists are fundamental data structures that store elements in a linear fashion, unlike arrays which use contiguous memory allocation. Each element, or node, in a linked list contains data and a reference (link) to the next node in the sequence. This dynamic structure offers unique advantages and considerations compared to arrays.

1. Structure and Terminology:

  • Node: The building block of a linked list, containing data and a pointer to the next node.
  • Head: The first node in the list, often used for accessing and traversing the list.
  • Tail: The last node in the list, typically holding a null or empty pointer.

2. Operations:

  • Insertion: New nodes can be inserted at the beginning (head), end (tail), or any specific position within the list by adjusting pointers accordingly.
  • Deletion: Nodes can be removed by modifying the pointers of surrounding nodes to skip the targeted element.
  • Searching: Traversal is required to find specific elements by iterating through each node and comparing its data with the search value.

3. Advantages of Linked Lists:

  • Dynamic size: Unlike arrays, linked lists don't require pre-defined sizes, allowing for efficient growth and shrinkage as needed.
  • Flexibility: Insertion and deletion operations can be performed efficiently at any position without reallocating memory blocks.
  • Memory efficiency: Only used memory is allocated, potentially saving space compared to fixed-size arrays with unused elements.

4. Disadvantages of Linked Lists:

  • Random access: Unlike arrays where elements can be directly accessed by index, linked lists require traversal to reach specific positions, making random access slower.
  • Memory overhead: Each node stores an additional pointer, increasing memory usage compared to arrays storing only data.

5. Implementation Examples:

Python:

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None

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

    def insert_at_beginning(self, data):
        new_node = Node(data)
        new_node.next = self.head
        self.head = new_node

    def print_list(self):
        current = self.head
        while current:
            print(current.data, end=" -> ")
            current = current.next
        print("None")

# Example usage
linked_list = LinkedList()
linked_list.insert_at_beginning(5)
linked_list.insert_at_beginning(3)
linked_list.insert_at_beginning(1)
linked_list.print_list()  # Output: 1 -> 3 -> 5 -> None

Rust:

struct Node {
    data: i32,
    next: Option<Box<Node>>,
}

impl Node {
    fn new(data: i32) -> Self {
        Node { data, next: None }
    }
}

struct LinkedList {
    head: Option<Box<Node>>,
}

impl LinkedList {
    fn new() -> Self {
        LinkedList { head: None }
    }

    fn insert_at_beginning(&mut self, data: i32) {
        let new_node = Box::new(Node::new(data));
        new_node.next = self.head.take();
        self.head = Some(new_node);
    }

    fn print_list(&self) {
        let mut current = self.head.as_ref();
        while let Some(node) = current {
            print!("{} -> ", node.data);
            current = node.next.as_ref();
        }
        println!("None");
    }
}

fn main() {
    let mut linked_list = LinkedList::new();
    linked_list.insert_at_beginning(5);
    linked_list.insert_at_beginning(3);
    linked_list.insert_at_beginning(1);
    linked_list.print_list();  // Output: 1 -> 3 -> 5 -> None
}

Go:

package main

import (
    "fmt"
)

// Node represents a single element in the linked list
type Node struct {
    Data int
    Next *Node
}

// LinkedList represents the entire linked list structure
type LinkedList struct {
    Head *Node
}

// InsertAtBeginning inserts a new node at the beginning of the list
func (l *LinkedList) InsertAtBeginning(data int) {
    newNode := &Node{Data: data}
    newNode.Next = l.Head
    l.Head = newNode
}

// InsertAtEnd inserts a new node at the end of the list
func (l *LinkedList) InsertAtEnd(data int) {
    newNode := &Node{Data: data}

    if l.Head == nil {
        l.Head = newNode
        return
    }

    current := l.Head
    for current.Next != nil {
        current = current.Next
    }
    current.Next = newNode
}

// Delete removes the first node with the specified data value
func (l *LinkedList) Delete(data int) {
    if l.Head == nil {
        return
    }

    if l.Head.Data == data {
        l.Head = l.Head.Next
        return
    }

    current := l.Head
    for current.Next != nil {
        if current.Next.Data == data {
            current.Next = current.Next.Next
            return
        }
        current = current.Next
    }
}

// PrintList iterates through the list and prints the data of each node
func (l *LinkedList) PrintList() {
    current := l.Head
    for current != nil {
        fmt.Printf("%d -> ", current.Data)
        current = current.Next
    }
    fmt.Println("nil")
}

func main() {
    linkedList := &LinkedList{}

    linkedList.InsertAtBeginning(5)
    linkedList.InsertAtEnd(3)
    linkedList.InsertAtBeginning(1)

    fmt.Println("List after insertions:")
    linkedList.PrintList() // Output: 1 -> 5 -> 3 -> nil

    linkedList.Delete(3)

    fmt.Println("List after deleting 3:")
    linkedList.PrintList() // Output: 1 -> 5 -> nil
}

Having explored the basic structure and operations of linked lists, let's delve into additional concepts and considerations:

6. Different Types of Linked Lists:

Beyond the fundamental singly linked list, various types offer specific functionalities:

  • Doubly Linked List: Each node contains an additional pointer to the previous node, enabling efficient traversal in both directions. This is useful for scenarios requiring frequent backward movement or modifying previous elements.

Python:

class DoublyNode:
    def __init__(self, data):
        self.data = data
        self.prev = None
        self.next = None

class DoublyLinkedList:
    def __init__(self):
        self.head = None
        self.tail = None

    # ... (insertion, deletion, and traversal methods similar to singly linked list)
  • Circular Linked List: The last node's next pointer points back to the head, creating a continuous loop. This structure is useful for implementing algorithms like round-robin scheduling or Josephus problem.

Rust:

struct CircularNode {
    data: i32,
    next: Option<Box<CircularNode>>,
}

impl CircularNode {
    fn new(data: i32) -> Self {
        CircularNode { data, next: None }
    }
}

impl CircularLinkedList {
    fn new() -> Self {
        CircularLinkedList { head: None }
    }

    // ... (insertion, deletion, and traversal methods adapted for circular structure)
}

7. Applications of Linked Lists:

Linked lists find diverse applications in various programming contexts:

  • Stacks: LIFO (Last-In-First-Out) data structure implemented using singly linked lists, where insertion and deletion happen at the head (top).
  • Queues: FIFO (First-In-First-Out) data structure often implemented using singly or doubly linked lists, with insertion at the tail (back) and deletion at the head (front).
  • Undo/Redo Functionality: Linked lists can be used to store previous states of data, enabling undo and redo operations in applications like text editors or image editing software.
  • Graph Representation: Linked lists can be used to represent vertices and edges in graphs, where each node stores vertex data and pointers to connected nodes.

8. Comparison with Other Data Structures:

While linked lists offer certain advantages, it's essential to compare them with other data structures based on specific use cases:

  • Arrays: Arrays provide faster random access due to contiguous memory allocation, but lack the dynamic size and flexibility of linked lists for frequent insertions and deletions.
  • Trees: Trees offer hierarchical organization and efficient searching for specific data based on key values. However, they might be less suitable for simple linear sequences compared to linked lists.

Conclusion:

Understanding linked lists expands your data structure toolbox, enabling you to solve problems requiring dynamic data management, efficient insertion and deletion, and potentially complex relationships between elements. Choosing the right data structure depends on the specific requirements of your application and the trade-offs involved.

This article has provided a foundational understanding of linked lists. Remember to explore further based on your specific interests and delve into advanced topics like different types of linked list operations, optimizations, and their applications in various programming domains.

Part 7 of the Data Structures series

My Bookshelf

Reading Now

Other Stuff