0、前要:顺序表的缺陷
- 在倍数增容的时候,存储空间存在一定的空间浪费
- 增容需要申请空间、拷贝数据、释放旧空间,有不小的消耗
- 头部或者中部的插入、删除效率低下,时间复杂度是O(N)
- 查找搜索缓慢,但是这个其实是可以靠树来提速的,不过这个和本此讨论的链表没有太大关系
1、链表的基本认知
(1)链表的概念
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的(逻辑结构是想象出来的/物理结构是是实实在在的,内存中如何存储表示他们之间的关系)
(2)链表的分类
- 单向、双向
- 带头、不带头
- 循环、非循环
尽管在实际中,还是单向和双向链表用的比较多
(3)链表的第一个节点和尾节点
一般叫做plist/phead和pback
(4)链接的本质
将后一个节点的地址存放在前一个节点里
(5)最简单和最复杂的链表结构
- 无头+无循环+单向+链表(最简单):一般不会直接单独存放数据,实际中更多是作为其他数据结构的子结构,例如哈希桶、图的邻接表等,这种结构在面试的时候也有很多
- 有头+有循环+双向+链表(最复杂):一般直接单独存储数据,实际中使用的链表数据结构,大都是“有哨兵位头+有循环+双向+链表”,虽然结构复杂,但是在一些接口函数实现的时候就会发现反倒是变得简单了
2、无头+无循环+单向+链表的实现(结构最简单)
(1)“无头+无循环+单向+链表”结构体
typedef int SLTDateType;
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next;
}SListNode;
(2)动态申请一个结点
SListNode* BuySListNode(SLTDateType x)
{
//单独申请一个节点
SListNode* chche = (SListNode*)malloc(sizeof(SListNode));
if (!chche)
{
perror("fail malloc");//注意perror只有在malloc、文件操作的时候才可用
return NULL;
}
else
{
//初始化该节点
chche->data = x;
chche->next = NULL;
return chche;
}
}
(3)“无头+无循环+单向+链表打印
void SListPrint(SListNode* plist)
{
SListNode* cur = plist;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
(4)“无头+无循环+单向+链表”尾插
void SListPushBack(SListNode** pplist, SLTDateType x)
{
assert(pplist);
//单独申请一个节点
SListNode* chche = BuySListNode(x);
//开始尾插节点
SListNode* cur = *pplist;
if (cur == NULL)//如果是空链表
{
*pplist = chche;
}
else//如果是非空链表
{
while (cur->next != NULL)
{
cur = cur->next;
}
cur->next = chche;
}
/*千万不能直接cur找到整个链表的NULL,然后将新增加的节点赋值给cur(因为上一个节点的next依旧指向NULL,链表是断开的)*/
/*赋值的本质是拷贝*/
}
(5)“无头+无循环+单向+链表”的头插
void SListPushFront(SListNode** pplist, SLTDateType x)
{
//单独申请一个节点
SListNode* chche = BuySListNode(x);
chche->next = *pplist;//这个语句保证了两种情况都可以(NULL/非NULL)
*pplist = chche;
}
(6)“无头+无循环+单向+链表”的尾删
void SListPopBack(SListNode** pplist)
{
assert(pplist && *pplist);//避免空指针和空链表
if ((*pplist)->next == NULL)//单独处理只有一个节点的情况
{
free(*pplist);//哎呀,忘记释放内存了,补上
*pplist = NULL;
return;
}
//开始尾删节点
SListNode* prev = *pplist;//这一步我一开始没有想到hhhhhh,prev是为了记录上一个节点
SListNode* cur = *pplist;
while (cur->next != NULL)
{
prev = cur;
cur = cur->next;
}
free(cur);//释放掉尾节点
prev->next = NULL;//重新定义尾节点
}
(7)“无头+无循环+单向+链表”头删
void SListPopFront(SListNode** pplist)
{
assert(pplist && *pplist);
SListNode* p = *pplist;
*pplist = (*pplist)->next;//这里容易写出bug,因为“->”的优先级要比“*”高
free(p);
}
(8)“无头+无循环+单向+链表”查找
SListNode* SListFind(SListNode* plist, SLTDateType x)
{
assert(plist);//保证不传空指针
SListNode* cur = plist;
if ((cur->next == NULL) && (cur->data == x))
{
return cur;
}
while (cur->next != NULL)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
(9)“无头+无循环+单向+链表”在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?tips:因为没有办法往前插入,这是单向链表结构上的缺陷
void SListInsertAfter(SListNode* pos, SLTDateType x)
{
//单独申请一个节点
assert(pos);
SListNode* chche = BuySListNode(x);
chche->next = pos->next;
pos->next = chche;
}
(10)“无头+无循环+单向+链表”删除pos位置之后的值
// 分析思考为什么不删除pos位置?tips:也是由于单向结构的原因,不能找回上一个节点
void SListEraseAfter(SListNode* pos)
{
assert(pos && pos->next);
SListNode* p1 = pos->next;
SListNode* p2 = p1->next;
pos->next = p2;
free(p1);
}
(11)“无头+无循环+单向+链表”的销毁
void SListDestroy(SListNode** pplist)
{
assert(pplist);//注意空链表是可以销毁的
SListNode* cur = *pplist;
while (cur)
{
SListNode* cache = cur->next;
free(cur);
cur = cache;
}
*pplist = NULL;//这里是使用二级指针的依据,如果不在函数内部释放掉,就需要在使用完这个函数后手动将原本指向链表的指针置空,这个时候这个函数就可以使用一级指针来构造了
}
(12)链表测试用例
void test()
{
SListNode* a = NULL;
SListPushFront(&a, 100);//头插
SListPushFront(&a, 200);//头插
SListPushFront(&a, 300);//头插
SListPrint(a);//打印
SListNode* p = SListFind(a, 300);//查找
printf("%p\n", p);
SListPopFront(&a);//头删
SListPrint(a);//打印
SListPopFront(&a);//头删
SListPrint(a);//打印
SListPopFront(&a);//头删
SListPrint(a);//打印
//SListPopFront(&a);//头删(头删了空链表)
//SListPrint(a);//打印
SListPushBack(&a, 3);//尾插
SListPushBack(&a, 2);//尾插
SListPushBack(&a, 1);//尾插
SListPushBack(&a, 0);//尾插
SListPushFront(&a, 100);//头插
SListPushFront(&a, 200);//头插
SListPushFront(&a, 300);//头插
SListPrint(a);//打印
SListPopBack(&a);//尾删
SListPrint(a);//打印
SListPopBack(&a);//尾删
SListPopBack(&a);//尾删
SListPopBack(&a);//尾删
SListPrint(a);//打印
SListPopBack(&a);//尾删
SListPopBack(&a);//尾删
SListPopBack(&a);//尾删
//SListPopBack(&a);//尾删(尾删了空链表)
SListPrint(a);//打印
SListPushBack(&a, 3);//尾插
SListPushBack(&a, 2);//尾插
SListPushBack(&a, 1);//尾插
SListPushBack(&a, 0);//尾插
SListPushFront(&a, 100);//头插
SListPushFront(&a, 200);//头插
SListPushFront(&a, 300);//头插
SListPushFront(&a, 400);//头插
SListPrint(a);//打印
p = SListFind(a, 100);//先查找得到节点地址
SListInsertAfter(p, 4);//任意插入节点
SListPrint(a);//打印
SListPopFront(&a);//头删
SListPopBack(&a);//尾删
SListPopFront(&a);//头删
SListPopBack(&a);//尾删
SListPopFront(&a);//头删
SListPopBack(&a);//尾删
SListPopFront(&a);//头删
SListPopBack(&a);//尾删
p = SListFind(a, 4);//先查找得到节点地址
SListInsertAfter(p, 100000);//任意插入节点
SListPrint(a);//打印
SListPopFront(&a);//头删
SListPopFront(&a);//头删
SListPrint(a);//打印
SListPushBack(&a, 1);//尾插
SListPushBack(&a, 0);//尾插
SListPushFront(&a, 100);//头插
SListPushFront(&a, 200);//头插
SListPrint(a);//打印
p = SListFind(a, 200);//先查找得到节点地址
SListEraseAfter(p);
SListPrint(a);//打印
SListEraseAfter(p);
SListPrint(a);//打印
SListEraseAfter(p);
SListPrint(a);//打印
SListDestroy(&a);//销毁链表
}
int main()
{
test();
return 0;
}
3、有头+有循环+双向+链表的实现(结构最复杂)
看我另外一篇文章:有头+有循环+双向+链表