一. 初识单链表
链表是一种物理结构上非连续,非顺序的存储结构,数据元素的逻辑顺序 是通过链表中的指针链接次序实现的。(逻辑结构是线性的,物理结构不一定是线性的)

我们知道,火车由一节一节的车厢 组成,那么,链表由一个一个的结点 组成。其中,这个结点包括两部分:保存的数据和指针(始终保存下一个结点的地址)
注:链表每个结点在堆区是独立存在的,不是连续的!

(一)结点
与顺序表不同的是,链表里的每节"车厢"都是独立申请下来的空间,我们称之为:结点。图中指针变量plist保存的是第一个结点的地址,我们称plist此时"指向"第一个结点,如果我们希望plist指向第二个结点时,只需要修改plist保存的内容为0x0012FFA0。
链表中每个结点都是独立申请的(即需要插入数据时才去申请一块结点的空间),我们需要通过指针变量来保存下一个结点的位置才能从当前结点找到下一个结点。
(二)链表的性质
①链式结构在逻辑上是连续的,在物理结构上不一定是连续的。
②结点一般是从堆上申请的。
③从堆上申请下来的空间,是按照一定策略分配出来的,每次申请的空间可能连续,可能不连续。
(三)链表的打印
给定的链表结构中,如何实现结点从头到尾的打印?
cpp
void SLTprint(SLTNode* phead)
{
SLTNode* pcur = phead;
while (pcur != NULL)
{
printf("%d-> ", pcur->data);
pcur = pcur->next;
}
printf("NULL\n");
}
二. 单链表的尾插及测试
cpp
//1.单链表的尾插
SLTNode* SLTbuynode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
SLTNode* SLTpushback(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
//申请新节点
SLTNode* newnode=SLTbuynode(x);
//判断链表是否为空
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* ptail = *pphead;
while (ptail->next != NULL)
{
ptail = ptail->next;
}
//找到尾结点
ptail->next = newnode;
}
}
cpp
void test02()
{
//创建空链表
SLTNode* plist = NULL;
SLTpushback(&plist, 1);
SLTpushback(&plist, 2);
SLTpushback(&plist, 3);
SLTpushback(&plist, 4);
SLTPrint(plist);
}
三. 单链表的头插
cpp
//2.单链表的头插
SLTNode* SLTpushfront(SLTNode** phead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTbuynode(x);
newnode->next = *pphead;
*pphead = newnode;
}
四. 单链表的尾删
cpp
// 3.单链表的尾删
SLTNode* SLTpopback(SLTNode** pphead)
{
//链表为空不能删除
assert(pphead && *pphead);
//链表只有一个结点的情况
if ((*pphead->next == NULL))
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = NULL;
SLTNode* ptail = *pphead;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
五. 单链表的头删
cpp
//4.单链表的头删
void SLTpopfront(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
六. 关于顺序表与链表问题的思考
1.中间/头部的插入删除,时间复杂度为O(N)。
2.增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3.增容一般是呈2倍增长,势必会有一定的空间浪费。
但是链表头部插入删除,时间复杂度为O(1),并且不需要增容,不存在空间浪费。
所以 ,有时我们要根据不同的情形,来选择不同的数据结构。例如:如果一个场景频繁需要头插,则建议选择链表 更为合适;如果一个场景频繁需要尾插,则建议选择顺序表更为合适。
七. 单链表的查找
cpp
//5.查找
SLTNode* SLTfind(SLTNode* phead, SLTDataType x)
{
SLTNode* pcur = phead;
while (pcur)
{
if (pcur->data == x)
return pcur;
pcur = pcur->next;
}
return NULL;
}
八. 在指定位置之前插入数据
cpp
//6.在指定位置之前插入数据
void SLTinsert(SLTNode** pphead, SLTDataType x, SLTNode* pos)
{
assert(pphead && pos);
STLNode* newnode = SLTbuynode(x);
//若pos就是头结点,此时要当成头插处理
if (pos == *pphead)
{
//头插
SLTpushfront(pphead, x);
}
else
{
//找pos的前一个结点
SLTNode* prev = *pphead;
while (*prev->next != pos)
{
prev = prev->next;
}
//找到了,开始牵手
prev->next = newnode;
newnode->next = pos;
}
}
九. 在指定位置之后插入数据
cpp
//7. 在指定位置之后插入数据
void SLTinsertafter(SLTDataType x, SLTNode* pos)
{
assert(pos);
STLNode* newnode = STLbuynode(x);
newnode->next = pos->next;
pos->next->newnode;
}
十. 删除pos结点
cpp
//8.删除pos结点
void SLTerase(SLTNode** pphead, SLTNode* pos)
{
//pos刚好就是头结点
if (pos == *pphead)
{
SLTpopfront(pphead);
}
else
{
assert(pphead && pos);
SLTNode* pev = *pphead;
while (pev->next != pos)
{
pev = pev->next;
}
pev->next = pos->next;
free(pos);
pos = NULL;
}
}
十一. 删除pos位置之后的一个结点
cpp
//9.删除pos位置之后的一个结点
void SLTeraseafter(SLTNode* pos)
{
assert(pos&&pos->next);
SLTNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
十二.销毁链表
cpp
//10.销毁链表
void SLTdestory(SLTNode** pphead)
{
assert(pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
free(pcur);
pcur = next;
}
*pphead=NULL;
}
以上就是今天的内容,下一节我将讲述单链表的应用,喜欢的朋友们可以一键三连~