前言
大家好!今天我们来聊聊数据结构中的基础但非常重要的内容——单链表。我会用最详细的方式,从零开始教你如何用C语言实现一个单链表(无头结点、不循环)。即使你完全没有链表的概念,相信通过这篇博客也能完全理解!
什么是单链表?
单链表是一种常见的线性数据结构,它由一系列节点组成,每个节点包含两部分:
-
数据域 - 存储实际的数据
-
指针域 - 存储下一个节点的地址
这些节点通过指针连接起来,形成一个链式结构。单链表的特点是:
-
不需要连续的内存空间
-
插入和删除效率高(O(1)时间复杂度)
-
访问效率相对较低(O(n)时间复杂度)
代码实现
头文件设计 (LinkedList.h)
首先我们创建头文件,定义数据结构和函数声明:
#ifndef __LINKEDLIST_H__ #define __LINKEDLIST_H__ #include <stdio.h> #include <stdlib.h> #include <assert.h> // 定义数据类型,使用typedef可以方便地更改存储的数据类型 typedef int DataType; // 定义链表节点结构体 typedef struct LinkedListNode { DataType data; // 数据域 struct LinkedListNode* next; // 指针域(指向下一个节点) } LinkedListNode; // 定义链表结构体(管理整个链表) typedef struct LinkedList { LinkedListNode* head; // 指向链表的第一个节点 size_t size; // 记录链表中节点的数量 } LinkedList; // 函数声明 // 初始化链表 void LinkedListInit(LinkedList* list); // 创建新节点 LinkedListNode* BuyNode(DataType data); // 在链表头部插入节点 void LinkedListPushFront(LinkedList* list, DataType data); // 在链表尾部插入节点 void LinkedListPushBack(LinkedList* list, DataType data); // 在指定位置插入节点 void LinkedListInsert(LinkedList* list, size_t pos, DataType data); // 删除链表头部节点 void LinkedListPopFront(LinkedList* list); // 删除链表尾部节点 void LinkedListPopBack(LinkedList* list); // 删除指定位置节点 void LinkedListErase(LinkedList* list, size_t pos); // 查找节点 LinkedListNode* LinkedListFind(LinkedList* list, DataType data); // 获取链表长度 size_t LinkedListSize(const LinkedList* list); // 检查链表是否为空 int LinkedListEmpty(const LinkedList* list); // 清空链表 void LinkedListClear(LinkedList* list); // 销毁链表 void LinkedListDestroy(LinkedList* list); // 打印链表 void LinkedListPrint(const LinkedList* list); #endif // __LINKEDLIST_H__
源文件实现 (LinkedList.c)
现在我们来逐一实现这些函数:
#include "LinkedList.h" // 初始化链表 void LinkedListInit(LinkedList* list) { // 使用断言确保传入的指针有效 assert(list != NULL); // 将头指针初始化为NULL,表示空链表 list->head = NULL; list->size = 0; } // 创建新节点 LinkedListNode* BuyNode(DataType data) { // 为新节点申请内存空间 LinkedListNode* newNode = (LinkedListNode*)malloc(sizeof(LinkedListNode)); // 检查内存是否申请成功 if (newNode == NULL) { printf("内存分配失败!\n"); exit(-1); // 分配失败则退出程序 } // 初始化新节点的数据域和指针域 newNode->data = data; newNode->next = NULL; return newNode; } // 在链表头部插入节点 void LinkedListPushFront(LinkedList* list, DataType data) { assert(list != NULL); // 创建新节点 LinkedListNode* newNode = BuyNode(data); // 新节点指向原来的头节点 newNode->next = list->head; // 更新头指针指向新节点 list->head = newNode; // 链表节点数增加 list->size++; } // 在链表尾部插入节点 void LinkedListPushBack(LinkedList* list, DataType data) { assert(list != NULL); // 创建新节点 LinkedListNode* newNode = BuyNode(data); // 如果链表为空,新节点就是头节点 if (list->head == NULL) { list->head = newNode; } else { // 找到最后一个节点 LinkedListNode* cur = list->head; while (cur->next != NULL) { cur = cur->next; } // 将新节点连接到链表尾部 cur->next = newNode; } // 链表节点数增加 list->size++; } // 在指定位置插入节点 void LinkedListInsert(LinkedList* list, size_t pos, DataType data) { assert(list != NULL); // 检查位置是否有效 if (pos > list->size) { printf("插入位置无效!\n"); return; } // 如果插入位置是0,相当于头插 if (pos == 0) { LinkedListPushFront(list, data); return; } // 如果插入位置是末尾,相当于尾插 if (pos == list->size) { LinkedListPushBack(list, data); return; } // 创建新节点 LinkedListNode* newNode = BuyNode(data); // 找到插入位置的前一个节点 LinkedListNode* prev = list->head; for (size_t i = 0; i < pos - 1; i++) { prev = prev->next; } // 插入新节点 newNode->next = prev->next; prev->next = newNode; // 链表节点数增加 list->size++; } // 删除链表头部节点 void LinkedListPopFront(LinkedList* list) { assert(list != NULL); // 检查链表是否为空 if (list->head == NULL) { printf("链表为空,无法删除!\n"); return; } // 保存要删除的节点 LinkedListNode* toDelete = list->head; // 更新头指针 list->head = list->head->next; // 释放节点内存 free(toDelete); // 链表节点数减少 list->size--; } // 删除链表尾部节点 void LinkedListPopBack(LinkedList* list) { assert(list != NULL); // 检查链表是否为空 if (list->head == NULL) { printf("链表为空,无法删除!\n"); return; } // 如果只有一个节点 if (list->head->next == NULL) { free(list->head); list->head = NULL; } else { // 找到倒数第二个节点 LinkedListNode* prev = list->head; while (prev->next->next != NULL) { prev = prev->next; } // 释放最后一个节点 free(prev->next); prev->next = NULL; } // 链表节点数减少 list->size--; } // 删除指定位置节点 void LinkedListErase(LinkedList* list, size_t pos) { assert(list != NULL); // 检查位置是否有效 if (pos >= list->size) { printf("删除位置无效!\n"); return; } // 如果删除位置是0,相当于头删 if (pos == 0) { LinkedListPopFront(list); return; } // 如果删除位置是末尾,相当于尾删 if (pos == list->size - 1) { LinkedListPopBack(list); return; } // 找到要删除节点的前一个节点 LinkedListNode* prev = list->head; for (size_t i = 0; i < pos - 1; i++) { prev = prev->next; } // 保存要删除的节点 LinkedListNode* toDelete = prev->next; // 绕过要删除的节点 prev->next = toDelete->next; // 释放节点内存 free(toDelete); // 链表节点数减少 list->size--; } // 查找节点 LinkedListNode* LinkedListFind(LinkedList* list, DataType data) { assert(list != NULL); // 遍历链表查找数据 LinkedListNode* cur = list->head; while (cur != NULL) { if (cur->data == data) { return cur; // 找到返回节点指针 } cur = cur->next; } return NULL; // 没找到返回NULL } // 获取链表长度 size_t LinkedListSize(const LinkedList* list) { assert(list != NULL); return list->size; } // 检查链表是否为空 int LinkedListEmpty(const LinkedList* list) { assert(list != NULL); return list->head == NULL; } // 清空链表 void LinkedListClear(LinkedList* list) { assert(list != NULL); // 逐个删除所有节点 while (list->head != NULL) { LinkedListPopFront(list); } // 确保链表状态正确 list->size = 0; } // 销毁链表 void LinkedListDestroy(LinkedList* list) { // 清空链表内容 LinkedListClear(list); // 注意:这里不需要free(list),因为链表对象可能是在栈上分配的 // 这个函数主要是为了语义上的完整性 } // 打印链表 void LinkedListPrint(const LinkedList* list) { assert(list != NULL); printf("链表内容(长度=%zu): ", list->size); LinkedListNode* cur = list->head; while (cur != NULL) { printf("%d", cur->data); if (cur->next != NULL) { printf(" -> "); } cur = cur->next; } printf("\n"); }
测试代码 (test.c)
最后,我们编写一个测试程序来验证我们的链表实现:
#include "LinkedList.h" void TestLinkedList() { // 创建链表 LinkedList list; // 初始化链表 LinkedListInit(&list); printf("初始化后: "); LinkedListPrint(&list); // 测试尾插法 printf("\n=== 测试尾插法 ===\n"); for (int i = 1; i <= 5; i++) { LinkedListPushBack(&list, i); printf("尾插 %d 后: ", i); LinkedListPrint(&list); } // 测试头插法 printf("\n=== 测试头插法 ===\n"); for (int i = 6; i <= 10; i++) { LinkedListPushFront(&list, i); printf("头插 %d 后: ", i); LinkedListPrint(&list); } // 测试指定位置插入 printf("\n=== 测试指定位置插入 ===\n"); LinkedListInsert(&list, 3, 99); printf("在位置3插入99后: "); LinkedListPrint(&list); LinkedListInsert(&list, 0, 88); printf("在位置0插入88后: "); LinkedListPrint(&list); // 测试查找 printf("\n=== 测试查找 ===\n"); LinkedListNode* found = LinkedListFind(&list, 99); if (found != NULL) { printf("找到节点99,它的下一个节点是: %d\n", found->next->data); } else { printf("未找到节点99\n"); } // 测试删除 printf("\n=== 测试删除 ===\n"); LinkedListPopFront(&list); printf("删除头节点后: "); LinkedListPrint(&list); LinkedListPopBack(&list); printf("删除尾节点后: "); LinkedListPrint(&list); LinkedListErase(&list, 3); printf("删除位置3的节点后: "); LinkedListPrint(&list); // 测试清空 printf("\n=== 测试清空 ===\n"); LinkedListClear(&list); printf("清空链表后: "); LinkedListPrint(&list); // 测试空链表操作 printf("\n=== 测试空链表操作 ===\n"); LinkedListPopFront(&list); // 应该输出错误信息但不崩溃 LinkedListPopBack(&list); // 应该输出错误信息但不崩溃 } int main() { TestLinkedList(); return 0; }
关键概念详解
1. 为什么使用typedef?
typedef int DataType;
使用typedef可以让我们轻松地更改链表存储的数据类型。如果以后想存储double类型的数据,只需要修改这一行:
typedef double DataType;
而不需要修改所有使用数据类型的地方。
2. 节点结构体的递归定义
struct LinkedListNode { DataType data; struct LinkedListNode* next; // 指向同类型结构体的指针 };
这里next
指针指向的是另一个同类型的节点,这就是递归定义。它使得节点能够链接起来形成链式结构。
3. 为什么需要BuyNode函数?
BuyNode
函数封装了节点创建的逻辑,使代码更加模块化和可维护。所有需要创建新节点的操作都可以调用这个函数,避免了代码重复。
4. 断言(assert)的作用
assert(list != NULL);
断言用于检查程序中的假设是否成立。如果条件不满足,程序会立即终止并输出错误信息。这帮助我们及早发现程序中的错误。
5. 内存管理
我们使用malloc
动态分配内存,使用free
释放内存。这是C语言中手动内存管理的典型应用。务必确保每个malloc
都有对应的free
,否则会导致内存泄漏。
常见问题解答
Q1: 什么是"无头结点"的单链表?
无头结点意味着链表的第一个节点直接存储数据,而不是一个不存储数据的辅助节点。我们的实现就是无头结点的。
Q2: 什么是"不循环"的单链表?
不循环意味着链表的最后一个节点的next
指针指向NULL
,而不是指向第一个节点形成循环。
Q3: 为什么需要记录链表大小(size)?
记录大小可以让我们在O(1)时间内获取链表长度,而不需要每次遍历整个链表来计算。
Q4: 头插法和尾插法有什么区别?
-
头插法:新节点插入到链表头部,时间复杂度为O(1)
-
尾插法:新节点插入到链表尾部,需要遍历到链表末尾,时间复杂度为O(n)
Q5: 如何选择使用头插法还是尾插法?
-
如果需要保持插入顺序,使用尾插法
-
如果不需要保持顺序或者想要逆序,使用头插法
-
如果需要高效的插入操作,使用头插法
注意:在实现单链表各个功能时,建议采用写一测一,而不是一股脑写完再统一测试,当每个函数功能实现正常时,就可以整体调用尝试了