C语言链表全面指南

引言
在计算机科学中,数据结构是存储和组织数据的一种方式,它不仅影响着数据的存取效率,还决定了算法的设计和实现。链表作为一种基础而强大的线性数据结构,在实际应用中占据着重要的地位。与数组相比,链表提供了更加灵活的内存使用方式,允许在运行时动态地调整其大小,并且易于实现插入和删除操作。然而,链表也因其指针操作的复杂性而闻名,对于初学者来说可能较为难以掌握。
本文旨在为读者提供一个全面深入的指南,涵盖单向链表、双向链表、单向循环链表及双向循环链表的基础知识、节点操作、插入删除方法以及遍历等核心概念。通过详细的代码示例和解释,帮助读者理解链表的工作原理,并能够在实际编程中灵活运用这些知识。无论是正在学习数据结构的计算机专业学生,还是希望提升编程技能的职业开发者,都能从中受益匪浅。
接下来的部分,我们将逐步介绍每种类型的链表,包括如何定义节点结构、创建新节点、进行各种插入和删除操作,以及如何遍历和反转链表。每一步都将配有详尽的代码片段和注释,以便读者能够轻松跟随并实践。让我们一起探索链表的奥秘吧!
一、单向链表
单向链表节点定义
在定义一个单向链表之前,我们需要首先定义一个节点(Node)。每个节点包含两个部分:存储数据的部分(例如整型变量)和一个指向下一个节点的指针。这里我们定义了一个名为 SingleNode 的结构体来表示单向链表中的节点。
struct SingleNode {
int data; // 存储数据的字段
struct SingleNode *next; // 指向下一个节点的指针
};
typedef struct SingleNode SingleNode; // 定义一个类型别名简化后续代码
创建新节点
创建新节点时,我们需要动态分配内存来存储这个节点,并且设置它的数据域和指针域。如果内存分配失败,我们需要报告错误并退出程序。
SingleNode *createSingleNode(int data) {
SingleNode *newNode = (SingleNode*)malloc(sizeof(SingleNode)); // 动态分配内存
if (newNode == NULL) {
perror("Memory allocation failed"); // 如果分配失败,则打印错误信息
exit(EXIT_FAILURE); // 终止程序
}
newNode->data = data; // 设置数据域
newNode->next = NULL; // 设置指向下一个节点的指针为 NULL
return newNode; // 返回新创建的节点
}
在单向链表中插入节点
在单向链表中插入节点可以有三种方式:插入到头部、插入到尾部以及在指定节点之后插入。下面分别给出这三种情况下的实现。
// 在链表头部插入节点
void insertAtHeadSingle(SingleNode **head, int data) {
SingleNode *newNode = createSingleNode(data); // 创建新节点
newNode->next = *head; // 新节点的下一个指针指向原来的头节点
*head = newNode; // 更新头指针指向新节点
}
// 在链表尾部插入节点
void insertAtTailSingle(SingleNode **head, int data) {
SingleNode *newNode = createSingleNode(data); // 创建新节点
if (*head == NULL) { // 如果链表为空
*head = newNode; // 直接更新头指针
} else { // 如果链表非空
SingleNode *current = *head; // 当前节点初始化为头节点
while (current->next != NULL) { // 遍历至最后一个节点
current = current->next;
}
current->next = newNode; // 在末尾添加新节点
}
}
// 在指定节点之后插入节点
void insertAfterSingle(SingleNode *prevNode, int data) {
if (prevNode == NULL) { // 检查前一个节点是否有效
printf("Previous node cannot be NULL.\n");
return;
}
SingleNode *newNode = createSingleNode(data); // 创建新节点
newNode->next = prevNode->next; // 新节点的下一个指针指向前一个节点的下一个节点
prevNode->next = newNode; // 前一个节点的下一个指针指向新节点
}
在单向链表中删除节点
删除节点也有三种情况:删除头部节点、删除尾部节点以及删除指定节点。下面是相应的实现代码。
// 删除链表头部节点
void deleteHeadSingle(SingleNode **head) {
if (*head == NULL) return; // 如果链表为空,则直接返回
SingleNode *temp = *head; // 临时保存头节点
*head = (*head)->next; // 头指针指向下一个节点
free(temp); // 释放原头节点占用的内存
}
// 删除链表尾部节点
void deleteTailSingle(SingleNode **head) {
if (*head == NULL) return; // 如果链表为空,则直接返回
if ((*head)->next == NULL) { // 如果只有一个节点
free(*head); // 释放内存
*head = NULL; // 头指针置空
} else { // 如果有多个节点
SingleNode *current = *head; // 当前节点初始化为头节点
while (current->next->next != NULL) { // 遍历至倒数第二个节点
current = current->next;
}
free(current->next); // 释放尾节点占用的内存
current->next = NULL; // 将当前节点的下一个指针置空
}
}
// 删除指定节点
void deleteNodeSingle(SingleNode **head, SingleNode *del) {
if (*head == NULL || del == NULL) return; // 如果链表为空或待删除节点无效,则直接返回
if (*head == del) { // 如果待删除节点是头节点
*head = del->next; // 更新头指针
}
SingleNode *current = *head; // 当前节点初始化为头节点
while (current->next != del) { // 寻找待删除节点的前一个节点
current = current->next;
}
current->next = del->next; // 将前一个节点的下一个指针指向待删除节点的下一个节点
free(del); // 释放待删除节点占用的内存
}
遍历单向链表
遍历链表是一个基本操作,它允许我们查看链表中的所有元素。我们可以从头节点开始,沿着 next 指针遍历直到 NULL。
void printListSingle(SingleNode *head) {
SingleNode *current = head; // 当前节点初始化为头节点
while (current != NULL) { // 遍历至最后一个节点
printf("%d ", current->data); // 输出数据
current = current->next; // 移动到下一个节点
}
printf("\n"); // 换行
}
反转单向链表
反转链表意味着改变所有节点的 next 指针方向,使得链表从尾到头。这可以通过迭代地交换每个节点的 next 指针来实现。
void reverseListSingle(SingleNode **head) {
SingleNode *prev = NULL; // 上一个节点初始化为 NULL
SingleNode *current = *head; // 当前节点初始化为头节点
SingleNode *next = NULL; // 临时保存下一个节点
while (current != NULL) { // 遍历链表
next = current->next; // 保存下一个节点
current->next = prev; // 反转当前节点的下一个指针
prev = current; // 当前节点成为上一个节点
current = next; // 下一个节点成为当前节点
}
*head = prev; // 更新头指针指向最后一个节点(即反转后的头节点)
}

