LINKED LISTS IN C PROGRAMMING

Introduction to Linked Lists

A linked list is a linear data structure where elements are stored in nodes, and each node points to the next node in the sequence.

Unlike arrays, linked lists don't require contiguous memory allocation, allowing dynamic memory usage.

Basic Node Structure in C:

struct Node {
    int data;           // Data stored in the node
    struct Node* next;  // Pointer to the next node
};
Key Characteristics:
  • Dynamic size (can grow/shrink during runtime)
  • Efficient insertions/deletions (O(1) at head)
  • No random access (sequential access only)
  • Extra memory needed for pointers

Types of Linked Lists

Singly Linked List

Each node points only to the next node in sequence.

struct Node {
    int data;
    struct Node* next;
};
Insert/Delete at head: O(1) Search: O(n)

Doubly Linked List

Nodes contain pointers to both next and previous nodes.

struct Node {
    int data;
    struct Node* next;
    struct Node* prev;
};
More memory Bidirectional traversal

Circular Linked List

Last node points back to the first node (can be singly or doubly linked).

// Circular singly linked list
struct Node* last->next = head;
Circular traversal Useful for round-robin

Basic Operations

Creating a Linked List

// Function to create a new node
struct Node* createNode(int data) {
    struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
    if (newNode == NULL) {
        printf("Memory allocation failed!\n");
        exit(1);
    }
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

Insertion Operations

1. Insert at Beginning
void insertAtBeginning(struct Node** head, int data) {
    struct Node* newNode = createNode(data);
    newNode->next = *head;
    *head = newNode;
}
2. Insert at End
void insertAtEnd(struct Node** head, int data) {
    struct Node* newNode = createNode(data);
    if (*head == NULL) {
        *head = newNode;
        return;
    }
    struct Node* temp = *head;
    while (temp->next != NULL) {
        temp = temp->next;
    }
    temp->next = newNode;
}

Deletion Operations

1. Delete by Value
void deleteNode(struct Node** head, int key) {
    struct Node *temp = *head, *prev = NULL;
    
    if (temp != NULL && temp->data == key) {
        *head = temp->next;
        free(temp);
        return;
    }
    
    while (temp != NULL && temp->data != key) {
        prev = temp;
        temp = temp->next;
    }
    
    if (temp == NULL) return;
    
    prev->next = temp->next;
    free(temp);
}

Common Linked List Problems

Reverse a Linked List

// Iterative method
struct Node* reverseList(struct Node* head) {
    struct Node *prev = NULL, *current = head, *next = NULL;
    
    while (current != NULL) {
        next = current->next;  // Store next node
        current->next = prev;  // Reverse current node's pointer
        prev = current;         // Move pointers one position ahead
        current = next;
    }
    
    return prev;  // New head
}
Time Complexity: O(n) | Space Complexity: O(1)

Detect Cycle in Linked List

Floyd's Cycle-Finding Algorithm (Tortoise and Hare):

int hasCycle(struct Node *head) {
    struct Node *slow = head, *fast = head;
    
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;          // Moves one step
        fast = fast->next->next;    // Moves two steps
        
        if (slow == fast) {         // Cycle detected
            return 1;
        }
    }
    
    return 0;  // No cycle
}
Efficiency: O(n) time with O(1) space

Merge Two Sorted Lists

struct Node* mergeSortedLists(struct Node* l1, struct Node* l2) {
    if (l1 == NULL) return l2;
    if (l2 == NULL) return l1;
    
    if (l1->data < l2->data) {
        l1->next = mergeSortedLists(l1->next, l2);
        return l1;
    } else {
        l2->next = mergeSortedLists(l1, l2->next);
        return l2;
    }
}
Note: This is the recursive approach (O(n+m) time, O(n+m) space due to recursion stack)

Best Practices & Common Pitfalls

Tips for Working with Linked Lists

  1. Always check for NULL pointers: Before dereferencing any pointer, verify it's not NULL.
  2. Memory management: Free allocated memory to prevent leaks when deleting nodes.
  3. Edge cases: Handle empty lists, single-node lists, and operations at head/tail.
  4. Pointer manipulation: Draw diagrams to visualize pointer changes during complex operations.
  5. Dummy nodes: Use dummy nodes to simplify edge cases in some problems.
  6. Debugging: Implement a print function to verify list contents during development.