第 2 章 线性表
1 | digraph demo{ |
2.1 线性表的定义和基本操作
2.1.1 线性表的定义
线性表是具有相同数据类型的个元素的有限序列,其中为表长
若用命名线性表,则其一般表示为
线性表的逻辑特性
- 除第一个元素外,每个元素有且仅有一个直接前驱
- 除最后一个元素外,每个元素有且仅有一个直接后继
线性表的特点有
- 表中元素的个数有限
- 表中元素具有逻辑上的顺序性,在序列中各元素排序有其先后顺序
- 表中元素都是数据元素,每个元素都是单个数据
- 表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间
- 表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容
注意: 线性表是一种逻辑结构,表示元素之间的一对一的相邻关系,顺序表和链表是指存储结构,两者属于不同层面的概念,因此不要混淆
2.1.2 线性表的基本操作
数据结构的基本操作
- 一个数据结构的基本操作是指其最核心、最基本的操作
- 其他较复杂的操作可以通过基本操作来实现
线性表的主要操作
InitList(&L)
: 初始化表,构建一个空的线性表Length(L)
: 求表长,返回线性表的长度,即中数据元素的个数LocateElem(L,e)
: 按值查找操作,在表中查找具有给定关键字值的元素GetElem(L,i)
: 按位查找操作。获取表中第个位置的元素的值ListInsert(&L, i, e)
:插入操作,在表中的第个位置上插入指定元素ListDelete(&L, i ,&e)
:删除操作,删除表中第个位置的元素,并用返回删除的元素的值PrintList(L)
:输出操作。按前后顺序输出线性表的所有元素值Empty(L)
:判空操作。若为空表,则返回true
,否则返回false
DestroyList(&L)
: 销毁操作。销毁线性表,并释放线性表所占的内存空间
2.2 线性表的顺序表示
2.2.1 顺序表的定义
线性表的顺序存储又称顺序表,它是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使逻辑上相邻的两个元素在物理位置上也相邻
顺序表的特点是表中元素的逻辑顺序与其物理顺序相同
顺序表支持随机访问,即通过首地址和元素序号可在时间内找到指定的元素
顺序表的存储密度高,每个节点除了数据元素外,不存储其他多余的值
顺序表逻辑上相邻的元素物理上也相邻,所以插入和删除操作需要移动大量元素
2.2.2 顺序表上基本操作的实现
1 插入操作
1 | Status ListInsert(SqList *l, int i, ElemType e){ |
插入操作的时间复杂度
- 最好情况: 在表尾插入,元素不需要后移,时间复杂度为
- 最坏情况: 在表头插入,元素整体都后移,时间复杂度
- 平均情况: 时间复杂度为
2 删除操作
1 | //删除元素 |
删除操作的时间复杂度
- 最好情况: 删除表尾元素(),无须移动元素,时间复杂度为
- 最坏情况: 删除表头元素(),需要移动所有元素,时间复杂度为
- 平均情况: 时间复杂度为
3 按值查找
1 | //按值查找元素 |
按值查找的时间复杂度
- 最好情况: 查找的元素就在表头,仅需比较次,时间复杂度为
- 最坏情况: 查找的元素在表尾,需比较次,时间复杂度为
- 平均情况: 时间复杂度为
2.3 线性表的链式表示
链式存储线性表时,不需要使用地址连续的存储单元,即它不要求逻辑上相邻的两个元素在物理位置上也相邻
它通过"链"建立起数据元素之间的逻辑关系,因此对于线性表的插入、删除不需要移动元素,而只需要修改指针
2.3.1 单链表的定义
线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素
为了建立数据元素之间的线性关系,对每个链表节点,除存放元素本身的信息外,还需要存放一个指向其后继的指针
单链表中节点类型的描述
1 | typedef struct LNode{ |
非随机存储
- 由于单链表的元素是离散地分布在存储空间中的
- 所以单链表是非随机存取的存储结构
- 不能直接找到表中某个特定的节点
头指针和头节点
- 通常用头指针来标识一个单链表
- 为了操作方便,在单链表第一个节点之前附加一个节点,称为头节点。头节点的数据域可以不设任何信息,也可以记录表长等相关信息
头节点和头指针的区别
- 不管带不带头节点,头指针始终指向链表的第一个节点
- 而头节点是带头节点的链表中的第一个节点,节点内通常不存储信息
引入头节点之后的优点
- 由于开始节点的位置被存在头节点的指针域中,所以在链表的第一个位置上的操作与在表的其他位置上的操作一致,无须进行特殊处理
- 无论链表是否为空,其头指针都指向头节点的非空指针
2.3.2 单链表上基本操作的实现
1 插入操作
1 | Status ListInsert(LinkedList *l, int i, ElemType e){ |
时间复杂度:
扩展: 对某节点的前插操作
- 前插操作是指在某节点的前面插入一个新节点,与后插操作相反
- 要找到插入节点的前驱节点,然后对这个节点执行后插操作
2 删除操作
1 | Status ListDelete(LinkedList *l, int i, ElemType *e){ |
时间复杂度:
扩展:删除节点*p
- 可以将其后继节点的值赋予其自身,然后删除后继节点
- 这样可以让时间复杂度为
3 按序号查找操作
1 | //按位置查找元素 |
时间复杂度
4 按值查找操作
1 | Status LocateElem(LinkedList *l, ElemType e, int *locate){ |
时间复杂度:
2.3.3 双链表
双链表节点中有两个指针prior
和next
分别指向前驱节点和后继节点
2.3.4 循环链表
1 循环单链表
循环单链表和单链表的区别是,表中最后一个节点的指针不是NULL
,而是改为指向头节点,从而整个链表形成一个环
有时对单链表常做的操作是在表头和表尾进行的,此时对循环单链表不设头指针而仅设尾指针,从而效率更高,对表头和表尾进行操作都是
2 循环双链表
略
2.3.5 静态链表
静态链表是借助数组来描述线性表的链式存储结构
节点也有数据域data
和指针域next
,但是,这里的指针是节点的相对地址(数组下标)
静态链表结构类型如下
1 |
|
静态链表以next == -1
为结束标志
总体来说,静态链表没有单链表使用起来方便
2.3.6 顺序表和链表的比较
1 存取方式
顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素
2 逻辑结构与物理结构
采用顺序存储时,逻辑上相邻的元素,其对应的物理存储位置也相邻
采用链式存储时,逻辑上相邻的元素,其物理存储位置不一定相邻
3 查找、插入和删除操作
按值查找操作
- 顺序表无序时,两者的时间复杂度都为
- 顺序表有序时,可用折半查找,顺序表时间复杂度为
按位置查找
- 顺序表为
- 链表为
插入和删除
- 链表的插入和删除只需要修改相关节点的指针域;而顺序表平均需要移动半个表长的元素
- 但是由于链表的每个节点都带有指针域,因而在存储空间上要比顺序存储代价大,存储密度不够大
4 空间分配动态存储
- 顺序表在静态存储分配情形下,一旦存储空间装满就不能扩充。在动态存储分配下,虽然存储空间可以扩充,但需要移动大量元素,效率很低
- 链式存储的节点空间只在需要时申请分配,灵活高效
5 如何选择存储结构
-
基于存储考虑
- 难以估计线性表的长度和存规模时,不宜采用顺序表
-
基于运算考虑
- 若经常做的运算是按序号访问数据元素,则显然顺序表优于链表
- 若插入和删除操作较多,建议使用链表,因为不需要大量移动元素
-
基于环境考虑