C语言——链表的基础操作
引言
链表是一种常见的重要的数据结构。它是动态地进行存储分配的一种结构。链表有一个 头指针 变量,它存放一个地址,该地址指向一个元素,链表中每一个元素称为 结点,每个结点都应包括两个部分,一为用户需要用的实际数据,二为下一个结点的地址。可以看出,头指针 head 指向第一个元素,第一个元素又指向第二个元素…………直到最后一个元素,该元素不再指向其他元素,它称为 表尾,它的地址部分放一个 NULL(表示 空地址)链表到此结束。
基础操作代码结构
- 链表节点结构体定义
- 节点创建函数
- 插入操作(头插法、尾插法、指定位置插入)
- 删除操作(按值删除)
- 查找操作(按值查位置、按位置查值)
- 其他操作(替换元素、链表倒置、打印、清空)
- 主函数(测试与交互)
1. 链表节点结构体定义
struct Node {int data;struct Node *next;
};
- 这是链表的基本组成单位 —— 节点的结构体定义
- 每个节点包含两个成员:
data
:存储节点的数据next
:是指向 struct Node 类型的指针,用于指向链表中的下一个节点
图示逻辑
一个节点就像一节 "火车车厢",data
是车厢里的货物,next
是连接下一节车厢的挂钩。
+----------+
| data | // 比如存一个整数 10
+----------+
| next | // 指向 NULL(暂时没有下一节车厢)
+----------+
|
v
NULL
2. 节点创建函数
// 创建一个节点函数
Node *createNode(int data) {Node *newNode = (Node *)malloc(sizeof(Node));if (newNode == NULL) {return NULL;}newNode->data = data;newNode->next = NULL;return newNode;
}
- 功能:创建一个新的链表节点,并初始化其数据
- 步骤:
- 使用
malloc
函数分配一块大小为Node
结构体的内存空间 - 检查内存分配是否成功(
newNode == NULL
表示分配失败) - 给新节点的
data
成员赋值 - 将新节点的
next
指针初始化为NULL
- 返回新创建的节点指针
- 使用
图示逻辑
eg.创建一个存储数据10
的节点:
申请内存前:
newNode -> NULL(空指针)申请内存并初始化后:
newNode -> +----------+
| data=5 |
+----------+
| next=NULL |
+----------+
3. 插入操作
3.1 头插法
// 头插法
Node *addFirst(int data, Node *head) {struct Node *newNode = createNode(data);if (newNode == NULL) {printf("创建失败,内存分配失败\n");return head;}newNode->next = head;return newNode;
}
- 功能:在链表的头部插入一个新节点
- 步骤:
- 调用
createNode
函数创建新节点 - 检查节点创建是否成功
- 将新节点的
next
指针指向原来的头节点(head
) - 返回新节点作为链表新的头节点
- 调用
图示逻辑:
原链表:head -> A -> B -> NULL
头插新节点C后:
newNode(C) -> next = head(A)
新链表:newNode(C) -> A -> B -> NULL
步骤1:创建新节点
newNode -> +----------+
| data=0 |
+----------+
| next=? | // 暂时不知道指向哪里
+----------+步骤2:新节点指向原头节点
newNode -> +----------+
| data=0 |
+----------+
| next=head
+----------+
|
v
head -> +----------+ +----------+ +----------+
| data=1 | | data=2 | | data=3 |
+----------+ +----------+ +----------+
| next----+--->| next----+--->| next=NULL
+----------+ +----------+ +----------+步骤3:新节点成为新的 head
newNode 变成新 head -> +----------+
| data=0 |
+----------+
| next----+--->+----------+
+----------+ | data=1 |
+----------+
| next----+--->...
+----------+
运行结果:
3.2 尾插法
// 尾插法
Node *add(int data, Node *head) {Node *newNode = createNode(data);if (newNode == NULL) {printf("创建失败,内存分配失败\n");return head;}if (head == NULL) {return newNode;}Node *temp = head;while (temp->next != NULL) {temp = temp->next;}temp->next = newNode;return head;
}
- 功能:在链表的尾部插入一个新节点
- 步骤:
- 创建新节点并检查是否成功
- 特殊情况处理:如果链表为空(
head == NULL
),直接返回新节点作为头节点 - 遍历链表找到最后一个节点:
- 使用临时指针
temp
从头部开始移动 - 循环条件
temp->next != NULL
表示还没到最后一个节点
- 使用临时指针
- 将最后一个节点的
next
指针指向新节点 - 返回原头节点(尾插不改变头节点)
图示逻辑:
原链表:head -> A -> B -> NULL
尾插新节点C后:
找到最后一个节点B,令B->next = newNode(C)
新链表:head -> A -> B -> C -> NULL
步骤1:创建新节点
newNode -> +----------+
| data=4 |
+----------+
| next=NULL
+----------+步骤2:找到最后一个节点(3的节点)
temp 从 head 开始移动:
temp -> 1 -> 2 -> 3(此时 temp->next 是 NULL,停止移动)步骤3:最后一个节点指向新节点
+----------+ +----------+
| data=3 | | data=4 |
+----------+ +----------+
| next----+--->| next=NULL
+----------+ +----------+
运行结果:
3.3 指定位置插入
// 插入元素
Node *insert(int index, int data, Node *head) {Node *newNode = createNode(data);if (newNode == NULL) {printf("创建失败,内存分配失败\n");return head;}if (head == NULL) {return newNode;}int count = 0;Node *temp = head;while (temp->next != NULL) {if (count == index) {newNode->next = temp->next;temp->next = newNode;break;}temp = temp->next;count++;}if (count == index) {temp->next = newNode;} else {printf("输入的位置不存在\n");}return head;
}
- 功能:在链表的指定位置插入新节点
- 步骤:
- 创建新节点并检查是否成功
- 特殊情况处理:如果链表为空,直接返回新节点
- 遍历链表寻找插入位置:
- 使用
count
计数器记录当前位置 - 当
count
等于目标index
时,执行插入操作
- 使用
- 插入操作核心:
- 新节点的
next
指向当前节点的next
(newNode->next = temp->next
) - 当前节点的
next
指向新节点(temp->next = newNode
)
- 新节点的
- 处理插入位置在链表尾部的情况
- 如果位置不存在,打印提示信息
图示逻辑:
现在在索引1
处插入数据10
:
步骤1:创建新节点(10)
newNode -> +-----------+
| data=10 |
+-----------+
| next=? |
+-----------+步骤2:找到插入位置的前一个节点(索引0的节点,即1的节点)
temp 指向 1 的节点(因为 index-1=0)步骤3:插入新节点
先让 newNode 指向 temp 的下一个节点(2的节点):
newNode->next = temp->next
+-----------+ +----------+
| data=10 | | data=2 |
+-----------+ +----------+
| next----+--->| next----+--->3
+-----------+ +----------+再让 temp 指向 newNode:
temp->next = newNode
+----------+ +-----------+ +----------+
| data=1 | | data=10 | | data=2 |
+----------+ +-----------+ +----------+
| next----+--->| next----+--->| next----+--->3
+----------+ +-----------+ +----------+
运行结果:
4. 删除操作
// 删除元素(按值删除)
struct Node *deleteByE(int data, struct Node *head) {if (head == NULL) {printf("链表为空,无法删除\n");return head;}// 处理头节点是目标节点的情况if (head->data == data) {struct Node *temp = head;head = head->next;free(temp);printf("删除成功\n");return head;}struct Node *prev = head;struct Node *curr = head->next;while (curr != NULL) {if (curr->data == data) {prev->next = curr->next;free(curr);printf("删除成功\n");return head;}prev = curr;curr = curr->next;}printf("元素不存在,无法删除\n");return head;
}
- 功能:删除链表中第一个值为
data
的节点 - 步骤:
- 特殊情况处理:如果链表为空,直接返回
- 处理头节点就是目标节点的情况:
- 用临时指针保存头节点
- 更新头节点为原头节点的下一个节点
- 释放原头节点的内存
- 处理其他位置的节点:
- 使用两个指针
prev
(前一个节点)和curr
(当前节点)遍历 - 找到目标节点后,将前一个节点的
next
指向当前节点的下一个节点 - 释放当前节点的内存
- 使用两个指针
- 如果遍历完链表都没找到目标节点,打印提示信息
图示逻辑:
原链表:A -> B -> C -> NULL
删除B节点:
prev(A)->next = curr(B)->next(C)
释放B节点内存
新链表:A -> C -> NULL
现在删除值为10的节点
步骤1:定义 prev 和 curr
prev 指向 1 的节点,curr 指向 10 的节点步骤2:找到要删除的节点(curr 就是 10 的节点)
+----------+ +-----------+ +----------+
| data=1 | | data=10 | | data=2 |
+----------+ +-----------+ +----------+
| next----+--->| next----+--->| next----+--->3
+----------+ +-----------+ +----------+
prev curr步骤3:删除节点
prev->next = curr->next(1的节点直接指向2的节点)
+----------+ +----------+
| data=1 | | data=2 |
+----------+ +----------+
| next----+---------------------+ next----+--->3
+----------+ +----------+释放 curr 指向的节点(10的节点被释放)
运行结果:
5. 查找操作
5.1 按值查找位置
// 根据元素查找位置
int getByE(int v, Node *head) {if (head == NULL) {printf("链表为空,不存在任何数据\n");return -1;}int index = 0;int f = -1;Node *temp = head;while (temp->next != NULL) {if (temp->data == v) {f = 1;break;}temp = temp->next;index++;}if (f == 1) {return index;} else {return f;}return index;
}
- 功能:查找指定值在链表中的位置
- 步骤:
- 特殊情况处理:链表为空时返回 - 1
- 初始化索引
index
为 0,标志位f
为 - 1(表示未找到) - 遍历链表:
- 若找到目标值,将标志位
f
设为 1 并跳出循环 - 否则移动到下一个节点,索引加 1
- 若找到目标值,将标志位
- 再根据标志位返回索引或 - 1(未找到)
图示逻辑:
链表:1 -> 2 -> 3 -> 4 -> NULL
查找值3
的位置:
index=0,temp指向1:不匹配
index=1,temp指向2:不匹配
index=2,temp指向3:匹配,返回2
运行结果:
5.2 按位置查找值
// 根据位置查找元素
int getByIndex(int index, struct Node *head) {if (head == NULL) {printf("链表为空\n");return -1;}if (index < 0) {printf("位置不能为负数\n");return -1;}struct Node *temp = head;int currIndex = 0;while (temp != NULL) {if (currIndex == index) {return temp->data;}temp = temp->next;currIndex++;}printf("位置超出链表长度\n");return -1;
}
- 功能:查找链表中指定索引位置的元素值
- 步骤:
- 特殊情况处理:
- 链表为空时返回 - 1
- 索引为负数时返回 - 1(位置无效)
- 遍历链表:
- 使用
currIndex
记录当前位置 - 当
currIndex
等于目标index
时,返回当前节点的数据
- 使用
- 如果遍历完链表都没找到对应位置,打印提示信息并返回 - 1
- 特殊情况处理:
图示逻辑:
链表:1 -> 2 -> 3 -> 4 -> NULL
查找索引2
的值:
currIndex=0,temp指向1:不匹配
currIndex=1,temp指向2:不匹配
currIndex=2,temp指向3:匹配,返回3
运行结果:
6.替换操作
// 根据元素替换元素
void replaceByE(int oldVal, int newVal, struct Node *head) {if (head == NULL) {printf("链表为空,无法替换\n");return;}struct Node *temp = head;while (temp != NULL) {if (temp->data == oldVal) {temp->data = newVal;printf("替换成功\n");return;}temp = temp->next;}printf("元素不存在,无法替换\n");
}
- 功能:将链表中第一个值为
oldVal
的节点替换为newVal
- 步骤:
- 特殊情况处理:链表为空时直接返回
- 遍历链表查找值为
oldVal
的节点 - 找到后更新该节点的
data
为newVal
并返回 - 未找到则打印提示信息
运行结果:
7.链表倒置
// 倒置链表
Node *reverseList(struct Node *head) {if (head == NULL || head->next == NULL) {return head; // 空链表或单节点直接返回}struct Node *prev = NULL;struct Node *curr = head;struct Node *next = NULL;while (curr != NULL) {next = curr->next; // 保存下一个节点curr->next = prev; // 反转当前节点指针prev = curr; // 前指针后移curr = next; // 当前指针后移}printf("链表倒置完成\n");return prev; // 新的头节点是原尾节点
}
- 功能:将链表反转
- 步骤:
- 特殊情况处理:空链表或只有一个节点时直接返回
- 使用三个指针遍历并反转链表:
prev
:指向当前节点的前一个节点curr
:指向当前节点next
:保存当前节点的下一个节点(防止反转后丢失)
- 核心操作:
curr->next = prev
(反转指针方向) - 循环结束后,
prev
指向原链表的最后一个节点,成为新的头节点
图示逻辑:
原链表:A -> B -> C -> NULL
反转过程:
1. A->next = NULL, prev=A, curr=B
2. B->next = A, prev=B, curr=C
3. C->next = B, prev=C, curr=NULL
结果:C -> B -> A -> NULL
初始状态:
prev=NULL,curr=1,next=?第1轮循环:
next = curr->next(next=2)
curr->next = prev(1->next=NULL)
prev = curr(prev=1)
curr = next(curr=2)
此时:1 -> NULL,prev=1,curr=2第2轮循环:
next = curr->next(next=3)
curr->next = prev(2->next=1)
prev = curr(prev=2)
curr = next(curr=3)
此时:2 -> 1 -> NULL,prev=2,curr=3第3轮循环:
next = curr->next(next=NULL)
curr->next = prev(3->next=2)
prev = curr(prev=3)
curr = next(curr=NULL)
此时:3 -> 2 -> 1 -> NULL,prev=3循环结束,返回 prev(3 成为新的头节点)
运行结果:
8.打印链表
// 打印链表
void printLinkedList(Node *head) {printf("链表:[");if (head == NULL) {printf("]\n");return;}Node *temp = head;while (temp->next != NULL) {printf("%d->", temp->data);temp = temp->next;}printf("%d]\n", temp->data);
}
- 功能:打印链表中的所有元素
- 步骤:
- 先打印固定格式 "链表:["
- 特殊情况处理:链表为空时直接打印 "]" 并换行
- 遍历链表:
- 对于非最后一个节点,打印 "数据 ->"
- 对于最后一个节点,直接打印数据和 "]"
9. 清空链表
//清空链表
void freeList(Node *head) {Node *curr = head;while (curr != NULL) {Node *temp = curr;curr = curr->next;free(temp);}
}
- 功能:释放链表中所有节点的内存,防止内存泄漏
- 步骤:
- 使用
curr
指针遍历链表 - 每次循环中:
- 用
temp
保存当前节点 - 移动
curr
到下一个节点 - 释放
temp
指向的节点内存
- 用
- 使用
图示逻辑:
链表:1 -> 2 -> 3 -> NULL
步骤1:curr指向1,temp=1,释放1,curr=2
步骤2:curr指向2,temp=2,释放2,curr=3
步骤3:curr指向3,temp=3,释放3,curr=NULL
最终所有节点都被释放,链表为空
运行结果:
10. 主函数与测试
int main() {int cid = 0;// 头节点Node *head = NULL;// 初始链表head = add(1, head);head = add(2, head);head = add(3, head);head = add(4, head);head = add(5, head);printLinkedList(head);while (1) {printf("链表的基本操作\n");printf("== 101:添加元素(尾插)\n");printf("== 102:添加元素(头插)\n");printf("== 201:删除元素\n");printf("== 301:根据位置插入元素\n");printf("== 401:查找元素存在的位置\n");printf("== 402:根据位置查找元素\n");printf("== 501:根据元素替换元素\n");printf("== 601:倒置元素\n");printf("== 701:清空链表\n");printf("== 999:退出程序\n");printf("== 请输入功能编号:\n");scanf("%d", &cid);if (cid == 101) {printf("[101:添加元素(尾插)]>> 请输入一个数字:\n");int data = 0;scanf("%d", &data);head = add(data, head);} else if (cid == 102) {printf("[102:添加元素(头插)]>> 请输入一个数字:\n");int data = 0;scanf("%d", &data);head = addFirst(data, head);} else if (cid == 201) {printf("[201:删除元素]>> 请输入要删除的数字:\n");int data = 0;scanf("%d", &data);head = deleteByE(data, head);} else if (cid == 301) {printf("[301:根据位置插入元素]>> 请输入一个数字 和 一个位置下标: \n");int data = 0;int index;scanf("%d%d", &data, &index);head = insert(index, data, head);} else if (cid == 401) {printf("[401:查找元素存在的位置]>> 请输入一个数字: \n");int data = 0;scanf("%d", &data);int index = getByE(data, head);if (index == -1) {printf("数据不存在\n");} else {printf("%d在链表中的位置:%d\n", data, index);}} else if (cid == 402) {printf("[402:根据位置查找元素]>> 请输入位置下标: \n");int index;scanf("%d", &index);int val = getByIndex(index, head);if (val != -1) {printf("位置%d的元素是:%d\n", index, val);}} else if (cid == 501) {printf("[501:根据元素替换元素]>> 请输入旧数字和新数字: \n");int oldVal, newVal;scanf("%d%d", &oldVal, &newVal);replaceByE(oldVal, newVal, head);} else if (cid == 601) {printf("[601:倒置元素]\n");head = reverseList(head);} else if (cid == 701) {printf("[701:清空链表]\n");freeList(head);head = NULL;printf("[清空成功 内存释放完成] \n");} else if (cid == 999) {printf("退出程序,内存释放\n");freeList(head);exit(0);} else {printf("无效的功能编号\n");}printLinkedList(head);}
}
- 功能:提供一个交互式界面,让用户可以测试各种链表操作
- 初始设置:创建了一个包含 1,2,3,4,5 的初始链表,便于测试
- 主循环:
- 打印功能菜单
- 读取用户输入的功能编号
- 根据编号执行相应的链表操作
- 操作完成后打印当前链表状态
- 输入 999 时退出程序并释放内存
运行结果:
总结:链表操作核心逻辑
操作 | 核心思路 |
---|---|
头插法 | 新节点.next = 原头节点,新节点成为新头 |
尾插法 | 找到最后一个节点,最后节点.next = 新节点 |
插入(中间) | 新节点先连后,再让前节点连新节点(new->next=temp->next; temp->next=new ) |
删除节点 | 前节点跳过待删节点(prev->next = curr->next ),释放待删节点内存 |
链表倒置 | 用 3 个指针反转每个节点的指向(curr->next = prev) |
关键知识点总结
链表的基本概念:
- 链表由节点组成,每个节点包含数据和指向下一个节点的指针
- 链表不需要连续的内存空间,节点之间通过指针连接
- 头节点是链表的入口,通过头节点可以访问整个链表
指针的重要性:
- 链表的各种操作都依赖指针来实现节点之间的连接
- 理解指针的指向变化是掌握链表操作的关键
- 操作链表时要注意避免 "悬空指针" 和 "内存泄漏"
链表操作的核心思想:
- 插入操作:先连接后一个节点,再连接前一个节点
- 删除操作:找到待删除节点的前一个节点,修改其指针绕过待删除节点
- 遍历操作:使用临时指针从头部开始,通过
next
指针依次访问每个节点
内存管理:
- 使用
malloc
分配节点内存,使用free
释放节点内存 - 任何时候删除节点都要记得释放其内存
- 程序结束前要清空链表,避免内存泄漏
- 使用
特殊情况处理:
- 空链表(头节点为 NULL)
- 单节点链表
- 操作头节点的情况
- 操作尾节点的情况