二、双向链表
双向链表节点定义
与单向链表不同,双向链表的每个节点除了有一个指向下个节点的指针外,还有一个指向上一个节点的指针。这样可以在两个方向上访问链表中的节点。
struct DoubleNode {
int data; // 数据域
struct DoubleNode *prev; // 指向前一个节点的指针
struct DoubleNode *next; // 指向下一个节点的指针
};
typedef struct DoubleNode DoubleNode; // 定义一个类型别名简化后续代码
创建新节点
创建双向链表的新节点类似于单向链表的新节点创建,只是多了指向前一个节点的指针。
DoubleNode *createDoubleNode(int data) {
DoubleNode *newNode = (DoubleNode*)malloc(sizeof(DoubleNode)); // 动态分配内存
if (newNode == NULL) {
perror("Memory allocation failed"); // 如果分配失败,则打印错误信息
exit(EXIT_FAILURE); // 终止程序
}
newNode->data = data; // 设置数据域
newNode->prev = NULL; // 设置指向前一个节点的指针为 NULL
newNode->next = NULL; // 设置指向下一个节点的指针为 NULL
return newNode; // 返回新创建的节点
}
在双向链表中插入节点
插入节点的操作与单向链表相似,只是需要额外处理前一个节点的指针。
// 在链表头部插入节点
void insertAtHeadDouble(DoubleNode **head, int data) {
DoubleNode *newNode = createDoubleNode(data); // 创建新节点
newNode->next = *head; // 新节点的下一个指针指向原来的头节点
if (*head != NULL) (*head)->prev = newNode; // 如果原来的头节点存在,则更新其前一个指针
*head = newNode; // 更新头指针指向新节点
}
// 在链表尾部插入节点
void insertAtTailDouble(DoubleNode **head, int data) {
DoubleNode *newNode = createDoubleNode(data); // 创建新节点
if (*head == NULL) { // 如果链表为空
*head = newNode; // 直接更新头指针
} else { // 如果链表非空
DoubleNode *current = *head; // 当前节点初始化为头节点
while (current->next != NULL) { // 遍历至最后一个节点
current = current->next;
}
current->next = newNode; // 在末尾添加新节点
newNode->prev = current; // 新节点的前一个指针指向当前节点
}
}
// 在指定节点之后插入节点
void insertAfterDouble(DoubleNode *prevNode, int data) {
if (prevNode == NULL) { // 检查前一个节点是否有效
printf("Previous node cannot be NULL.\n");
return;
}
DoubleNode *newNode = createDoubleNode(data); // 创建新节点
newNode->next = prevNode->next; // 新节点的下一个指针指向前一个节点的下一个节点
newNode->prev = prevNode; // 新节点的前一个指针指向当前节点
if (prevNode->next != NULL) prevNode->next->prev = newNode; // 如果前一个节点的下一个节点存在,则更新其前一个指针
prevNode->next = newNode; // 前一个节点的下一个指针指向新节点
}
在双向链表中删除节点
删除节点时,除了要处理下一个节点的指针外,还需要处理前一个节点的指针。
// 删除链表头部节点
void deleteHeadDouble(DoubleNode **head) {
if (*head == NULL) return; // 如果链表为空,则直接返回
DoubleNode *temp = *head; // 临时保存头节点
*head = (*head)->next; // 头指针指向下一个节点
if (*head != NULL) (*head)->prev = NULL; // 如果新的头节点存在,则更新其前一个指针
free(temp); // 释放原头节点占用的内存
}
// 删除链表尾部节点
void deleteTailDouble(DoubleNode **head) {
if (*head == NULL) return; // 如果链表为空,则直接返回
if ((*head)->next == NULL) { // 如果只有一个节点
free(*head); // 释放内存
*head = NULL; // 头指针置空
} else { // 如果有多个节点
DoubleNode *current = *head; // 当前节点初始化为头节点
while (current->next->next != NULL) { // 遍历至倒数第二个节点
current = current->next;
}
free(current->next); // 释放尾节点占用的内存
current->next = NULL; // 将当前节点的下一个指针置空
}
}
// 删除指定节点
void deleteNodeDouble(DoubleNode **head, DoubleNode *del) {
if (*head == NULL || del == NULL) return; // 如果链表为空或待删除节点无效,则直接返回
if (*head == del) { // 如果待删除节点是头节点
*head = del->next; // 更新头指针
}
if (del->prev != NULL) del->prev->next = del->next; // 如果待删除节点的前一个节点存在,则更新其下一个指针
if (del->next != NULL) del->next->prev = del->prev; // 如果待删除节点的下一个节点存在,则更新其前一个指针
free(del); // 释放待删除节点占用的内存
}
遍历双向链表
遍历双向链表的方式与单向链表相同,只是我们有了更多的选择,可以从头节点开始也可以从尾节点开始。
void printListDouble(DoubleNode *head) {
DoubleNode *current = head; // 当前节点初始化为头节点
while (current != NULL) { // 遍历至最后一个节点
printf("%d ", current->data); // 输出数据
current = current->next; // 移动到下一个节点
}
printf("\n"); // 换行
}
反转双向链表
反转双向链表的过程与单向链表类似,但是需要同时处理 prev 和 next 指针。
void reverseListDouble(DoubleNode **head) {
DoubleNode *current = *head; // 当前节点初始化为头节点
DoubleNode *temp = NULL; // 临时保存前一个节点
while (current != NULL) { // 遍历链表
temp = current->prev; // 保存前一个节点
current->prev = current->next; // 反转当前节点的前一个指针
current->next = temp; // 反转当前节点的下一个指针
current = current->prev; // 当前节点成为前一个节点
}
if (temp != NULL) *head = temp->prev; // 更新头指针指向最后一个节点(即反转后的头节点)
}

