线性表(List):零个或多个数据元素的有限序列
线性表,从名字上可以感觉到,是具有像线一样的性质的表。
注意;
- 首先它是一个序列。也就是说,元素之间是有序的,若元素存在多个,则第一个元素无前驱,最后一个元素无后继,其他每个元素有且只有一个前驱和后继。
- 线性表强调有限,元素个数是有限的。
其结构如下图:
线性表元素的个数n(n≥0)定义为线性表的长度,当n=0时,称为空表。在非空表中的每个元素都有一个确定的位置,如a1是第一个元素,an是最后一个元素,ai是第i各元素,i称为数据元素ai在线性表中的位序。
在较复杂的线性表中,一个元素可以由若干个数据项组成。
线性表的顺序存储结构
一、顺序存储结构
用一段地址连续的存储单元依次存储线性表的数据元素
用一维数组实现顺序存储结构
1.存储空间的起始位置:数据data,它的存储位置就是存储空间的位置
2.线性表的最大存储容量: 数组长度MaxSize
3.线性表当前的长度: length
数组长度和线性长度区别
1.数组的长度是存放线性表的存储空间的长度,存储分配后这个容量一般是不变的。
2.线性表的长度是线性表中数据元素的个数,随着线性表插入和删除操作的进行,这个量是变化的。
3.在任意时刻,线性表的长度应该小于等于数组的长度。
地址计算方法
用数组存储顺序表意味着,要分配固定长度的数组空间,由于线性表中可以进行插入和删除操作,
因此分配的数组空间要大于等于当前线性表的长度。
存储器中的每个存储单元都有自己的编号,这个编号成为地址。
顺序存储结构的插入与删除
插入操作:
思路:
1.如果插入位置不合理,抛出异常。
2.如果线性表长度大于等于数组长度,则抛出异常或动态增加容量。
3.从最后一个元素开始向前遍历到第i个位置,分别将他们都向后移动一位
4.将要插入的元素填入位置i
5.表长加1.
删除操作:
删除算法的思路:
1.如果删除位置不合理,抛出异常。
2.取出删除元素
3.从删除元素位置开始遍历到最后一个元素的位置,分别将他们都向前移动一个位置
4.表长减一
操作整理
- 读取、存入数据: O(1)
- 插入、删除数据: O(n)
总的来说,线性表的顺序存储结构:读取快,增删慢。
线性表顺序存储结构的优缺点
优点 | 缺点 |
1.无需为表示表中元素之间的逻辑关系而增加额外的存储空间 | 1.插入和删除都需要移动大量的元素 |
2.可以快速的存取表中任一位置的元素,随机访问 | 2.当线性表长度变化较大时,难以确定存储空间的容量 |
3.造成存储空间的碎片 |
二、线性表的链式存储结构
先看个图
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的。这就意味着,这些数据元素可以存在内存未被占用的任意位置。
以前的顺序存储结构中,每个数据元素只需要存储数据元素就可以了。现在链式结构中,处理要存储数据元素信息之外,还要存储它的后继元素的存储地址。
为了表示每个元素ai与其直接后继元素ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需要存储一个指示其直接后继的信息(即直接后继的存储位置)。把存储数据元素的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息称作指针或链。这两部分信息组成数据元素ai的存储映像,称为结点。
n个结点链结成一个链表,即为线性表(a1,a2,a3...an)的;链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
链表的第一个结点存储位置叫做头指针,最后一个结点指针指向NULL。如图:
一般地,我们为了方便,会在单链表的第一个结点前附设一个结点,称为头节点。头节点的数据域可以不存任何数据,也可以存一些线性表的长度等信息。
头指针与头结点的异同
头指针 | 头结点 |
1.头指针是指链表指向第一个节点的指针,若链表有头结点,则是指向头结点的指针。 | 1.头结点为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度) |
2.头指针具有标识作用,所以常用头指针冠以链表的名字 | 2.有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作就统一。 |
3.无论链表是否为空,头指针均不为空,头指针是链表的必要元素 | 3.头结点不一定是链表的必须要素 |
综上,结点由存放数据元素的数据域和存放后继结点的地址的指针域组成。
单链表的读取
算法思路:
- 声明一个指针p指向链表第一个结点,初始化j从1开始;
- 当j<i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
- 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,返回结点p的数据。
其核心思想就是工作指针后移。其时间复杂度为O(n)。
单链表的插入和删除
单链表的插入
看图说话
算法思路:
- 声明一指针p指向链表头节点,初始化j从1开始;
- 当j < i 时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
- 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,在系统中生成一个空结点s;
- 将数据元素e赋值给 s->data;
- 单链表的插入标准语句 s->next; p->next=s;
- 返回成功。
单链表的删除
上图
算法思路:
- 声明一指针p指向链表的头指针,初始化j从1开始;
- 当j< i时,就遍历链表,让p的指针向后移动,不断指向下一个结点,j累加1;
- 若到链表末尾p为空,则说明第i个结点不存在;
- 否则查找成功,将欲删除的结点p->next赋值给q;
- 单链表的删除标准语句 p->next=q->next;
- 将q结点中的数据赋值给e,作为返回;
- 释放q结点;
- 返回成功。
从整个算法来说,单链表的删除和插入的时间复杂度都是O(n)。
显然,对于插入或删除数据越频繁的操作,单链表的效率优势就越明显。
单链表的整表创建
顺序存储结构的创建,其实就是一个数组的初始化,即声明一个类型和大小的数组并赋值的过程。
对于单链表来说,它所占用的空间的大小和位置是不需要预先分配划定的,可以根据系统情况即时生效。
算法思路:
- 声明一指针p和计数器变量i;
- 初始化一空链表L;
- 让L的头节点的指针指向NULL,即建立一个带头节点的单链表;
- 循环: 生成一新结点赋值给p; 随机生成一个数字赋值给p的数据域p->data; 将p插入到头节点与前一新结点之间。
以上思路称作头插法,当然对应还有尾插法,不再累赘。
单链表的整表删除
算法思路:
- 声明一结点p和q;
- 将第一个结点赋值给p;
- 循环: 将下一节点赋值给q; 释放p; 将q赋值给p。
两种结构优缺点
存储分配方式
- 顺序存储结构用一段来内需的存储单元依次存储线性表的数据元素;
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素;
时间性能
a、查找 顺序存储结构O(1) 单链表O(n)
b、插入和删除 顺序存储结构需要平均移动表长一般的元素,时间为O(n) 单链表在选出某位置的指针后,插入和删除的时间仅为O(1)
空间性能
- 顺序存储结构需要预分配存储空间,分大了,浪费,分小了易发生上溢;
- 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制。
总结来说:
- 顺序结构查找快,增删慢;
- 链表 查找慢,增删快;
- 元素个数不大或根本不变或事先知道其最大容量时,可以采用顺序存储结构;
- 当元素变化大或根本不知道多大,应采用链表,不考虑内存空间大小的问题。
了解过线性表的链式存储结构以后,有人就想出来用数组来代替指针,来描述单链表。看看他们是怎么做到的。
静态链表
让数组的元素都由两个数据域组成,data和cur。也就是说,数组的每个下标都有对应的一个data和cur。数据域data,用来存放数据元素,而cur相当于单链表中的next指针,存放该元素的后继在数组中的下标,我们把cur叫做游标。
这种用数组描述的链表叫做静态链表,我们把这种描述叫做游标实现法。
另外我们对数组第一个和最后一个元素作为特殊元素处理,不存数据。我们通常把未使用的数组元素称为备用链表。
数组第一个元素,即下标为0的元素的cur存放备用链表的第一个节点的下标;而数组最后一个元素的cur存放第一个有数值的元素的下标,相当于单链表中的头节点的作用。
如下图:
我们对静态链表的插入和删除操作简单了解以下:
静态链表中要解决的是:如何用静态模拟动态链表的存储空间的分配,需要时申请,无用时释放。
静态链表的插入
静态链表的修改
静态链表的删除
静态链表的优缺点
- 在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点。
- 没有解决连续内存分配带来的表长度难以确定的问题。
- 失去了顺序存储结构随机存取的特点。
循环链表
对于单链表,由于每个结点只存储了向后的指针,到了尾标就停止了向后链的操作,这样,当某一个结点就无法找到它的前驱结点了。
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
显然解决了一个问题:当从一个结点出发,访问链表的所有结点。
双向链表
双向链表:是在单链表的每个结点中,再设置一个指向其前驱结点的指针域。
双向链表的好处:某个结点对前后结点的操作更快;
双向链表的不足:一个结点,两个指针,耗内存更大。
既然单链表可以由循环链表,那么双向链表当然也可以是循环表,其结构如下:
双向链表的插入
双向链表的删除
总结: