Tutorials Logic, IN info@tutorialslogic.com

C Linked Lists Singly, Doubly, Insert, Delete: Tutorial, Examples, FAQs & Interview Tips

C Linked Lists Singly, Doubly, Insert, Delete

C in C is best learned by connecting the rule to a small command-line program. Start with the smallest function, observe the output, and then add one realistic constraint so the concept becomes practical.

The key habit for this lesson is to watch pointer, array, or file buffer as it changes. That makes the topic easier to debug, easier to explain in interviews, and easier to use in real code without memorizing isolated syntax.

What is a Linked List?

A linked list is a dynamic data structure where each element (called a node) contains data and a pointer to the next node. Unlike arrays, linked lists do not require contiguous memory - nodes can be scattered anywhere in the heap.

Feature Array Linked List
Memory Contiguous, fixed size Dynamic, scattered
Access by index O(1) - direct O(n) - traverse
Insert at beginning O(n) - shift elements O(1)
Insert at end O(1) (if space) O(n) or O(1) with tail pointer
Delete O(n) - shift elements O(1) with pointer to node
Memory overhead None Extra pointer per node

Singly Linked List

Each node has data and a next pointer. The last node's next is NULL.

Singly Linked List - Full Implementation

Singly Linked List - Full Implementation
#include <stdio.h>
#include <stdlib.h>

// Node structure
typedef struct Node {
    int data;
    struct Node *next;
} Node;

// Create a new node
Node* createNode(int data) {
    Node *node = (Node*)malloc(sizeof(Node));
    if (!node) { perror("malloc"); exit(1); }
    node->data = data;
    node->next = NULL;
    return node;
}

// Insert at beginning - O(1)
Node* insertFront(Node *head, int data) {
    Node *node = createNode(data);
    node->next = head;
    return node;  // new head
}

// Insert at end - O(n)
Node* insertEnd(Node *head, int data) {
    Node *node = createNode(data);
    if (!head) return node;
    Node *curr = head;
    while (curr->next) curr = curr->next;
    curr->next = node;
    return head;
}

// Delete a node by value - O(n)
Node* deleteNode(Node *head, int data) {
    if (!head) return NULL;
    if (head->data == data) {
        Node *temp = head->next;
        free(head);
        return temp;
    }
    Node *curr = head;
    while (curr->next && curr->next->data != data)
        curr = curr->next;
    if (curr->next) {
        Node *temp = curr->next;
        curr->next = temp->next;
        free(temp);
    }
    return head;
}

// Search - O(n)
int search(Node *head, int data) {
    int pos = 0;
    while (head) {
        if (head->data == data) return pos;
        head = head->next;
        pos++;
    }
    return -1;
}

// Print the list
void printList(Node *head) {
    while (head) {
        printf("%d", head->data);
        if (head->next) printf(" -> ");
        head = head->next;
    }
    printf(" -> NULL\n");
}

// Free all nodes
void freeList(Node *head) {
    while (head) {
        Node *temp = head;
        head = head->next;
        free(temp);
    }
}

int main() {
    Node *head = NULL;

    head = insertEnd(head, 10);
    head = insertEnd(head, 20);
    head = insertEnd(head, 30);
    head = insertFront(head, 5);

    printf("List: "); printList(head);
    // 5 -> 10 -> 20 -> 30 -> NULL

    head = deleteNode(head, 20);
    printf("After deleting 20: "); printList(head);
    // 5 -> 10 -> 30 -> NULL

    printf("Search 30: position %d\n", search(head, 30));  // 2
    printf("Search 99: position %d\n", search(head, 99));  // -1

    freeList(head);
    return 0;
}

Doubly Linked List

Each node has both a next and a prev pointer. This allows traversal in both directions and O(1) deletion when you have a pointer to the node.

Doubly Linked List

Doubly Linked List
#include <stdio.h>
#include <stdlib.h>

typedef struct DNode {
    int data;
    struct DNode *prev;
    struct DNode *next;
} DNode;

DNode* createDNode(int data) {
    DNode *node = (DNode*)malloc(sizeof(DNode));
    node->data = data;
    node->prev = NULL;
    node->next = NULL;
    return node;
}

DNode* insertFront(DNode *head, int data) {
    DNode *node = createDNode(data);
    if (head) {
        node->next = head;
        head->prev = node;
    }
    return node;
}

