线性表的链式存储结构
线性表的链式存储结构
0_动态内存分配
先看俩个例子:
char t[10] = “hi”;à可变à内存动态存储区域à数组在定义的时候在动态内存声明了相应的长度的区域,其大小取决于数组的长度,这样的”hi”是和下面不一样的è {“hi”} à {‘h’,’i’,’\0’}
char *p = “hi”; à变量不可变à内存静态存储区域à”hi”是字面量是常量,不能通过”hi”=”go”来修改;指针p指向的是一个静态内存,里面的数据不能动态更改,如果指向数组则可以改,重点是看其指向的区域是静态还是动态内存.
以上是字符串的俩种初始化方式以及区别
1_链式存储结构_概念
顺序存储结构最大的问题的插入和删除时都需要移动大量的元素,这显然需要耗费大量时间.这是由于其相邻的俩元素的存储位置也具有邻居关系—他们在内存中的位置是挨着的,中间没有空隙,这样自然无法快速介入, 而删除后,当中留出的空袭需要弥补.
而链式存储结构并不需要考虑相邻的位置,但是每个元素知道下一个元素的位置(内存地址)在哪里,这样所有的元素都可以通过遍历找到,其结构图如下:
存储数据元素信息的域位数据域,存储后继位置/地址的域位指针域,指针域中存储的信息称作指针或者链,这俩个部分组成的数据元素组成链表的一个小单元à结点(Node)
线性表需要一个头和尾的,链表中第一个结点的存储位置叫做头指针,最后一个结点的指针位空,通常用NULL或者^表示,如下图:
为了方便对链表进行操作,会在链表的第一个结点前附设一个结点,称之为,头节点,可以不存储任何信息,也可以存储线性表的长度等信息,头节点的指针域存储指向第一个结点的指针,如下所示:
1.1_头节点和头指针的区别:
头指针:
- 头指针是指链表指向第一个结点的指针,当存在头结点时头指针指向头结点
- 当不存在头结点时,头指针指向首结点,如果这时候删除首结点那么头指针发生改变
- 一般具有标志作用,通常以此冠以链表的名字
- 无论链表是否位空,头指针均不为空,
- 头指针是链表的必要元素
头节点:
- 头节点一般是为了操作统一和方便设立的,放在第一个元素结点/首节点之前,数据域一般无意义(也可以存储链表的长度)
- 有了头节点,在对第一个元素进行插入,删减的操作就和其他元素一致了
- 头节点不一定是链表必须的元素
2_代码操作
2.1_线性表顺序存储结构代码描述:
若是一个头节点的指针则示意图如下:
空链表如下所示:
在C语言中可以用结构体指针来表示:
一个结点有存放数据元素的数据区域和存放后继结点的地址的指针区域组成,假设p是指向线性表中第i个元素的指针,则该节点ai的数据区域可以用pàdata来表示,指针区域可以用pànext来表示,指向ai+1元素,如下图:
链表是为了动态
2.2_单链表的创建
顺序存储结构的创建其实就是数组的初始化,即声明一个类型和大小的数组并赋值的过程,但是链表结构所占用的空间并不是需要预先分配的,根据系统的情况生成,所以创建一个单链表的过程看作是一个动态生成链表的过程,从”空表”开始,依次建立各个元素的结点,并逐个插入链表à有俩种方法,从头部开始插入,从尾部开始插入.
2.2.1_头插法
头节点数据存链表的长度,L始终的头结点,p从后面插入,并让p的后继不断指向空,L的后继不断指向p,这样就始终可以让新的结点在第一的位置,如下图:
代码如下:需要注意的是这个里面使用了指向pNode结构体的二级指针,这是因为创建链表是需要改变后面结点的指针的指向的,而函数传入的是值指针,于是使用二级指针
2.2.2_尾插法
与之相反,尾插法则是将新结点都放到最后面, 这比较符合正常的排队的思路à新来的排到最后面, 代码如下:
L始终是指向头节点,头结点中存储长度n数据,rà尾部结点,最后让他指向空, pà中间变量,这个变化比较懵看着,画图的话就比较方便理解了.rànext = p的意思是建立起新建立的结点空间和尾部结点的联系:也就是r结点àp结点,这个时候,p成为了最后一个结点,但是规定r是尾部的结点,于是r = p,示意图如下:
循环结束后需要将最后一个结点指向空.
2.3_单链表的遍历
这里只是需要读取指针指向的结点的数据,于是只是用一级指针就可以了, 头节点中存储链表的长度, 注意的是循环中的打印数据是需要在指针后移前面的,注意看最后p最后是指向NULL的,这个时候会打印NULLàdata编译器是会崩溃的.
这里看一下头插法和尾插法输出的区别,打印的结果是反着来的,
2.4_单链表的读取
相对于线性结构,第i个元素在哪并不能一开始找到,需要从头开始找,所以对于单链表实现获取第i个元素的数据操作GetElem会比较麻烦一点,
思路:获取链表第i个数据的算法思路à从头开始找
1_声明一个结点p指向链表的第一个结点,初始化j=1;
2_当j<i时就不断遍历链表,让p不断指向下一个结点,不断后移,j++;
3_若到链表末尾了àp指向空, 则说明第i个元素不存在
4_否则则查找成功,返回p结点的数据
代码如下:
时间复杂度为O[n],由于单链表的结构不知道其表长,所以不能事先知道循环多少次,于是只能一个一个指针后移,
2.5_单链表的插入和删除
2.5.1_插入
假设存储元素e的结点为s,将其插入进ai和ai+1中,只需 s->next = p->next; p->next = s;这样赋值的意思就是左边指向右边, 也就是让p的后继变成s的后继,让s变成p的后继,这俩顺序不可调换.插入和删除是需要改变指针的指向的于是乎也是需要用到二级指针的.
插入代码如下:这里是从头指针开始的,于是i=1其实是找到了头节点,在头节点和首结点之间插入数值.i=n+1则是增加一个尾部结点.
结果如下:
2.5.2_删除
单链表的删除,设存储元素ai的结点为q,要实现将结点q删除单链表的操作其实只需要将它的前结点的指针绕过它指向它的后即可,也就是q = pànext; pànext = qànext;如下:
也就是让p的后继的后继变成p的后继,当然处理完指针后,还得将结点占据的空间释放掉才可以.
代码如下,思路和插入一样,先找到需要删除的结点的前面一个结点,然后进行指针操作.
对于一个数据的插入,时间复杂度和顺序存储结构是一样的都是o[n],但是对于大量的数据,链表只需找到那个需要插入的结点,然后简单赋值移动指针,时间复杂度是o[1],而顺序存储结构则是需要不断移动后面的元素,所以对于插入和删除数据很多的操作时,单链表的效率优势是很明显的.