一、 什么是单链表?
单链表(Singly Linked List)是一种线性数据结构,它的特点是每个节点通过指针链接到下一个节点。不同于顺序表(数组),链表的每个元素(节点)并不存储在连续的内存空间中,而是由一个节点指向下一个节点,以形成链式结构。
你可以把单链表想象成一列火车,每节车厢都装载着数据(元素),而每节车厢的尾部都连着下一节车厢,直至最后一节车厢指向空(NULL
),表示链表的结束。
二、 单链表的基本结构
在C语言中,单链表由节点构成。每个节点包含两部分:
- 数据域:存储当前节点的数据信息。
- 指针域:存储指向下一个节点的地址。
单链表的节点可以通过结构体来定义,如下所示:
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data; // 数据域
struct Node* next; // 指针域,指向下一个节点
};
int data
:存储节点中的数据,可以是任何数据类型。struct Node* next
:指向下一个节点的指针,若当前节点是链表中的最后一个节点,则next
的值为NULL
。
三、 单链表的基本操作
1. 创建节点
单链表的节点是通过动态内存分配创建的,在C语言中可以使用malloc
来分配内存。以下是创建单链表节点的代码:
struct Node* createNode(int data) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node)); // 动态分配内存
newNode->data = data; // 设置节点的数据
newNode->next = NULL; // 初始状态下指向NULL
return newNode;
}
代码解释:
struct Node* createNode(int data)
:定义了一个函数,用于创建一个新节点。malloc(sizeof(struct Node))
:动态分配内存,大小为struct Node
的大小。newNode->data = data
:为新节点赋值。newNode->next = NULL
:新节点的next
初始化为NULL
,表示它暂时是链表的最后一个节点。
2. 插入节点
在进入插入节点这一内容前我们有一个非常非常非常重要的知识点需要理解:
为什么传参需要二级指针?为什么有的函数只要一级指针?
我们一起来仔细品味:
在处理单链表的某些操作时,比如插入或删除节点,尤其是修改链表头节点的操作中,我们通常需要使用二级指针。这是因为C语言中的参数传递是值传递,当我们在函数中传递一个指针时,实际上传递的是该指针的一个拷贝。如果我们想在函数内部修改链表的头节点(即head
指针指向的地址),我们需要传递指向指针本身的指针,也就是二级指针。
什么时候用一级指针?
对于不涉及修改链表头节点的操作,比如遍历链表或只修改链表中的某个节点的值时,只需要传递链表的头节点(即一级指针)即可,因为我们并不打算修改指针本身,只是通过指针访问链表的内容。
void printList(struct Node* head) {
struct Node* temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
这里我们只需要传递链表的头节点head
,通过它来遍历链表。由于我们不会修改链表的结构(比如不更改头节点),因此不需要使用二级指针。
什么时候用二级指针?
当我们需要修改链表的结构,特别是修改链表的头节点时,传递二级指针是必须的。例如,插入新节点到链表头部,或者删除链表的头节点。如果我们只传递一个一级指针,函数内部对头节点的修改不会影响到外部的链表结构。
void insertAtHead(struct Node** head, int data) {
struct Node* newNode = createNode(data); // 创建新节点
newNode->next = *head; // 新节点的next指向当前的头节点
*head = newNode; // 头节点更新为新节点
}
在这里,head
是二级指针,即struct Node** head
。*head
指向链表的头节点,通过修改*head
,我们就能改变链表的头节点,使其指向新插入的节点。这样在函数外部,链表的头节点也会正确更新。
如果我们只传递一级指针,即struct Node* head
,在函数内部修改head
的值并不会影响外部的链表结构,因为函数接收到的是头节点指针的拷贝,修改的只是拷贝的内容,而不是原始指针。
理解了上面的问题我们就能继续单链表的插入操作了:
单链表中有三种常见的插入操作:
- 头部插入:在链表的最前面插入节点。
- 尾部插入:在链表的最后面插入节点。
- 中间插入:在指定位置插入节点。
我们先来看看头插:
头部插入节点
void insertAtHead(struct Node** head, int data) {
struct Node* newNode = createNode(data); // 创建新节点
newNode->next = *head; // 新节点的next指向当前的头节点
*head = newNode; // 头节点更新为新节点
}
void insertAtHead(struct Node** head, int data)
:定义了一个函数,接受链表头节点的指针和要插入的数据。newNode->next = *head
:将新节点的next
指向当前的头节点。*head = newNode
:将链表的头节点更新为新节点。
尾部插入节点
void insertAtTail(struct Node** head, int data) {
struct Node* newNode = createNode(data); // 创建新节点
if (*head == NULL) {
*head = newNode; // 如果链表为空,新节点为头节点
} else {
struct Node* temp = *head;
while (temp->next != NULL) {
temp = temp->next; // 找到链表的最后一个节点
}
temp->next = newNode; // 最后一个节点的next指向新节点
}
}
- 如果链表为空,直接将新节点作为头节点。
- 否则,遍历链表直到找到最后一个节点,将新节点插入到尾部。
中间插入节点
void insertAtPosition(struct Node** head, int data, int pos) {
struct Node* newNode = createNode(data);
if (pos == 1) {
newNode->next = *head; // 如果位置是1,则插入头部
*head = newNode;
} else {
struct Node* temp = *head;
for (int i = 1; i < pos - 1 && temp != NULL; i++) {
temp = temp->next; // 找到指定位置的前一个节点
}
if (temp != NULL) {
newNode->next = temp->next; // 插入新节点
temp->next = newNode;
}
}
}
- 如果插入位置是1,直接头部插入。
- 否则,遍历链表找到指定位置,将新节点插入到链表中。
3. 删除节点
单链表中删除节点时,需要找到要删除节点的前一个节点,将它的next
指向被删除节点的下一个节点。
void deleteNode(struct Node** head, int key) {
struct Node* temp = *head;
struct Node* prev = NULL;
// 如果要删除的是头节点
if (temp != NULL && temp->data == key) {
*head = temp->next;
free(temp);
return;
}
// 查找要删除的节点
while (temp != NULL && temp->data != key) {
prev = temp;
temp = temp->next;
}
// 节点不存在
if (temp == NULL) return;
prev->next = temp->next; // 前一个节点指向要删除节点的下一个节点
free(temp); // 释放要删除的节点
}
- 首先检查要删除的是否是头节点。
- 如果不是,遍历链表找到要删除的节点,并调整指针,使链表重新连接。
- 释放被删除节点的内存。
4. 查找节点
查找某个节点时,只需要遍历链表,找到与目标值匹配的节点即可。
int search(struct Node* head, int key) {
struct Node* current = head;
while (current != NULL) {
if (current->data == key) {
return 1; // 找到节点
}
current = current->next;
}
return 0; // 未找到
}
- 遍历链表,逐个比较节点的数据域,直到找到匹配的节点。
- 如果找到,返回1;如果未找到,返回0。
5. 遍历链表
遍历链表时,从头节点开始,逐个访问每个节点,直到链表结束(即next
为NULL
)。
void printList(struct Node* head) {
struct Node* temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
- 从头节点开始,依次输出每个节点的数据,直到到达链表末尾(
next
为NULL
)。
4. 单链表的优缺点与应用场景
恭喜你,看到这,有关单链表的基本操作和细节我们就学完啦~
同时我们也要知道单链表与顺序表(数组)相比有一些显著的优缺点和应用场景:
优点:
- 动态大小:链表不需要预先分配内存,可以根据需要动态增加节点,避免了顺序表大小固定的限制。
- 插入和删除效率高:在链表中插入和删除节点的时间复杂度为O(1),只需调整指针,而不需要像数组那样移动元素。
缺点:
- 随机访问效率低:链表不支持随机访问,查找某个节点的时间复杂度为O(n),需要从头遍历到目标位置。
- 额外内存消耗:每个节点都需要额外存储一个指针(
next
),相较于数组,链表的内存开销更大。
单链表非常适合那些需要频繁插入和删除操作的场景,比如:
- 实现动态队列和栈:链表可以轻松实现动态的队列和栈,适合那些数据量不固定的场景。
- 内存有限的嵌入式系统:由于链表的动态内存分配特性,适合在一些内存有限的嵌入式系统中使用。
说在最后
单链表是学习指针和动态内存管理的重要基础。通过理解单链表的结构和操作,我相信你能够掌握如何高效管理内存,并为后续学习双向链表、循环链表等更复杂的数据结构打下坚实的基础。
在学习单链表的过程中,建议你多动手编写代码,自己尝试实现插入、删除、查找等操作,深入理解指针的使用和内存管理技巧。
最后如果觉得有收获的话记得点赞加收藏哦~~