前言
hello,大家好呀,我是Humble 在之前的两篇博客,我们学完了数据结构中的顺序表,还对它进行了一个应用,做了一个通讯录的小项目
那今天我们再来学习一个新的数据结构——链表
引入
我们来回忆一下顺序表
对于顺序表,我们发现它有下面的这些问题
1.中间/头部的插入删除,时间复杂度为O(N)
2.增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗
3.增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到
200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间
思考:如何解决以上问题呢?有没有以一种数据结构,它可以解决顺序表的这些问题呢?
这就是我们今天要讲的链表了
链表的概念及结构
链表在物理存储结构上是非连续、非顺序的存储的、
其数据元素的逻辑顺序是通过链表中的指针链接次序实现的
而与顺序表不同的是,链表是由节点组成的
节点的组成主要有两个部分:
1.当前节点要保存的数据
2.保存下一个节点的地址(指针变量)
变量来保存下一个节点位置才能从当前节点找到下一个节点
结合结构体的知识,我们可以给出每个节点对应的结构体代码:
struct SListNode
{
int data; //节点数据,我们假设当前保存的节点为整型
struct SListNode* next; //指针变量⽤保存下⼀个节点的地址
};
当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数
据,也需要保存下一个节点的地址
所以,当我们想要从第一个节点走到最后一个节点时,只需要在前一个节点拿上下一 个节点的地址就可以了(有点绕,请耐心理解哦)
那么,给定的链表结构中,我们来实现一下节点从头到尾的打印吧~
我们在创建一个SList 的工程表示单链表
然后创建3个文件,分别是我们的SList.h 头文件 ,SList.c源文件以及测试文件test.c
(这个大家应该已经很熟悉了吧)
在三个文件中,我们分别去实现各自的职能
SList.h
#pragma oncetypedef int SLTDataType;typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;void SLTPrint(SLTNode* phead);//打印
SList.c
#include"SList.h"void SLTPrint(SLTNode* phead)
{SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
test.c
#include "SList.h"void SlistTest01() {//一般我们不会这样去创建链表,这里只是为了给大家展示链表的打印SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));node1->data = 1;SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));node2->data = 2;SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));node3->data = 3;SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));node4->data = 4;node1->next = node2;node2->next = node3;node3->next = node4;node4->next = NULL; SLTNode* plist = node1;SLTPrint(plist); //打印1->2->3->4->NULL
}int main()
{SlistTest01();return 0;
}
我们来测试一下,按照我们的想法,应该打印1->2->3->4->NULL
运行结果:
单链表的实现
找到了链表的打印,我们就来实现链表的各个功能吧
链表的尾插
这要分两种情况来讨论
1.链表不为空
2.链表为空
先画张图来辅助理解一下:
假设我们要在链表插入 元素4
下面我们来写尾插STLPushBack的代码:
void SLTPushBack(SLTNode** pphead, SLTDataType x) //注意这里pphead是二级指针,用**
{assert(pphead);SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));newnode->data = x;newnode->next = NULL;//链表为空,新节点作为pheadif (*pphead == NULL) {*pphead = newnode;return;}//链表不为空,找尾节点SLTNode* ptail = *pphead;while ((ptail->next) != NULL) //遍历{ptail = ptail->next;}//遍历完之后ptail就是尾节点ptail->next = newnode; //完成尾插
}
下面我们来测试一下
我们在test.c中这样写:
void SlistTest02()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1); //我们要把plist指针的地址传过去,这个很重要!SLTPrint(plist); //预计结果1->NULL
}int main()
{SlistTest02();return 0;
}
运行一下:
当然,因为我们下面的操作都要设计申请节点,每次都要写:
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
我们干脆就再写一个函数,之后直接调用就行
这样代码就会变成这样
SLTNode* SLTBuyNode(SLTDataType x) //申请新节点
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));newnode->data = x;newnode->next = NULL;return newnode;}void SLTPushBack(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = SLTBuyNode(x);//链表为空,新节点作为pheadif (*pphead == NULL) {*pphead = newnode;return;}//链表不为空,找尾节点SLTNode* ptail = *pphead;while (ptail->next){ptail = ptail->next;}//ptail就是尾节点ptail->next = newnode;
}
接下来我们来看一下头插SLTPushFront:
它同样分2种情况,但它们的代码是一样的,所以就不用分了
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);SLTNode* newnode = SLTBuyNode(x);newnode->next = *pphead;*pphead = newnode;
}
测试一下:
void SlistTest02()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPushFront(&plist, 5); SLTPushFront(&plist, 6); SLTPushFront(&plist, 7);SLTPrint(plist); //期望结果为:7->6->5->1->2->3->4->NULL
}int main()
{SlistTest02();return 0;
}
运行结果如下:
接下来看一下尾部删除SLTPopBack吧~
既然要删除,我们要保证链表不为空,所以相比前面的这几种操作,它还要加上
assert(*pphead);//表示链表不能为空
此外,要分链表是否只有一个节点,即是否有前驱节点这2中情况
void SLTPopBack(SLTNode** pphead)
{assert(pphead);assert(*pphead);//保证链表不能为空//链表只有一个节点if ((*pphead)->next == NULL) {free(*pphead);*pphead = NULL;return;}//链表有多个节点SLTNode* ptail = *pphead;SLTNode* prev = NULL;while ((ptail->next)!=NULL){prev = ptail;ptail = ptail->next;}prev->next = NULL;//销毁尾结点free(ptail);ptail = NULL;
}
我们也来测试一下:
void SlistTest02()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPopBack(&plist);SLTPrint(plist); //预期结果为1->2->3->NULL}int main()
{SlistTest02();return 0;
}
运行结果如下:
接下来看一下头部删除SLTPopFront吧~
这个也很简单,我们直接上代码~
//头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead);//链表不能为空assert(*pphead);//让第二个节点成为新的头//把旧的头结点释放掉SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}
接下来我们也是测试一下
void SlistTest03()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);//头删SLTPopFront(&plist);SLTPrint(plist); //2->3->4->NULLSLTPopFront(&plist);SLTPrint(plist); //3->4->NULL
}int main()
{SlistTest03();return 0;
}
运行结果:
好,我们已经实现了头部和尾部的插入和删除的操作,接下来我们来实现一下查找的操作~
//查找
SLTNode* SLTFind(SLTNode** pphead, SLTDataType x)
{assert(pphead);//遍历链表SLTNode* pcur = *pphead;while (pcur) //等价于pcur != NULL{if (pcur->data == x) {return pcur;}pcur = pcur->next;}//没有找到return NULL;}
接下来测试一下:
void SlistTest03()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTNode* FindRet = SLTFind(&plist,1); //1 在链表中,可以找到if (FindRet) {printf("找到了!\n");}else {printf("未找到!\n");}}int main()
{SlistTest03();return 0;
}
运行结果:
接下来我们看一下在指定位置插入数据~
它分为2种,在指定位置之前插入和在指定位置之后插入数据
先看在指定位置之前插入数据吧
它要分要插入的位置是头节点和不是头节点2种情况讨论哦
实现代码如下:
//在指定位置之前插入数据void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(*pphead);//链表 不能为空!assert(pos);SLTNode* newnode = SLTBuyNode(x);//pos刚好是头结点if (pos == *pphead) {//头插SLTPushFront(pphead, x);return;}//pos不是头结点的情况SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = newnode;newnode->next = pos;}
好,我们来测试一下~
void SlistTest03()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTNode* FindRet = SLTFind(&plist,1);SLTInsert(&plist, FindRet, 100); SLTPrint(plist);//预期是100->1->2->3->4->NULL}int main()
{SlistTest03();return 0;
}
运行结果:
接下来我们再看一下在指定位置之后插入数据SLTInsertAfter吧~
这个实现起来要比在指定位置之前插入要简单
我们看代码:
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next; //特别注意一下这里的顺序哦~pos->next = newnode;}
写完后也测试一下:
void SlistTest03()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTNode* FindRet = SLTFind(&plist, 1);SLTInsertAfter(FindRet, 100); SLTPrint(plist);//预期是1->100->2->3->4->NULL}int main()
{SlistTest03();return 0;
}
测试一下:
那么,插入讲完了,我们接下来再看一下删除操作
分别是删除pos节点以及删除pos之后的节点
先看一下删除pos节点 的情况吧~
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(*pphead);assert(pos);//pos刚好是头结点,没有前驱节点,执行头删if (*pphead == pos) {//头删SLTPopFront(pphead);return;}//pos不是头结点SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}
下面来测试一下:
void SlistTest03()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTNode* FindRet = SLTFind(&plist, 4);SLTErase(&plist, FindRet);SLTPrint(plist);//预期是1->2->3->NULL}int main()
{SlistTest03();return 0;
}
运行结果:
再看一下删除pos之后的节点吧~
下面是实现的代码~
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{assert(pos);//pos->next不能为空assert(pos->next);SLTNode* del = pos->next; //定义一个中间的变量用来保存pos->next = pos->next->next;free(del);del = NULL;}
下面进行测试:
void SlistTest03()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTNode* FindRet = SLTFind(&plist, 2);SLTEraseAfter(FindRet);SLTPrint(plist);//预期是1->2->4->NULL}int main()
{SlistTest03();return 0;
}
好,最后我们来看一下链表的销毁操作吧~
//销毁链表
void SListDesTroy(SLTNode** pphead)
{assert(pphead);assert(*pphead);SLTNode* pcur = *pphead; //pur依旧是作为临时变量,用于保存~while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;}
关于链表的销毁,我们可以通过调试来观察,这里就不再演示了,大家可以自己测试一下~
好,到这,我们就把单链表的实现给讲完了~(鼓掌鼓掌)
好,那么这里又出现了一个新的问题,我们在这里花了这么多精力说了单链表的各种操作,那么链表究竟有多少种类呢?它与单链表又是什么关系呢?
接下来,我们就来说说链表的分类
链表的分类
不知道大家有没有想过为什么我创建的这个工程名为SList?
其实它是Single Linked list 的简写,也就是单链表的意思
我们上面的对链表的各种插入,删除都是对单链表进行操作的
那其实 链表的种类有很多,单链表的全称就是不带头单向不循环链表
我们在平时为了方便就称为单链表了~
既然有不带头就有带头的,由单向也就有双向的,有不循环的也就有循环的
如此这般三三组合,其实就可以推出链表的种类有2*2*2=8种
各个种类的关系如图:
看到这么多种类的链表,大家也不要太焦虑,去想单单一种类型的单链表就学了这么久,更何况还有7种.....
其实,我们实际中最常用只有两种结构:单链表 和 带头双向循环链表(简称双向链表),后者我们会在之后的博客中进行介绍与分享的~
最后我们在来看一下单链表 和 双向链表各自的一些特点吧~
1.单链表(不带头单向不循环链表):结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等
这种结构也是在笔试面试中出现很多
2.双向链表(带头双向循环链表):结构最复杂,一般用在单独存储数据
实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,所以实现反而简单了,这个我们代码实现了就知道了,这里只要先大致有一个印象就行,不必担心~
结语
好了,今天关于链表的分享就到这里了
在学习编程的道路上Humble与各位同行,加油吧各位!
最后希望大家点个免费的赞或者关注吧(感谢感谢),也欢迎大家订阅我的专栏
让我们在接下来的时间里一起成长,一起进步吧!