Doubly Linked List: A Bi-Directional Bridge for Data

Time: Column:Backend & Servers views:241

A doubly linked list is a data structure where each node is connected to the next and the previous nodes through two pointers. Each node contains two pointers: one pointing to the previous node and another pointing to the next node. Doubly linked lists with a head node are common in practical applications. This article will provide a detailed explanation of how to implement a doubly linked list, along with a complete example in C.

一. Types of Linked Lists

Linked lists can be structured in various ways, leading to 8 possible combinations (2 x 2 x 2) of different types:

Doubly Linked List: A Bi-Directional Bridge for Data

1. Singly or Doubly Linked

Doubly Linked List: A Bi-Directional Bridge for Data

  • Singly Linked List:

    • Each node contains a pointer to the next node.

    • Traversal is one-way (from head to tail).

    • Insertion and deletion are simple, but nodes need to be found starting from the head.

  • Doubly Linked List:

    • Each node contains two pointers: one pointing to the next node and one pointing to the previous node.

    • Can be traversed both ways (head to tail and tail to head).

    • Insertion and deletion are more flexible but require more memory for the additional pointers.

2. With or Without Head Node

Doubly Linked List: A Bi-Directional Bridge for Data

  • With Head Node:

    • The list has an additional head node that does not store actual data but serves as the starting point for the list.

    • Operations like insertion and deletion are simpler, as the head node simplifies boundary conditions.

  • Without Head Node:

    • The list starts directly from the first data node, with no extra head node.

    • Special handling is required for empty lists and boundary conditions.

3. Circular or Non-Circular

Doubly Linked List: A Bi-Directional Bridge for Data

  • Circular Linked List:

    • The tail node points to the head node, creating a circular structure.

    • Traversal can start from any node, avoiding the "end" problem.

  • Non-Circular Linked List:

    • The tail node points to NULL, marking the end of the list.

    • During traversal, checks are needed to see if the end has been reached.

Although there are many types of linked list structures, the two most commonly used in practice are:

  1. Singly Linked Non-Circular List: Simple structure, generally used as part of other data structures like hash buckets or adjacency lists in graphs. It is also a common type in interviews and exams.

  2. Doubly Circular Linked List with Head Node: More complex structure, commonly used for storing data independently. This type of list is frequently used in practical applications, as it simplifies operations despite its more complex structure.


二. Concept and Basic Structure of a Doubly Linked List

2.1 Concept

A doubly linked list is a variation of a linked list where each node contains three parts:

  • Data to be stored.

  • A pointer to the previous node (prev).

  • A pointer to the next node (next).

A doubly linked list with a head node includes a special node called the head node, which does not store valid data and serves only as the starting auxiliary node of the list. The prev pointer of the head node points to itself, and its next pointer points to the first valid node in the list.

2.2 Basic Structure

The basic structure of a doubly linked list is defined as:

typedef struct ListNode
{
    DataType data; // Data field
    struct ListNode* prev; // Pointer to the previous node
    struct ListNode* next; // Pointer to the next node
} LN;

Doubly Linked List: A Bi-Directional Bridge for Data


三. Common Operations on a Doubly Linked List

  1. Creating a Node

// Create a new node
LN* ListBuyNode(DataType x)
{
    LN* node = (LN*)malloc(sizeof(LN));
    if (node == NULL)
    {
        perror("malloc fail!");
        exit(1);
    }
    node->data = x;
    node->prev = node->next = node; // Both pointers point to itself, forming a circular list
    return node;
}
  1. Initializing the List

// Initialize the list
void ListInit(LN** pphead)
{
    *pphead = ListBuyNode(-1); // Create the head node with a value of -1 (unused)
}
  1. Insert at the Head

// Insert a node at the front
void ListPushFront(LN* phead, DataType x)
{
    assert(phead);
    LN* newnode = ListBuyNode(x);
    newnode->next = phead->next; // New node's next points to the head's next node
    newnode->prev = phead; // New node's prev points to the head
    phead->next->prev = newnode; // The previous node's prev points to the new node
    phead->next = newnode; // The head's next points to the new node
}

Doubly Linked List: A Bi-Directional Bridge for Data

  1. Insert at the Tail

// Insert a node at the end
void ListPushBack(LN* phead, DataType x)
{
    assert(phead);
    LN* newnode = ListBuyNode(x);
    newnode->prev = phead->prev; // New node's prev points to the old tail node
    newnode->next = phead; // New node's next points to the head
    phead->prev->next = newnode; // The old tail's next points to the new node
    phead->prev = newnode; // The head's prev points to the new node
}