void printForward(DNode *head) {
    printf("Forward:  ");
    DNode *tail = NULL;
    while (head) {
        printf("%d ", head->data);
        tail = head;
        head = head->next;
    }
    printf("\n");

    printf("Backward: ");
    while (tail) {
        printf("%d ", tail->data);
        tail = tail->prev;
    }
    printf("\n");
}

int main() {
    DNode *head = NULL;
    head = insertFront(head, 30);
    head = insertFront(head, 20);
    head = insertFront(head, 10);

    printForward(head);
    // Forward:  10 20 30
    // Backward: 30 20 10

    return 0;
}

Reverse a Linked List

Reverse a Singly Linked List

Reverse a Singly Linked List
#include <stdio.h>
#include <stdlib.h>

typedef struct Node { int data; struct Node *next; } Node;

Node* reverse(Node *head) {
    Node *prev = NULL;
    Node *curr = head;
    while (curr) {
        Node *next = curr->next;  // save next
        curr->next = prev;        // reverse link
        prev = curr;              // move prev forward
        curr = next;              // move curr forward
    }
    return prev;  // new head
}

// helper: build list from array
Node* buildList(int arr[], int n) {
    Node *head = NULL, *tail = NULL;
    for (int i = 0; i < n; i++) {
        Node *node = malloc(sizeof(Node));
        node->data = arr[i]; node->next = NULL;
        if (!head) head = tail = node;
        else { tail->next = node; tail = node; }
    }
    return head;
}

void printList(Node *h) {
    while (h) { printf("%d%s", h->data, h->next ? " -> " : "\n"); h = h->next; }
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    Node *head = buildList(arr, 5);
    printf("Original: "); printList(head);
    head = reverse(head);
    printf("Reversed: "); printList(head);
    return 0;
}
// Original: 1 -> 2 -> 3 -> 4 -> 5
// Reversed: 5 -> 4 -> 3 -> 2 -> 1

Applied guide for C

Use C when the program needs a clear answer to a specific problem, not because the keyword looks familiar. In a real C task, first name the input, then name the transformation, then name the output. This small discipline shows whether the topic is being used correctly or only copied from an example.

A reliable practice flow is: create the smallest working function, add one normal case, add one edge case such as empty list, first node, and last node changes, and then confirm the result with compiler warnings and printed output. If the result surprises you, reduce the code until the behavior is visible again.

The most common trap here is losing the next or previous link while inserting nodes. Avoid it by writing one sentence before the code that explains why C is the right choice. After the code runs, verify the lesson by doing this: walk the list after every operation.

  • Identify the exact problem solved by C.
  • Trace pointer, array, or file buffer before and after the main operation.
  • Keep one intentionally broken version and explain the fix.
  • Connect the example to a small command-line program so the idea feels concrete.
Key Takeaways
  • I can explain where C fits inside a small command-line program.
  • I can point to the exact pointer, array, or file buffer affected by this topic.
  • I tested a normal case and an edge case involving empty list, first node, and last node changes.
  • I verified the result with compiler warnings and printed output instead of assuming it worked.
  • I can describe the main mistake: losing the next or previous link while inserting nodes.
Common Mistakes to Avoid
WRONG Losing the next or previous link while inserting nodes.
RIGHT Write the expected behavior first, then make the example prove it.
A one-line expectation turns the code from copied syntax into a testable idea.
WRONG Practicing only the perfect input.
RIGHT Also test empty list, first node, and last node changes before considering the lesson complete.
The edge case is where most interview follow-up questions begin.
WRONG Looking only at the final output.
RIGHT Trace pointer, array, or file buffer through each important step.
Tracing makes debugging faster because you can see the first incorrect state.

Practice Tasks

  • Build one small function that demonstrates C in a small command-line program.
  • Change the example to include empty list, first node, and last node changes and record the difference.
  • Break the example by deliberately losing the next or previous link while inserting nodes, then write the corrected version.
  • Explain the finished example in five bullet points: input, operation, output, failure case, and verification.

Frequently Asked Questions

Use it when the problem matches the behavior shown in the example and when the result can be verified through compiler warnings and printed output.

Start with a tiny case, then test empty list, first node, and last node changes. The main warning sign is losing the next or previous link while inserting nodes.

Trace pointer, array, or file buffer, predict the result, run the example, and compare your prediction with the actual output.

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.