C语言链表详解(单链表、双向链表、循环链表)

C 语言链表详解

一、引言

在 C 语言编程中,链表是一种非常重要且基础的数据结构。与数组不同,链表的元素在内存中并非连续存储,而是通过指针将各个元素连接起来。这种数据结构具有动态分配内存、插入和删除元素效率高的特点,在很多场景下都有广泛的应用,比如实现栈、队列、图等更复杂的数据结构,或者用于动态管理数据。接下来,我们将详细探讨 C 语言中链表的相关知识。

二、链表的基本概念

2.1 链表的定义

链表由一系列节点组成,每个节点包含两部分:数据域和指针域。数据域用于存储实际的数据,指针域则存储指向下一个节点的指针(地址)。通过这种方式,节点之间形成了一个链式的结构。

2.2 链表的类型

常见的链表类型有单链表、双向链表和循环链表:

  • 单链表:每个节点只有一个指向下一个节点的指针,链表的遍历只能从链表头开始,沿着指针依次向后访问。
  • 双向链表:每个节点除了有一个指向下一个节点的指针外,还有一个指向前一个节点的指针,这样可以方便地进行双向遍历。
  • 循环链表:单循环链表的尾节点的指针指向头节点,形成一个闭环;双循环链表的头节点的前向指针指向尾节点,尾节点的后向指针指向头节点。

三、单链表的实现

3.1 节点的定义

在 C 语言中,我们可以使用结构体来定义链表的节点。以下是一个简单的单链表节点的定义示例:

#include 
#include 

// 定义单链表节点结构体
typedef struct Node {
    int data;           // 数据域,这里以存储整数为例
    struct Node* next;  // 指针域,指向下一个节点
} Node;

在这个示例中,Node 结构体包含一个整数类型的数据域 data 和一个指向 Node 结构体的指针 next,用于指向下一个节点。

3.2 创建新节点

为了向链表中添加元素,我们需要先创建新的节点。以下是创建新节点的函数:

// 创建新节点
Node* createNode(int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));  // 分配内存
    if (newNode == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    newNode->data = data;  // 初始化数据域
    newNode->next = NULL;  // 初始化指针域
    return newNode;
}

这个函数接受一个整数参数 data,使用 malloc 函数为新节点分配内存,然后将 data 赋值给新节点的数据域,将指针域初始化为 NULL,最后返回新节点的指针。

3.3 在链表头部插入节点

在链表头部插入节点是一种常见的操作。以下是实现该操作的函数:

// 在链表头部插入节点
Node* insertAtHead(Node* head, int data) {
    Node* newNode = createNode(data);  // 创建新节点
    newNode->next = head;  // 新节点的指针指向原链表头
    return newNode;        // 返回新的链表头
}

这个函数接受链表头指针 head 和一个整数 data 作为参数,创建一个新节点,将新节点的指针指向原链表头,然后返回新节点的指针作为新的链表头。

3.4 遍历链表

遍历链表是访问链表中每个节点的操作。以下是遍历链表并打印节点数据的函数:

// 遍历链表
void traverseList(Node* head) {
    Node* current = head;
    while (current != NULL) {
        printf("%d ", current->data);  // 打印节点数据
        current = current->next;       // 移动到下一个节点
    }
    printf("\n");
}

这个函数接受链表头指针 head 作为参数,使用一个指针 current 从链表头开始,依次访问每个节点,打印节点的数据,然后将 current 指针移动到下一个节点,直到链表结束(currentNULL)。

3.5 删除链表节点

删除链表节点也是常见的操作。以下是删除指定数据的节点的函数:

// 删除指定数据的节点
Node* deleteNode(Node* head, int data) {
    Node* current = head;
    Node* prev = NULL;

    // 找到要删除的节点
    while (current != NULL && current->data != data) {
        prev = current;
        current = current->next;
    }

    // 如果未找到要删除的节点
    if (current == NULL) {
        return head;
    }

    // 如果要删除的是头节点
    if (prev == NULL) {
        head = current->next;
    } else {
        prev->next = current->next;
    }

    free(current);  // 释放节点内存
    return head;
}

这个函数接受链表头指针 head 和一个整数 data 作为参数,使用两个指针 currentprev 遍历链表,找到要删除的节点。如果要删除的是头节点,则更新链表头指针;否则,将前一个节点的指针指向要删除节点的下一个节点。最后,释放要删除节点的内存,并返回更新后的链表头指针。

3.6 完整示例代码

#include 
#include 

// 定义单链表节点结构体
typedef struct Node {
    int data;
    struct Node* next;
} Node;

