数据结构——顺序表与链表(最全,含代码以及思路讲解)

1.线性表

目录

1.线性表

2.顺序表

1.初始化以及扩容

2.尾插

3.尾删

4.头插

5.头删

6.删除pos位置数据

7.pos之前插入数据

8.查找数据

9.销毁顺序表

3.链表

        单链表

 1.单链表的打印

2.申请一个结点

3.尾插

4.头插​编辑

5.头删

6.尾删

7.查找

8.指定位置之前插入数据

9.指定位置之后插入数据

10.删除pos结点

11.删除pos之后的结点

12.销毁链表

双向链表

1.申请一个新结点

2.初始化

3.销毁双向链表

 ​编辑

4.打印双向链表

5.尾删

6.头删

7.在pos之后插入数据

8.删除pos结点

9.头插

9.尾插

10.判空

11.查找数据


        在此之前,咱们先来认识一下线性表:

        线性表(linearlist)是n个具有相同特性的数据元素的有限序列。线性表是⼀种在实际中广泛使用的 数据结构,常见的线性表:顺序表、链表、栈、队列、字符串... 线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的,线性 表在物理上存储时,通常以数组和链式结构的形式存储。

2.顺序表

        顺序表是用⼀段物理地址连续的存储单元依次存储数据元素的线性结构,⼀般情况下采用数组存储。即顺序表的底层是数组来实现的,那么顺序表与数组之间有什么区别呢?

                顺序表的底层结构是数组,对数组的封装,实现了常用的增删改查等接口

那么顺序表也分为静态顺序表与动态顺序表,自然,顾名思义,静态顺序表空间给少了不够用,给多了造成空间浪费。而动态顺序表却可以自由的扩容,按需申请等。

那么下面咱们主要讨论动态顺序表。

typedef int SLDataType;
typedef struct SeqList
{
    SLDataType* arr;
    int size;//有效数据个数
    int capacity;//空间容量
}SL;

1.初始化以及扩容

思路:顺序表的初始化没什么好说的,来看扩容,如果说capacity与size一样大,说明该扩容了,又因为咱们上面有定义了capacity为0,所以先给一个三目操作符,判断要扩容的空间个数,然后下面就可以realloc,每个空间大小为sizeof(SLDataType),之后把扩的空间赋给原空间,newCapacity也是这样。

2.尾插

思路:注意得先判断一下空间是否满了,若满了就扩容,之后尾插(size++)即可。

3.尾删

要想删除的话,要注意的是你得有数据才可以删除,所以一开始先断言一下。之后再size--即可。

4.头插

头插也要注意扩容的问题,同时,头插的话,是不是得将前面的数据全部往后挪动一位,给最前面空出来一个空位置来,才可以头插。插完后,记得将size++一下,确保size始终在最后一个数据的下一个位置。

5.头删

还是一样,得先判断里面得有数据,才能头删,就是将除了第一个数据之外的数据都往前移动一格,这样就实现了头删,记得把size--一下,让size始终保持在最后一个数据的下一个位置。

6.删除pos位置数据

同样,里面得有数据才可以删除,不过这里有一个注意点,就是pos不能比size大,否则删除一个越界的数据,您觉得合理吗?之后将pos之后的数据都往前移动一个,确保将pos位置的数据给覆盖了即可。然后别忘了size--,确保size在最后一个数据的下一个位置。

7.pos之前插入数据

插入的,一定要注意空间大小是否足够,注意扩容,之后将pos位置之前的那一个数据之后的所有数据依次往后挪动一格,确保可以往pos位置之前的位置插入数据。之后别忘了size++。理由同上。

8.查找数据

还是很简单,就是遍历一下数组,找出数据就返回它的下标,否则,返回-1.

9.销毁顺序表

即把arr置为空,size与capacity置为0即可。

 那么顺序表有以下问题:

1. 中间/头部的插入删除,时间复杂度为O(N) 

2.增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。

