1. 什么是链表?(线性表的链式存储结构)
链表是由节点(Node) 组成的线性结构,每个节点包含两部分:
- 数据域(data):存储实际业务数据(如整数、字符串、结构体等);
- 指针域(next):存储下一个节点的内存地址,相当于“链条”,将分散的节点串联成一个整体。 与数组相比,链表的存储单元可以是不连续的——这意味着它无需提前申请大块连续内存,数据新增时动态分配内存即可,极大提升了内存利用率。

2. 定义链表结点结构
- 创建一个数据域
data来存储数据 - 创建一个指向目标类型的指针
next来存储后继结点的地址,实现连接操作
typedef struct Node{
int data;
struct Node *next;
}Node;
3. 初始化链表,创建头结点
头结点是链表的“哨兵节点”,不存储实际业务数据,仅用于简化操作。没有头结点的链表(无头链表)会面临以下问题:
- 插入第一个数据节点时,需要修改头指针(空表时头指针为NULL);
- 删除第一个数据节点时,同样需要修改头指针(设计二级指针的问题),逻辑判断复杂。 而带头结点的链表,无论链表是否为空,头指针始终指向头结点——这让“插入/删除第一个节点”与“插入/删除其他节点”的逻辑完全统一,无需额外处理边界条件。
Node * Init_List(){
//分配头结点内存
Node * head = (Node*)malloc(sizeof(Node));
if (head == NULL){
printf("分配失败!\n");
return NULL;
}
head->next = NULL;//指向的后继在链表为空时为NULL
return head;
}
定义了一个如图所示的head头结点

4. 插入操作
因为进行的都是地址层面的操作(链表的本质还是通过指针指向各个结点的地址实现连接操作),所以函数参数自然也是指针类型
头插法(在链表头部插入新节点)
void Head_Insert(Node *head,int data)
{
Node * newNode = (Node*)malloc(sizepf(Node));
if (newNode == NULL)
{
printf("插入失败,内存分配错误!\n");
return ;
}
newNode->data = data;
//接下来为了防止head后继所有节点的丢失,需要先让newNode的后继节点指向head的后继节点
newNode->next = head->next;
//此时只需要让head的后继节点指向newNode自身的地址便实现了头插法插入结点
head->next = newNode;
}


尾插法(在链表尾部插入新结点)
实现思路与头插法类似,但是由于需要找到整个链表的尾部,需要先经历一次遍历寻找尾部
可以新定义一个指针p赋以head中的地址,用while循环来寻找尾部,当不满足条件时,指针p后移,直到找到尾部
为啥不能直接用
head = head->next呢?其实在这并无所谓,出于函数值传递的特性,虽然不影响外部head,但函数内部也无法保留遍历结果(副本会销毁),且逻辑上这是对参数的误用(参数的作用是传入链表入口,而非作为遍历的临时指针)。
void Tail_Insert(Node *head,int data)
{
Node * newNode = (Node*)malloc(sizepf(Node));
if (newNode == NULL)
{
printf("插入失败,内存分配错误!\n");
return ;
}
newNode->data = data;
Node *p = head;
while (head->next != NULL)
{
p = p->next;
}
p->next = newNode;
newNode->next = NULL;
}


指定位置插入
与尾插法的逻辑其实是类似的,只不过要在循环遍历中找到‘要插入的位置’
这里以在70 80两个链表间插入新结点为例

void Insert_ByPos(Node *head,int data,int pos)//pos为实际存储数据的结点位置
{
if (pos < 1) {
printf("插入失败:位置不合法(pos >= 1)\n");
return;
}
// 找到第pos-1个节点(插入位置的前驱)
Node* p = head;
int i = 0;
while (p != NULL && i < pos - 1) {
p = p->next;
i++;
}
// 若p为NULL,说明pos超过链表长度
if (p == NULL) {
printf("插入失败:位置超过链表长度\n");
return;
}
// 创建新节点并插入
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode == NULL) {
printf("插入失败:内存分配错误\n");
return;
}
newNode->data = data;
newNode->next = p->next; // 新节点指向前驱的后继
p->next = newNode; // 前驱指向新节点
}


5. 删除结点
按位置删除
其实原理与插入也差不多,只不过多了一步释放结点的步骤,那就要有一个临时指针指q向此结点,调用free函数
- 找到要删除节点的前置节点 p
- 用指针 q 记录要删除的节点
- 通过改变 p 的后继节点实现删除
- 释放删除节点的空间
void Delete_ByPos(Node *head,int pos)
{
if (pos < 1)
{
printf("删除失败,位置不合法");
return ;
}
Node *p = head;
int i = 0;
while (p != NULL && i < pos - 1)
{
p = p->next;
i++;
}
Node *q = p->next;
p->next = q->next;
free(q);
q = NULL;
}
为啥要有这个
temp呢?其实还是为了保存后继节点不让其丢失
步骤类似于:


按值删除
思路同上
void Delete_ByValue(Node* head, int value) {
// 查找值为value的节点的前驱
Node* p = head;
while (p->next != NULL && p->next->data != value) {
p = p->next;
}
// 若p->next为NULL,说明未找到该值
if (p->next == NULL) {
printf("删除失败:值 %d 不存在\n", value);
return;
}
// 删除节点
Node* temp = p->next;
p->next = temp->next;
free(temp);
temp = NULL;
}
适用场景:仅知道数据内容,不知道位置(如删除“成绩为60的学生节点”)。
6. 查找结点
其实接下来的几个操作都是基本思路都是一样的,都离不开遍历链表—寻找目标的步骤,接下来直接给出相关代码
按位置查找(返回该位置的值,-1表示位置无效)
int Find_ByPos(Node *head,int pos)
{
Node *p = head->next;
int i = 1;
while (p != NULL && i < pos)
{
p = p->next;
i++;
}
if (p == NULL)
{
printf("位置不存在!");
return -1;
}
return p->data;
}
按值查找(返回第一个匹配节点的位置,-1表示不存在)
int Find_ByValue(Node * head, int value){
Node * p = head->next;
int pos = 1;
while(p != NULL){
if(p->data == value){
return pos;
}
p = p->next;
}
return -1;
}
7. 修改结点数据
按位置修改
void Update_ByPos(Node *head, int pos, int newData){
if(pos < 1){
printf("位置错误");
return ;
}
Node *p = head->next;
int i = 1;
while(p != NULL && i < pos){
p = p->next;
i++;
}
if(p == NULL){
printf("位置不存在");
return ;
}
p->data = newData;
}
按值修改(修改第一个匹配的节点为新值)
void Update_ByValue(Node * head, int oldValue, int newValue){
Node * p = head->next;
while (p != NULL && p->data != oldValue){
p = p->next;
}
if(p == NULL){
printf("没有此值");
return;
}
p->data = newValue;
}
8. 遍历链表(打印所有节点数据)
void Traverse_List(Node* head){
Node *p = head->next;
if(p == NULL){
printf("链表为空");
return;
}
while (p != NULL){
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
9. 销毁链表(释放所有节点内存,包括头结点)
链表的节点是动态分配的( malloc ),程序结束前必须手动释放所有节点,否则会导致内存泄漏。销毁逻辑是“从表头开始,逐个释放节点”。
void Free_List(Node *head){
if(head == NULL){
return;
}
Node *p = head;
Node *temp;
while(p != NULL){
temp = p;
p = p->next;
free(temp);
}
printf("链表节点已全部释放");
}
最后别忘了在main函数主体内调用head = NULL;`来释放头结点避免野指针的问题!

1万+

被折叠的 条评论
为什么被折叠?