// 创建新节点
Node* createNode(int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("内存分配失败!\n");
        exit(1);
    }
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

// 在链表头部插入节点
Node* insertAtHead(Node* head, int data) {
    Node* newNode = createNode(data);
    newNode->next = head;
    return newNode;
}

// 遍历链表
void traverseList(Node* head) {
    Node* current = head;
    while (current != NULL) {
        printf("%d ", current->data);
        current = current->next;
    }
    printf("\n");
}

// 删除指定数据的节点
Node* deleteNode(Node* head, int data) {
    Node* current = head;
    Node* prev = NULL;

    while (current != NULL && current->data != data) {
        prev = current;
        current = current->next;
    }

    if (current == NULL) {
        return head;
    }

    if (prev == NULL) {
        head = current->next;
    } else {
        prev->next = current->next;
    }

    free(current);
    return head;
}

int main() {
    Node* head = NULL;  // 初始化链表头为 NULL

    // 在链表头部插入节点
    head = insertAtHead(head, 3);
    head = insertAtHead(head, 2);
    head = insertAtHead(head, 1);

    // 遍历链表
    printf("链表元素: ");
    traverseList(head);

    // 删除节点
    head = deleteNode(head, 2);

    // 再次遍历链表
    printf("删除节点 2 后链表元素: ");
    traverseList(head);

    return 0;
}

在这个示例中,我们首先初始化链表头为 NULL,然后使用 insertAtHead 函数在链表头部插入三个节点,接着使用 traverseList 函数遍历链表并打印节点数据,再使用 deleteNode 函数删除节点 2,最后再次遍历链表并打印节点数据。

四、双向链表和循环链表

4.1 双向链表

双向链表的节点除了有一个指向下一个节点的指针外,还有一个指向前一个节点的指针。以下是双向链表节点的定义示例:

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

双向链表的插入和删除操作相对复杂一些,需要同时更新前向指针和后向指针。具体操作见后文。

4.2 循环链表

循环链表分为单循环链表和双循环链表。单循环链表的尾节点的指针指向头节点,双循环链表的头节点的前向指针指向尾节点,尾节点的后向指针指向头节点。以下是单循环链表的遍历示例:

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

4.3 双向链表的插入和删除操作

4.3.1 双向链表节点结构定义

在实现双向链表的插入和删除操作之前,我们先定义双向链表的节点结构。双向链表的每个节点包含三个部分:数据域、指向前一个节点的指针和指向后一个节点的指针。以下是用 C 语言实现的节点结构定义:

#include 
#include 

// 定义双向链表节点结构体
typedef struct DNode {
    int data;             // 数据域
    struct DNode* prev;   // 指向前一个节点的指针
    struct DNode* next;   // 指向后一个节点的指针
} DNode;
4.3.2 双向链表插入操作
在指定节点之后插入新节点
// 在指定节点之后插入新节点
void insertAfter(DNode* prevNode, int newData) {
    if (prevNode == NULL) {
        printf("前一个节点不能为空\n");
        return;
    }

    // 创建新节点
    DNode* newNode = (DNode*)malloc(sizeof(DNode));
    newNode->data = newData;

    // 插入新节点
    newNode->next = prevNode->next;
    if (prevNode->next != NULL) {
        prevNode->next->prev = newNode;
    }
    prevNode->next = newNode;
    newNode->prev = prevNode;
}

操作步骤

  1. 检查 prevNode 是否为空,如果为空则输出错误信息并返回。
  2. 为新节点分配内存,并将数据存储到新节点的数据域。
  3. 将新节点的 next 指针指向 prevNode 的下一个节点。
  4. 如果 prevNode 的下一个节点不为空,将其 prev 指针指向新节点。
  5. prevNodenext 指针指向新节点。
  6. 将新节点的 prev 指针指向 prevNode
在链表头部插入新节点
// 在链表头部插入新节点
DNode* insertAtHead(DNode* head, int newData) {
    DNode* newNode = (DNode*)malloc(sizeof(DNode));
    newNode->data = newData;
    newNode->prev = NULL;
    newNode->next = head;

    if (head != NULL) {
        head->prev = newNode;
    }

    return newNode;
}

操作步骤

  1. 为新节点分配内存,并将数据存储到新节点的数据域。
  2. 将新节点的 prev 指针置为 NULLnext 指针指向原链表头节点。
  3. 如果原链表头节点不为空,将其 prev 指针指向新节点。
  4. 返回新节点作为新的链表头节点。