3. 增容⼀般是呈2倍的增长,势必会有⼀定的空间浪费。例如当前容量为100,满了以后增容到200, 我们 再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。

那么如何解决这个问题呢?

3.链表

链表是⼀种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

        单链表

        相信大家都坐过火车,火车的车厢都是一节一节的连接的,

其实,链表的实现跟这个差不多,链表是通过存储下一个结点的指针,然后通过这个指针来找到下一个结点,依次往复。即:链表中每个结点都是独立申请的(即需要插入数据时才去申请⼀块结点的空间),我们需要通过指针变量来保存下⼀个结点位置才能从当前结点找到下一个结点。

那么链表的性质:

1、链式机构在逻辑上是连续的,在物理结构上不一定连续

2、结点一般是从堆上申请的

3、从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能连续,可能不连续

假设咱们存储的数据是整形,那么:

typedef int SLDataType;
typedef struct SListNode
{
    SLDataType data;//存储的数据
    struct SListNode* next;//指向下一个结点的指针

}SLNode;

当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数 据,也需要保存下⼀个结点的地址(当下一个结点为空时保存的地址为空)。 当我们想要从第⼀个结点走到最后⼀个结点时,只需要在当前结点拿上下一个结点的地址就可以了。

OK,那么接下来就来实现单链表 

 1.单链表的打印

这里得先定义一个变量用来代替头结点,因为后面头结点得移动,而头结点又不能动,始终指向链表的第一个结点。之后循环条件是pcur不等于空即可,之后打印出每个结点中的数据,打印完之后,让pcur往后挪动一个即可(通过指针来实现)。注意最后打印完记得打印一个空指针,代表打印完了。 

2.申请一个结点

申请空间就是malloc  LTNode这个类型个字节,就是向内存要了一块空间来存储这个结点,之后就是让x赋值给结点里面的data,结点指向的下一个为NULL。

3.尾插

注意这里传的是二级指针,即指向结点的指针的指针,这样做的目的是为了可以改变指向结点的指针的指向,比如如果这个链表为空,那么尾插就相当于插入了第一个结点,那么原本指向空的指向结点的指针,现在要改变指向了,指向刚存进来的新结点newnode。之后,若链表不为空,那么先找最后一个结点,(注意还是要定义一个指针变量,然后让指向头结点的指针赋值给它)。找到最后一个结点之后,让最后一个结点的指针不再指向空,而是指向newnode(这也需要用到二级指针)。

4.头插

 这里唯一要注意的点是指向头结点的指针始终要是*ps,所以再头插后,记得将*ps移动到新的头结点处。

5.头删

这里注意:1.优先级的问题。->的优先级比*高,所以*ps要加上括号。2.注意删除第一个数据后,记得将指向头结点的指针让它指向第二个结点。

6.尾删

1.如果链表中就只有一个结点,那么就是第一种情况。if里的

2.如果里面 有多个结点,else里的,这里还需要另外定义一个prev,它作为最后一个结点的前一个结点,作用是让prev的下一个结点置为空,这样就可以安心的释放最后一个结点了。最后别忘了把其置为空(你也不想它变为野指针吧)。

7.查找

老规矩,定义一个指针变量,存储指向第一个结点的指针,然后遍历单链表,如果找到了,返回指向那个结点的指针,没找到就继续往后找,如果最后还是没找到,只能返回空指针了,代表没找到。

8.指定位置之前插入数据

 1.如果链表里面就一个结点,那么就是头插了。

2.单链表里面有多个数据,那么就遍历链表,找到pos的前一个结点,然后让pos的前一个结点指向新结点,然后新结点指向pos结点。

9.指定位置之后插入数据

 1.单链表里面就一个结点,那么相当于尾插。

2.否则pos的下一个结点是newnode,newnode的下一个结点是原pos的下一个结点。

10.删除pos结点

1.单链表中就一个结点,那么就相当于头删。

2.单链表中有多个结点:先找出pos结点的前一个结点,然后让pos的前一个结点指向pos的下一个结点。之后才可以free掉pos结点,如果说你先free掉pos结点,那么你就找不到原pos的下一个结点了。别忘了置空。

