链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) +指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。
而为了便于理解,我们可以将链表以图形的形式来展现,可以思考两种,一种是逻辑结构,也就是我们想象出来的,更形象,方便理解,逻辑结构图如下:
还有一种是物理结构,也就是在内存中实实在在如何存储的,物理结构图如下:
(1)空间不够了,需要扩容,而扩容是有消耗的。
(2)头部或中间位置的插入删除都需要挪动,挪动数据页数有消耗的。
(3)避免频繁扩容,一次一般都是按倍数扩容(一般是2倍),可能存在一定空间浪费。
支持随机访问。有些算法,需要结构支持随机访问。比如:二分查找、优化的快排…
每存一个数据,都要存一个指针去链接后面的数据节点。不支持随机访问(用下标直接访问第i个)
(1)按需要申请空间,不用了就释放空间(更合理的使用了空间)。
(2)头部中间插入删除数,不需要挪动数据,不存在空间浪费
代码如下:
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead; // cur和phead指向同一个位置
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
}
在这个代码里面,cur是结构体指针,可以通过->访问结构体成员,结构体有两个成员。一个是data存放数据,还有一个是next(指针)cur->next就是取右边的值,右边的值存的就是第二个节点的地址
换句话说,表面上看见cur在不断移动,实际上cur存的是不同节点的地址,看上去像是移动了的。
在实现尾插前,我们首先都知道,形参是实参的一份临时拷贝,因而,我们要想改变实参的值传的应当将实参的地址传给形参,这里的形参是一级指针,而如果实参是一个指针,要想改变它的值,我们要传一个指针的指针也就是二级指针。
我们第一步操作是找到链表的尾端,首先我们先定义一个变量tail作用类似于打印链表中的cur
代码如下:
void SListPushBack(SLTNode** pphead, SLTDateType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;//开辟一个新的节点newnode
if (*pphead == NULL)
{
*pphead = newnode; // 链接起来
}
else
{
// 找到尾节点
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
然后画个图我们更好理解
我们一样通过改变tail存放的地址来使它存放下一个节点的地址实现尾插。
头插单链表代码如下,我们将开辟空间malloc那一部分的代码用一个新的函数BuyListNode统一写,这一部分后面同样将会多次用到。代码如下:
void SListPushFront(SLTNode** pphead, SLTDateType x)
{
SLTNode* newnode = BuyListNode(x);
newnode->next = *pphead; //*pphead就是plist
*pphead = newnode;
}
尾删函数因为删到最后可能为空,所以我们也要传二级指针。同时,尾删我们要考虑两种情况,链表只有一个节点以及链表有两个及以上节点的情况。如果只有一个节点的话我们直接free为空就好了。为了避免出现针的情况发生。我们定义了tail这个变量如图:
而单链表有个缺陷就是只能找到下一个位置但是不能找到上一个位置,如图:
考虑到链表并不确定有一个或两个节点,我们分开实现,代码如下:
void SListPopBack(SLTNode** pphead)
{
// 法一
/*if (*pphead == NULL)
{
return;
}*/
assert(*pphead != NULL);
// 1、一个节点
// 2、两个及以上节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
//while(tail->next != NULL) 比较表达式返回值都是一个逻辑值(也就是真或者假),这里空为假
while (tail->next)// 转换成逻辑条件判断。0为假,两者写法都可以
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
尾删还有一种写法是这样子的:
SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
这一种方法非常的好理解,如图:
我们通过tail->next->next找到了后面的后面的节点进行判断是否为空,然后再释放就会简便不少。
头删单链表的实现并不复杂。直接上代码;
void SListPopFront(SLTNode** pphead)
{
assert(*pphead != NULL);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
查找的实现,我们传参的时候是不需要传二级指针的。我们只是找到某个数而对它并不做修改,因此我们只要传一级指针就可以了。代码如下:
SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;// 到最后都没找到就置为空
}
我们先假设要在3的位置插上一个30这样的数字,我们先开辟出newnode新的节点,然后posprev指向这个节点,再next指向pos从而实现中间pos位置的插入
代码如下:
//Insert是在pos之前去插入一个节点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
SLTNode* newnode = BuyListNode(x); //如果说pos在第一个节点,相当于头插
if (*pphead == pos)
{
newnode->next = *pphead;
*pphead = newnode;
}
else
{
//找到pos的前一个位置
SLTNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = newnode;
newnode->next = pos;
}
}
但是事实上单链表是不适合再pos的前面插入的,我们应当选择在pos的后面位置插入,如图:
代码如下:
// 在pos的后面插入,这个更适合,也更简单
void SListInsertAfter(SLTNode* pos, SLTDateType x)
{
SLTNode* newnode = BuyListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
啊,删除数据非常之奇妙,相信大家肯定早就学会了,我就不多说了,怕大家局的我烦(其实懒病发作,写不下去了。时间太晚了,肝不动了)直接上代码好伐。
删除:
void SListErase(SLTNode** pphead, SLTNode* pos)
{
if (*pphead == pos)
{
/**pphead = pos->next;
free(pos);*/
SListPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
//pos = NULL;
}
}
删除后一个:
void SListEraseAfter(SLTNode* pos)
{
assert(pos->next);
assert(pos);
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
//next = NULL;
}
革命已经到了最后一刻,销毁单链表。我们还是画一个逻辑结构图这样好理解
在必不可少的断言惯例后,我们就free,free。上代码走起:
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
这样,走到这里相信你已经完全学会了单链表。没错,你一定能看懂学会。
test.c
#include "SList.h"
void TestSList1()
{
SLTNode* plist = NULL;
/*SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);*/
/*SListPrint(plist);*/
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPrint(plist);
/*SListPopBack(&plist, 1);*/
SListPopFront(&plist, 1);
SListPrint(plist);
SListPopFront(&plist, 2);
SListPrint(plist);
}
void TestSList2()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 2);
SListPushFront(&plist, 4);
SListPushFront(&plist, 2);
SListPushFront(&plist, 4);
SLTNode* pos = SListFind(plist, 2);
int i = 1;
while (pos)
{
printf("第%d个pos节点:%p->%d\n", i++, pos, pos->data);
pos = SListFind(pos->next, 2);//我们找到第一个2了可以在第一个2后面的节点接着找
}
// 修改 3->30
pos = SListFind(plist, 3);
if (pos)
{
pos->data = 30;
}
SListPrint(plist);
}
void TestSList3()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPrint(plist);
SLTNode* pos = SListFind(&plist, 3);
if (pos)
{
SListInsert(&plist, pos, 30);
}
SListPrint(plist);
}
int main()
{
//TestSList1();
//TestSList2();
TestSList3();
return 0;
}
SList.c
#include "SList.h"
SLTNode* BuyListNode(SLTDateType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
printf("malloc fail\n");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead; // cur和phead指向同一个位置
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next; // cur是结构体指针,可以通过->访问结构体成员,结构体有两个成员
// 一个是data存放数据,还有一个是next(指针)cur->next就是取右边的值,右边的值存的就是第二个的地址
}
printf("NULL\n");
}
void SListPushBack(SLTNode** pphead, SLTDateType x)
{
SLTNode* newnode = BuyListNode(x);
//SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
//newnode->data = x;
//newnode->next = NULL;//开辟一个新的节点newnode
if (*pphead == NULL)
{
*pphead = newnode; // 链接起来
}
else
{
// 找到尾节点
SLTNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = newnode;
}
}
void SListPushFront(SLTNode** pphead, SLTDateType x)
{
SLTNode* newnode = BuyListNode(x);
newnode->next = *pphead; //*pphead就是plist
*pphead = newnode;
}
void SListPopBack(SLTNode** pphead)
{
// 法一
/*if (*pphead == NULL)
{
return;
}*/
assert(*pphead != NULL);
// 1、一个节点
// 2、两个及以上节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
//while(tail->next != NULL) 比较表达式返回值都是一个逻辑值(也就是真或者假),这里空为假
while (tail->next)// 转换成逻辑条件判断。0为假,两者写法都可以
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
// 方法二
/*SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
*/
}
}
void SListPopFront(SLTNode** pphead)
{
assert(*pphead != NULL);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
SLTNode* SListFind(SLTNode* phead, SLTDateType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;// 到最后都没找到就置为空
}
// 在pos的后面插入,这个更适合,也更简单
void SListInsertAfter(SLTNode* pos, SLTDateType x)
{
SLTNode* newnode = BuyListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
//Insert是在pos之前去插入一个节点
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x)
{
SLTNode* newnode = BuyListNode(x); //如果说pos在第一个节点,相当于头插
if (*pphead == pos)
{
newnode->next = *pphead;
*pphead = newnode;
}
else
{
//找到pos的前一个位置
SLTNode* posPrev = *pphead;
while (posPrev->next != pos)
{
posPrev = posPrev->next;
}
posPrev->next = newnode;
newnode->next = pos;
}
}
void SListErase(SLTNode** pphead, SLTNode* pos)
{
if (*pphead == pos)
{
/**pphead = pos->next;
free(pos);*/
SListPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
//pos = NULL;
}
}
void SListEraseAfter(SLTNode* pos)
{
assert(pos->next);
assert(pos);
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
//next = NULL;
}
void SListDestroy(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
SList.h
#pragma once
#include
#include
#include
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SLTNode;
void SListPrint(SLTNode* phead); // phead是一个指针,指向第一个节点
void SListPushBack(SLTNode** pphead, SLTDateType x);// 尾插
void SListPushFront(SLTNode** pphead, SLTDateType x);// 头插
void SListPopBack(SLTNode** pphead);//尾删
void SListPopFront(SLTNode** pphead);//头删
SLTNode* SListFind(SLTNode* phead, SLTDateType x);//查找也可实现修改
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDateType x);//插入(配合find一起用)
void SListErase(SLTNode** pphead, SLTNode* pos);//删除
void SListEraseAfter(SLTNode* pos);// 删除后一个
void SListDestroy(SLTNode** pphead);// 链表销毁
单链表到此为止就结束了
链表我们不光有单链表,还有双向链表等,数据结构同样还有很多知识等待我们去学习探索,在这里给各位大佬们磕一个,感谢大家阅读我的文章,文章中还有很多不足的地方欢迎大家在评论区给我留言,我会不定期不定时有缘回复
最后依然还是我们的“只因”哥镇图