4.3.3 双向链表删除操作
删除指定节点
// 删除指定节点
void deleteNode(DNode** headRef, DNode* delNode) {
    if (*headRef == NULL || delNode == NULL) {
        return;
    }

    if (*headRef == delNode) {
        *headRef = delNode->next;
    }

    if (delNode->next != NULL) {
        delNode->next->prev = delNode->prev;
    }

    if (delNode->prev != NULL) {
        delNode->prev->next = delNode->next;
    }

    free(delNode);
}

操作步骤

  1. 检查链表头指针或要删除的节点是否为空,如果为空则直接返回。
  2. 如果要删除的节点是链表头节点,将链表头指针更新为该节点的下一个节点。
  3. 如果要删除的节点的下一个节点不为空,将其 prev 指针指向要删除节点的前一个节点。
  4. 如果要删除的节点的前一个节点不为空,将其 next 指针指向要删除节点的下一个节点。
  5. 释放要删除节点的内存。

4.4 循环链表的插入和删除操作

4.4.1 循环链表节点结构定义

循环链表的节点结构与单链表类似,只是尾节点的 next 指针指向头节点。以下是用 C 语言实现的节点结构定义:

#include 
#include 

// 定义循环链表节点结构体
typedef struct CNode {
    int data;
    struct CNode* next;
} CNode;
4.4.2 循环链表插入操作
在链表头部插入新节点
// 在循环链表头部插入新节点
CNode* insertAtHead(CNode* head, int newData) {
    CNode* newNode = (CNode*)malloc(sizeof(CNode));
    newNode->data = newData;

    if (head == NULL) {
        newNode->next = newNode;
        return newNode;
    }

    CNode* last = head;
    while (last->next != head) {
        last = last->next;
    }

    newNode->next = head;
    last->next = newNode;
    return newNode;
}

操作步骤

  1. 为新节点分配内存,并将数据存储到新节点的数据域。
  2. 如果链表为空,将新节点的 next 指针指向自身,返回新节点作为新的链表头节点。
  3. 找到链表的尾节点。
  4. 将新节点的 next 指针指向原链表头节点。
  5. 将尾节点的 next 指针指向新节点。
  6. 返回新节点作为新的链表头节点。
4.4.3 循环链表删除操作
删除指定值的节点
// 删除循环链表中指定值的节点
CNode* deleteNode(CNode* head, int key) {
    if (head == NULL) {
        return NULL;
    }

    CNode* current = head;
    CNode* prev = NULL;

    do {
        if (current->data == key) {
            if (current == head) {
                if (current->next == head) {
                    free(current);
                    return NULL;
                }
                CNode* last = head;
                while (last->next != head) {
                    last = last->next;
                }
                head = current->next;
                last->next = head;
            } else {
                prev->next = current->next;
            }
            free(current);
            return head;
        }
        prev = current;
        current = current->next;
    } while (current != head);

    return head;
}

操作步骤

  1. 检查链表是否为空,如果为空则直接返回 NULL
  2. 初始化 current 指针指向头节点,prev 指针指向 NULL
  3. 遍历链表,找到要删除的节点。
  4. 如果要删除的节点是头节点:
    • 如果链表只有一个节点,释放该节点的内存并返回 NULL
    • 找到链表的尾节点,更新头节点为要删除节点的下一个节点,将尾节点的 next 指针指向新的头节点。
  5. 如果要删除的节点不是头节点,将 prev 节点的 next 指针指向要删除节点的下一个节点。
  6. 释放要删除节点的内存,返回更新后的链表头节点。

4.5 代码测试示例

#include 
#include 

// 双向链表部分
typedef struct DNode {
    int data;
    struct DNode* prev;
    struct DNode* next;
} DNode;

void insertAfter(DNode* prevNode, int newData) {
    if (prevNode == NULL) {
        printf("前一个节点不能为空\n");
        return;
    }
    DNode* newNode = (DNode*)malloc(sizeof(DNode));
    newNode->data = newData;
    newNode->next = prevNode->next;
    if (prevNode->next != NULL) {
        prevNode->next->prev = newNode;
    }
    prevNode->next = newNode;
    newNode->prev = prevNode;
}

DNode* insertAtHead(DNode* head, int newData) {
    DNode* newNode = (DNode*)malloc(sizeof(DNode));
    newNode->data = newData;
    newNode->prev = NULL;
    newNode->next = head;
    if (head != NULL) {
        head->prev = newNode;
    }
    return newNode;
}

