文章目录
单链表
1. 链表的概念及结构
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的 。
区分物理结构与逻辑结构
物理结构: 实际存在的,可以通过肉眼观察到
逻辑结构: 实际不存在, 人为想象出来的
注意:
- 从上图可以看出,链式结构在逻辑上是连续的,但是在物理上不一定连续。
- 现实中的结点一般都是从堆上申请出来的。
- 从堆上申请出来的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续。
2. 链表的分类
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
2. 带头或者不带头:
3. 循环或者非循环:
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
来很多优势,实现反而简单了,后面我们代码实现了就知道了。
3. 单链表项目创建
文件名 | 功能 |
---|---|
SList.h | 创建单链表,完成函数名的声明 |
SList.c | 实现单链表的各个功能函数 |
test.c | 测试单链表函数的正确性 |
3.1 定义单链表
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data; //需要存储的数据
struct SListNode* next; //指向下一个结点的指针
}SLTNode;
3.2 动态申请一个结点
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newhead = (SLTNode*)malloc(sizeof(SLTNode));
if (newhead == NULL)
{
perror("malloc fail");
exit(-1);
}
newhead->data = x;
newhead->next = NULL;
return newhead;
}
3.3 构造一个有n个结点的链表(快速构造链表)
SLTNode* CreateList(SLTDataType n)
{
SLTNode* ptail = NULL; //首指针
SLTNode* phead = NULL; //尾指针
for (int i = 0; i < n; i++)
{
SLTNode* newnode = BuySLTNode(i + 10);
if (phead == NULL) //链表为空
{
phead = ptail = newnode;
}
else //链表不为空
{
ptail->next = newnode;
ptail = newnode; //变成新的尾
}
}
return phead; //返回保留的链表头
}
3.4 打印单链表
先定义一个新的结点保留链表的头,然后while循环遍历打印
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead; //保留链表的头
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
3.5 单链表尾插
错误示例
void SListPushBack(SLTNode* phead,SLTDataType x)
{
SLTNode* tail = phead;
while(tail->next != NULL)
{
tail = tail->next;
}
SLTNode* newnode = BuySLTNode(x);
tail->next = newnode;
}
上面写法有2处错误:
- phead未进行判空, 当链表为空时, 会存在对空指针的解引用, 出现错误。
- 函数传参错误,
plist
(实参)的类型为SLTNode *
,而我们形参类型也是SLTNode *
,这属于值传递,值传递相当于 形参是实参的一份临时拷贝,形参的改变并不会影响实参的值。想要修改实参的值就需要进行传址操作,在这里传plist
的地址.形参用二级指针**pphead
思路
判断链表是否为空:
(1)为空, 让头指针指向新开辟的结点
(2)不为空, 遍历找到尾结点(即其next为空),将新开辟的结点连接到后面
正确写法
void SLTPushBack(SLTNode** pphead,SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);
if (*pphead == NULL) //链表为空尾插
{
*pphead = newnode;
}
else //非空链表尾插
{
SLTNode* tail = *pphead;
//找尾
while (tail->next)
{
tail = tail->next;
}
tail->next= newnode;
}
}
3.6 单链表尾删
判断链表是否为空:
链表为空不能删除
只有一个结点(即头结点),直接释放头结点并将其置空
有一个或一个以上结点, 两种方法: (1) 找尾的前一个, 将其保存起来,释放尾后,使尾的前一个指向空。
(2) 找尾下一个的下一个
版本1
void SLTPopBack(SLTNode**pphead)
{
assert(*pphead); //链表为空不能删除
if ((*pphead)->next == NULL) //只有一个结点要单独处理
{
free(*pphead);
*pphead = NULL;
}
else
{
//思路1: 找尾的前一个
SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next)
{
prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
版本2
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;
}
}
3.7 单链表头插
思路:直接开辟一个新的结点,将这个结点连接到头结点的前面,这个结点变成新的头结点
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = BuySLTNode(x);
newnode->next=*pphead;
*pphead = newnode;
}
3.8 单链表头删
思路:将头结点的后一个结点保存起来,释放头结点,原头结点的后一个结点变成新的头结点
注意: 链表为空不能删除
void SLTPopFront(SLTNode** pphead)
{
assert(*pphead); //链表为空不能删除
SLTNode* next = (*pphead)->next; //保存后一个结点,作为新链表的头
free(*pphead);
*pphead = next; //变成新的头
}
3.9 查找单链表中的元素
思路:先将链表头指针记录下来,然后while循环遍历查找元素,找到返回结点指针,找不到返回NULL
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
3.10 单链表在pos位置之后插入元素
思路: 和头插思路相似,找到pos位置后,将新开辟的结点连接到pos的后面
注意: 判断pos位置的合法性
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos); //pos位置的合法性
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
3.11 单链表删除pos位置之后的元素
思路: 和头删思路相似,找到pos位置后,链表中只有一个结点直接释放,
有多个结点时,保存pos后一个结点,pos后一个结点连接到pos后一个结点的后一个,释放掉原pos的后一个结点
注意: 判断pos位置的合法性
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
if (pos->next == NULL) //链表中只剩一个结点
{
return;
}
else
{
SLTNode* nextnode = pos->next;
pos->next = nextnode->next;
free(nextnode); //nextnode置不置空都行,它是局部变量,出了作用域变量销毁
}
}
3.12 单链表在pos位置之前插入元素
思路: 和尾插思路相似,找到pos位置后,pos位置是头直接复用头插
不是头,找pos的前一个,将新开辟的结点连接到pos前一个结点的后面,pos结点的前面
注意: 判断pos位置的合法性
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pos);
if (*pphead == pos) //pos是链表的第一个结点,头删
{
SLTPushFront(pphead, x);
}
else
{ //找pos的前一个
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
3.13 单链表删除pos位置的元素
思路: 和尾删思路相似,找到pos位置后,pos位置是头直接复用头删
不是头,找pos的前一个记录下来,将pos的后一个结点连接到pos前一个结点的后面,释放pos结点
注意: 判断pos位置的合法性
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pos);
if (*pphead == pos) //pos是链表的第一个结点,头删
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos); //pos置空时要用二级指针
}
}
3.14 销毁单链表
while循环遍历销毁,链表中的结点需要一个一个释放
void SLTDetroy(SLTNode** pphead)
{
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL; //将plist置空
}
4. 总结
优缺点:
单链表的头插和头删很简单, 尾插和尾删比较复杂(要考虑两种情况)
顺序表和链表的区别
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持 O(1) | 不支持:O(N) |
任意位置插入或者删除元素 | 可能需要搬移元素,效率低 O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |
缓存利用率高低 | 高 | 低 |
备注:缓存利用率参考存储体系结构 以及 局部原理性。