1
0
mirror of https://github.com/Didnelpsun/CS408.git synced 2026-02-10 06:05:51 +08:00
Files
CS408/Data-Structrue/1-linear-list.md
Didnelpsun 1795de9caa 数据结构完结
对原有的文件添加上序号
2021-05-06 15:42:57 +08:00

8.9 KiB
Raw Blame History

线性表

基本概念

逻辑结构

是具有相同数据类型的n个数据元素的有限序列。n表示表长。

$L=(a_1,a_2,\cdots,a_i,\cdots,a_n)$,其中$i$表示元素在线性表中的位序,从一开始。

  • 存在惟一的第一个元素。
  • 存在惟一的最后一个元素。
  • 除第一个元素之外,每个元素均只有一个直接前驱。
  • 除最后一个元素之外,每个元素均只有一个直接后继。

物理结构

  • 顺序存储结构:顺序表。
  • 链式存储结构:链表。

顺序表

把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来实现。

顺序表特点

  1. 随机访问,可以在$O(1)$时间内找到对应元素。
  2. 存储密度高,只用存储数据。
  3. 拓展容量不方便。
  4. 插入删除操作不方便。

顺序表定义

使用C语言的结构体定义顺序表使用typedef定义一个ElemType表示数据基本类型并定义最大长度MaxSize

可以使用静态分配空间,也可以使用动态分配空间:

MAXSIZE表示动态顺序表当前可以使用的最大存储空间。

顺序表操作

顺序表初始化

静态顺序表因为数组部分在创建时就已经设置好了,所以初始化就直接设置数据长度就可以了。

动态顺序表不仅需要设置数据长度与最大长度,还得分配数组初始空间。

顺序表增长数据空间长度

只有动态顺序表才能增加。

顺序表插入

倒序移动元素,最后将数据插入对应索引并长度减一。

插入时间复杂度为:$T(n)=O(n)$,空间复杂度为$S(n)=O(1)$。

顺序表删除

正序移动元素并长度减一。

顺序表的删除时间复杂度为:$T(n)=O(n)$,空间复杂度为$S(n)=O(1)$。

顺序表查找

按位查找时间复杂度为$T(n)=O(1)$。

按值查找一般都是找到第一个元素等于指定值的元素,返回其位序,如果没有找到就返回-1。按位查找时间复杂度为$T(n)=O(n)$。

单链表

每个结点只包含一个指针域,也称为线性链表。

通常用头指针来标识一个单链表如单链表L。

单链表特点

  • 不要求大量连续空间,删除和插入方便。
  • 不可随机存取。
  • 要花费多余空间存放指针。

单链表定义

使用LinkNode表示一个单链表结点的结构体而使用LinkList表示一个单链表其实LinkList是一个指向LinkNode的指针变量。如定义LinkList L等价于LinkNode* L。

单链表操作

单链表初始化

有带头节点与不带头节点的初始化的区别,带头节点代表第一个结点不存放数据,只是用于标识单链表的开始,但是区别不大,带头结点更好使用。

单链表插入

插入方式一共分为下面几种:

  • 按位序插入:
    • 带头点结。
    • 不带头结点。
  • 指定结点插入:
    • 前插入。
    • 后插入。

假定从第一个结点开始就是第0索引的结点。

带头结点的单链表头结点就是0号结点不带头节点的第一个数据结点就是0号结点。

带头结点的单链表只能往头结点之后插入所以插入索引必须从1开始。

头插法建立单链表:

  • 每个结点的插入时间为$O(1)$设单链表长为n则总时间复杂度为$O(n)$。
  • 实现了输入数据的就地逆置。

尾插法建立单链表

  • 增设尾指针r。
  • 生成的链表中结点数据与输入数据顺序一致。
  • 总时间复杂度为$O(n)$。

插入有/无头节点单链表元素函数的后面代码可以使用后插入单链表元素函数来替代。

使用前插入的方法插入元素,可以使用头指针来得到整个链表信息,从而就能找到链表中的这个结点,但是如果没有头指针那么就无法实现了。且这种遍历的时间复杂度是$O(n)$。

还有另一种方式实现前插法,先后插一个元素,把前面结点的数据移动到这个新加的结点,把要新加的数据放在原来的结点,这就实现了后插,虽然地址没有变化,但是从数据上看就是前插,且时间复杂度是$O(1)$。

