Skip to content

07 The Doubly Linked List with Double Sentinels

1. The Concept & Purpose

What is it? A Doubly Linked List that has two dummy nodes: 1. Head Sentinel: Guards the front. 2. Tail Sentinel: Guards the back.

Visual Model (The Sandwich):

[Head Dummy] \(\leftrightarrow\) [Data A] \(\leftrightarrow\) [Data B] \(\leftrightarrow\) [Tail Dummy]

Why introduce it? (The Logic) In a standard Doubly Linked List, you still have to check for NULL at the boundaries. * Standard: If I delete the last node, I must update tail. If I insert into an empty list, I must update head AND tail. * With Double Sentinels: * The list is never empty (it always has at least the two sentinels). * Every data node is guaranteed to have a non-NULL prev and a non-NULL next. * Result: We eliminate ALL boundary checks (if head == NULL, if tail == NULL). One generic logic handles insertion/deletion anywhere.


2. Modifications Required

A. Initialization (createList)

We must allocate two nodes immediately and link them together.

typedef struct DList {
    DNode* head; // Points to Head Dummy
    DNode* tail; // Points to Tail Dummy
} DList;

DList* createList() {
    DList* list = (DList*)malloc(sizeof(DList));

    // 1. Create the two sentinels
    list->head = (DNode*)malloc(sizeof(DNode));
    list->tail = (DNode*)malloc(sizeof(DNode));

    // 2. Link them to each other (The Empty State)
    list->head->next = list->tail; // Head points to Tail
    list->tail->prev = list->head; // Tail points to Head

    // 3. Seal the outer boundaries
    list->head->prev = NULL;
    list->tail->next = NULL;

    return list;
}

B. The "Universal" Insertion (insertBetween)

Instead of writing separate logic for pushFront and pushBack, we write one private helper function. * Logic: Insert a node between any two existing nodes A and B.

// Private Helper: Inserts 'val' between node 'before' and node 'after'
void _insertBetween(DNode* before, DNode* after, int val) {
    DNode* newNode = (DNode*)malloc(sizeof(DNode));
    newNode->data = val;

    // The 4-Pointer Update (Standard Double Link Logic)
    newNode->prev = before;
    newNode->next = after;
    before->next = newNode;
    after->prev = newNode;
}

// Public Wrappers are now trivial:
void pushFront(DList* list, int val) {
    // Insert between HeadDummy and the first actual node
    _insertBetween(list->head, list->head->next, val);
}

void pushBack(DList* list, int val) {
    // Insert between the last actual node and TailDummy
    _insertBetween(list->tail->prev, list->tail, val);
}

C. The "Universal" Deletion (removeNode)

Similarly, deleting a node never requires checking if it is the first or last node.

void removeNode(DNode* target) {
    // We don't need to check if target->prev is NULL, 
    // because Sentinels guarantee neighbors exist.
    DNode* before = target->prev;
    DNode* after = target->next;

    before->next = after;
    after->prev = before;

    free(target);
}

D. Traversal (The Loop Condition)

  • Change: We start after the Head Sentinel and stop before the Tail Sentinel.
void printList(DList* list) {
    // Start at the first real data node
    DNode* curr = list->head->next;

    // Stop when we hit the Tail Sentinel (NOT NULL)
    while (curr != list->tail) {
        printf("%d <-> ", curr->data);
        curr = curr->next;
    }
    printf("END\n");
}

3. Summary of the Evolution

Version Main Pain Point Solution
No Sentinel head variable changes; strict NULL checks. Use Node** or if/else.
Single Sentinel Modifying index 0 is special. Add Head Dummy.
Tail Pointer Inserting at the end is \(O(N)\). Add Tail Variable.
Double Sentinel Inserting at the end still requires if(tail==NULL) logic. Add Tail Dummy.

Verdict: The Doubly Linked List with Double Sentinels requires slightly more memory (2 extra nodes) but provides the cleanest, most bug-resistant code for complex applications.