void deleteNode(DNode** headRef, DNode* delNode) {
    if (*headRef == NULL || delNode == NULL) {
        return;
    }
    if (*headRef == delNode) {
        *headRef = delNode->next;
    }
    if (delNode->next != NULL) {
        delNode->next->prev = delNode->prev;
    }
    if (delNode->prev != NULL) {
        delNode->prev->next = delNode->next;
    }
    free(delNode);
}

// 循环链表部分
typedef struct CNode {
    int data;
    struct CNode* next;
} CNode;

CNode* insertAtHead(CNode* head, int newData) {
    CNode* newNode = (CNode*)malloc(sizeof(CNode));
    newNode->data = newData;
    if (head == NULL) {
        newNode->next = newNode;
        return newNode;
    }
    CNode* last = head;
    while (last->next != head) {
        last = last->next;
    }
    newNode->next = head;
    last->next = newNode;
    return newNode;
}

CNode* deleteNode(CNode* head, int key) {
    if (head == NULL) {
        return NULL;
    }
    CNode* current = head;
    CNode* prev = NULL;
    do {
        if (current->data == key) {
            if (current == head) {
                if (current->next == head) {
                    free(current);
                    return NULL;
                }
                CNode* last = head;
                while (last->next != head) {
                    last = last->next;
                }
                head = current->next;
                last->next = head;
            } else {
                prev->next = current->next;
            }
            free(current);
            return head;
        }
        prev = current;
        current = current->next;
    } while (current != head);
    return head;
}

// 测试函数
int main() {
    // 测试双向链表
    DNode* dHead = NULL;
    dHead = insertAtHead(dHead, 1);
    insertAfter(dHead, 2);
    deleteNode(&dHead, dHead->next);

    // 测试循环链表
    CNode* cHead = NULL;
    cHead = insertAtHead(cHead, 3);
    cHead = insertAtHead(cHead, 4);
    cHead = deleteNode(cHead, 3);

    return 0;
}

这个测试代码分别对双向链表和循环链表的插入和删除操作进行了简单的测试。可以根据需要添加更多的测试用例来验证这些操作的正确性。

五、链表的应用场景

链表作为一种基础且重要的数据结构,在计算机科学和软件开发的众多领域都有广泛的应用。以下将详细介绍链表在不同场景下的应用。

5.1. 操作系统领域

5.1.1 进程管理
  • 在操作系统中,进程是程序在操作系统中的一次执行过程。操作系统需要管理多个进程的状态和调度信息。链表可以用于实现进程控制块(PCB)的管理。每个进程控制块包含了进程的各种信息,如进程ID、状态、优先级等。这些进程控制块通过链表连接起来,操作系统可以方便地对进程进行创建、删除、调度等操作。
  • 例如,在一个多任务操作系统中,当有新的进程创建时,操作系统会为该进程分配一个新的进程控制块,并将其插入到进程链表中。当进程结束时,操作系统会从链表中删除该进程的控制块。
5.1.2 内存管理
  • 操作系统在管理内存时,需要记录内存的使用情况。空闲内存块可以通过链表进行管理,每个节点代表一个空闲内存块,包含内存块的起始地址和大小等信息。当有程序申请内存时,操作系统可以从链表中查找合适的空闲内存块分配给程序;当程序释放内存时,操作系统会将释放的内存块插入到空闲内存链表中。
  • 比如,伙伴系统是一种常见的内存管理算法,它使用链表来管理不同大小的空闲内存块。当需要分配内存时,系统会根据请求的大小在相应的链表中查找合适的内存块。

5.2. 数据结构实现领域

5.2.1 栈和队列的实现
  • :栈是一种后进先出(LIFO)的数据结构。可以使用链表来实现栈,链表的头部作为栈顶。入栈操作相当于在链表头部插入一个新节点,出栈操作相当于删除链表头部的节点。这种实现方式可以动态地分配和释放内存,避免了使用数组实现栈时可能出现的栈溢出问题。
  • 队列:队列是一种先进先出(FIFO)的数据结构。可以使用链表来实现队列,链表的头部作为队头,尾部作为队尾。入队操作相当于在链表尾部插入一个新节点,出队操作相当于删除链表头部的节点。使用链表实现队列可以方便地处理元素的入队和出队操作,并且可以动态调整队列的长度。
5.2.2 图的邻接表表示
  • 在图的表示中,邻接表是一种常用的方法。对于一个图,每个顶点可以用一个链表来表示其所有的邻接顶点。链表中的每个节点代表一个邻接顶点,包含邻接顶点的编号和边的权重等信息。这种表示方法可以有效地节省存储空间,特别是对于稀疏图(边的数量远小于顶点数量的平方)。
  • 例如,在社交网络中,每个用户可以看作是图的一个顶点,用户之间的关系可以看作是边。使用邻接表可以方便地表示社交网络中用户之间的关系。