Doubly Linked List: A Bi-Directional Bridge for Data

  1. Delete from the Head

// Delete the node at the front
void ListPopFront(LN* phead)
{
    assert(phead && phead->next != phead); // Ensure the list is not empty
    LN* del = phead->next;
    del->next->prev = phead; // The next node's prev points to the head
    phead->next = del->next; // The head's next points to the deleted node's next
    free(del); // Free the memory of the deleted node
    del = NULL;
}

Doubly Linked List: A Bi-Directional Bridge for Data

  1. Delete from the Tail

// Delete the node at the end
void ListPopBack(LN* phead)
{
    assert(phead && phead->next != phead); // Ensure the list is not empty
    LN* del = phead->prev;
    del->prev->next = phead; // The previous node's next points to the head
    phead->prev = del->prev; // The head's prev points to the deleted node's prev
    free(del); // Free the memory of the deleted node
    del = NULL;
}

Doubly Linked List: A Bi-Directional Bridge for Data

  1. Print the List

// Print the list
void ListPrint(LN* phead)
{
    LN* pcur = phead->next;
    while (pcur != phead)
    {
        printf("%d->", pcur->data); // Print the current node's data
        pcur = pcur->next;
    }
    printf("NULL\n"); // Print the end of the list
}

Doubly Linked List: A Bi-Directional Bridge for Datag

  1. Find a Node

// Find a node in the list
LN* ListFind(LN* phead, DataType x)
{
    LN* pcur = phead->next;
    while (pcur != phead)
    {
        if (pcur->data == x)
        {
            return pcur; // Return the node if found
        }
        pcur = pcur->next;
    }
    return NULL; // Return NULL if the node is not found
}
  1. Insert a Node at a Position

// Insert a node after a specified position
void ListInsert(LN* pos, DataType x)
{
    assert(pos);
    LN* newnode = ListBuyNode(x);
    newnode->next = pos->next; // New node's next points to pos's next
    newnode->prev = pos; // New node's prev points to pos
    pos->next->prev = newnode; // pos's next's prev points to the new node
    pos->next = newnode; // pos's next points to the new node
}

Doubly Linked List: A Bi-Directional Bridge for Data

  1. Delete a Node at a Position

// Delete a node at a specified position
void ListErase(LN* pos)
{
    assert(pos);
    pos->next->prev = pos->prev; // The next node's prev points to the previous node
    pos->prev->next = pos->next; // The previous node's next points to the next node
    free(pos); // Free the memory of the node
    pos = NULL;
}

QQDoubly Linked List: A Bi-Directional Bridge for Data

  1. Destroy the List

// Destroy the list
void ListDestroy(LN* phead)
{
    assert(phead);
    LN* pcur = phead->next;
    while (pcur != phead)
    {
        LN* next = pcur->next;
        free(pcur); // Free the current node's memory
        pcur = next;
    }
    free(phead); // Free the head node's memory
    phead = NULL;
}

Doubly Linked List: A Bi-Directional Bridge for Data

Note: In the ListErase and ListDestroy functions, the parameter phead should ideally beaffected if passed by a single pointer level, but for consistency, the function interfaces use a single pointer. As a workaround, after calling these functions, set the actual parameter to NULL manually.


四. Complete Code

1. List.h
This section contains the definition of the ordered list structure and function declarations.

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int DataType;
typedef struct ListNode
{
    DataType data; // Data field
    struct ListNode* prev; // Pointer to the previous node
    struct ListNode* next; // Pointer to the next node
} LN;

// Function Declarations:
void ListInit(LN** pphead);           // Initialize the list
void ListPushBack(LN* phead, DataType x);  // Insert node at the end
void ListPushFront(LN* phead, DataType x); // Insert node at the front
void ListPrint(LN* phead);            // Print the list
void ListPopBack(LN* phead);          // Remove node from the end
void ListPopFront(LN* phead);         // Remove node from the front
LN* ListFind(LN* phead, DataType x); // Find a node
void ListInsert(LN* pos, DataType x); // Insert node after the specified position
void ListErase(LN* pos);              // Delete a node
void ListDestroy(LN* phead);          // Destroy the list

2. List.c
This section contains the implementation of the functions.

#define _CRT_SECURE_NO_WARNINGS
#include"List.h"