11.删除pos之后的结点

先把pos的下一个结点存起来,这样是为了好找pos下一个结点的下一个结点。之后free掉pos的下一个结点,别忘了置空

12.销毁链表

这里销毁链表也需要遍历链表,要注意的是这里一开始的头结点在循环中也被销毁了,所以最后就光置空即可。看清楚它的销毁是如何销毁的。

上面图片相信可以帮助大家理解单链表的销毁过程。

双向链表

        咱们在单链表中提到了一个词叫做“头结点”,但其实这种叫法是错误的,只有在双向链表中,第一个结点起到了哨兵位(只是占位子的作用),哨兵位其实并不存储任何数据,这个才叫做“头结点”,而单链表中所谓的“头结点”就是第一个结点,而我们为了方便一点,干脆就叫他头结点,但现在要知道了什么叫“头结点”。

在单链表中,要想找到最后一个结点,或者找其中一个结点,只有从左往右遍历这一种办法,因为这是单向的。而双向链表不同,它是双向的,你知道了其中的一个结点的位置,那么双向链表中所有结点的位置你也就知道了。而且双向链表有一个特点,就是它的头结点的前一个结点是尾结点,而尾结点的下一个结点是头结点。

由于头结点只起到占位置的作用,所以打印的时候,可以不用考虑它。但是:最后销毁链表的时候,记得把哨兵位也给销毁了。

先来看双链表的定义方式:

typedef int LTDataType;
typedef struct ListNode
{
    LTDataType data;
    struct ListNode* prev;//指向前一个结点的指针
    struct ListNode* next;//指向下一个结点的指针
}LTNode;

1.申请一个新结点

 也是申请LTNode类型的字节的空间,并且这儿,当双向链表只有一个结点的时候,它的指向前一个结点的指针与指向后一个结点的指针都是指向同一个结点,就是它自己。最后返回指向这个结点的指针。

2.初始化

用-1来初始化,其实用数字几来初始化都是一样的,别忘了返回指向第一个结点的指针(只要有了第一个结点的指针,那么后面所有的结点都可以顺藤摸瓜的找到)。

3.销毁双向链表

 

老规矩,还是需要定义一个临时变量,不过存储的是指向头结点下一个结点的指针,因为头结点没有数据,所以要先从头结点的下一个结点开始销毁。最后别忘了销毁头结点,因为头结点并没有在while循环中被销毁(这个与单链表不同,单链表的第一个结点直接 在while中被销毁了)。

作者画了一个图方便大家理解。

4.打印双向链表

这里没什么好说的,就是循环打印而已,注意从头结点的下一个结点开始打印。

5.尾删

下面那个图用来辅助大家理解。这里事先定义一个del变量,用来存储原头结点的 前一个结点,之后按次连接起来即可。最后注意别忘了释放掉del,并将其置为空。

6.头删

这里注意头删,删除的是头结点的下一个结点,不是头结点!别搞错了。删除完之后记得将你创建的一个临时结构体指针变量释放掉,最后置空。

7.在pos之后插入数据

注意这里别忘了先申请一个结点。之后咱们按照上面我自己画的图来连接即可。

8.删除pos结点

这里的注意点还是那几个老生常谈的问题,即释放后别忘了置为空。

9.头插

注意头插的位置,别搞错了。之后按照作者画的图连接即可。

9.尾插

这儿也没有什么注意点,按照作者画的图连接即可。

10.判空

注意,bool类型返回的是,即return的语句,一定一定是对的才可以。

11.查找数据

这里查找数据也需要用到遍历,找到了就返回起始指针,没找到,就往后继续一个一个的找。若最后实在没找到,就返回空指针。

ok,终于写完了,每一个写博客对作者来说都是时间与精力的大量投入。

制作不易,若能看到最后,请三连支持一下,谢谢!

以上内容均我个人理解,若有不对,请指出,谢谢!

本篇完....................

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值