5.3. 应用程序开发领域

5.3.1 文本编辑器
  • 在文本编辑器中,链表可以用于实现文本的存储和编辑。每个链表节点可以存储一行文本,通过链表的连接关系可以表示文本的行顺序。当用户进行插入、删除、修改等操作时,只需要对链表进行相应的节点插入、删除和修改操作即可。
  • 例如,当用户在文本中插入一行新的文本时,编辑器会创建一个新的链表节点,并将其插入到合适的位置。
5.3.2 多媒体播放器
  • 在多媒体播放器中,播放列表可以使用链表来实现。每个链表节点可以存储一首歌曲或一个视频的信息,如文件名、时长、播放地址等。通过链表的连接关系,可以方便地实现播放列表的顺序播放、随机播放、循环播放等功能。
  • 例如,当用户添加一首新的歌曲到播放列表时,播放器会创建一个新的链表节点,并将其插入到播放列表的末尾。

5.4. 数据库领域

5.4.1 索引管理
  • 在数据库中,索引是一种用于提高数据查询效率的数据结构。链表可以用于实现某些类型的索引,如链式哈希表。链式哈希表是一种哈希表的实现方式,当发生哈希冲突时,将冲突的元素存储在同一个链表中。通过链表的连接关系,可以方便地处理哈希冲突。
  • 例如,在一个学生信息数据库中,以学生的学号作为索引。当有多个学生的学号哈希到同一个位置时,这些学生的信息可以存储在一个链表中。
5.4.2 事务管理
  • 数据库中的事务是一组不可分割的数据库操作序列。链表可以用于管理事务的执行顺序和状态。每个链表节点可以存储一个事务的相关信息,如事务ID、事务状态、操作内容等。通过链表的连接关系,数据库管理系统可以方便地对事务进行调度和回滚等操作。
  • 例如,当一个事务开始执行时,数据库管理系统会创建一个新的链表节点,并将其插入到事务链表中。当事务执行完成或出现错误需要回滚时,数据库管理系统会根据链表中的信息进行相应的处理。

5.5. 游戏开发领域

5.5.1 游戏对象管理
  • 在游戏开发中,游戏场景中通常包含多个游戏对象,如角色、道具、敌人等。链表可以用于管理这些游戏对象。每个链表节点可以存储一个游戏对象的信息,如对象的位置、状态、属性等。通过链表的连接关系,游戏引擎可以方便地对游戏对象进行更新、渲染和碰撞检测等操作。
  • 例如,在一个角色扮演游戏中,每个角色可以看作是一个游戏对象。游戏引擎可以使用链表来管理所有角色的信息,当角色移动或进行攻击时,游戏引擎可以根据链表中的信息更新角色的状态。
5.5.2 动画帧管理
  • 游戏中的动画通常由多个动画帧组成。链表可以用于管理动画帧的顺序和播放时间。每个链表节点可以存储一个动画帧的信息,如帧的图像数据、播放时间等。通过链表的连接关系,游戏引擎可以按照顺序播放动画帧,实现动画效果。
  • 例如,在一个二维游戏中,角色的行走动画可以由多个动画帧组成。游戏引擎可以使用链表来管理这些动画帧,当角色行走时,游戏引擎会按照链表中的顺序依次播放动画帧。

六、链表的优缺点

6.1 优点

  • 动态内存分配:链表可以根据需要动态分配和释放内存,不需要预先分配固定大小的内存空间,适合处理数据量不确定的情况。
  • 插入和删除效率高:在链表中插入和删除节点只需要修改指针,时间复杂度为 O ( 1 ) O(1) O(1)(在已知节点位置的情况下),比数组的插入和删除操作效率高。

6.2 缺点

  • 随机访问效率低:链表只能通过遍历的方式访问节点,无法像数组那样通过下标直接访问指定位置的元素,因此随机访问的时间复杂度为 O ( n ) O(n) O(n)
  • 额外的指针开销:每个节点都需要额外的指针来指向下一个节点(双向链表还需要指向前一个节点的指针),会增加内存开销。

七、总结

链表是 C 语言中一种重要的数据结构,具有动态分配内存、插入和删除效率高的特点。通过合理使用链表,我们可以解决很多实际编程中的问题。在使用链表时,要注意内存的分配和释放,避免内存泄漏。同时,要根据具体的应用场景选择合适的链表类型,如单链表、双向链表或循环链表。

你可能感兴趣的:(C语言,c语言,链表,开发语言)