在上一关,我们通过了顺序表的实现,但是,顺序表还有一些缺陷:
1. 空间不够,需要扩容,扩容(尤其是异地扩容)是有一定代价的,其次还可能存在一定的空间浪费。
2.头部和中部插入删除,需要挪动数据,效率低下
优化方案:
1.按需申请空间
2.不要挪动数据
1.链表的概念及结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
结构:
注意:
1从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
2.现实中的结点一般都是从堆上申请出来的
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
2.链表的分类
1.单项或者双向
2.带头或者不带头
3.循环或者非循环
可以看出这三种分类多可以结合处8个不同类型的单链表
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
下面我们主要对无头单向非循环链表进行实现:
3.链表的实现
3.1结点的定义
typedef int SLTDataType;
typedef struct SlistNode
{
SLTDataType data;
struct SlistNode* next;
}SLTNode;
3.2函数的声明
// 动态申请一个结点
SLTNode* BuySLTNode(SLTDataType x);//返回类型是:SLTnode* 结构体地址
//单链表打印
void SListPrint(SLTNode* phead);
//尾插,尾删
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
//头插,头删
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopFront(SLTNode** pphead);
//查找一个数,返回其地址
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
//在单链表pos之后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//在pos之前插入,因为在前面插入会改变phead的指针,所以需要传双指针
void SLTInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//删除pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos后的节点
void SLTEraseAfter(SLTNode* pos);
//单链表的释放:
void SLTDestroy(SLTNode** pphead);
3.3函数的实现和原理解释:
1)动态申请一个结点
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
malloc申请一个动态结点,如果malloc调用失败,返回NULL则直接结束掉程序
成功,赋值,并返回
2)打印单链表
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
传函数头结点的地址,并用cur进行接收,以防止phead的值被改变,从而导致该链表找不到头
3)尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
这里传递双指针的原因是:如果要改变头结点的数据,需要2次传参,就需要用双指针
改变之前地址就要用地址的地址去进行
(你要改变int,传int的地址int*,你要改变int*,就要传递int*的地址int**)
(下面的函数传参中,需要改变头结点的数据时,都需要用双指针)
用newnode接受一个新的结点,然后判断结点是否为空,空则直接赋值,不为空则找处尾结点的地址再进行链接
4)尾删
void SLTPopBack(SLTNode** pphead)
{
assert(*pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
如果链表为空,则不能再进行删除,用assert断言判断一下
要删掉尾节点,就要找到尾结点的前一个结点,将前一个结点的->next=NULL
所以再找的时候找的时tail->next->next,然后将tail->next=NULL
这样写的话,如果该单链表只有一个数,tail->next->next不存在,则代码就会出现错位
所以要加上一个判断,用(*pphead)->next == NULL来判断该单链表时否只有一个结点。
错误写法:
正确写法如实现中那样,也可以这样写:
先定义一个prev,tail再最后一个,prev再tail的前一个,如果tail为空,直接让prev的next为空即可
5)头插和头删
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SLTPopFront(SLTNode** pphead)
{
assert(*pphead);
SLTNode* Next = (*pphead)->next;
free(*pphead);
*pphead = Next;
}
头插和头删是单链表主要的优势,又快又简单
6)查找一个数,返回其地址
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
用cur等于phead防止phead改变,找到返回cur,未找到,返回NULL
7)在单链表pos之后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
先用newnode的next指向pos的next
再让pos的next指向newnode
8)在pos之前插入,因为在前面插入会改变phead的指针,所以需要传双指针
void SLTInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pos);
if (pos == *pphead)//如果在头节点前插入就相当于头插
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySLTNode(x);
newnode->next = prev->next;
prev->next = newnode;
}
}
在之前插入,我们的插入可能在1之前,也就是要改变头节点,相当于头插,就需要二级指针
是头插,直接调用头插函数就行
先找到pos的前一个,我们定义一个prev,prev->next=pos时,即可在中间插入newnode
9)删除pos后的节点
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
if (pos->next == NULL)//只有一个头节点
{
return;
}
else
{
SLTNode* nextNode = pos->next;
pos->next = nextNode->next;
free(nextNode);
}
}
和插入一样,只有一个头节点要单独处理
10)删除pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pos);
assert(pphead);
if (pos == *pphead)
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
先用prev来表示前一个,当prev->next=pos时,进行删除
11)单链表的释放:
void SLTDestroy(SLTNode** pphead)
{
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* nextnode = cur->next;
free(cur);
cur = nextnode;
}
*pphead = NULL;
}
单链表不像顺序表,需要一次一次进行释放
如果先释放了1,就找不到后面的结点了
所以先要定义一个next或者nextnode来接受cur的下一个结点
然后再free,最后将nextnode赋值给cur
最后将*pphead置为NULL
4.单链表的OJ题
1.删除链表中等于给定值 val 的所有结点。OJ题链接
2.反转一个单链表。OJ题链接
3.给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。OJ题链接
4.. 输入一个链表,输出该链表中倒数第k个结点。OJ题链接
5.将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的。OJ题链接
6.编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。OJ题链接
7.链表的回文结构。OJ题链接
8.输入两个链表,找出它们的第一个公共结点。OJ题链接
9.给定一个链表,判断链表中是否有环。OJ题链接
10.给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 NULL。OJ题链接
11.给定一个链表,每个结点包含一个额外增加的随机指针,该指针可以指向链表中的任何结点或空结点。OJ题链接
OJ题的代码和讲解在下一篇文章中发布
5.源代码如下(VS2022下编译):
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//无头+单向+非循环链表增删查改实现
//声明
typedef int SLTDataType;
typedef struct SlistNode
{
SLTDataType data;
struct SlistNode* next;
}SLTNode;
// 动态申请一个结点
SLTNode* BuySLTNode(SLTDataType x);//返回类型是:SLTnode* 结构体地址
//单链表打印
void SListPrint(SLTNode* phead);
//尾插,尾删
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
//头插,头删
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopFront(SLTNode** pphead);
//查找一个数,返回其地址
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
//在单链表pos之后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//在pos之前插入,因为在前面插入会改变phead的指针,所以需要传双指针
void SLTInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//删除pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos后的节点
void SLTEraseAfter(SLTNode* pos);
//单链表的释放:
void SLTDestroy(SLTNode** pphead);
//实现
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SLTPopBack(SLTNode** pphead)
{
assert(*pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
void SLTPopFront(SLTNode** pphead)
{
assert(*pphead);
SLTNode* Next = (*pphead)->next;
free(*pphead);
*pphead = Next;
}
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
void SLTInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pos);
if (pos == *pphead)//如果在头节点前插入就相当于头插
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySLTNode(x);
newnode->next = prev->next;
prev->next = newnode;
}
}
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pos);
assert(pphead);
if (pos == *pphead)
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
if (pos->next == NULL)//只有一个头节点
{
return;
}
else
{
SLTNode* nextNode = pos->next;
pos->next = nextNode->next;
free(nextNode);
}
}
void SLTDestroy(SLTNode** pphead)
{
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* nextnode = cur->next;
free(cur);
cur = nextnode;
}
*pphead = NULL;
}
SLTNode* CreateSlist(int n)
{
SLTNode* phead = NULL, * ptail = NULL;
for (int i = 0;i < n;i++)
{
SLTNode* newnode = BuySLTNode(i);
if (phead == NULL)
{
ptail = phead = newnode;
}
else
{
ptail->next = newnode;
ptail = newnode;
}
}
return phead;
}
//测试
void TestSList1()
{
SLTNode* plist = NULL;
printf("尾插:\n");
SLTPushBack(&plist, 100);
SLTPushBack(&plist, 200);
SLTPushBack(&plist, 300);
SListPrint(plist);
printf("\n");
printf("尾删:\n");
SLTPopBack(&plist);
SLTPopBack(&plist);
SLTPopBack(&plist);
//SLTPopBack(&plist);删到了空,再继续删会报错
SListPrint(plist);
printf("\n");
printf("头插:\n");
SLTPushFront(&plist, 100);
SLTPushFront(&plist, 300);
SLTPushFront(&plist, 600);
SLTPushFront(&plist, 10);
SLTPushFront(&plist, 50);
SLTPushFront(&plist, 360);
SLTPushFront(&plist, 369);
SListPrint(plist);
printf("\n");
printf("头删:\n");
SLTPopFront(&plist);
SLTPopFront(&plist);
SListPrint(plist);
printf("\n");
printf("查找600,返回其位置用pos接受,并在其后面插入88:\n");
SLTNode* pos = SListFind(plist, 600);
SLTInsertAfter(pos, 88);
SListPrint(plist);
printf("\n");
printf("查找600,并在其前面插入666:\n");
SLTInsertBefore(&plist, pos, 666);
SListPrint(plist);
printf("\n");
printf("如果 pos=phead,查看是否可以插入33\n");
SLTInsertBefore(&plist, plist, 33);
SListPrint(plist);
printf("\n");
printf("删除600后的节点88\n");
SLTEraseAfter(pos);
SListPrint(plist);
printf("\n");
printf("删除600位置的节点\n");
SLTErase(&plist, pos);
SListPrint(plist);
printf("\n");
printf("点链表释放\n");
SLTDestroy(&plist);
SListPrint(plist);
printf("\n");
}
int main()
{
TestSList1();//测试函数
return 0;
}