目录
1:前言
链表是继顺序表后数据结构又一线性表,学好链表能为后续深入学习数据结构打好基础。
顺序表与链表相比既有优点又有缺点,那么让我们来一起看看顺序表比起链表有哪些优点和缺点吧
优点:
顺序表在访问表内的数据时可以直接采用下标访问的方式,比起链表的遍历访问来说更加快捷,方便。
缺点:
一:如果顺序表的空间不足需要增容的话,会付出一定的性能消耗,其次可能存在一定的空间浪费。
二:在顺序表的头部或者中部插入数据效率非常低----O(N).
2:链表的概念及结构
链表的逻辑结构就像这样 :
![](https://i-blog.csdnimg.cn/blog_migrate/d15fd4fe24a1f3149a9ba42635390483.png)
![](https://i-blog.csdnimg.cn/blog_migrate/009c865235f72636d5839e4127d6fd9d.png)
链表按内部结构的不同可以分为八大类:
1:单向,双向
2:带头,不带头
3:循环,不循环
3:单链表的实现
1:SListCreat(链表的创建)
typedef int SLTDateType; //为了在修改链表内部存储数据类型时更加方便
//创建一个结构体,其中包含节点的值以及指向下一个节点的地址.
typedef struct slistnode {
SLTDateType data;
struct slistnode* next;
}SLTNode;
2:SListPrint(链表的打印)
那我们要怎么打印整个链表呢?
我们记链表的头节点为phead,当然在使用前这个头节点为空
SLTNode* phead=NULL;
//链表的打印
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead; //创建一个节点来代表当前节点,通过这个节点遍历整个链表
while (cur != NULL) { //结束标志
printf("%d ", cur->data); //打印当前节点的值
cur = cur->next; //继续向后遍历下一个节点
}
}
3:SListPushBack(链表的尾插)
依旧是这样一个链表,如果我们想要在4节点后面插入一个新的节点,我们要怎么做呢?
首先我们要遍历链表找到尾节点,然后把新节点插入到尾节点之后即可(尾节点的next=新节点)
也许你会这样去写你的代码
//链表的尾插
void SListPushBack(SLTNode** phead,SLTDateType x) //这里传入头节点的二级指针是因为形参的改变不会影响实参,要想改变实参的值我们要传入实参的地址。
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); //创建一个新的节点来存放x的值
newnode->data = x;
newnode->next = NULL;
SLTNode* tail = *phead;
while (tail->next != NULL) { //在链表的尾部插入新生成的节点
tail = tail->next;
}
tail->next = newnode;
}
但是我们运行程序之后发现编译器会给我们报错
我们可以看到编译器告诉我们tail节点是NULL,这时你也许会醒悟:最初时phead节点为空节点,我们不能访问空节点的next,这样是错误的,所以经过更正后的代码变成了这样:
//链表的尾插
void SListPushBack(SLTNode** phead,SLTDateType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
if (*phead == NULL) { //当phead为空时,我们直接把新创建的节点赋给phead
*phead = newnode;
return;
}
SLTNode* tail = *phead;
while (tail->next != NULL) {
tail = tail->next;
}
tail->next = newnode;
}
我们不妨尾插几个数后再打印一下看一下效果
void TestSList()
{
SLTDateType x;
SLTNode* phead = NULL;
SListPushBack(&phead,1); //在链表中依次尾插1,2,3,4,5
SListPushBack(&phead, 2);
SListPushBack(&phead, 3);
SListPushBack(&phead, 4);
SListPushBack(&phead, 5);
SListPrint(phead);
printf("\n");
}
调用SListPrint函数,看看结果
4:SListPushFront(链表头插)
头插相比于尾插更加容易,我们只需要让新节点的next=头节点,再把这个新节点的值赋给头节点即可。
代码如下
//链表的头插
void SListPushFront(SLTNode** phead, SLTDateType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); //创建一个新节点来存放要插入的值
newnode->data = x;
newnode->next = *phead;
*phead = newnode; //因为头节点发生了改变,所以最后
要更新头节点的值
}
5:SListPopBack(链表的尾删)
我们想要删除链表的尾部
我们可以用一个节点遍历链表,找到链表的尾,让尾节点的前一个节点的next=NULL即可
所以我们要用到两个节点,cur(用于寻找链表尾节点),pre(用于记录尾节点的前一个节点)
代码如下
//链表的尾删
void SListPopBack(SLTNode** phead) {
SLTNode* cur = *phead;
SLTNode* pre = *phead;
while (cur->next != NULL) { //如果cur不是尾节点,那么我们要用pre记录当前的cur节点,并且
将cur继续向后遍历
pre = cur;
cur = cur->next;
}
pre->next = NULL; //找到尾节点之后我们将pre节点的next置空即可
}
然后聪明的你就会发现一些特殊情况下该程序的问题了
1:链表无节点(cur==NULL时cur->next会出错误)
2:链表只有一个节点(不能把phead节点删去)
所以经过修改我们得到了:
//链表的尾删
void SListPopBack(SLTNode** phead) {
assert(*phead); //断言,如果phead为空,那么终止程序并报错
if ((*phead)->next == NULL) { //如果只有一个节点,那么直接将phead变成NULL
*phead = NULL;
return;
}
SLTNode* cur = *phead;
SLTNode* pre = *phead;
while (cur->next != NULL) {
pre = cur;
cur = cur->next;
}
pre->next = NULL;
}
6:SListFind(查找链表中某个节点的位置)
给定某个节点的值,你能否在链表中查找到该节点的位置并返回它呢?
应该很容易吧,我们只需要遍历链表即可,那么我们直接放上代码
//链表的查找
SLTNode* SListFind(SLTNode* phead, SLTDateType x) { //x是待查找的节点的data值
SLTNode* cur = phead;
while (cur) {
if (cur->data == x) {
return cur;
}
else {
cur = cur->next;
}
}
return NULL; //如果在链表遍历完都没找到符合条件的节点,
那么没找到,返回空指针。
}
7:SListInsert(链表在pos位置前插入指定值)
先查找指定节点的位置
SLTNode* pos = SListFind(phead, 4); //查找链表中结点值为4的节点的位置
if (pos) {
SListInsert(&phead, pos, 100); //执行插入操作
}
我们在查找到指定节点的位置之后就可以进行插入,逻辑图如图
这样我们就可以完成指定位置节点的插入
照着这样我们可以写出如下代码
//链表指定位置的插入,我们先通过SListFind函数来找到指定位置pos,再将其传入此函数进行插入
void SListInsert(SLTNode** phead, SLTNode* pos, SLTDateType x) {
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
SLTNode* cur = *phead; //遍历链表来寻找pos节点
while (cur->next!=pos) {
cur = cur->next;
}
newnode->next = pos; //插入新节点
cur->next = newnode;
}
但是我们很快就会发现这样的程序依旧会出问题,那就是当pos是链表的第一个节点时,程序会给我们报错,我们可以看看是什么原因导致的
所以我们要对这种特殊情况进行单独处理,修改过后的代码如下
//链表的插入
void SListInsert(SLTNode** phead, SLTNode* pos, SLTDateType x) {
if (pos == *phead) { //在pos处于头节点时,相当于对链表进行头插,我们可以
直接调用之前写过的头插函数
SListPushFront(phead, x);
return;
}
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
newnode->data = x;
newnode->next = NULL;
SLTNode* cur = *phead;
while (cur->next!=pos) {
cur = cur->next;
}
newnode->next = pos;
cur->next = newnode;
}
我们可以随便插入一个数试试结果
SLTNode* pos = SListFind(phead, 4); //在1,2,3,4,5的链表中节点值为4的节
点前插入100
if (pos) {
SListInsert(&phead, pos, 100);
}
SListPrint(phead);
8:SListErase(链表指定位置删除节点)
SLTNode* pos = SListFind(phead, 4);
if (pos) {
SListInsert(&phead, pos, 100);
}
pos = SListFind(phead, 4);
if (pos) {
SListErase(&phead, pos);
}
SListPrint(phead);
逻辑图如下
那我们仍然要用到链表的查找函数来找到指定节点,然后进行删除
//链表的删除
void SListErase(SLTNode** phead, SLTNode* pos) {
SLTNode* cur = *phead;
while (cur->next != pos) {
cur = cur->next;
}
cur->next = pos->next;
free(pos);
}
但同样和insert函数一样,当pos在头节点位置时我们程序会出问题
我们要做出修改
//链表的删除
void SListErase(SLTNode** phead, SLTNode* pos) {
if (pos == *phead) { //pos处于头节点时,直接让*phead等于*phead的next即可
*phead = (*phead)->next;
return;
}
SLTNode* cur = *phead;
while (cur->next != pos) {
cur = cur->next;
}
cur->next = pos->next;
free(pos);
}
运行结果
所以说在平时写程序时都要考虑自己的代码能否完成某些边界条件的输入输出,如果完不成要及时进行修改。
后续的更多链表内容还会继续更新,以及一些链表的例题。
谢谢大家的阅读!!!!感谢关注点赞!!!