作者:@阿亮joy.
专栏:《数据结构与算法要啸着学》
座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在上一篇博客中,我们已经讲到了顺序表。那现在再来总结一下顺序表的相关问题。
顺序表的问题及思考
问题:
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈 2 倍的增长,势必会有一定的空间浪费。例如当前容量为 100,满了以后增容到 200,我们再继续插入了 5 个数据,后面没有数据插入了,那么就浪费了 95 个数据空间。
思考:如何解决以上问题呢?下面给出了链表的结构来看看。
概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的 。
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
- 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
链表和顺序表一样,都要实现增删查改的功能。链表需要实现的函数接口有:申请节点、打印链表、销毁链表、头插数据、尾插数据、尾删数据、头删数据、查找数据、在pos
位置之前插入数据、在pos
位置之后插入数据、删除pos
位置的数据和删除pos
位置之后的数据。由于要实现的函数接口比较多,所以我们还是需要采取三个模块的方式来写代码。SList.h
源文件里面是头文件的包含、结构体的声明、类型的重命名以及函数接口的声明。SList.c
源文件里面是函数接口的实现。Test.c
源文件用来测试我们实现的函数接口是否正确。
现在来看一下每个模块的代码。
#pragma once
#include
#include
#include
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
// 申请节点
SLTNode* BuySLTNode(SLTDataType x);
// 打印链表
void SListPrint(SLTNode* phead);
// 销毁链表
void SListDestory(SLTNode** pphead);
// 头插数据
void SListPushFront(SLTNode** pphead, SLTDataType x);
// 尾插数据
void SListPushBack(SLTNode** pphead, SLTDataType x);
// 尾删数据
void SListPopBack(SLTNode** pphead);
// 头删数据
void SListPopFront(SLTNode** pphead);
// 查找数据
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
// 在pos位置之前插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
// 在pos位置之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x);
// 删除pos位置的数据
void SListErase(SLTNode** pphead, SLTNode* pos);
// 删除pos位置后面的数据
void SListEraseAfter(SLTNode* pos);
链表与顺序表的函数接口大多数是相同的,与顺序表不同的是,链表没有初始化的函数接口。那为什么链表不需要初始化呢?是因为我们只需要拿一个SLTNode*
指针就能管理整个链表。
还有一点就是,除了打印链表和查找数据的函数接口的参数是一级指针,其它的函数接口的参数都是二级指针。这又是为什么呢?其实是因为打印链表和查找数据的函数不需要改变头节点,而其它函数有可能要改变头节点。这个知识点在接下来的内容将会跟大家讲解。
#include "SList.h"
// 申请节点
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
// 打印链表
void SListPrint(SLTNode* phead)
{
//assert(phead);
SLTNode* cur = phead;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
// 销毁链表
void SListDestory(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
// 头插数据
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
// 尾插数据
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
// 链表为空
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
// 找尾节点
SLTNode* tail = *pphead;
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
}
}
// 尾删数据
void SListPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);// 判断是否为空链表
// 只有一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;
SLTNode* prev = NULL;
while (tail->next)
{
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);
assert(*pphead); // 判断是否为空链表
SLTNode* newHead = (*pphead)->next; // 新的头节点
free(*pphead); // 释放旧的头节点
*pphead = newHead;
}
// 查找数据
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
// 循环遍历链表
while (cur)
{
if (cur->data == x)
{
return cur; // 找到了
}
cur = cur->next; // 继续往后找
}
return NULL; // 没找到
}
// 在pos位置之前插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
// 头插
if (*pphead == pos)
{
SListPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
assert(prev);
// 暴力检查,如果prev为空,那么就说明pos不在链表中,参数pos传错了
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
// 在pos位置之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
// 删除pos位置的数据
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
// 头删数据
if (*pphead == pos)
{
SListPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
assert(prev);
// 暴力检查,如果prev为空,那么就说明pos不在链表中,参数pos传错了
}
prev->next = pos->next;
free(pos);
}
}
// 删除pos位置后面的数据
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
// pos位置执行NULL
if (pos->next == NULL)
{
return;
}
else
{
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
}
以上就是SList.c
源文件实现的函数接口,大家可以先看一下,在下面再来详细讲解每一个函数接口的实现。
申请节点
用来存储数据的节点是在插入数据时,一个一个地向堆区申请空间的。如果申请节点失败,那就直接结束程序,没有必要继续往下执行代码了。如果申请节点成功,那么
newnode->data = x, newnode->next = NULL
,最后将newnode
的值返回。
// 申请节点
SLTNode* BuySLTNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
打印链表
打印链表就是利用while
循环将整个链表的数据打印出来。因为每个节点都存储着数据和下一个节点的地址,所以我们可以通过该地址找到下一个节点,依次类推就能遍历整个链表了。所以需要定义一个指针SLTNode* cur = head
,当cur = NULL
时,遍历链表结束。
// 打印链表
void SListPrint(SLTNode* phead)
{
//assert(phead); //不需要断言phead
SLTNode* cur = phead;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
销毁链表
可以发现销毁链表函数的参数是二级指针
SLTNode** pphead
,即头指针SLTNode* plist
的地址,该地址是不可能为空指针NULL
的,所以要对pphead
进行断言。销毁链表后,plist
的值要置为NULL
。如果函数的参数是一级指针SLNode* phead
的话,将无法将plist
的值置为NULL
。因为形参只是实参的一份临时拷贝,对形参的修改不会影响实参。
如果还是不能理解的话,请看下面的例子:
#include
//交换x、y的值
void Swap1(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
//交换px、py的值
void Swap2(int** ppx, int** ppy)
{
int* tmp = *ppx;
*ppx = *ppy;
*ppy = tmp;
}
int main()
{
int x = 10;
int y = 20;
int* px = &x;
int* py = &y;
printf("x:%d y:%d\n", x, y);
Swap1(&x, &y);
printf("x:%d y:%d\n", x, y);
printf("px:%p py:%p\n", px, py);
Swap2(&px, &py);
printf("px:%p py:%p\n", px, py);
return 0;
}
从上面的例子可以看出,如果想要改变int
类型变量的值,函数的参数就要设置为int*
;如果要改变int*
类型变量的值,函数的参数就要设置为int**
。因为插入和删除数据都有可能影响头节点,所以要传二级指针SLTNode** pphead
。只有传二级指针SLTNode** pphead
,才能够改变头指针SLTNode* plist
的值。
// 销毁链表
void SListDestory(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
头插数据
1.申请新节点
newnode
2.新节点指向原来的头节点newnode->next = *pphead
3.改变头节点*pphead = newnode
// 头插数据
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
尾插数据
1.申请新节点
newnode
2.当链表为空时,此时的尾插数据为头插数据。需要改变头节点*pphead = newnode
3.当链表不为空时,需要找到尾结点tail,原来的尾结点指向新节点tail->next = newnode
,这样节点newnode
就成为新的尾结点了
// 尾插数据
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySLTNode(x);
// 链表为空
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
// 找尾节点
SLTNode* tail = *pphead;
while (tail->next)
{
tail = tail->next;
}
tail->next = newnode;
}
}
尾删数据
1.对
*pphead
进行断言,判断链表是否为空链表
2.当(*pphead)->next == NULL
时,链表只有一个节点。此时尾删数据为头删数据。需要先释放节点free(*pphead)
,再改变头节点*pphead = NULL
3.当(*pphead)->next != NULL
时,链表有多个节点。此时需要找到尾结点tail
和尾结点的上一个节点prev
,先释放尾结点free(tail)
,再让尾结点的上一个节点成为新的尾结点prev->next = NULL
// 尾删数据
void SListPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);// 判断是否为空链表
// 只有一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;
SLTNode* prev = NULL;
while (tail->next)
{
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;
}
}
头删数据
1.判断链表是否为空链表
2.保存新的头节点newHead = (*pphead)->next
3.是否旧的头节点free(*pphead)
4.改变头节点*pphead = newHead
// 头删数据
void SListPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead); // 判断是否为空链表
SLTNode* newHead = (*pphead)->next; // 新的头节点
free(*pphead); // 释放旧的头节点
*pphead = newHead;
}
查找数据
利用
while
循环遍历链表,如果有节点的数据等于要查找的数据x
,就返回节点的地址cur
;如果在链表中找不到x
,就返回空指针NULL
。
// 查找数据
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
// 循环遍历链表
while (cur)
{
if (cur->data == x)
{
return cur; // 找到了
}
cur = cur->next; // 继续往后找
}
return NULL; // 没找到
}
在pos
位置之前插入数据
1.对
pos
位置进行断言
2.当*pphead == pos
时,此时的插入数据为头插数据
3.当*pphead != pos
时,利用while
循环pos
位置的前一个位置prev
,申请新节点newnode
,插入数据prev->next = newnode, newnode->next = pos
4.注意:需要在while
循环中对prev
进行断言assert(prev)
。如果prev
为空,那么就说明pos
不在链表中,参数pos
传错了
// 在pos位置之前插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
// 头插
if (*pphead == pos)
{
SListPushFront(pphead, x);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
assert(prev);
// 暴力检查,如果prev为空,那么就说明pos不在链表中,参数pos传错了
}
SLTNode* newnode = BuySLTNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
在pos
位置之后插入数据
申请新节点
newnode
,插入数据newnode->next = pos->next, pos->next = newnode
(特别要注意修改的顺序)。
// 在pos位置之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySLTNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
删除pos
位置的数据
1.当
*pphead == pos
时,此时的删除数据为头删数据,可以调用头删函数SListPopFront
2.当*pphead != pos
时,利用while
循环找到pos
位置的前一个位置prev
。删除数据prev->next = pos->next, free(pos)
3.注意:需要在while
循环中对prev
进行断言assert(prev)
。如果prev
为空,那么就说明pos
不在链表中,参数pos
传错了
// 删除pos位置的数据
void SListErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
// 头删数据
if (*pphead == pos)
{
SListPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
assert(prev);
// 暴力检查,如果prev为空,那么就说明pos不在链表中,参数pos传错了
}
prev->next = pos->next;
free(pos);
}
}
删除pos
位置之后的数据
1.当
pos->next == NULL
时,直接返回return
2.保存pos
位置的下一个位置next
,删除数据pos->next = next->next, free(next)
// 删除pos位置之后的数据
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
// pos位置执行NULL
if (pos->next == NULL)
{
return;
}
else
{
SLTNode* next = pos->next;
pos->next = next->next;
free(next);
}
}
Test.c
源文件主要负责测试函数的功能实现是否正确,有没有BUG
。需要提醒大家的一件事,学习数据结构不太需要写菜单,没什么必要。重点的是掌握该结构如何实现增删查改的功能。
#include "SList.h"
#include "SList.h"
// 测试头插、头删
void TestSList1()
{
SLTNode* plist = NULL;
SListPushFront(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 3);
SListPushFront(&plist, 4);
SListPrint(plist); // 4->3->2->1->NULL
SListPopFront(&plist);
SListPrint(plist); // 3->2->1->NULL
SListPopFront(&plist);
SListPrint(plist); // 2->1->NULL
SListPopFront(&plist);
SListPrint(plist); // 1->NULL
SListPopFront(&plist);
SListPrint(plist); // NULL
SListDestory(&plist);
}
// 测试尾插、尾删
void TestSList2()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPrint(plist); // 1->2->3->4->NULL
SListPopBack(&plist);
SListPrint(plist); // 1->2->3->NULL
SListPopBack(&plist);
SListPrint(plist); // 1->2->NULL
SListPopBack(&plist);
SListPrint(plist); // 1->NULL
SListPopBack(&plist);
SListPrint(plist); //NULL
SListDestory(&plist);
}
// 测试查找、插入
void TestSList3()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPushBack(&plist, 2);
SListPushBack(&plist, 2);
SListPrint(plist); // 1->2->3->4->2->2->NULL
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);
}
pos = SListFind(plist, 3);
if (pos)
{
pos->data *= 10; // 修改
printf("该数据已修改为原来的10倍\n");
}
else
{
printf("链表中没有该数据\n");
}
SListPrint(plist); // 1->2->30->4->2->2->NULL
pos = SListFind(plist, 2);
if (pos)
{
SListInsert(&plist, pos, 20);
}
SListPrint(plist); // 1->20->2->30->4->2->2->NULL
pos = SListFind(plist, 1);
if (pos)
{
SListInsert(&plist, pos, 10);
}
SListPrint(plist); // 10->1->20->2->30->4->2->2->NULL
SListDestory(&plist);
}
// 测试删除
void TestSList4()
{
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushBack(&plist, 2);
SListPushBack(&plist, 3);
SListPushBack(&plist, 4);
SListPrint(plist); // 1->2->3->4->NULL
SLTNode* pos = SListFind(plist, 4);
SListInsertAfter(pos, 40);
SListPrint(plist); //1->2->3->4->40->NULL
pos = SListFind(plist, 3);
SListErase(&plist, pos);
SListPrint(plist); // 1->2->4->40->NULL
pos = SListFind(plist, 4);
SListEraseAfter(pos);
SListPrint(plist); // 1->2->4->NULL
SListDestory(&plist);
}
int main()
{
//TestSList1();
//TestSList2();
//TestSList3();
TestSList4();
return 0;
}
为了解决动态顺序表的问题,我们采取了单向链表的结构。但是,单向链表这种结构只适合头插和头删(时间复杂度为
O(1)
)。能够真正实现任意位置的高效插入删除,还需要学习双向链表。这种结构将会在下一篇博客中讲解,敬请期待。
要求时间复杂度为O(1)
,删除pos
位置的数据,能实现吗?
在上面的讲解中,删除pos
位置的数据,需要找到它的前一个位置prev
才能删除,那么这样时间复杂度就是O(N)
了。那有没有一种方式能做到时间复杂度为O(1)
呢?其实是有的,请看下图:
要求时间复杂度为O(1)
,在pos
位置之前插入数据,能实现吗?
在上面的讲解中,在pos
位置之前插入数据,需要找到pos位置的前一个位置prev
才能将新节点newnode
插入到链表中。这样的解法时间复杂度也是O(N)
。那有没有一种方式能做到时间复杂度为O(1)
呢?其实也是有的,请看下图:
为了解决顺序表插入删除时间复杂度高、扩容的问题,我们引入了单向链表的结构并实现其函数接口。不过单向链表只有头插头删时间复杂度为
O(1)
,其他位置的插入删除都是O(N)
。想要实现高效地插入删除,就要学习双向链表的结构了,这个会在下一篇博客讲解。以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家啦!❣️