上一篇中,我们已经了解过了顺序表了,但是,顺序表有哪些不便的地方?
一.顺序表的缺陷问题:
1. 中间/头部的插入删除,时间复杂度为O(N)
头插N个数据时间复杂度:O(N^2)
需要挪动数据,效率低下
解释:因为头插要将原本所有的数都向后移动一位
尾插N个数据时间复杂度:O(N)
所以尽量避免去使用头插。2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
对此,我们为了解决上述问题,链表就大大的解决了这些困扰。
正文开始:
链表
介绍
1.链表的概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
可以先简单理解成下图:
物理结构
实实在在数据在内存中的变化
下面的是逻辑结构,为了方便理解,形象化画出来的‘
原理:上一个节点要存下一个节点的地址
我们可以看出它的地址相同的部分知道,它们是一个又一个地连在一起。
后面画图更简便就成了下图:
构造:
typedef int SLTDataType; 为了后面一眼就能看到它的类型的意思typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;
解读:struct SListNode* next;通过指针链接起来。
首先,我们先区分下面可能出现的问题吧:
指针类型是什么?
结构体指针
结构体肯定不能嵌套结构体
相当于自己里面一个我自己,这就造成无穷套娃了
但是可以结构体里面嵌套一个结构体指针,只是这个类型是结构体
是结构体
结构体没有执行的概念,函数才讲执行
因为编译的寻找规则:从上面找,
struct SListNode* next
不能:SListNode* next,虽然你在下面自定义了,但编译器只能往上找啊,找不到,就显示错误了。
初始化问题
对比:我们在顺序表时,为什么要进行初始化呢?
因为在顺序表时,我们是实实在在地开辟了一个空间(已经开辟好了的),来供后续使用,因此需要来进行初始化这块空间。
但是在链表中呢?
在链表中,我们是当要使用了,才会链接成新的结点,此时新结点的空间才会开辟使用,这时候就不用额外初始化了。
打印部分:
我们知道顺序表是通过数组历遍打印出来的。
如:
void SLPrint(SL* ps)
{assert(ps);for (int i = 0; i < ps->sz; i++){printf("%d ", ps->a[i]);}printf("\n");
}
链表中打印与顺序表不同:
void SLTPrint(SLTNode* phead)
{SLTNode* cur = phead;//while (cur->next != NULL)//while(cur != NULL) 这几种都是一样的意思while (cur){printf("%d->", cur->data);cur = cur->next;这里可不能使用cur++;}printf("NULL\n");
}
易错:
链表打印当中(如左图),想要从1->2,可不能直接cur++。
因为链表的每一个节点都是单独malloc出来的,你只有一次mallooc很多个(如10个),能保证它的空间是连续的才可,而链表不能保证它连续,
有人又说我们让它连续不就好了吗?
你想想这样的话它不就变成了数组了吗?顺序表因此!
要cur指向2做法:就cur=cur->next
解释:cur是一个结构体指针,加->这个符号就是结构体的内容,next是结构体成员
next是下一个节点的地址,赋值后就去到了2的地址位置,依次去到最后位置
注意:
这里while循环是不能:while(cur->next !=NULL)
这里到了最后一个数据时,为空了,最后一个数据就不能打印了
所以应该是while(cur!=NULL),也可以while(cur),因为0就是假,非0就是真
空就是0.
同时,我们会发现,为什么,链表的打印部分不用断言,顺序表那边需要断言。这与我们之前学到认为指针,基本都需要断言的认知是错误的:
下面给出解释:
答案:
链表:这个指针是指向第一个存有数据的节点
顺序表,指针指向一个结构体,而数据不是存在结构体上面的
如果有数据,则它存在的是这样一个空间上面(下图)
链表(下图)
对于链表为空,那么phead就为空,但,对于顺序表为空,那么这个指针(指向结构体)不能为空,哪怕这个顺序表一个数据都没有,那么这个结构体也是有的,只是你这个a为不为空也不重要,它到底有没有数据,取决于size是不是0,如0就没有数据,所以有没有空间都不是最重要的
因此!!!我们可以得出:!!所以不要看到指针就写断言!!!!要按照具体需求来判断。
创建新结点
因为,下面写道的尾插,尾删,头插,头删,中间位置插,删都用到新结点的内容,为了让后面的代码更简便,我们在这里创建一个独立的函数,后面直接调用就可以了。
SLTNode* BuySLTNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail");return NULL;}newnode->data = x;newnode->next = NULL;return newnode;
}
图解:将x赋值到newnode中,再将newnode的下一个置空
尾插部分:
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{SLTNode* newnode = BuySLTNode(x);if (*pphead == NULL){*pphead = newnode;}else{// 找尾SLTNode* tail = *pphead;while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}
}
它的过程分析如下:
首先把你要尾插的值放到新的节点;
接着为了后面方便直接找到头节点,所以不使用它本身来找尾。因此重新弄了个tail的指针,开始位置也再*pphead那里
然后,开始正式找尾,以tail的下一个为准,若下一个为空指针,则说明已经找到尾。就跳出循环,把新节点和尾巴连接在一起。若不为空,就把tail跳到tail的下一个,依次判断。
注意易错的点:
链表的连接一定是:上一个节点要存下一个节点的地址
尾插的细节
本质:原尾结点中要存储新的尾结点的地址
while(nur !=NULL)
{
tail=tail->next;
}
tail=newnode 错误的这样就链接不上去了
(图解析看上面)
正确是tail->next !=NULL
tail->next=newnode尾插的时候要改变的是结构体,要改变结构体就得改变结构体的成员,结构体的成员也是一个指针而已,但是有了结构体的指针已经能够改变它的成员了
另外,我们为什么传的是二级指针呢?
我们先了解:
形参的改变不影响实参
非常实用的理解方式:
改变的是int,使用的就是int的指针
改变的是int*,使用的就是int*的地址,int**指针
可以这样理解:
不改变实参就直接传,改变就传地址而我们现在的,就是要改变结构体指针里面的值,所以要双指针。
现在我们来分析分析:
我们可以看出,形参的改变不影响实参,出去了之后就销毁了。就无法回到mian函数
再看我们 如果这样?
通过使用它的地址,解引用保存了,就可以不被因为销毁,而不能达到目的。
有了以上的认识后,我们可以解释本次的问题了:
头插部分:
传二指针的问题,跟上面一样。
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{SLTNode* newnode = BuySLTNode(x);newnode->next = *pphead;*pphead = newnode;
}
现在来分析它的过程:
先把原本的头赋值给新头的next。
接着再将plist指向newnode
尾删部分
void SLTPopBack(SLTNode** pphead)
{// 暴力检查assert(*pphead);// 温柔的检查//if (*pphead == NULL)// return;// 1、只有一个节点// 2、多个节点if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{// 找尾:第一种方法//SLTNode* prev = NULL;//SLTNode* tail = *pphead;//while (tail->next != NULL)//{// prev = tail;// tail = tail->next;//}//free(tail);//tail = NULL;//prev->next = NULL;第二种方法:SLTNode* tail = *pphead;while (tail->next->next != NULL){tail = tail->next;}free(tail->next);tail->next = NULL;}
}
它的过程分析:
先创建一个prev(为NULL),tail(初始在头位置)
接着就是找尾,标志为:tail的下一个是否为0;若是则尾,不是就继续向右移动
最后找到了之后,将tail(即尾巴释放)
除此之外,我们还要释放prev的下一个为空。
为什么呢?有人会问,它不是tail已经为空了吗?
但是,你想一想,你都已经释放掉tail了,你还弄空,还有用吗?这不就相当于下面这种情况了吗?
这就是为什么我们在开始之前,要再定义一个prevx新指针。
释放prev的下一个为空。就成了下面的图解释的那样了。
第二种方法:图解
头删部分
void SLTPopFront(SLTNode** pphead)
{// 暴力检查assert(*pphead);// 温柔的检查//if (*pphead == NULL)// return;SLTNode* first = *pphead;*pphead = first->next;free(first);first = NULL;
}
这部分的原理有了上面的铺垫,也是比较简单的(相对上面的 )就不具体分析了,给下图,应该能够明白的!
查找部分:
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* cur = phead;while (cur){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}
这一部分没有什么好解释的。
插入任意中间的位置部分
其中又分为两种情况:
1.pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pos);assert(pphead);if (pos == *pphead){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;}
}
它的过程分析:
先在原本头位置设定一个prev指针
接着找到pos的前一个位置
然后找到后,就当前prev位置的下一个指针指向newnode
newnode指向pos位置,就完成了中间插。
2.在pos之后插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = BuySLTNode(x);newnode->next = pos->next; | 这两步可不能调乱pos->next = newnode; |
}
为什么不能调乱呢?请看下面
如果调乱了,这第一步就不是把2-3的箭头去掉了吗,那后面的地址(next)就找不到了,连不到一起了 。
由代码量就可以看出来,这种方法更加简单
删除任意中间的位置部分
分为两部分
1.pos位置前删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos);//assert(*pphead);if (*pphead == pos){SLTPopFront(pphead);}else{// 找到pos的前一个位置SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);//pos = NULL;}
}
它的分析过程:
2.pos位置后删除
void SLTEraseAfter(SLTNode* pos)
{assert(pos);assert(pos->next);//SLTNode* del = pos->next;//pos->next = pos->next->next;//free(del);//del = NULL;SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}
它的分析过程图:
最后一个销毁部分
方法一:
void SLTDestory(SLTNode** pphead)
{assert(pphead);SLTNode* cur=*pphead;while(cur){SLTNode* temp=cur->next;free(cur);cur=temp;}*pphead=NULL;
}
方法二:在调用时使用返回方式。
void SLTDestory(SLTNode* phead)
{SLTNode* cur=phead;while(cur){SLTNode* temp=cur->next;free(cur);cur=temp;}
它的过程分析:
易错:这里可不能
至此,所有的就已经结束了。
附上所有的代码:
SLT.C
#define _CRT_SECURE_NO_WARNINGS 1
#include "SLT.h"void SLTPrint(SListNode* phead)
{SListNode* cur = phead;while (cur){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}SListNode* BuySLTNode(SLTDatatype x)
{SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));if (newnode == NULL){perror("malloc fail");return;}newnode->data = x;newnode->next = NULL;return newnode;
}
void SLTPushBack(SListNode** pphead,SLTDatatype x)
{assert(pphead);SListNode* newnode = BuySLTNode(x);if (*pphead == NULL){*pphead = newnode;}else{//找尾SListNode* tail = *pphead;while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}
}//头插
void SLTPushFront(SListNode** pphead, SLTDatatype x)
{assert(pphead);SListNode* newnode = BuySLTNode(x);newnode->next = *pphead;*pphead = newnode;
}//尾删
void SLTPopBack(SListNode** pphead)
{assert(pphead);assert(*pphead);//只有一个节点// 有多个节点if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}//找尾else{SListNode* tail = *pphead;SListNode* prev = NULL;while (tail->next != NULL){prev = tail;tail = tail->next;}free(tail);tail = NULL;prev->next = NULL;}
}//头删
void SLTPopFront(SListNode** pphead)
{assert(pphead);SListNode* frist = *pphead;*pphead = frist->next;free(frist);frist = NULL;}//查找
SListNode* SLTFind(SListNode* phead, SLTDatatype x)
{SListNode* cur = phead;while (cur){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}//内插
//在pos之前插
void SLTInsert(SListNode** pphead, SListNode* pos, SLTDatatype x)
{assert(pos);assert(pphead);if (pos == *pphead){SLTPushFront(pphead, x);}else{//找到pos之前的数字SListNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}//创建一个新的空间SListNode* newnode = BuySLTNode(x);prev->next = newnode;newnode->next = pos;}
}//pos位置删除
void SLTErase(SListNode** pphead, SListNode* pos)
{assert(pos);assert(pphead);if (*pphead == pos){SLTPopFront(pphead);}else{//找到pos之前的位置SListNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}}
//pos后面插入
void SLTInsertAfter( SListNode* pos, SLTDatatype x)
{assert(pos);SListNode* newnode = BuySLTNode(x);newnode->next = pos->next;pos->next = newnode;}//pos之后删除
void SLTEarseAfter(SListNode* pos)
{assert(pos);assert(pos->next);SListNode* del = pos->next;pos->next = del->next;free(del);del = NULL;}
void SLTDestory(SLTNode** pphead)
{assert(pphead);SLTNode* cur=*pphead;while(cur){SLTNode* temp=cur->next;free(cur);cur=temp;}*pphead=NULL;
}
头文件SLT.h
#pragma once
#include <stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int SLTDatatype;typedef struct SListNode
{SLTDatatype data;struct SListNode* next;
}SListNode;void SLTPrint(SListNode* phead);
//ͷ
void SLTPushBack(SListNode** phead, SLTDatatype x);
void SLTPushFront(SListNode** pphead, SLTDatatype x);
void SLTPopBack(SListNode** pphead);
void SLTPopFront(SListNode** pphead);
SListNode* SLTFind(SListNode* phead, SLTDatatype x);
void SLTInsert(SListNode** pphead, SListNode* pos, SLTDatatype x);
void SLTErase(SListNode** pphead, SListNode* pos);
void SLTInsertAfter( SListNode* pos, SLTDatatype x);
void SLTEarseAfter(SListNode* pos);
test.c:这一部分只是用做测试,没太大作用。
#define _CRT_SECURE_NO_WARNINGS 1
#include "SLT.h"
Test1()
{SListNode* s=NULL;SLTPushBack(&s, 1);SLTPrint(s);SLTPushBack(&s, 2);SLTPrint(s);SLTPushBack(&s, 3);SLTPrint(s);SLTPushBack(&s, 4);SLTPrint(s);}
Test2()
{SListNode* s = NULL;SLTPushBack(&s, 1);SLTPushBack(&s, 2);SLTPushBack(&s, 3);SLTPushBack(&s, 4);SLTPrint(s);SLTPushFront(&s, 5);SLTPushFront(&s, 6);SLTPushFront(&s, 7);SLTPushFront(&s, 8);SLTPrint(s);SLTPopBack(&s);SLTPrint(s);SLTPopBack(&s);SLTPrint(s);SListNode* ret = SLTFind(s, 4);SLTInsert(&s, ret, 9);SLTPrint(s);}
Test3()
{SListNode* s = NULL;SLTPushBack(&s, 1);SLTPushBack(&s, 2);SLTPushBack(&s, 3);SLTPushBack(&s, 4);SLTPrint(s);SListNode* ret = SLTFind(s, 4);SLTInsert(&s, ret, 9);SLTPrint(s);}Test4()
{SListNode* s = NULL;SLTPushBack(&s, 1);SLTPushBack(&s, 2);SLTPushBack(&s, 3);SLTPushBack(&s, 4);SLTPrint(s);SListNode* ret = SLTFind(s, 4);//SLTErase(&s, ret);SLTInsertAfter(ret, 6);SLTPrint(s);
}
int main()
{Test4();return 0;
}
今次鸡汤:
对未来的真正慷慨,是把一切都献给现在,
你和我!一起奋斗吧!会越来越好的!