【计算机408数据结构】第二章:基本数据结构之线性表
线性表
基本概念
逻辑结构
是具有相同数据类型的nnn个数据元素的有限序列。nnn表示表长。
L=(a1,a2,⋯ ,ai,⋯ ,an)L=(a_1,a_2,\cdots,a_i,\cdots,a_n)L=(a1,a2,⋯,ai,⋯,an),其中iii表示元素在线性表中的位序,从一开始。
- 存在唯一的第一个元素。
- 存在唯一的最后一个元素。
- 除第一个元素(表头元素)之外,每个元素均只有一个直接前驱。
- 除最后一个元素(表尾元素)之外,每个元素均只有一个直接后继。
物理结构
- 顺序存储结构:顺序表。
- 链式存储结构:链表。
顺序表
把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来实现。iii是元素aia_iai在线性表中的位序。
顺序表特点
- 随机访问,可以在O(1)O(1)O(1)时间内找到对应元素。
- 存储密度高,只用存储数据。
- 拓展容量不方便。
- 插入删除操作不方便。
- 表中元素的逻辑地址与物理地址顺序相同。
顺序表定义
使用CCC语言的结构体定义顺序表,使用typedef
定义一个ElemType
表示数据基本类型,并定义最大长度MAXSIZE
:
// 初始化最大长度
#define MAXSIZE 25
// 定义默认值
#define DEFAULTELEM 0
// 定义最大值
#define INFINITY 32767
// 定义默认数据类型
typedef char element_type;
可以使用静态分配空间:
// 静态顺序表
typedef struct {element_type data[MAXSIZE];// 长度int length;
} StaticSequenceList;
也可以使用动态分配空间,动态分配空间还是顺序的,只不过可以替换原来空间:
// 动态顺序表
typedef struct {// 给一个指针来分配动态数组element_type *data;// 已分配的最大容量int max_size;// 长度int length;
} DynamicSequenceList;
其中长度是指有数据的长度,而最大容量是指已经分配给动态数组的长度,插入时要考虑这个长度,不能溢出。
顺序表操作
顺序表初始化
静态顺序表因为数组部分在创建时就已经设置好了,所以初始化就直接设置数据长度就可以了。
动态顺序表不仅需要设置数据长度与最大长度,还得分配数组初始空间。
顺序表增长数据空间长度
只有动态顺序表才能增加。
顺序表插入
倒序移动元素,最后将数据插入对应索引并长度加一。(这是一个较好的方式,因为如果插入的话其他元素会被挤住,倒序移动元素可以正好空出位置)
插入时间复杂度为:T(n)=O(n)T(n)=O(n)T(n)=O(n),空间复杂度为S(n)=O(1)S(n)=O(1)S(n)=O(1)。
平均时间复杂度:假设pip_ipi(ni=1n+1n_i=\dfrac{1}{n+1}ni=n+11)是iii位置上插入一个结点的概率,则在长度为nnn的线性表中插入一个结点时所需要移动结点的平均次数为∑i=1n+1pi(n−i+1)=∑i=1n+11n+1(n−i+1)=1n+1∑i=1n+1(n−i+1)=1n+1×n(n+1)2=n2\sum\limits_{i=1}^{n+1}p_i(n-i+1)=\sum\limits_{i=1}^{n+1}\dfrac{1}{n+1}(n-i+1)=\dfrac{1}{n+1}\sum\limits_{i=1}^{n+1}(n-i+1)=\dfrac{1}{n+1}\times\dfrac{n(n+1)}{2}=\dfrac{n}{2}i=1∑n+1pi(n−i+1)=i=1∑n+1n+11(n−i+1)=n+11i=1∑n+1(n−i+1)=n+11×2n(n+1)=2n。
顺序表删除
正序移动元素并长度减一。
顺序表的删除时间复杂度为:T(n)=O(n)T(n)=O(n)T(n)=O(n),空间复杂度为S(n)=O(1)S(n)=O(1)S(n)=O(1)。
顺序表查找
按位查找时间复杂度为T(n)=O(1)T(n)=O(1)T(n)=O(1)。
按值查找一般都是找到第一个元素等于指定值的元素,返回其位序,如果没有找到就返回−1-1−1。按位查找时间复杂度为T(n)=O(n)T(n)=O(n)T(n)=O(n)。
单链表
每个结点只包含一个指针域,也称为线性链表。
通常用头指针来标识一个单链表,如单链表LLL。
单链表特点
- 不要求大量连续空间,删除和插入方便。
- 不可随机存取。
- 要花费多余空间存放指针。
是非随机存取的存储结构。
单链表定义
使用LinkNode
表示一个单链表结点的结构体,而使用LinkList
表示一个单链表,其实LinkList
是一个指向LinkNode
的指针变量。如定义LinkList L
等价于LinkNode* L
。
// 单链表结点
typedef struct LinkListNode {element_type data;struct LinkListNode* next;
} LinkListNode, *LinkList;
单链表操作
单链表初始化
有带头节点与不带头节点的初始化的区别,带头节点代表第一个结点不存放数据,只是用于标识单链表的开始,但是区别不大,带头结点更好使用。
- 由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无须进行特殊处理。
- 无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就得到了统一。
单链表插入
插入方式一共分为下面几种:
- 按位序插入:
- 带头点结。
- 不带头结点。
- 指定结点插入:
- 前插入。
- 后插入。
假定从第一个结点开始就是第000索引的结点。
带头结点的单链表头结点就是000号结点,不带头节点的第一个数据结点就是000号结点。
带头结点的单链表只能往头结点之后插入,所以插入索引必须从111开始。
头插法建立单链表:
- 每个结点的插入时间为O(1)O(1)O(1),设单链表长为nnn,则总时间复杂度为O(n)O(n)O(n)。
- 实现了输入数据的就地逆置。
尾插法建立单链表
- 增设尾指针rrr。
- 生成的链表中结点数据与输入数据顺序一致。
- 总时间复杂度为O(n)O(n)O(n)。
插入有/无头节点单链表元素函数的后面代码可以使用后插入单链表元素函数来替代。
使用前插入的方法插入元素,可以使用头指针来得到整个链表信息,从而就能找到链表中的这个结点,但是如果没有头指针那么就无法实现了。且这种遍历的时间复杂度是O(n)O(n)O(n)。
还有另一种方式实现前插法,先后插一个元素,把前面结点的数据移动到这个新加的结点,把要新加的数据放在原来的结点,这就实现了后插,虽然地址没有变化,但是从数据上看就是前插,且时间复杂度是O(1)O(1)O(1)。
单链表删除
基本的方式和插入类似,都是转移nextnextnext结点。
带头结点的也只能删除从111开始的结点,000的头结点不能删除。
时间复杂度为O(n)O(n)O(n)。
无头结点需要额外处理第一个结点
如果删除指定结点而不知道其前驱,也可以使用之前前插结点的方式,把该结点后继的结点的数据复制到本结点上,然后把后继结点删除,就相当于删除了本结点。时间复杂度为O(1)O(1)O(1)。
所以单链表还是不算方便。
单链表查找
按位查找时间复杂度为O(n)O(n)O(n)。
这样插入元素函数InsertLinkListWithHead
只用GetLinkListNode(list,i-1)
和InsertNextLinkNode(p,elem)
两个函数完成。
单链表建立
可以使用尾插法建立单链表,从后面不断插入元素。需要定义一个尾指针来记录最后一位。
使用前插法建立单链表实际上也是使用后插操作,不过每一次后插的元素都是头结点,也不用使用尾指针。
前插法可以实现链表的逆置。
双链表
为了解决单链表只能单一方向扫描而无法两项遍历的缺点,使用了两个指针,prior
和next
,分别指向前驱和后继。
双链表定义
基本上与单链表的定义一致。
双链表操作
双链表插入
假如p
的结点后要插入结点s
,则基本代码如下:
// 将插入的结点的后续接上原来的p的后续
s->next=p->next
// 将p后的结点的前驱连接到s上
p->next->prior=s;
// 将s的前驱连接到p上
s->prior=p;
// 将p的后继连接到s上
p->next=s;
操作上都是成对的,其中第一条第二条指令必须在最后一条指令之前,否则p
的后继就会丢掉。
双链表删除
若删除结点p
的后继结点q
:
p->next=q->next;
q->next->prior=p;
free(q);
循环链表
分为循环单链表和循环双链表。基本上变化不大。
原来的单链表的尾部指向NULL
,但是循环单链表的尾部是指向头部。
循环单链表即使没有头结点的地址,也可以通过循环得到整个单链表的信息。
从头到尾单链表需要遍历整个链表,而循环单链表只用移动一位就可以从头到尾。
循环双链表除此之外,头结点的prior
指针还要指表尾结点(即某结点*p
为尾结点时,p->next==list
)。
循环双链表为空表时,头结点的prior
和next
域都等于list
(即,指向自身)。
循环链表定义
循环链表和链表的结点定义是一致的。
静态链表
静态链表本质是一个数组,不过其内的基本元素不是基本数据类型而是结构体类型。
静态链表借助数组来描述线性表的链式存储结构,结点也有数据域和指针域,这里的指针是结点的相对地址(数组下标),又称游标。
静态链表和顺序表一样需要预先分配一块连续的内存空间。
数组000号元素充当链表的头结点且不包含数据。
如果一个结点是尾结点,其游标设置为−1-1−1。
具体的实现方式有多种,也可以000号元素数据保存头节点下标。
考的比较少。
静态链表特点
- 增删改不需要移动大量数据元素。
- 不能随机存取,只能从头节点开始。
- 容量固定保持不变。
适用场景:
- 不支持指针的低级语言。
- 数据元素数量固定不变,如操作系统文件分配表FAT。
静态链表定义
静态链表操作
静态链表查找
需要从头结点往后逐个遍历结点,时间复杂度为O(n)O(n)O(n)。
静态链表插入
如果要插入位序为i,索引为i-1的结点:
- 找到一个空结点(如何判断为空?可以先让next游标为某个特殊值如-2等),存入数据元素。
- 然后从头结点出发扎到位序为i-1的结点。
- 修改新结点的next。
- 修改i-1位序结点的next。
顺序表与链表对比
- 从逻辑结构来看,其都是线性结构的。
- 从物理结构来看,顺序表可以随机存取,存储数据密度高,但是分配与改变空间不变;链表空间离散,修改方便,但是不可随机存储,存储数据密度低。
- 从创建来看,顺序表需要申请一片大小适合的空间;而链表无所谓。
- 从销毁来看,顺序表需要将length设置为0,从逻辑上销毁,再从物理上销毁空间,如果是静态分配的静态数组,系统会自动回收空间,而如果是动态数组,需要手动调用
free
函数;链表逐点进行free
就可以了。 - 从增加删除来看,顺序表都要对后续元素进行前移或后移,时间复杂度为O(n)O(n)O(n),主要来自于移动元素;而对于链表插入或删除元素只用修改指针就可以了,时间复杂度也为O(n)O(n)O(n),主要来自于查找目标元素,但是链表的查找元素所花费的时间可能远小于移动元素的时间。
- 从查找来看,顺序表因为有顺序所以按位查找时间复杂度为O(1)O(1)O(1),如果按值查找时间复杂度为O(n)O(n)O(n),如果值是有序的则可以通过二分查找等方式降低在O(log2n)O(\log_2n)O(log2n)的时间内找到;如果是链表的查找无论是按位还是按值都是O(n)O(n)O(n)的时间复杂度。