在上篇顺序表文章中,我们总结出了顺序表的一些缺点。
1.空间不够了需要增容,扩容有代价
2.为了减少扩容的代价,一般扩容扩大两倍,这样可能会导致浪费一定的空间
3.顺序表要求从头位置开始存储,如果我们从头部或者中间开始插入或者删除数据就要挪动数据,效率低
针对顺序表的缺陷,可以设计链表。
本文详细介绍简单的单链表的实现和多种接口函数
1.链表的定义
每个链表结点有两部分,第一部分存储这个结点的值。第二部分存储链表要连接的下一个结点的地址。
typedef struct SListNode
{
SLDataType val;
struct SListNode* next;
}SL;
2.链表接口函数的shixian(增删查改)
2.1创建一个新的结点
SL* CreatListNode(SLDataType x)
{
SL* newnode = (SL*)malloc(sizeof(SL));
if (newnode == NULL)
{
printf("malloc失败");
exit(-1);
}
newnode->val = x;
newnode->next = NULL;
return newnode;
}
每次要增加新的结点的时候调用这个函数即可
2.2打印整个链表的值
为了便于观察接口函数的功能 ,我们需要打印链表来观察是否出现错误
//打印链表
SL* PrintList(SL* phead)
{
SL* cur = phead;
assert(phead != NULL);
while (cur != NULL)
{
printf("%d->", cur->val);
cur = cur->next;
}
printf("NULL\n");
}
2.3尾插数据
//尾插数据
//注意要使用二级指针,使用一级指针只是传值,不会改变实际参数的值
void SlistPushback(SL** phead, SLDataType x)
{
//1.创建一个新的结点
SL* newnode = CreatListNode(x);
//2.将新的结点连接到链表的尾部
if (*phead == NULL)
*phead = newnode;
else
{
SL* cur = *phead;
while (cur->next != NULL)
{
cur = cur->next;
}
cur->next = newnode;
}
}
有一点要注意,由于我们没有哨兵结点,要插入数据需要传入结点的地址,故要使用二级指针。传入一级指针是传值。
2.4头插数据
//头插数据
void SlistPushFront(SL** pphead, SLDataType x)
{
//1.创建一个新的结点
SL* newnode = CreatListNode(x);
//2.头插数据
newnode->next = *pphead;
*pphead = newnode;
}
头插结点比较简单,无论链表是否为空,让新的结点指向这个链表的头节点即可
然后让这个结点成为新的头
2.5尾删数据
//尾删数据
void SlistPopback(SL** pphead)
{
assert(*pphead != NULL);
//1.链表中只有一个结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else//有一个以上的结点
{
//1.找到尾节点并且删除这个结点
SL* tail = *pphead;
SL* prev = NULL;//用来记录当前尾结点的前一个结点,删除尾结点后让这个结点指向空
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
//倒数第二个结点指向空
prev->next = NULL;
}
}
尾删除结点要注意结点空的个数有空,一个结点,多个结点三种情况需要处理
2.6头删结点
//头删数据
void SlistPopFront(SL** pphead)
{
//1.不能为空
assert(*pphead != NULL);
//2.删除头节点
SL* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
注意删除头节点前要先让记录第二个结点,删除头节点后让其称为新的头节点
2.7查找修改结点
//查找数据,查找的时候也能同时修改这个结点
SL* SlistFind(SL** pphead, SLDataType x)
{
assert(*pphead != NULL);
SL* cur = *pphead;
while (cur)
{
if (cur->val == x)
return cur;
else
cur = cur->next;
}
return NULL;
}
返回这个结点,通过返回值还能修改这个结点的值
2.8在pos位置的前一个位置插入结点
//在pos位置之前插入数据
void SlistInsert(SL** pphead, SL* pos, SLDataType x)
{
//1.创建一个新的结点
SL* newnode = CreatListNode(x);
//2.找到pos位置并在其前面插入数据
if (pos == *pphead)//头插
{
SlistPushFront(pphead, x);
}
else
{
SL* posprev = *pphead;
while (posprev->next != pos)
{
posprev = posprev->next;
}
posprev->next = newnode;
newnode->next = pos;
}
}
当pos位置为头节点直接调用头插结点即可
2.9在pos位置的后面插入结点
//在pos位置之前插入数据
void SlistInsertAfter(SL** phead, SL* pos, SLDataType x)
{
//1.创建一个新的结点
SL* newnode = CreatListNode(x);
//2.插入结点
newnode->next = pos->next;
pos->next = newnode;
}
pos后面插入比较简单
2.10删除pos位置的结点
//删除pos位置的结点
void SlistEarse(SL** pphead, SL* pos)
{
assert(*pphead != NULL);
if (pos == *pphead)//头删
{
SlistPopFront(pphead);
}
else
{
//找到pos位置的前一个结点
SL* posprev = *pphead;
while (posprev->next != pos)
{
posprev = posprev->next;
}
posprev->next = pos->next;
free(pos);
}
}
2.11删除pos位置后面结点
//删除pos位置后面的结点
void SlistEraseAfter(SL* pos)
{
assert(pos != NULL);
SL* next = pos->next;
pos->next = next->next;
free(next);
next = NULL;
}
2.12销毁链表
//销毁整个链表
void SlistDestroy(SL** pphead)
{
SL* cur = *pphead;
while (cur)
{
SL* next = cur->next;
free(cur);
cur == next;
}
free(*pphead);
*pphead = NULL;
}
3.链表的优缺点
1.按需分配空间,要多少给空间就分配多少空间(分配空间合理)
2.插入,删除数据不需要挪动数据,只需修改少数指针
3.不存在空间的浪费
4.链表的缺点
1.不支持随机访问
2.每个数据都需要一个额外的数据来存储