// Create a new node
LN* ListBuyNode(DataType x)
{
    LN* node = (LN*)malloc(sizeof(LN));
    if (node == NULL)
    {
        perror("malloc fail!");
        exit(1);
    }
    node->data = x;
    node->prev = node->next = node; // Both pointers point to itself, forming a circular list
    return node;
}

// Initialize the list
void ListInit(LN** pphead)
{
    *pphead = ListBuyNode(-1); // Create the head node with value -1 (not used)
}

// Insert node at the end
void ListPushBack(LN* phead, DataType x)
{
    assert(phead);
    LN* newnode = ListBuyNode(x);
    newnode->prev = phead->prev; // New node's previous pointer points to the original tail
    newnode->next = phead; // New node's next pointer points to the head node
    phead->prev->next = newnode; // Original tail's next points to new node
    phead->prev = newnode; // Head node's previous points to new node
}

// Insert node at the front
void ListPushFront(LN* phead, DataType x)
{
    assert(phead);
    LN* newnode = ListBuyNode(x);
    newnode->next = phead->next; // New node's next points to the head's next node
    newnode->prev = phead; // New node's previous points to the head
    phead->next->prev = newnode; // Head's next node's previous points to the new node
    phead->next = newnode; // Head node's next points to new node
}

// Remove node from the end
void ListPopBack(LN* phead)
{
    assert(phead && phead->next != phead); // Ensure the list is not empty
    LN* del = phead->prev;
    del->prev->next = phead; // Previous node's next points to the head node
    phead->prev = del->prev; // Head node's previous points to the deleted node's previous
    free(del); // Free the memory of the deleted node
    del = NULL;
}

// Remove node from the front
void ListPopFront(LN* phead)
{
    assert(phead && phead->next != phead); // Ensure the list is not empty
    LN* del = phead->next;
    del->next->prev = phead; // Deleted node's next node's previous points to head
    phead->next = del->next; // Head node's next points to the deleted node's next
    free(del); // Free the memory of the deleted node
    del = NULL;
}

// Print the list
void ListPrint(LN* phead)
{
    LN* pcur = phead->next;
    while (pcur != phead)
    {
        printf("%d->", pcur->data); // Print the data of the current node
        pcur = pcur->next;
    }
    printf("NULL\n"); // Print the end marker of the list
}

// Find a node in the list
LN* ListFind(LN* phead, DataType x)
{
    LN* pcur = phead->next;
    while (pcur != phead)
    {
        if (pcur->data == x)
        {
            return pcur; // Return the node if a match is found
        }
        pcur = pcur->next;
    }
    return NULL; // Return NULL if no match is found
}

// Insert a node after the specified position
void ListInsert(LN* pos, DataType x)
{
    assert(pos);
    LN* newnode = ListBuyNode(x);
    newnode->next = pos->next; // New node's next points to pos's next node
    newnode->prev = pos; // New node's previous points to pos
    pos->next->prev = newnode; // pos's next node's previous points to the new node
    pos->next = newnode; // pos's next points to new node
}

// Delete a node at the specified position
void ListErase(LN* pos)
{
    assert(pos);
    pos->next->prev = pos->prev; // Next node's previous points to the deleted node's previous
    pos->prev->next = pos->next; // Previous node's next points to the deleted node's next
    free(pos); // Free the memory of the deleted node
    pos = NULL;
}

// Destroy the list
void ListDestroy(LN* phead)
{
    assert(phead);
    LN* pcur = phead->next;
    while (pcur != phead)
    {
        LN* next = pcur->next;
        free(pcur); // Free the memory of the current node
        pcur = next;
    }
    free(phead); // Free the memory of the head node
    phead = NULL;
}

3. test.c
Test, calling the functions.

#define _CRT_SECURE_NO_WARNINGS
#include"List.h"

void test01()
{
    LN* plist = NULL;
    ListInit(&plist);

    // ListPushFront(plist, 1);
    ListPushFront(plist, 2);
    ListPushFront(plist, 3);
    ListPushFront(plist, 4);

    LN* find = ListFind(plist, 3);

    ListInsert(find, 99);
    ListErase(find);
    find = NULL;

    ListPrint(plist);
    ListDestroy(plist);
    plist = NULL;
}

int main()
{
    test01();
    return 0;
}

6. Conclusion
A doubly linked list with a head node offers flexibility when inserting and deleting nodes. The presence of the head node simplifies boundary conditions and reduces special handling for empty lists or boundary cases. With the implementation and example code provided, you should be able to master basic operations on doubly linked lists and apply them in practical programming tasks.

Hope this blog post is helpful!