LinkList Introduction

2024-01-12

Linked List Overview

Memory space is a shared resource for all programs. In a complex system environment, free memory space may be scattered throughout memory. We know that the memory space for storing arrays must be contiguous. However, when the array is very large, it might not be possible to provide such a large contiguous space in memory. This is where the flexibility of linked lists becomes apparent.

A linked list is a linear data structure where each element is a node object, and the nodes are connected via "references." A reference stores the memory address of the next node, allowing access to the next node from the current one.

The design of linked lists allows the nodes to be dispersed throughout memory without needing contiguous memory addresses.

The first node of a linked list is known as the "head node," and the last node is called the "tail node." The tail node points to "null," represented as null in Java, nullptr in C++, and None in Python. In languages like C, C++, Go, and Rust that support pointers, the above "references" are replaced with "pointers."

1. Initializing a Linked List

To build a linked list, we first initialize the node objects and then establish the references between nodes. Once initialized, we can traverse all nodes starting from the head node via the next references.

Inserting a node in a linked list is easy. As shown in the diagram, to insert a new node P between two adjacent nodes n0 and n1, we only need to change two references (pointers), with a time complexity of O(1).

2. Inserting a Node

Compared to arrays, where the time complexity of inserting an element is O(n), linked lists are more efficient with large data. Inserting a node in a linked list is convenient; only a single reference (pointer) needs to be changed.

3. Deleting a Node

Note that even after the deletion operation is complete and node P still points to n1, it is effectively no longer part of the linked list as it can't be accessed during traversal.

4. Accessing a Node

Accessing a node in a linked list is less efficient. As mentioned earlier, we can access any element in an array in O(1) time. However, for linked lists, the program needs to start from the head node and traverse sequentially to find the target node. That is, accessing the i-th node in a linked list requires i iterations, making the time complexity O(n).

5. Finding a Node

Traverse the linked list to find the node with a value equal to target and return its index in the list. This process is also a linear search.

# Initializing the linked list: 1 -> 3 -> 2 -> 5 -> 4
# Initializing nodes
n0 = ListNode(1)
n1 = ListNode(3)
n2 = ListNode(2)
n3 = ListNode(5)
n4 = ListNode(4)
# Establishing references between nodes
n0.next = n1
n1.next = n2
n2.next = n3
n3.next = n4


def insert(n0: ListNode, P: ListNode):
    """Insert node P after node n0 in the linked list"""
    n1 = n0.next
    P.next = n1
    n0.next = P

def remove(n0: ListNode):
    """Delete the first node after node n0 in the linked list"""
    if not n0.next:
        return
    # n0 -> P -> n1
    P = n0.next
    n1 = P.next
    n0.next = n1

def access(head: ListNode, index: int) -> ListNode | None:
    """Access the node at index in the linked list"""
    for _ in range(index):
        if not head:
            return None
        head = head.next
    return head

def find(head: ListNode, target: int) -> int:
    """Find the first node with value `target` in the linked list"""
    index = 0
    while head:
        if head.val == target:
            return index
        head = head.next
        index += 1
    return -1