Delete from End (Doubly Linked List)

With a tail pointer, removing the last node is O(1): move tail to tail->prev, set its next to NULL, and free the old tail. This is a major advantage over a singly linked list, where delete-at-end usually requires an O(n) walk to find the predecessor unless you use extra tricks.

C program

#include <stdio.h>
#include <stdlib.h>

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

void insert_end(struct Node **head, struct Node **tail, int data)
{
    struct Node *n = malloc(sizeof(struct Node));
    if (n == NULL)
        return;
    n->data = data;
    n->next = NULL;
    if (*head == NULL) {
        n->prev = NULL;
        *head = *tail = n;
        return;
    }
    n->prev = *tail;
    (*tail)->next = n;
    *tail = n;
}

void delete_end(struct Node **head, struct Node **tail)
{
    if (*head == NULL)
        return;
    if (*head == *tail) {
        free(*head);
        *head = *tail = NULL;
        return;
    }
    struct Node *t = *tail;
    *tail = t->prev;
    (*tail)->next = NULL;
    free(t);
}

void print_list(struct Node *head)
{
    while (head != NULL) {
        printf("%d ", head->data);
        head = head->next;
    }
    printf("\n");
}

int main(void)
{
    struct Node *head = NULL;
    struct Node *tail = NULL;

    insert_end(&head, &tail, 10);
    insert_end(&head, &tail, 20);
    insert_end(&head, &tail, 30);

    print_list(head);   /* 10 20 30 */

    delete_end(&head, &tail);
    delete_end(&head, &tail);
    delete_end(&head, &tail);

    print_list(head);   /* empty */

    return 0;
}

Output

First print_list(head) after three insert_end calls:

10 20 30

After three delete_end calls, the second print_list prints only a newline (empty list).

Step-by-step execution table

Each delete_end removes the current last node; tail moves backward via prev.

Step Operation List state (forward) List state (backward) head tail
0 (after insert_end ×3) 10 → 20 → 30 30 → 20 → 10 Node 10 Node 30
1 delete_end() 10 → 20 20 → 10 Node 10 Node 20
2 delete_end() 10 10 Node 10 Node 10
3 delete_end() empty empty NULL NULL

Memory layout table (before first delete)

Address Node data prev next Points to (prev / next)
0x1000 Node A 10 NULL 0x1008 prev: NULL, next: Node B
0x1008 Node B 20 0x1000 0x1010 prev: Node A, next: Node C
0x1010 Node C 30 0x1008 NULL prev: Node B, next: NULL

Pointer variables (initial list)

  • head = 0x1000 — Node A (first).
  • tail = 0x1010 — Node C (last).

Traversal path for print_list(head) (before deletes)

Start at head (0x1000) → Node A [10]
    ↓ (next)
Node B [20]
    ↓ (next)
Node C [30]
    ↓ (next)
NULL → Stop

Output: 10 20 30

Comparison: delete_end (DLL) vs SLL delete-at-end

Feature DLL with tail SLL with only head
Find predecessor of tail O(1) via tail->prev O(n) walk from head
Update last link (*tail)->prev becomes new tail; set next = NULL Second-last node’s next = NULL
Time O(1) O(n) without extra pointers

Key operations in delete_end

Code / idea Purpose
if (*head == *tail) Single node: free and null both pointers
*tail = t->prev New last node is the old tail’s predecessor
(*tail)->next = NULL Open chain ends at the new tail
free(t) Release the removed last node

Compare Delete from End (SLL) — linear scan without prev. · Delete from Beginning (DLL)