单链表删除

基本的方式和插入类似都是转移next结点。

带头结点的也只能删除从1开始的结点0的头结点不能删除。

时间复杂度为$O(n)$。

无头结点需要额外处理第一个结点

如果删除指定结点而不知道其前驱,也可以使用之前前插结点的方式,把该结点后继的结点的数据复制到本结点上,然后把后继结点删除,就相当于删除了本结点。时间复杂度为$O(1)$。

所以单链表还是不算方便。

单链表查找

按位查找时间复杂度为$O(n)$。

这样插入元素函数InsertLinkListWithHead只用GetLinkListNode(list,i-1)InsertNextLinkNode(p,elem)两个函数完成。

单链表建立

可以使用尾插法建立单链表,从后面不断插入元素。需要定义一个尾指针来记录最后一位。

使用前插法建立单链表实际上也是使用后插操作,不过每一次后插的元素都是头结点,也不用使用尾指针。

前插法可以实现链表的逆置。

双链表

为了解决单链表只能单一方向扫描而无法两项遍历的缺点使用了两个指针prior和next分别指向前驱和后继。

双链表定义

基本上与单链表的定义一致。

双链表操作

双链表初始化

双链表插入

双链表删除

循环链表

分为循环单链表和循环双链表。基本上变化不大。

原来的单链表的尾部指向NULL但是循环单链表的尾部是指向头部。

循环单链表即使没有头结点的地址,也可以通过循环得到整个单链表的信息。

从头到尾单链表需要遍历整个链表,而循环单链表只用移动一位就可以从头到尾。

循环双链表除此之外头结点的prior指针还要指表尾结点即某结点*p为尾结点时p->next==list

循环双链表为空表时头结点的prior和next域都等于list指向自身

循环链表定义

循环链表和链表的结点定义是一致的。

循环链表操作

循环单链表初始化

循环双链表初始化

循环双链表插入

循环双链表删除

静态链表

静态链表本质是一个数组,不过其内的基本元素不是基本数据类型而是结构体类型。

静态链表借助数组来描述线性表的链式存储结构,结点也有数据域和指针域,这里的指针是结点的相对地址(数组下标),又称游标。

静态链表和顺序表一样需要预先分配一块连续的内存空间。

数组0号元素充当链表的头结点且不包含数据。

如果一个结点是尾结点,其游标设置为-1。

考的比较少。

静态链表特点

  1. 增删改不需要移动大量数据元素。
  2. 不能随机存取,只能从头节点开始。
  3. 容量固定保持不变。

适用场景:

  1. 不支持指针的低级语言。
  2. 数据元素数量固定不变如操作系统文件分配表FAT。

静态链表定义

静态链表操作

静态链表查找

需要从头结点往后逐个遍历结点,时间复杂度为$O(n)$。

静态链表插入

如果要插入位序为i索引为i-1的结点

  1. 找到一个空结点如何判断为空可以先让next游标为某个特殊值如-2等存入数据元素。
  2. 然后从头结点出发扎到位序为i-1的结点。
  3. 修改新结点的next。
  4. 修改i-1位序结点的next。

顺序表与链表对比

  • 从逻辑结构来看,其都是线性结构的。
  • 从物理结构来看,顺序表可以随机存取,存储数据密度高,但是分配与改变空间不变;链表空间离散,修改方便,但是不可随机存储,存储数据密度低。
  • 从创建来看,顺序表需要申请一片大小适合的空间;而链表无所谓。
  • 从销毁来看顺序表需要将length设置为0从逻辑上销毁再从物理上销毁空间如果是静态分配的静态数组系统会自动回收空间而如果是动态数组需要手动调用free函数;链表逐点进行free就可以了。
  • 从增加删除来看,顺序表都要对后续元素进行前移或后移,时间复杂度为$O(n)$,主要来自于移动元素;而对于链表插入或删除元素只用修改指针就可以了,时间复杂度也为$O(n)$,主要来自于查找目标元素,但是链表的查找元素所花费的时间可能远小于移动元素的时间。
  • 从查找来看,顺序表因为有顺序所以按位查找时间复杂度为$O(1)$,如果按值查找时间复杂度为$O(n)$,如果值是有序的则可以通过二分查找等方式降低在$O(\log_2n)$的时间内找到;如果是链表的查找无论是按位还是按值都是$O(n)$的时间复杂度。