三、单向循环链表
单向循环链表节点定义
单向循环链表与单向链表的区别在于,最后一个节点的 next 指针指向的是头节点,形成了一个闭环。
struct CircularSingleNode {
int data; // 数据域
struct CircularSingleNode *next; // 指向下一个节点的指针
};
typedef struct CircularSingleNode CircularSingleNode; // 定义一个类型别名简化后续代码
创建新节点
创建单向循环链表的新节点与创建普通单向链表的新节点相同。
CircularSingleNode *createCircularSingleNode(int data) {
CircularSingleNode *newNode = (CircularSingleNode*)malloc(sizeof(CircularSingleNode)); // 动态分配内存
if (newNode == NULL) {
perror("Memory allocation failed"); // 如果分配失败,则打印错误信息
exit(EXIT_FAILURE); // 终止程序
}
newNode->data = data; // 设置数据域
newNode->next = NULL; // 设置指向下一个节点的指针为 NULL
return newNode; // 返回新创建的节点
}
在单向循环链表中插入节点
插入节点时,需要注意最后一个节点的 next 指针应该指向头节点。
// 在链表头部插入节点
void insertAtHeadCircularSingle(CircularSingleNode **head, int data) {
CircularSingleNode *newNode = createCircularSingleNode(data); // 创建新节点
if (*head == NULL) { // 如果链表为空
*head = newNode; // 直接更新头指针
newNode->next = newNode; // 形成单个节点的循环
} else { // 如果链表非空
newNode->next = *head; // 新节点的下一个指针指向原来的头节点
CircularSingleNode *tail = *head; // 临时保存尾节点
while (tail->next != *head) { // 遍历至最后一个节点
tail = tail->next;
}
tail->next = newNode; // 更新尾节点的下一个指针
*head = newNode; // 更新头指针指向新节点
}
}
// 在链表尾部插入节点
void insertAtTailCircularSingle(CircularSingleNode **head, int data) {
CircularSingleNode *newNode = createCircularSingleNode(data); // 创建新节点
if (*head == NULL) { // 如果链表为空
*head = newNode; // 直接更新头指针
newNode->next = newNode; // 形成单个节点的循环
} else { // 如果链表非空
CircularSingleNode *tail = *head; // 临时保存尾节点
while (tail->next != *head) { // 遍历至最后一个节点
tail = tail->next;
}
tail->next = newNode; // 更新尾节点的下一个指针
newNode->next = *head; // 新节点的下一个指针指向头节点
}
}
// 在指定节点之后插入节点
void insertAfterCircularSingle(CircularSingleNode *prevNode, int data) {
if (prevNode == NULL) { // 检查前一个节点是否有效
printf("Previous node cannot be NULL.\n");
return;
}
CircularSingleNode *newNode = createCircularSingleNode(data); // 创建新节点
newNode->next = prevNode->next; // 新节点的下一个指针指向前一个节点的下一个节点
prevNode->next = newNode; // 前一个节点的下一个指针指向新节点
}
在单向循环链表中删除节点
删除节点时,需要特别注意最后一个节点的 next 指针应该重新指向头节点。
// 删除链表头部节点
void deleteHeadCircularSingle(CircularSingleNode **head) {
if (*head == NULL) return; // 如果链表为空,则直接返回
CircularSingleNode *temp = *head; // 临时保存头节点
*head = (*head)->next; // 头指针指向下一个节点
CircularSingleNode *tail = *head; // 临时保存尾节点
while (tail->next != *head) { // 遍历至最后一个节点
tail = tail->next;
}
tail->next = *head; // 更新尾节点的下一个指针
free(temp); // 释放原头节点占用的内存
}
// 删除链表尾部节点
void deleteTailCircularSingle(CircularSingleNode **head) {
if (*head == NULL) return; // 如果链表为空,则直接返回
CircularSingleNode *current = *head; // 当前节点初始化为头节点
CircularSingleNode *prev = NULL; // 前一个节点初始化为 NULL
while (current->next != *head) { // 寻找倒数第二个节点
prev = current;
current = current->next;
}
if (prev == NULL) { // 如果只有一个节点
free(current); // 释放内存
*head = NULL; // 头指针置空
} else { // 如果有多个节点
prev->next = *head; // 更新前一个节点的下一个指针
free(current); // 释放尾节点占用的内存
}
}
// 删除指定节点
void deleteNodeCircularSingle(CircularSingleNode **head, CircularSingleNode *del) {
if (*head == NULL || del == NULL) return; // 如果链表为空或待删除节点无效,则直接返回
if (*head == del) { // 如果待删除节点是头节点
*head = del->next; // 更新头指针
}
if (del->next != NULL) del->next->prev = del->prev; // 如果待删除节点的下一个节点存在,则更新其前一个指针
free(del); // 释放待删除节点占用的内存
}
遍历单向循环链表
遍历单向循环链表时,我们需要确保不会陷入无限循环。为此,我们在遍历时检查当前节点是否再次到达头节点。
void printListCircularSingle(CircularSingleNode *head) {
CircularSingleNode *current = head; // 当前节点初始化为头节点
do { // 遍历直至回到头节点
printf("%d ", current->data); // 输出数据
current = current->next; // 移动到下一个节点
} while (current != head);
printf("\n"); // 换行
}
反转单向循环链表
反转单向循环链表时,除了反转 next 指针外,还需要重新连接头节点以形成闭环。
void reverseListCircularSingle(CircularSingleNode **head) {
// 断开循环
CircularSingleNode *last = *head;
while (last->next != *head) {
last = last->next;
}
last->next = NULL;
// 反转链表
CircularSingleNode *prev = NULL;
CircularSingleNode *current = *head;
CircularSingleNode *next = NULL;
while (current != NULL) {
next = current->next;
current->next = prev;
prev = current;
current = next;
}
*head = prev;
// 重新连接形成循环
last = *head;
while (last->next != NULL) {
last = last->next;
}
last->next = *head;
}

