C语言:手撕单链表
1.链表的概念
概念
链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表 中的指针链接次序实现的。
链表的结构跟⽕⻋⻋厢相似,淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只 需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。
⻋厢是独⽴存在的,且每节⻋厢都有⻋⻔。想象⼀下这样的场景,假设每节⻋厢的⻋⻔都是锁上的状 态,需要不同的钥匙才能解锁,每次只能携带⼀把钥匙的情况下如何从⻋头⾛到⻋尾? 最简单的做法:每节⻋厢⾥都放⼀把下⼀节⻋厢的钥匙。
在链表⾥,每节“⻋厢”是什么样的呢?
与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/节点” 节点的组成主要有两个部分:当前节点要保存的数据和保存下⼀个节点的地址(指针变量)。 图中指针变量plist保存的是第⼀个节点的地址,我们称plist此时“指向”第⼀个节点,如果我们希望plist“指向”第⼆个节点时,只需要修改plist保存的内容为0x0012FFA5。
为什么还需要指针变量来保存下⼀个节点的位置? 链表中每个节点都是独⽴申请的(即需要插⼊数据时才去申请⼀块节点的空间),我们需要通过指针 变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点。 结合前⾯学到的结构体知识,我们可以给出每个节点对应的结构体代码: 假设当前保存的节点为整型:
struct SListNode
{
int data; //节点数据
struct SListNode* next; //指针变量⽤保存下⼀个节点的地址
};
当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数 据,也需要保存下⼀个节点的地址(当下⼀个节点为空时保存的地址为空)。 当我们想要从第⼀个节点⾛到最后⼀个节点时,只需要在前⼀个节点拿上下⼀个节点的地址(下⼀个 节点的钥匙)就可以了。 给定的链表结构中,如何实现节点从头到尾的打印?
补充说明
1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续 2、节点⼀般是从堆上申请的3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续
2.手撕单链表
2.1说明
定义三个文件:
SingleLinkedList.h SingleLinkedList.c test.c
头文件用于包含要用到的库文件和定义链表节点的结构,及各类接口的声明
SingleLinkedList.c用于实现各类接口
test.c用于接口的测试
2.2尾插
头文件声明:
void SLLPushBack(SLLNode**pphead,SLLDataType x);
实现:
void SLLPushBack(SLLNode** pphead, SLLDataType x)
{
assert(pphead);
SLLNode* newnode = BuynewNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLLNode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
首先我们添加断言,不能传空指针过来解引用,在尾插前我们先获取一个新节点见标题2.4
这里有两种情况,当链表为空时,我们直接让头结点*pphead指向新节点,
链表不空时,先定义一个尾巴节点,先让它指向头结点,开始遍历,链表,直到ptail找到最后一个链表节点停下来,然后将尾节点的next指针指向刚刚我们获得的新节点newnode。
2.3打印链表
那么在text.c中定义一个链表结构,然后尾插四个值打印出来看看:
首先,先看代码
头文件声明:
void SLLPrint(SLLNode* phead);
实现:
void SLLPrint(SLLNode* phead)
{
SLLNode* pcur = phead;
while (pcur)
{
printf("%d->", pcur->x);
pcur = pcur->next;
}
printf("NULL\n");
}
test.c测试:
#include"SingleLinkedList.h"
void test01()
{
//定义一个单链表
SLLNode* s1=NULL;
//尾插测试
SLLPushBack(&s1, 1);
SLLPushBack(&s1, 2);
SLLPushBack(&s1, 3);
SLLPushBack(&s1, 4);
//打印链表
SLLPrint(s1);
}
int main()
{
test01();
return 0;
}
运行结果:
2.4获取一个新节点
头文件声明:
SLLNode* BuynewNode(SLLDataType x);
实现:
SLLNode* BuynewNode( SLLDataType x)
{
SLLNode* newnode = (SLLNode*)malloc(sizeof(SLLNode));
if (newnode == NULL)
{
perror("malloc fail!");
return;
}
newnode->x = x;
newnode->next = NULL;
return newnode;
}
使用malloc函数申请一个链表节点大小的空间,给newnode,如果申请空间失败,我们直接退出,申请成功,那么将数据赋值给我们的newnode->x,并将next指针指向NULL。
2.5头插
头文件声明:
void SLLPushFront(SLLNode** pphead, SLLDataType x);
实现:
void SLLPushFront(SLLNode** pphead, SLLDataType x)
{
assert(pphead);
SLLNode* newnode = BuynewNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
如同尾插那样,先添加断言,防止传过来空指针解引用崩溃,
先获取一个新节点,然后将新节点的next指针指向我们的头结点,再将头结点指向新头插过来的newnode节点即可完成头插
不论链表有无数据都可以完成头插。
//头插测试 SLLPushFront(&s1, 5); SLLPushFront(&s1, 6); SLLPushFront(&s1, 7); SLLPushFront(&s1, 8);
我们运行一下:
2.6尾删
头文件声明:
void SLLPopBack(SLLNode** pphead);
实现:
void SLLPopBack(SLLNode** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLLNode* prev = *pphead;
SLLNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
先添加断言,不能传空指针过来,其次,链表必须至少有一个节点,没节点不能进行尾删
两种情况:
第一种,当链表只存在一个节点,直接进行头结点释放空间,将头结点*pphead指向NULL;
第二种,当链表存在多个节点,我们先定义一个前驱指针prev,和找尾指针ptail,都先指向*pphead头指针,然后进行循环找链表的尾巴,再每次指向新尾巴的前一步,我们让前驱指针prev先指向ptail,这样循环完毕,我们可以得到倒数第一个节点(即尾节点)和倒数第二个节点,然后将倒数第二个节点的next指针指向NULL,将ptail空间释放,ptail指向NULL,完成尾删
看下列图示:
//定义一个单链表 SLLNode* s1=NULL; //尾插测试 SLLPushBack(&s1, 1); SLLPushBack(&s1, 2); SLLPushBack(&s1, 3); SLLPushBack(&s1, 4); //头插测试 SLLPushFront(&s1, 5); SLLPushFront(&s1, 6); SLLPushFront(&s1, 7); SLLPushFront(&s1, 8); //尾删测试 SLLPopBack(&s1); SLLPopBack(&s1); SLLPopBack(&s1); SLLPopBack(&s1);
运行一下:
2.7头删
头文件声明:
void SLLPopFront(SLLNode** pphead);
实现:
void SLLPopFront(SLLNode** pphead)
{
assert(pphead && *pphead);
SLLNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
先添加断言,不能传空指针过来,其次,链表必须至少有一个节点,没节点不能进行头删
先将头结点的下一个节点用保存起来,这里我们定义一个next指针指向*pphead的下一个节点
然后释放头结点,将头结点*pphead指向next,让next成为新的头节点,从而完成头删。
//定义一个单链表 SLLNode* s1=NULL; //尾插测试 SLLPushBack(&s1, 1); SLLPushBack(&s1, 2); SLLPushBack(&s1, 3); SLLPushBack(&s1, 4); //头插测试 SLLPushFront(&s1, 5); SLLPushFront(&s1, 6); SLLPushFront(&s1, 7); SLLPushFront(&s1, 8); //头删测试 SLLPopFront(&s1); SLLPopFront(&s1); SLLPopFront(&s1); SLLPopFront(&s1);
运行一下:
2.8查找
头文件声明:
SLLNode* SLLFind(SLLNode* phead, SLLDataType x);
实现:
SLLNode* SLLFind(SLLNode* phead, SLLDataType x)
{
assert(phead);
SLLNode* pcur = phead;
while (pcur)
{
if (pcur->x == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
链表不能为空,由于我们只是找值,不改值,所以,传一级指针,如果要找的数据x链表中存在就返回该数据所在节点,否则就返回NULL
//查找测试 SLLNode*find=SLLFind(s1, 3); if (find != NULL) { printf("找到了!\n"); } else { printf("找不到!\n"); }
运行一下:
2.9在指定位置之前插入数据
头文件声明:
void SLLInsert(SLLNode** pphead, SLLNode* pos, SLLDataType x);
void SLLInsert(SLLNode** pphead, SLLNode* pos, SLLDataType x)
{
assert(pphead&&*pphead);
assert(pos);
SLLNode* newnode = BuynewNode(x);
if (*pphead==pos)
{
SLLPushFront(pphead, x);
}
else
{
SLLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
先添加断言,不能传空指针过来解引用,链表不能为空,否则指定位置就没有意义,其次,指定位置pos不能为空
先获取一个新节点,这里有两种情况,
第一种:当头结点与pos位置相同时,那么pos位置之前插入数据,就是头插,我们直接调用刚刚写的头插接口即可
第二种:当链表有多个节点时,我们先找到pos位置的前一个节点,那么这里定义一个前驱指针prev先指向头节点*pphead,然后开始遍历直到在pos位置的前一个节点停下来,然后让新节点的next指针指向pos,再将prev的next指针指向新节点,完成插入,图示如下:
第一种:
第二种:
//查找测试 SLLNode*find=SLLFind(s1, 3); //在指定位置之前插入数据测试 SLLInsert(&s1, find, 55);
运行一下:
2.10在指定位置之后插入数据
头文件声明:
void SLLInsertAfter( SLLNode* pos, SLLDataType x);
实现:
void SLLInsertAfter(SLLNode* pos, SLLDataType x)
{
assert(pos);
SLLNode* newnode = BuynewNode(x);
SLLNode* next = pos->next;
newnode->next = next;
pos->next = newnode;
}
由于是在pos位置后面插入数据,所以只需要一个pos位置的节点指针就可以,不需要传头节点
断言pos指针不可为空,先获取一个新节点,然后先找到pos的下一位置,我们定义一个指针next存放pos的下一位置,让newnode新节点的next指针指向next,然后吧pos的next指针指向newnode新节点,图示如下:
//查找测试 SLLNode*find=SLLFind(s1, 3); //在指定位置之后插入数据 SLLInsertAfter(find, 68 );
运行一下:
2.11删除指定位置之后的数据
头文件声明:
void SLLEraseAfter(SLLNode* pos);
实现:
void SLLEraseAfter(SLLNode* pos)
{
assert(pos&&pos->next);
SLLNode* del = pos->next;
pos->next=del->next;
free(del);
del = NULL;
}
断言pos位置,不可为空指针,且pos后面得有数据才能进行删除
先将pos下一位置存起来,这里我们定义一个del指针指向pos->next
然后将pos的next指针指向del的下一节点即指向del->next
然后释放del(pos后面的节点)
将del置为NULL
图示如下:
//查找测试 SLLNode*find=SLLFind(s1, 3); //删除指定位置之后的数据 SLLEraseAfter(find);
运行一下:
2.12删除指定位置的数据
头文件声明:
void SLLErase(SLLNode**pphead,SLLNode* pos);
实现:
void SLLErase(SLLNode** pphead, SLLNode* pos)
{
assert(pphead&&*pphead);
assert(pos);
if (*pphead == pos)
{
SLLPopFront(pphead);
}
else
{
SLLNode* del = pos;
SLLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = del->next;
free(pos);
pos = NULL;
}
}
断言不可传入空指针,链表不可为空,pos指针也不能为空
这里有两种情况:
第一种:当头结点和pos位置相同时:我们直接调用先前写的头删的接口
第二种:当链表有多个节点时,定义一个del指针指向pos位置
定义一个前驱指针prev先指向头结点,然后开始遍历prev指针,直到找到pos的前一个位置停下来,此时将prev的next指针指向del的下一节点,即指向del->nex,然后释放pos指向的空间,pos置为NULL;
图示如下:
第一种:
第二种:
//查找测试 SLLNode*find=SLLFind(s1, 3); //删除指定位置的数据 SLLErase(&s1,find);
运行一下:
2.13销毁链表
头文件声明:
void SLLDestroy(SLLNode**pphead);
实现:
void SLLDestroy(SLLNode** pphead)
{
assert(pphead && *pphead);
while (*pphead)
{
SLLNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
}
链表的销毁:需要我们一个一个节点的释放,当所有节点都释放完毕,从而达到链表的销毁的目的,一开始我们先断言,不可传入空指针进行解引用,且链表非空的情况下
开始遍历头结点,在循环体内定义一个next指针,每次都保存头节点的下一节点,这样在释放完头结点,让头结找到链表的下一节点,直到next和*pphead都走到NULL为止
图示如下:
销毁前:
//定义一个单链表 SLLNode* s1=NULL; //尾插测试 SLLPushBack(&s1, 1); SLLPushBack(&s1, 2); SLLPushBack(&s1, 3); SLLPushBack(&s1, 4); //打印链表 SLLPrint(s1); //销毁链表 SLLDestroy(&s1); 打印链表 SLLPrint(s1);
销毁后:
2.14完整源码
SingleLinkedList.h文件:
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SLLDataType;
typedef struct _SingleLinkedListNode
{
SLLDataType x;
struct _SingleLinkedListNode* next;
}SLLNode;
//获取一个新节点
SLLNode* BuynewNode(SLLDataType x);
//尾插
void SLLPushBack(SLLNode**pphead,SLLDataType x);
//头插
void SLLPushFront(SLLNode** pphead, SLLDataType x);
//尾删
void SLLPopBack(SLLNode** pphead);
//头删
void SLLPopFront(SLLNode** pphead);
//查找
SLLNode* SLLFind(SLLNode* phead, SLLDataType x);
//在指定位置之前插入数据
void SLLInsert(SLLNode** pphead, SLLNode* pos, SLLDataType x);
//在指定位置之后插入数据
void SLLInsertAfter( SLLNode* pos, SLLDataType x);
//删除指定位置之后的数据
void SLLEraseAfter(SLLNode* pos);
//删除指定位置的数据
void SLLErase(SLLNode**pphead,SLLNode* pos);
//打印链表
void SLLPrint(SLLNode* phead);
//销毁链表
void SLLDestroy(SLLNode**pphead);
SingleLinkedList.c文件:
#include"SingleLinkedList.h"
//打印链表
void SLLPrint(SLLNode* phead)
{
SLLNode* pcur = phead;
while (pcur)
{
printf("%d->", pcur->x);
pcur = pcur->next;
}
printf("NULL\n");
}
//获取一个新节点
SLLNode* BuynewNode( SLLDataType x)
{
SLLNode* newnode = (SLLNode*)malloc(sizeof(SLLNode));
if (newnode == NULL)
{
perror("malloc fail!");
return;
}
newnode->x = x;
newnode->next = NULL;
return newnode;
}
//尾插
void SLLPushBack(SLLNode** pphead, SLLDataType x)
{
assert(pphead);
SLLNode* newnode = BuynewNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLLNode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
//头插
void SLLPushFront(SLLNode** pphead, SLLDataType x)
{
assert(pphead);
SLLNode* newnode = BuynewNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
//尾删
void SLLPopBack(SLLNode** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLLNode* prev = *pphead;
SLLNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
//头删
void SLLPopFront(SLLNode** pphead)
{
assert(pphead && *pphead);
SLLNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
//查找
SLLNode* SLLFind(SLLNode* phead, SLLDataType x)
{
assert(phead);
SLLNode* pcur = phead;
while (pcur)
{
if (pcur->x == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//在指定位置之前插入数据
void SLLInsert(SLLNode** pphead, SLLNode* pos, SLLDataType x)
{
assert(pphead&&*pphead);
assert(pos);
SLLNode* newnode = BuynewNode(x);
if (*pphead==pos)
{
SLLPushFront(pphead, x);
}
else
{
SLLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;
prev->next = newnode;
}
}
//在指定位置之后插入数据
void SLLInsertAfter(SLLNode* pos, SLLDataType x)
{
assert(pos);
SLLNode* newnode = BuynewNode(x);
SLLNode* next = pos->next;
newnode->next = next;
pos->next = newnode;
}
//删除指定位置之后的数据
void SLLEraseAfter(SLLNode* pos)
{
assert(pos&&pos->next);
SLLNode* del = pos->next;
pos->next=del->next;
free(del);
del = NULL;
}
//删除指定位置的数据
void SLLErase(SLLNode** pphead, SLLNode* pos)
{
assert(pphead&&*pphead);
assert(pos);
if (*pphead == pos)
{
SLLPopFront(pphead);
}
else
{
SLLNode* del = pos;
SLLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = del->next;
free(pos);
pos = NULL;
}
}
//销毁链表
void SLLDestroy(SLLNode** pphead)
{
assert(pphead && *pphead);
while (*pphead)
{
SLLNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
}
test.c文件:
#include"SingleLinkedList.h"
void test01()
{
//定义一个单链表
SLLNode* s1=NULL;
//尾插测试
SLLPushBack(&s1, 1);
SLLPushBack(&s1, 2);
SLLPushBack(&s1, 3);
SLLPushBack(&s1, 4);
//头插测试
/*SLLPushFront(&s1, 5);
SLLPushFront(&s1, 6);
SLLPushFront(&s1, 7);
SLLPushFront(&s1, 8);*/
//尾删测试
/*SLLPopBack(&s1);
SLLPopBack(&s1);
SLLPopBack(&s1);
SLLPopBack(&s1);*/
//头删测试
/*SLLPopFront(&s1);
SLLPopFront(&s1);
SLLPopFront(&s1);
SLLPopFront(&s1);*/
//查找测试
// SLLNode*find=SLLFind(s1, 3);
/*if (find != NULL)
{
printf("找到了!\n");
}
else
{
printf("找不到!\n");
}*/
//在指定位置之前插入数据测试
// SLLInsert(&s1, find, 55);
在指定位置之后插入数据
//SLLInsertAfter(find, 68 );
//删除指定位置之后的数据
//SLLEraseAfter(find);
//删除指定位置的数据
//SLLErase(&s1,find);
//打印链表
SLLPrint(s1);
销毁链表
//SLLDestroy(&s1);
打印链表
//SLLPrint(s1);
}
int main()
{
test01();
return 0;
}
3.链表的分类
链表的结构⾮常多样,以下情况组合起来就有8种(2x2x2)链表结构:
链表说明
虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构:单链表和双向带头循环链表
1.⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦结构,如哈希桶、图的邻接表等等。
2.带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带 来很多优势,实现反⽽简单了