考点(线性表是算法题命题的重点) 这类算法题实现起来比较容易且代码量较少,但是要求具有最优的性能(时间复杂度、空间复杂度,才能获得满分。因此,应固掌握线性表的各种基本操作(基于两种存储结构),在平时的学习中多注重培养动手能力。另外,需要提醒的是,算法最重要的是思想!考场上的时间紧迫,在试卷上不一定要求代码具有实际的可执行性,因此应尽力表达出算法的思想和步骤,而不必过于拘泥每个细节。 注意算法题只能用 C/C++语言实现。
1 线性表的相关概念
线性表是具有相同数据类型(值及包含的操作)的n个数据元素的有限序列(n为表长,n=0代表线性表是一个空表)。表示为
线性表有下面两条逻辑特性:
- a1是第一个元素,称为表头元素。除表头元素外,其余元素都有一个直接前驱。
- an是最后一个元素,称为表尾元素。除表尾元素外,其余元素都有一个直接后继。
线性表有下面五条特点:
- 元素个数有限
- 具有逻辑上的顺序性,表示元素先后次序
- 表中元素都是数据元素(数据记录,如学生记录(学号, 姓名, 宿舍))
- 数据类型相同
- 表中元素具有抽象性,(仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容)
线性表基本操作(其它操作都需要调用基本操作来完成)
- 初始化表
- 求表长
- 按值查找
- 按位查找
- 插入
- 删除
- 输出
- 判空
- 销毁
下面要总结的是基于两种存储结构(顺序结构和链式结构)实现的线性表,需要注意的是线性表是一种逻辑结构,顺序结构和链式结构是存储结构。
2 线性表的顺序表示(顺序表)
用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻。 下表第i个元素后面紧接着存储第i+1个元素,其中i称为元素ai在线性表的位序。
- LOC(A)代表数组A的起始位置。
- sizeof(ElemType)代表每个元素占用的存储空间大小,每个元素都与前一个元素相差一个元素存储空间大小。
- 虽然数组索引从0开始,但是数组的位序从1开始。
- 通过2、3两条可以看出顺序表可以随机存取
2.1 顺序存储类型描述
通过数组定义顺序表,而一位数组可以是静态分配也可以是动态分配的
- 静态分配由于数组大小和空间事先已经定义好,一旦空间占满,再加入新的数据会导致程序崩溃
- 动态分配一旦数据空间占满,就另外开辟一块更大的存储空间,用以替换原来的存储空间,从而达到扩充存储数组空间的目的,而不需要为线性表一次性地划分所有空间。
# define InitSize 100 // 表长度初始定义 type struct{ ElemType *data; int MaxSize, length; }
2.2 基本操作的实现
2.2.1 插入操作
在线性表的第i个位置插入数据元素e。若i输入不合法,则返回False。 否则将第i个元素后面的所有元素依次移动一个位置,将元素e插入位置i
注意这里i指的是位序(1到n) 时间复杂度:
- 最好时间复杂度:在表尾插入,不执行后移操作,O(1)
- 最坏时间复杂度:在表头插入,执行n次后移操作,O(n)
- 平均时间复杂度:计算节点平均移动的次数(过程如下图)为n/2,因此时间复杂度为O(n)
2.2.2 删除操作
删除顺序表L中第i个位置的元素。若i的输入不合法,返回False。否则,将被删元素赋予变量e,并将第i+1个元素及后面元素向前移动一格,返回True。
bool ListDelete(SqList &L, int i, Elemtype &e){ if(i<1 || i>L.length) return false; e=L.data[i-1]; for(int j=i; j<L.length; j++){ L.data[j-1] = L.data[j]; L.length--; return true; } }
- 最好时间复杂度:删除表尾元素,不需要移动元素,O(1)
- 最坏时间复杂度:删除表头元素,需要移动n-1个元素,O(n)
- 平均时间复杂度:假设pi是删除第i个位置的概率,计算移动结点的平均次数为(n-1)/2,则O(n)
2.2.3 按值查找、按序查找
在循序表L中找到一个值等于e的元素并返回位序。
- 最好时间复杂度:查找元素在表头,仅进行一次比较,O(1)
- 最坏时间复杂度:查找元素在头尾,进行n次比较,O(n)
- 平均时间复杂度:假设pi是需要查找的元素的在位置i上的概率,则计算平均查找次数为(n+1)/2,则O(n)
由于顺序表有随机存取的特点,因此按序查找时间复杂度是O(1)
2.2.4 总结
- 插入操作和删除操作时间主要耗费在移动元素上
- 查找操作时间主要耗费在比较上
3 线性表的链式表示
链式存储线性表时,不需要使用地址连续的存储单元,即不要求逻辑上相邻的元素在物理位置上也相邻,它通过“链”建立起元素之间的逻辑关系,因此插入和删除操作不需要移动元素,而只需修改指针。但也会失去顺序表可随机存取的优点。 链表分为单链表、双链表、循环链表、静态链表
3.1 单链表
通过一组任意的存储单元来存储线性表中的数据元素,每个元素结点/存储单元都包含元素信息和指向下一个元素的指针,以此建立元素之间的线性关系。
3.1.1 结点
单链表结点如上图所示,data为数据域存放元素数据,next为指针域存放后继结点的位置。 单链表的结点类型描述
3.1.2 带头结点的单链表
- 一般在第一个结点之前会设置一个头结点,头结点的数据域可以不存放任何信息或存放表长信息(此时头结点称为第一个结点)
- 当L指向NULL时,表示是一个空表。
区分头结点和头指针 如图2.4,头指针指的是指向第一个结点的指针,头结点指的是带头链表中第一个结点(通常带有存储信息)。
头结点带来的两个好处
- 第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理。
- 无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就得到了统一。
3.1.3 头插法建立单链表
- 创建头结点,并初始化链表(头指针L指向NULL)
- 创建新结点N
- 新结点后继结点指向头结点的后继结点
- 头结点后继结点指向新结点
假设每个结点插入时间为O(1),则n个结点为O(n)
3.1.4 尾插法建立单链表
- 创建头结点,并初始化链表(头指针L指向NULL)
- 表尾指针指向结点的后继结点指向新结点
- 表尾指针指向新结点
时间复杂度与头插法一致
3.1.5 按序号查找
从单链表头结点出发,顺着next指针搜索位置i的结点,如果没有则返回False
时间复杂度是O(n)
3.1.6 按值查找
从单链表第一个开始,顺着next依次比较值。若某结点的数据域的值等于给定值返回指向该结点的指针,否则返回False。
时间复杂度是O(n)
3.1.7 插入操作
在位置i插入给定值
- 找到第i个结点的前驱结点,即第 i-1个结点p
- 新结点后继结点指向p结点的后继结点b
- p结点的后继结点指向新结点
插入操作的时间主要耗费在寻找位序,复杂度为O(n)。而插入仅仅为O(1)
3.1.8 删除结点
将单链表的第i个结点删除。
- 查找删除结点的前驱结点,如上图B
- 将前驱节点B的后继结点指向删除结点的后继结点
- 释放删除结点的空间
与插入节点类似,时间主要耗费在查找上,复杂度为O(n)
3.1.9 求表长
计算单链表中数据结点(不含头结点)的个数。从第一个结点开始依次查找每个结点,同时设置一个计数器,每访问到一个结点,计数器加1,直至访问结点为空。时间复杂度为O(n)。 需要注意的是计算表长不算头结点。
3.2 双链表
双链表中存在两个指针——prior(指向前驱结点)和next(指向后继结点)
结点类型描述:
typedef struct DNode{ ElemType data; struct DNode *prior, *next; }DNode, *DLinkList;
双链表只是增加了一个前驱结点,因此按值查找和按位查找操作与单链表一致,但是插入和删除有了较大的改变。
3.2.1 插入操作
位置i插入结点
- 找到位置i结点的前一个结点和后一个结点,即p结点和c结点
- 插入结点的后继结点指向c结点,c结点的前驱结点指向插入结点
- p结点的后继节点指向插入结点,插入结点的前驱结点指向p结点
注意顺序不一致可能导致出错
3.2.3 删除操作
将第i个结点删除。
- 删除结点的前驱结点的后继节点指向删除结点的后继结点
- 删除结点的后继结点的前驱节点指向删除结点的前驱结点
- 释放删除结点的空间
3.3 循环链表
循环单链表和单链表的区别在于,表中最后一个结点的指针不是 NULL,而改为指向头结点从而整个链表形成一个环。分为循环单链表和循环双链表。
3.3.1 循环单链表
表尾结点的next指向不再为空,而是表头结点。因此,循环单链表判空的条件不再是头指针指向是否为空,而是指向是否是头指针自身。 一般情况下,对循环单链表只设置表尾指针。原因是,若设的是头指针,对在表尾插入元素需要 O()的时间复杂度,而若设的是尾指针r,r->next 即为头指针,对在表头或表尾插入元素都只需要 O(1)的时间复杂度。
3.3.2 循环双链表
与双链表对比不同的是表头结点的前驱结点指向表尾结点,表尾结点的后继结点指向表头结点
3.4 静态链表
静态链表借助数组来表达链式结构,如下图
结点也存在数据域和指针域,指针域存储的是下一个结点在数组中的地址(相对地址),称为游标。 需要注意的是静态链表也需要分配一个连续的内存空间,且以-1作为不存在下一个结点的标志。
数据类型描述为
3.5 单链表、双链表、循环链表的比较
单链表结点中只有一个指向其后继的指针,使得单链表只能从头结点依次顺序地向后遍历。要访问一个结点时间复杂度为O(n)。双链表改进了上述问题。 循环单链表一般仅设尾指针,以使得操作效率更高。原因是,若设的是头指针,对在表尾插入元素需要 O(n)的时间复杂度,而若设的是尾指针 r,r->next 即为头指针,对在表头或表尾插入元素都只需要 O(1)的时间复杂度。
4 顺序表和链表的比较
- 读取方式和结构
存取(读写)方式 |
逻辑结构和物理结构 |
扩容 |
|
顺序表 |
顺序存取、随机存取 |
逻辑上相邻的物理上也相邻 |
静态存储不能,动态存储能 |
链表 |
顺序存取 |
逻辑上相邻的物理上未必相邻 |
可以 |
需要注意态存储分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。因此,链式存储在这方面更加高效。
- 时间复杂度
顺序表 |
链表 |
|
按序查找 |
O(1) |
O(n) |
按值查找 |
顺序表无序时O(n),有序时 |
O(n) |
插入/删除 |
平均时间复杂度O(n) 最好时间复杂度O(1) 最坏时间复杂度O(n) |
查找+O(1) |
5 错题记录
第一题
线性表:相同数据类型(数据元素和操作),有限元素