四、双向循环链表
双向循环链表节点定义
双向循环链表结合了双向链表和单向循环链表的特点,每个节点都有指向前一个节点和下一个节点的指针,并且最后一个节点的 next 指针指向头节点,头节点的 prev 指针指向最后一个节点。
struct CircularDoubleNode {
int data; // 数据域
struct CircularDoubleNode *prev; // 指向前一个节点的指针
struct CircularDoubleNode *next; // 指向下一个节点的指针
};
typedef struct CircularDoubleNode CircularDoubleNode; // 定义一个类型别名简化后续代码
创建新节点
创建双向循环链表的新节点与创建普通双向链表的新节点相同。
CircularDoubleNode *createCircularDoubleNode(int data) {
CircularDoubleNode *newNode = (CircularDoubleNode*)malloc(sizeof(CircularDoubleNode)); // 动态分配内存
if (newNode == NULL) {
perror("Memory allocation failed"); // 如果分配失败,则打印错误信息
exit(EXIT_FAILURE); // 终止程序
}
newNode->data = data; // 设置数据域
newNode->prev = NULL; // 设置指向前一个节点的指针为 NULL
newNode->next = NULL; // 设置指向下一个节点的指针为 NULL
return newNode; // 返回新创建的节点
}
在双向循环链表中插入节点
插入节点时,除了要处理 next 指针外,还要处理 prev 指针,确保闭环正确。
// 在链表头部插入节点
void insertAtHeadCircularDouble(CircularDoubleNode **head, int data) {
CircularDoubleNode *newNode = createCircularDoubleNode(data); // 创建新节点
if (*head == NULL) { // 如果链表为空
*head = newNode; // 直接更新头指针
newNode->next = newNode; // 形成单个节点的循环
newNode->prev = newNode;
} else { // 如果链表非空
newNode->next = *head; // 新节点的下一个指针指向原来的头节点
newNode->prev = (*head)->prev; // 新节点的前一个指针指向原来的尾节点
(*head)->prev->next = newNode; // 原来的尾节点的下一个指针指向新节点
(*head)->prev = newNode; // 原来的头节点的前一个指针指向新节点
*head = newNode; // 更新头指针指向新节点
}
}
// 在链表尾部插入节点
void insertAtTailCircularDouble(CircularDoubleNode **head, int data) {
CircularDoubleNode *newNode = createCircularDoubleNode(data); // 创建新节点
if (*head == NULL) { // 如果链表为空
*head = newNode; // 直接更新头指针
newNode->next = newNode; // 形成单个节点的循环
newNode->prev = newNode;
} else { // 如果链表非空
newNode->prev = (*head)->prev; // 新节点的前一个指针指向原来的尾节点
newNode->next = *head; // 新节点的下一个指针指向原来的头节点
(*head)->prev->next = newNode; // 原来的尾节点的下一个指针指向新节点
(*head)->prev = newNode; // 原来的头节点的前一个指针指向新节点
}
}
// 在指定节点之后插入节点
void insertAfterCircularDouble(CircularDoubleNode *prevNode, int data) {
if (prevNode == NULL) { // 检查前一个节点是否有效
printf("Previous node cannot be NULL.\n");
return;
}
CircularDoubleNode *newNode = createCircularDoubleNode(data); // 创建新节点
newNode->next = prevNode->next; // 新节点的下一个指针指向前一个节点的下一个节点
newNode->prev = prevNode; // 新节点的前一个指针指向当前节点
prevNode->next->prev = newNode; // 如果前一个节点的下一个节点存在,则更新其前一个指针
prevNode->next = newNode; // 前一个节点的下一个指针指向新节点
}
在双向循环链表中删除节点
删除节点时,除了要处理 next 指针外,还要处理 prev 指针,确保闭环正确。
// 删除链表头部节点
void deleteHeadCircularDouble(CircularDoubleNode **head) {
if (*head == NULL) return; // 如果链表为空,则直接返回
CircularDoubleNode *temp = *head; // 临时保存头节点
*head = (*head)->next; // 头指针指向下一个节点
if (*head != NULL) (*head)->prev = temp->prev; // 如果新的头节点存在,则更新其前一个指针
free(temp); // 释放原头节点占用的内存
}
// 删除链表尾部节点
void deleteTailCircularDouble(CircularDoubleNode **head) {
if (*head == NULL) return; // 如果链表为空,则直接返回
CircularDoubleNode *current = *head; // 当前节点初始化为头节点
CircularDoubleNode *prev = NULL; // 前一个节点初始化为 NULL
while (current->next != *head) { // 寻找倒数第二个节点
prev = current;
current = current->next;
}
if (prev == NULL) { // 如果只有一个节点
free(current); // 释放内存
*head = NULL; // 头指针置空
} else { // 如果有多个节点
prev->next = *head; // 更新前一个节点的下一个指针
free(current); // 释放尾节点占用的内存
}
}
// 删除指定节点
void deleteNodeCircularDouble(CircularDoubleNode **head, CircularDoubleNode *del) {
if (*head == NULL || del == NULL) return; // 如果链表为空或待删除节点无效,则直接返回
if (*head == del) { // 如果待删除节点是头节点
*head = del->next; // 更新头指针
}
del->prev->next = del->next; // 如果待删除节点的前一个节点存在,则更新其下一个指针
del->next->prev = del->prev; // 如果待删除节点的下一个节点存在,则更新其前一个指针
free(del); // 释放待删除节点占用的内存
}
遍历双向循环链表
遍历双向循环链表的方式与单向循环链表相同,只需确保不会陷入无限循环。
void printListCircularDouble(CircularDoubleNode *head) {
CircularDoubleNode *current = head; // 当前节点初始化为头节点
do { // 遍历直至回到头节点
printf("%d ", current->data); // 输出数据
current = current->next; // 移动到下一个节点
} while (current != head);
printf("\n"); // 换行
}
反转双向循环链表
反转双向循环链表时,除了反转 next 指针外,还要反转 prev 指针,并重新连接头节点以形成闭环。
void reverseListCircularDouble(CircularDoubleNode **head) {
CircularDoubleNode *current = *head; // 当前节点初始化为头节点
CircularDoubleNode *temp = NULL; // 临时保存前一个节点
do { // 遍历链表
temp = current->prev; // 保存前一个节点
current->prev = current->next; // 反转当前节点的前一个指针
current->next = temp; // 反转当前节点的下一个指针
current = current->prev; // 当前节点成为前一个节点
} while (current != *head);
if (temp != NULL) *head = temp->prev; // 更新头指针指向最后一个节点(即反转后的头节点)
}
结论
链表是C语言中非常有用的数据结构,适用于需要频繁添加或删除元素的场景。理解链表的基本概念和操作方法是成为优秀程序员的关键。希望这篇指南能够帮助你更好地理解和使用链表。随着实践的深入,你将掌握更多高级技巧,并能灵活应用到实际开发中。
更多推荐
所有评论(0)