C语言实现单链表的操作
单链表(Singly Linked List)是一种 线性数据结构,由 多个节点(Node)组成,每个节点包含:
- 数据域(data):存储实际数据(如
int、float等)。- 指针域(next):指向下一个节点的地址(最后一个节点的
next为NULL)。
1. 单链表的基本结构
(1) 定义节点结构体
#include <stdio.h>
#include <stdlib.h> // 用于 malloc 和 free// 定义单链表的节点结构
typedef struct Node {int data; // 数据域(存储数据,这里用 int 举例)struct Node* next; // 指针域(指向下一个节点)
} Node;data:存储节点的数据(可以是任意类型,如char、float、结构体等)。next:指向下一个节点的指针(类型是struct Node*)。
(2) 定义链表头指针
Node* head = NULL; // 链表头指针(初始为空)head是整个链表的入口,指向第一个节点(如果链表为空,则head = NULL)。
2. 单链表的基本操作
(1) 创建新节点
Node* createNode(int data) {Node* newNode = (Node*)malloc(sizeof(Node)); // 分配内存if (newNode == NULL) {printf("内存分配失败!\n");exit(1); // 退出程序}newNode->data = data; // 存储数据newNode->next = NULL; // 新节点默认是最后一个节点,next=NULLreturn newNode;
}- 作用:动态分配内存,创建一个新节点,并初始化
data和next。 - **
malloc**:用于动态分配内存(需<stdlib.h>)。 - **
sizeof(Node)**:计算Node结构体的大小。
(2) 插入节点(头插法)
void insertAtHead(int data) {Node* newNode = createNode(data); // 创建新节点newNode->next = head; // 新节点指向原来的头节点head = newNode; // 更新头指针,指向新节点
}- 作用:在链表头部插入新节点(新节点成为新的头节点)。
- 时间复杂度:
O(1)(直接操作头指针)。
插入 3 → 插入 2 → 插入 1
链表:1 -> 2 -> 3 -> NULL
(3) 插入节点(尾插法)
void insertAtTail(int data) {Node* newNode = createNode(data); // 创建新节点if (head == NULL) { // 如果链表为空,新节点就是头节点head = newNode;return;}Node* temp = head;while (temp->next != NULL) { // 找到最后一个节点temp = temp->next;}temp->next = newNode; // 最后一个节点的 next 指向新节点
}- 作用:在链表尾部插入新节点(新节点成为最后一个节点)。
- 时间复杂度:
O(n)(需要遍历到链表末尾)。
插入 1 → 插入 2 → 插入 3
链表:1 -> 2 -> 3 -> NULL
(4) 遍历链表(打印所有节点)
void printList() {Node* temp = head;while (temp != NULL) { // 遍历直到最后一个节点(next=NULL)printf("%d -> ", temp->data);temp = temp->next;}printf("NULL\n");
}- 作用:从头节点开始,依次打印每个节点的数据,直到
NULL。
1 -> 2 -> 3 -> NULL
(5) 删除节点(删除头节点)
void deleteAtHead() {if (head == NULL) {printf("链表为空,无法删除!\n");return;}Node* temp = head; // 保存当前头节点head = head->next; // 头指针指向下一个节点free(temp); // 释放原头节点的内存
}- 作用:删除链表的第一个节点(头节点)。
- 时间复杂度:
O(1)。
(6) 释放整个链表(防止内存泄漏)
void freeList() {Node* temp;while (head != NULL) {temp = head; // 保存当前节点head = head->next; // 头指针指向下一个节点free(temp); // 释放当前节点}printf("链表已释放!\n");
}- 作用:遍历链表,逐个释放所有节点的内存,防止内存泄漏。
- 必须手动释放(C语言没有自动垃圾回收)。
(7) 单链表指定位置插入节点
在单链表中实现 指定位置插入节点 是常见操作,核心是找到插入位置的 前驱节点,然后调整指针完成插入。
指定位置插入通常有两种场景:
- 场景 1:在第
k个节点 之前 插入新节点(例如,在第 3 个节点前插入,新节点成为第 3 个节点)。 - 场景 2:在第
k个节点 之后 插入新节点(例如,在第 3 个节点后插入,新节点成为第 4 个节点)。
为了方便判断 k 是否有效,先实现一个计算链表长度的函数getLength:
int getLength() {int len = 0;Node* temp = head;while (temp != NULL) {len++;temp = temp->next;}return len;
}(7-1) 指定位置插入函数(第 k 个节点前插入)
// 在第 k 个节点前插入新节点(k 从 1 开始计数)
// 返回值:成功返回 0,失败返回 -1(如 k 无效)
int insertAtPosition(int data, int k) {// 情况 1:链表为空,仅当 k=1 有效if (head == NULL) {if (k == 1) {insertAtHead(data); // 头插法return 0;} else {printf("插入失败:链表为空,k 必须为 1\n");return -1;}}// 情况 2:k=1(插入为新的头节点)if (k == 1) {insertAtHead(data);return 0;}// 情况 3:k > 链表长度(插入到末尾)int len = getLength();if (k > len) {printf("插入失败:k=%d 超过链表长度 %d,插入到末尾\n", k, len);insertAtTail(data); // 尾插法return 0;}// 情况 4:正常插入(找到第 k-1 个节点)Node* prev = head;for (int i = 1; i < k - 1; i++) { // 遍历到第 k-1 个节点prev = prev->next;}// 创建新节点Node* newNode = createNode(data);if (newNode == NULL) {printf("内存分配失败!\n");return -1;}// 插入新节点newNode->next = prev->next; // 新节点指向原第 k 个节点prev->next = newNode; // 第 k-1 个节点指向新节点return 0;
}代码说明
(1) 边界条件处理
- 链表为空:仅允许
k=1(插入为头节点),否则报错。 - k=1:直接调用头插法(
insertAtHead)。 - k 超过链表长度:插入到末尾(调用尾插法
insertAtTail)。
(2) 找到前驱节点
通过循环遍历 k-1 次,找到第 k-1 个节点(prev)。例如:
- 若
k=3,需要找到第2个节点(prev),新节点插入到prev之后。
(3) 插入逻辑
- 新节点的
next指向prev->next(原第k个节点)。 prev->next指向新节点,完成插入。
(4)测试代码
int main() {// 初始化链表为空Node* head = NULL;// 测试 1:插入到 k=1(链表为空)insertAtPosition(10, 1); // 链表:10 -> NULLprintList();// 测试 2:插入到 k=2(链表长度 1,k=2 超过长度,插入到末尾)insertAtPosition(20, 2); // 链表:10 -> 20 -> NULLprintList();// 测试 3:插入到 k=2(正常插入)insertAtPosition(15, 2); // 链表:10 -> 15 -> 20 -> NULLprintList();// 测试 4:插入到 k=4(链表长度 3,k=4 超过长度,插入到末尾)insertAtPosition(25, 4); // 链表:10 -> 15 -> 20 -> 25 -> NULLprintList();// 释放链表freeList();return 0;
}插入失败:链表为空,k 必须为 1 (注:实际代码中 k=1 会成功,此处为示例错误提示调整) 10 -> NULL 插入失败:k=2 超过链表长度 1,插入到末尾 10 -> 20 -> NULL 10 -> 15 -> 20 -> NULL 插入失败:k=4 超过链表长度 3,插入到末尾 10 -> 15 -> 20 -> 25 -> NULL 链表已释放!
(7-2) 指定位置插入函数(第 k 个节点后插入)
若需要在第 k 个节点 之后 插入新节点
// 在第 k 个节点后插入新节点(k 从 1 开始计数)
int insertAfterPosition(int data, int k) {// 处理链表为空的情况if (head == NULL) {if (k == 0) { // 特殊处理:k=0 表示插入到空链表insertAtHead(data);return 0;} else {printf("插入失败:链表为空,k 必须为 0\n");return -1;}}// 找到第 k 个节点Node* curr = head;for (int i = 1; i < k; i++) {curr = curr->next;if (curr == NULL) { // k 超过链表长度printf("插入失败:k=%d 超过链表长度\n", k);return -1;}}// 创建新节点并插入Node* newNode = createNode(data);newNode->next = curr->next;curr->next = newNode;return 0;
}3. 总结
| 操作 | 关键步骤 | 时间复杂度 |
|---|---|---|
| 指定位置前插入 | 找到第 k-1 个节点 → 调整指针 | O(n)(最坏情况遍历整个链表) |
| 指定位置后插入 | 找到第 k 个节点 → 调整指针 | O(n)(最坏情况遍历整个链表) |
4. 完整代码示例
#include <stdio.h>
#include <stdlib.h>// 定义单链表节点
typedef struct Node {int data;struct Node* next;
} Node;Node* head = NULL; // 链表头指针// 创建新节点
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;
}// 头插法插入节点
void insertAtHead(int data) {Node* newNode = createNode(data);newNode->next = head;head = newNode;
}// 尾插法插入节点
void insertAtTail(int data) {Node* newNode = createNode(data);if (head == NULL) {head = newNode;return;}Node* temp = head;while (temp->next != NULL) {temp = temp->next;}temp->next = newNode;
}// 打印链表
void printList() {Node* temp = head;while (temp != NULL) {printf("%d -> ", temp->data);temp = temp->next;}printf("NULL\n");
}// 删除头节点
void deleteAtHead() {if (head == NULL) {printf("链表为空,无法删除!\n");return;}Node* temp = head;head = head->next;free(temp);
}// 释放整个链表
void freeList() {Node* temp;while (head != NULL) {temp = head;head = head->next;free(temp);}printf("链表已释放!\n");
}int main() {// 测试代码insertAtHead(3); // 3 -> NULLinsertAtHead(2); // 2 -> 3 -> NULLinsertAtHead(1); // 1 -> 2 -> 3 -> NULLprintList(); // 输出:1 -> 2 -> 3 -> NULLinsertAtTail(4); // 1 -> 2 -> 3 -> 4 -> NULLprintList(); // 输出:1 -> 2 -> 3 -> 4 -> NULLdeleteAtHead(); // 删除 1,剩下 2 -> 3 -> 4 -> NULLprintList(); // 输出:2 -> 3 -> 4 -> NULLfreeList(); // 释放链表return 0;
}5. 单链表的特点
✅ 优点:
- 动态大小:不需要预先分配固定内存,可以灵活增减节点。
- 插入/删除高效(特别是头插法
O(1))。
❌ 缺点:
- 查找慢(必须从头遍历,查找某个节点需要
O(n))。 - 额外空间:每个节点需要存储
next指针,占用额外内存。
