文章目录
-
-
- [🔗 什么是链表?](#🔗 什么是链表?)
-
- [1. 结点的构造](#1. 结点的构造)
- [2. 链表的"八大金刚"](#2. 链表的“八大金刚”)
- [🛠️ 单链表接口实现](#🛠️ 单链表接口实现)
- [💡 核心难点:传址调用与二级指针的深刻剖析](#💡 核心难点:传址调用与二级指针的深刻剖析)
- [📝 完整代码汇总](#📝 完整代码汇总)
-
🔗 什么是链表?
为了解决顺序表的痛点,链表应运而生。
链表是一种物理存储结构上非连续、非顺序的存储结构。数据元素的逻辑顺序,是通过链表中的指针链接次序来实现的。
形象比喻:火车
顺序表像是一整块固定的车厢,想加一节很难;而链表就像一列火车,每节车厢(结点)都是独立的。旺季时挂上几节,淡季时摘掉几节,互不影响。
1. 结点的构造
链表的每个结点包含两部分:
- 数据域:保存当前的数据(如整型、字符等)。
- 指针域:保存下一个结点的地址。
c
// 单链表结点结构
struct SListNode {
int data; // 数据
struct SListNode* next; // 指向下一个结点的指针
};
2. 链表的"八大金刚"
链表的结构非常多样,通过以下三个维度的组合,可以形成8种不同的链表结构:
- 单向 / 双向
- 带头 / 不带头(带头结点通常称为"哨兵位",不存有效数据,只为了操作方便)
- 循环 / 不循环
在实际应用和面试中,最常考察的是两种:
- 无头单向非循环链表:结构简单,常作为哈希桶、图的邻接表的子结构。
- 带头双向循环链表:结构最复杂,但实现起来逻辑反而更统一,常用于单独存储数据(如Linux内核链表)。
因为内容太多,这篇博客只讲解单链表,接下来让我们一起实现单链表的增删查改接口。
🛠️ 单链表接口实现
现在先来实现单链表。与顺序表一样,先来定义并且将列表与其中的数据重命名:
c
#include<stdlib.h>
typedef int SListDate;
typedef struct SLN
{
SListDate date;
struct SLN* next;
}SListNode;
与顺序表不同,链表是一个节点接着一个节点,因此并不需要扩容和初始化函数,与之相对的是申请新节点的函数:
申请新节点
- 参数类型 :
int x(需要存入节点的数据)。 - 返回值 :
SListNode*(新申请节点的指针)。 - 函数内实现逻辑 :通过
malloc在堆区申请一块与节点大小相同的内存;若申请失败则报错并返回NULL;若成功,则将传入的数据x赋值给数据域date,并将指针域next置为NULL,最后返回该新节点的指针。
c
SListNode* NewNode(int x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
perror("malloc fail:");
return NULL;
}
newnode->date = x;
newnode->next = NULL;
return newnode;
}
💡 核心难点:传址调用与二级指针的深刻剖析
在进行后续的增删改查之前,我们必须深刻剖析一个核心问题:为什么有的函数需要初始节点的二级指针作为参数,而有的不需要?
这涉及到 C 语言中传值调用 与传址调用 的本质:
当我们把一级指针(例如 SListNode* phead)作为参数传递给函数时,本质上是传递了一个"内容为地址的数值"。在函数内部,我们确实可以通过这个地址去修改它所指向的内存空间(比如修改节点里的 date 或 next)。但是,我们无法通过这个一级指针去修改"一级指针本身"的指向。 也就是说,如果在函数内部执行 phead = newnode;,这仅仅修改了函数内部的局部变量 phead,外部的真实头指针依然指向原来的位置。
因此,只要一个操作可能会改变"头结点指针本身的指向",我们就必须传递该一级指针的地址,即二级指针(SListNode** pphead)。 通过解引用 *pphead = newnode;,我们才能真正修改外部的头指针。反之,如果操作只是在链表中间修改某个节点的 next 指针,不涉及头指针的改变,那么传递一级指针就足够了。
尾插
- 参数类型 :
SListNode** pphead(链表起始节点的二级指针),SListDate x(插入的数据)。 - 返回值 :
void。 - 函数内实现逻辑 :首先断言
pphead不为空。因为链表可能为空,若为空,则需要通过*pphead = NewNode(x);直接修改外部头指针(这就是为什么尾插必须传二级指针的原因 )。若不为空,为了避免遍历后丢失链表起点,我们设置一个临时指针phead等于*pphead,通过while循环遍历直到phead->next == NULL,最后将新节点链接到尾部。
c
void SLpushback(SListNode** pphead,SListDate x)
{
assert(pphead );
SListNode* phead = *pphead;
if (phead==NULL)
{
phead= NewNode(x);
*pphead = phead; // 补充:将新节点赋给外部头指针
}
else
{
while (phead->next != NULL)
{
phead = phead->next;
}
phead->next = NewNode(x);
}
}
打印链表
- 参数类型 :
SListNode** pphead(链表初始位置的二级指针)。 - 返回值 :
void。 - 函数内实现逻辑 :断言
*pphead不为空。定义一个临时指针phead接收*pphead,通过while循环遍历链表,依次打印每个节点的数据,直到指针为NULL,最后打印NULL\n。
c
void SLprint(SListNode** pphead)
{
assert(*pphead);
SListNode* phead = *pphead;
while (phead)
{
printf("%d->", phead->date);
phead = phead->next;
}
printf("NULL\n");
}
头插
- 参数类型 :
SListNode** pphead(链表起始节点的二级指针),SListDate x(插入的数据)。 - 返回值 :
void。 - 函数内实现逻辑 :断言
pphead不为空。创建一个新的节点p1,将其next指向原来的第一个节点(即*pphead),然后通过*pphead = p1;修改外部头指针的指向 (因为头指针发生了改变,所以必须传二级指针)。
c
void SLpushfront(SListNode** pphead, SListDate x)
{
assert(pphead);
SListNode *p1 = NewNode(x);
p1->next = *pphead;
*pphead = p1;
}
尾删
- 参数类型 :
SListNode** pphead(链表起始节点的二级指针)。 - 返回值 :
void。 - 函数内实现逻辑 :断言
pphead和*pphead不为空。如果链表只有一个节点,释放该节点后必须将外部头指针置为NULL(涉及头指针修改,需二级指针 )。如果有多个节点,使用双指针遍历,找到倒数第二个节点,将其next指针置为NULL,并free掉最后一个节点。
c
void SLpopback(SListNode**pphead)
{
assert(pphead && *pphead);
SListNode* p1 = NULL;
SListNode* p2 = *pphead;
if (p2->next == NULL)
{
free(p2);
*pphead = NULL; // 补充:修改外部头指针
}
else
{
while (p2->next)
{
p1 = p2;
p2 = p2->next;
}
p1->next = NULL;
free(p2);
p2 = NULL;
}
}
头删
- 参数类型 :
SListNode** pphead(链表起始节点的二级指针)。 - 返回值 :
void。 - 函数内实现逻辑 :断言不为空。将外部头指针
*pphead指向第二个节点,然后free掉原来的第一个节点(因为头指针发生了改变,必须传二级指针)。
c
void SLpopfront(SListNode** pphead)
{
assert(pphead && *pphead);
SListNode* phead = *pphead;
*pphead = phead->next;
free(phead);
phead = NULL;
}
查找指定数据
- 参数类型 :
SListNode** pphead(链表起始节点的二级指针),SListDate x(要查找的数据)。 - 返回值 :
SListNode*(返回找到节点的地址,未找到返回NULL)。 - 函数内实现逻辑 :断言不为空。通过
while循环遍历链表,若当前节点的date等于x,直接返回该节点指针;若遍历结束未找到,返回NULL。
c
SListNode* SLfind(SListNode** pphead, SListDate x)
{
assert(pphead && *pphead);
SListNode* p1 = *pphead;
while (p1)
{
if (p1->date == x)
return p1;
p1 = p1->next;
}
return NULL;
}
删除指定节点
- 参数类型 :
SListNode** pphead(二级指针),SListNode* passign(要删除的节点地址)。 - 返回值 :
void。 - 函数内实现逻辑 :断言参数不为空。首先判断指定节点是否是第一个节点,如果是,直接调用头删(因为需要改变头指针,所以传二级指针 )。如果不是,则遍历找到该节点前面的一个节点,将其
next指针指向指定节点后的节点,最后free掉指定节点。
c
void SLassign(SListNode** pphead, SListNode* passign)
{
assert(pphead && *pphead&&passign);
SListNode* p1 = *pphead;
if (p1==passign)
{
SLpopfront(pphead);
return;
}
while (p1->next != passign)
{
p1 = p1->next;
}
p1->next = passign->next;
free(passign);
passign = NULL;
}
删除指定节点后面的数据
- 参数类型 :
SListNode* passign(指定节点的一级指针)。 - 返回值 :
void。 - 函数内实现逻辑 :断言
passign及其next不为空。因为只需修改指定节点的next指针,不涉及头指针的改变,因此只需要一级指针 。记录指定节点后面的节点,断开链接后将其free掉。此操作时间复杂度为 O(1)。
c
void SLassignback(SListNode*passign)
{
assert(passign&&passign->next);
SListNode* p1 = passign->next;
passign->next=NULL;
free(p1);
p1 = NULL;
}
删除指定位置前面的数据
- 参数类型 :
SListNode** pphead(二级指针),SListNode* passign(指定节点的一级指针)。 - 返回值 :
void。 - 函数内实现逻辑 :断言参数合法且
passign不是头节点。如果passign的前一个节点就是头节点,直接调用头删(需二级指针 )。否则,遍历找到passign前面第二个节点,修改其next指针跳过前一个节点,最后free掉被删除的节点。
c
void SLassignfront(SListNode** pphead, SListNode* passign)
{
assert(pphead && *pphead && passign&&(passign!=*pphead));
SListNode* p1 = *pphead;
SListNode* del=p1->next;
if (p1->next == passign)
{
SLpopfront(pphead);
return;
}
while (del->next != passign)
{
p1 = p1->next;
del= p1->next;
}
p1->next = passign->next;
free(del);
del = NULL;
}
在指定数据前面添加数据
- 参数类型 :
SListNode** pphead(二级指针),SListNode* passign(指定节点地址),SListDate x(新数据)。 - 返回值 :
void。 - 函数内实现逻辑 :断言参数不为空。因为单链表无法链接到前面的节点,若指定节点是第一个节点,直接调用头插(需二级指针)。如果不是,则遍历找到指定节点前面的节点,将新节点两端连接到链表中。
c
void SLassignfrontadd(SListNode** pphead, SListNode* passign,SListDate x)
{
assert(pphead && *pphead && passign);
SListNode* p1 = *pphead;
if (p1 == passign)
{
SLpushfront(pphead,x);
return;
}
while (p1->next != passign)
{
p1 = p1->next;
}
p1->next = NewNode(x);
p1->next->next=passign;
}
在指定数据后面添加数据
- 参数类型 :
SListNode* passign(指定节点的一级指针 ),SListDate x(新数据)。 - 返回值 :
void。 - 函数内实现逻辑 :断言
passign不为空。因为不再需要更改指定位置前面节点的指针,不涉及头指针的改变,只需一级指针 。若指定节点是最后一个节点,直接将新节点连接到其后;若不是,则将新节点插入到passign与其原后继节点之间。
c
void SLassignbackadd(SListNode* passign, SListDate x)
{
assert(passign);
SListNode* p1= passign->next;
if (passign->next==NULL)
{
passign->next = NewNode(x);
return;
}
passign->next = NewNode(x);
passign->next->next = p1;
}
📝 完整代码汇总
SListNode.h:
c
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdlib.h>
#include<assert.h>
#include<stdio.h>
typedef int SListDate;
typedef struct SLN
{
SListDate date;
struct SLN* next;
}SListNode;
SListNode* NewNode(int x);
void SLpushback(SListNode** pphead,SListDate x);
void SLprint(SListNode** pphead);
void SLpushfront(SListNode** pphead, SListDate x);
void SLpopback(SListNode** pphead);
void SLpopfront(SListNode** pphead);
SListNode* SLfind(SListNode** pphead, SListDate x);
void SLassign(SListNode** pphead, SListNode* passign);
void SLassignback(SListNode* passign);
void SLassignfront(SListNode** pphead, SListNode* passign);
void SLassignfrontadd(SListNode** pphead, SListNode* passign, SListDate x);
void SLassignbackadd(SListNode* passign, SListDate x);
SListNode.c:
c
#include "SListNode.h"
SListNode* NewNode(int x)
{
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (newnode == NULL)
{
perror("malloc fail:");
return NULL;
}
newnode->date = x;
newnode->next = NULL;
return newnode;
}
void SLpushback(SListNode** pphead,SListDate x)
{
assert(pphead );
SListNode* phead = *pphead;
if (phead==NULL)
{
phead= NewNode(x);
*pphead = phead;
}
else
{
while (phead->next != NULL)
{
phead = phead->next;
}
phead->next = NewNode(x);
}
}
void SLprint(SListNode** pphead)
{
assert(*pphead);
SListNode* phead = *pphead;
while (phead)
{
printf("%d->", phead->date);
phead = phead->next;
}
printf("NULL\n");
}
void SLpushfront(SListNode** pphead, SListDate x)
{
assert(pphead);
SListNode *p1 = NewNode(x);
p1->next = *pphead;
*pphead = p1;
}
void SLpopback(SListNode**pphead)
{
assert(pphead && *pphead);
SListNode* p1 = NULL;
SListNode* p2 = *pphead;
if (p2->next == NULL)
{
free(p2);
*pphead = NULL;
}
else
{
while (p2->next)
{
p1 = p2;
p2 = p2->next;
}
p1->next = NULL;
free(p2);
p2 = NULL;
}
}
void SLpopfront(SListNode** pphead)
{
assert(pphead && *pphead);
SListNode* phead = *pphead;
*pphead = phead->next;
free(phead);
phead = NULL;
}
SListNode* SLfind(SListNode** pphead, SListDate x)
{
assert(pphead && *pphead);
SListNode* p1 = *pphead;
while (p1)
{
if (p1->date == x)
return p1;
p1 = p1->next;
}
return NULL;
}
void SLassign(SListNode** pphead, SListNode* passign)
{
assert(pphead && *pphead&&passign);
SListNode* p1 = *pphead;
if (p1==passign)
{
SLpopfront(pphead);
return;
}
while (p1->next != passign)
{
p1 = p1->next;
}
p1->next = passign->next;
free(passign);
passign = NULL;
}
void SLassignback(SListNode*passign)
{
assert(passign&&passign->next);
SListNode* p1 = passign->next;
passign->next=NULL;
free(p1);
p1 = NULL;
}
void SLassignfront(SListNode** pphead, SListNode* passign)
{
assert(pphead && *pphead && passign&&(passign!=*pphead));
SListNode* p1 = *pphead;
SListNode* del=p1->next;
if (p1->next == passign)
{
SLpopfront(pphead);
return;
}
while (del->next != passign)
{
p1 = p1->next;
del= p1->next;
}
p1->next = passign->next;
free(del);
del = NULL;
}
void SLassignfrontadd(SListNode** pphead, SListNode* passign,SListDate x)
{
assert(pphead && *pphead && passign);
SListNode* p1 = *pphead;
if (p1 == passign)
{
SLpushfront(pphead,x);
return;
}
while (p1->next != passign)
{
p1 = p1->next;
}
p1->next = NewNode(x);
p1->next->next=passign;
}
void SLassignbackadd(SListNode* passign, SListDate x)
{
assert(passign);
SListNode* p1= passign->next;
if (passign->next==NULL)
{
passign->next = NewNode(x);
return;
}
passign->next = NewNode(x);
passign->next->next = p1;
}
test.c:
c
#include "SListNode.h"
int main()
{
SListNode* phead = NewNode(1);
SListNode* p2 = NewNode(2);
SListNode* p3 = NewNode(3);
SListNode* p4 = NewNode(4);
SListNode* p5 = NewNode(5);
phead->next = p2;
p2->next = p3;
p3->next = p4;
p4->next = p5;
p5->next = NULL;
//SLprint(&phead);
//SLpushback(&phead, 6);
//SLprint(&phead);
SLpushfront(&phead,6);
SLprint(&phead);
SLpopback(&phead);
SLprint(&phead);
SLpopfront(&phead);
SLprint(&phead);
SListDate x; printf("请输入指定的数据"); scanf("%d",&x);
//SLassign(&phead, SLfind(&phead, x));
//SLassignback(SLfind(&phead, x));
//SLassignfront(&phead, SLfind(&phead, x));
SListDate y; printf("请输入新节点的数据:"); scanf("%d", &y);
//SLassignfrontadd(&phead, SLfind(&phead, x), y);
SLassignbackadd(SLfind(&phead, x), y);
SLprint(&phead);
}
🌟 写在最后
代码写完了,但我想对正在阅读这篇博客的你说几句心里话。
最近身边有很多同学在迷茫、害怕,大体原因是只能靠自己一个人在这个世界打拼,无依无靠;而世界又在不断变化进步,不知道自己的出路在哪里。
我把送给他们的话,也送给正在屏幕前努力的你:
保持乐观,悲观者正确,乐观者前行。
相信时间的力量,就不必问何时才有回报。
拥有坚持的毅力,就不必在意暂时的苟且。
保持每天的进步,就不必对未来感到迷茫。
愿你在代码的世界里披荆斩棘,也在人生的道路上步履不停。