数据结构
文章目录
第一章:绪论
1.1基本概念和术语
1.1.1 概念
-
数据:信息的载体,程序加工的原料
-
数据元素:数据的基本单位,由数据项组成
-
数据对象:及有相同性质的数据元素的组合,是数据的子集
-
数据类型:是一个值的集合,和定义在这个集合上操作的总称,分为:
①原子类型,②结构类型,③抽象数据类型
-
数据结构:即数据+结构,是相互之间存在一种或多种特定关系的数据元素的集合,其中数据元素之间的关系被称为结构
1.1.2 数据结构三要素
-
数据的逻辑结构:
-
线性结构:
线性表:数据元素间只存在一对一的关系
-
非线性结构:
集合:数据元素之间除了“同属一个集合”外无其他关系
树形结构:数据元素之间存在一对多的关系
网状结构:数据元素之间存在多对多的关系
-
-
数据的存储结构:
是数据结构在计算机中的映像,是通过计算机语言实现的逻辑结构
-
顺序存储:
定义:把逻辑上相邻的元素存储在物理位置相邻的存储单元中
优点:可以实现随机存取,每个元素占用最少的存储空间
缺点:只能使用相邻的一整块单元,外部碎片较多
-
链式存储:
定义:通过数据元素中的指针来指示元素之间的逻辑关系
优点:不会出现碎片现象,能充分利用所有存储单元
缺点:数据元素因指针而占用更多的存储空间,且只能顺序存取
-
索引存储:
定义:在存储元素信息的同时,建立一个由关键字+地址组成的索引表
优点:检索速度快
缺点:索引表占用额外的空间,且对数据进行操作时可能需要修改索引表,导致所需时间变长
-
散列存储(哈希存储):
定义:通过某种函数和元素的关键字直接计算出元素的存储地址
优点:对数据元素的增删改查变快
缺点:若散列函数不好,可能出现存储单元的冲突,为解决这些冲突会增加时间和空间的开销
-
-
数据的运算:
包括运算的定义和实现,定义指出运算的功能,实现指出运算的具体步骤
1.2 算法和算法评价
1.2.1 算法概念
算法即对解决问题的方法的一系列代码描述。
1.2.2 时空复杂度的计算
一、时间复杂度
算法的时间复杂度是算法中基本运算的执行次数的数量级,记为O(f(n)),而基本运算是指算法中最深层循环的语句。在一般情况下,分析时间复杂度是分析最坏情况下的时间复杂度。
分析时间复杂度有以下两条规则:
①加法规则:
T
(
n
)
=
T
1
(
n
)
+
T
2
(
n
)
=
O
(
f
(
n
)
)
+
O
(
g
(
n
)
)
=
O
(
m
a
x
(
f
(
n
)
,
g
(
n
)
)
)
T(n)=T_1(n)+T_2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n)))
T(n)=T1(n)+T2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n)))
②乘法规则:
T
(
n
)
=
T
1
(
n
)
×
T
2
(
n
)
=
O
(
f
(
n
)
)
×
O
(
g
(
n
)
)
=
O
(
f
(
n
)
×
g
(
n
)
)
T(n)=T_1(n)×T_2(n)=O(f(n))×O(g(n))=O(f(n)×g(n))
T(n)=T1(n)×T2(n)=O(f(n))×O(g(n))=O(f(n)×g(n))
常见的渐进时间复杂度为:
O
(
1
)
<
O
(
log
2
n
)
<
O
(
n
)
<
O
(
n
2
)
<
O
(
n
3
)
<
O
(
2
n
)
<
O
(
n
!
)
<
O
(
n
n
)
O(1)<O(\log_2n)<O(n)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n)
O(1)<O(log2n)<O(n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
二、空间复杂度
算法的空间复杂度是该算法所需的(即算法执行过程中额外所需的)存储空间,记为O(g(n))。
第二章 线性表
2.1 线性表的定义和基本操作
2.1.1 线性表的定义
线性表是一种逻辑结构,是具有相同数据类型的n个数据元素的有限序列。其中每个非表头元素仅有一个直接前驱元素,非表尾元素仅有一个直接后继元素。
- 表中元素有限
- 表中元素具有逻辑上的顺序性,表中元素有其先后次序
- 表中元素都是数据元素,每个元素都是单个元素
- 表中每个元素的数据类型相同,这使得每个元素占有的存储空间相等
- 表中元素具有抽象性,仅讨论元素之间的逻辑关系,不考虑元素本身
2.1.2 线性表的基本操作
常见的基本操作有:初始化表、求表长、按值查找、按位(索引)查找、插入、删除、输出、判空、销毁等。
与数组不同的是,线性表的位序是从1开始的。
2.2 顺序表
2.2.1 顺序表的定义
顺序表是使用一组连续的存储单元依次存储数据元素的存储结构。
顺序表的空间分配方式分为两种,即静态分配和动态分配。静态分配会在初始化时就定义好顺序表的最大长度,而动态分配中,一旦顺序表的的数据空间被占满,那么就会另寻一块新的、更大的内存空间,再将原数据全部拷贝到新的空间中。
顺序表的优点:
- 可随机访问:可通过首元素地址和元素序号在O(1)时间内找到指定元素
- 存储密度高:每个节点只存储数据
顺序表的缺点:
- 元素的插入和删除需要移动大量元素
- 顺序表需要一段连续的地址空间,不够灵活
2.2.2 顺序表的基本操作
- 初始化
- 插入元素
- 删除元素
- 按值查找
相关代码见: SqList.cpp
2.3 线性表的链式表示
2.3.1 单链表的定义
单链表是指通过一组任意的存储单元来存储线性表中的数据元素,同时为了建立起数据元素之间的线性关系,对每个链表节点,除了存放数据之外,还需要存放指向其后继元素的指针。
在初始化单链表的时候,一般以头指针L标识一个单链表,也可使用一个节点作为头部,其数据域一般为空,也可以用于存储表长等信息。
引入头节点的好处:
- 由于第一个数据节点的位置被存放于头指针的指针域中,所以在链表第一个位置上的操作与其他位置上的操作相同,无需特殊处理。
- 无论链表是否为空,其头指针都是指向头节点的非空指针,使得空表与非空表的处理得到统一。
2.3.2 单链表的操作实现
- 初始化
- 求表长
- 按序号查找结点
- 按值查找节点
- 插入节点
- 删除节点
- 头插法建立单链表
- 尾插法建立单链表
相关代码见:LinkList.cpp
注:对于删除操作,即便有能够获取尾节点位置的方法,也不能够做到O(1)内直接删除尾节点,因为删除前需要将尾节点的上一个节点的next置NULL,而这需要遍历链表获取
2.3.3 双链表
相较于单链表的结构,双链表多了一个prior指针,指向数据元素的直接前驱,表头的prior和表位的next均为NULL。
相较于单链表的操作,双链表在插入、删除的时候能够保证修改时不断链。
相关代码见:DLinkList.cpp
2.3.4 循环链表
-
循环单链表:
循环单链表与单链表的区别在于:表中的尾节点的next指针指向头节点,是整个链表形成一个环。
-
循环双链表:
循环双链表与双链表的区别在于:表中头节点的prior指针指向尾节点,尾节点的next指针指向头节点。
当表为空表时,循环链表头节点的next和prior(如果有)均指向头节点本身。
2.3.5 静态链表
静态链表使用数组来表示的链式存储结构,使用前会分配一块连续的内存存储数据,其节点依旧是由数据域data和指针next组成,但其指针存放的是数组中的索引值。实例如下:
索引 | data | next |
---|---|---|
0 | b | 4 |
1 | a | 1 |
2 | d | -1 |
3 | ||
4 | c | 2 |
如上图所示,该链表的起始位置是数组中索引为1处,其next存储的是其后继元素对应的索引。
2.3.6 顺序表与链表的对比
-
读写方式:
顺序表可以随机存取,也可以顺序存取;链表只能顺序存取。
-
逻辑结构与物理结构:
顺序表采用顺序存储,逻辑上相邻的元素,物理结构上也相邻;链表采用链式存储,物理存储位置不一定相邻,元素之间的逻辑关系是通过指针链接来表示的。
-
查找、插入和删除:
查找:顺序表无序时,时间复杂度为O(n),有序时,采用折半查找后时间复杂度为O(logn);链表时间复杂度始终为O(n)。
插入和删除:找到元素插入或删除的位置后,顺序表平均需要移动n/2的元素方可完成,时间复杂度为O(n);链表则只需改变指针指向的节点即可,时间复杂度为O(1).
-
空间分配:
顺序表:对于静态分配内存的顺序表,其不够灵活的存储方式使得这种结构容易出现内存溢出;对于动态分配内存的顺序表,其每一次扩充存储空间都需要遍历一遍数组,时间开销极大。
链表:链式存储使得只要有空间就能够分配,但是由于每个节点都带有指针,使得存储密度不大。
第三章 栈、队列和数组
3.1 栈
3.1.1 栈的概念
即只允许在一端进行插入或删除的线性表。
3.1.2 栈的顺序存储结构
- 初始化
- 判栈空
- 元素入栈
- 元素出栈
- 读栈顶元素
相关代码见:SqStack.cpp
-
共享栈:
利用栈底元素相对不变的特性,使两个顺序栈共享一个一维数组空间,以达到节省空间,减少外部碎片的目的。两个顺序栈的栈底分别为一维数组空间的两端,判断栈满的条件为|top1-top0| == 0。
-
栈的链式存储:
类似于使用头插法实现的单链表。
3.2 队列
3.2.1 队列的概念
即只允许在一段进行插入,另一端进行删除的线性表。
3.2.2 队列的顺序存储结构
-
队列:
进队操作:送值到队尾,再将队尾指针加一
出队操作:从对头取值,再将队头指针加一
假溢出:由于进出队操作只会使指针的值不断增加,故经过若干次出入队操作后,即便队列中的data域为空,但头尾指针仍会指向data域外的区域,导致假溢出
-
循环队列:
对每次进出队操作后头/尾指针加一的操作对MaxSize取余。
-
区分队满和队空:
-
牺牲一个单元来区分队满和队空:
队满:(rear + 1) % MaxSize == front
队空:front == rear
-
增设size数据成员,删除成功减一,插入成功加一
队满:size == MaxSize
队空:size == 0
-
增设tag数据成员,删除成功置0,插入成功置1
队满:front == rear && tag == 1
队空:front == rear && tag == 0
-
3.2.3 队列的链式存储结构
- 一般而言,对应同时拥有头尾指针的单链表
3.2.4 双端队列
- 是指允许两端均可进行插入和删除操作的队列
- 操作受限的双端队列:
- 输入受限:两端均可进行删除,只有其中一端可进行插入
- 输出受限:两端均可进行插入,只有其中一段可进行删除
3.3 栈和队列的应用
3.3.1 栈在括号匹配中的应用
相关代码见:PareMatching.cpp
3.3.2 栈在表达式求值中的应用
-
中缀表达式:形如 A+B*(C-D)+E/F的算术表达式
-
后缀表达式:形如 ABCD-*+EF/+ 的算术表达式
-
中缀转后缀:
从左往右遍历
- 遇到操作数:加入后缀表达式
- 遇到运算符:若其优先级高于栈顶运算符(除括号外),则入栈;否则,依次弹出运算符,直到遇到括号为止,再将其入栈
- 遇到界限符(括号):若为左括号,直接入栈;为右括号,一次弹出栈中的运算符,直到遇到左括号
-
后缀表达式求值:
若为操作数,直接入栈;若为运算符,从栈中依次退出两个操作数X和Y,形成运算指令 XY ,再将结果压入栈中
3.3.3 栈在递归中的应用
当调用一个递归函数时,一般函数会层层嵌套调用,这时系统会开辟一块递归调用栈来存储数据,若递归次数过多,同样会造成栈溢出。
3.3.4 队列在计算机系统中的应用
-
缓冲区:
当输出设备输出速度低于输入设备输入的速度时,便可设置一个缓冲区域,输入设备将数据输入缓冲区,缓冲区满后便可先处理其他作业,待输出设备按先进先出原则取出数据并输出完毕后,再向输入设备发起请求,这样便保证输出数据的正确,也可提高输入设备的效率。
-
CPU资源调度竞争:
在一个多终端的计算机系统中,多个用户需要CPU执行程序,那么这些请求将被排成队列由CPU按照先进先出的原则分别执行。
3.4 数组和特殊矩阵
3.4.1 数组的定义
数组不会的话考啥研
3.4.2 数组的存储结构
数组不会的话考啥研
3.4.3 特殊矩阵的压缩存储
-
对称矩阵:
由于矩阵中上三角区和下三角区对称,故直接用一维数组来存储下三角区和主对角线的元素即可。
若将a[i][j] (1≤i,j≤n)中的数据压缩至b[k],则效标的对应关系如下:
k = { i ( i − 1 ) 2 + j − 1 , 如果 i ≥ j j ( j − 1 ) 2 + i − 1 , 如果 i < j k = \left\{ \begin{array}{ll} \frac{i(i-1)}{2}+j-1, & \text{如果 } i \geq j \\ \frac{j(j-1)}{2}+i-1, & \text{如果 } i < j \end{array} \right. k={2i(i−1)+j−1,2j(j−1)+i−1,如果 i≥j如果 i<j -
三角矩阵:
-
下三角矩阵:
和对称矩阵相似,但不同在于压缩后的数组多一位用于存储上三角的数据。
行优先: k = { i ( i − 1 ) 2 + j − 1 , 如果 i ≥ j n ( n + 1 ) 2 , 如果 i < j 列优先: k = { ( j − 1 ) ( 2 n − j + 2 ) 2 + i − j , 如果 i ≥ j n ( n + 1 ) 2 , 如果 i < j 行优先: k = \left\{ \begin{array}{ll} \frac{i(i-1)}{2}+j-1, & \text{如果 } i \geq j \\ \frac{n(n+1)}{2}, & \text{如果 } i < j \end{array} \right.\\ 列优先: k = \left\{ \begin{array}{ll} \frac{(j-1)(2n-j+2)}{2}+i-j, & \text{如果 } i \geq j \\ \frac{n(n+1)}{2}, & \text{如果 } i < j \end{array} \right. 行优先:k={2i(i−1)+j−1,2n(n+1),如果 i≥j如果 i<j列优先:k={2(j−1)(2n−j+2)+i−j,2n(n+1),如果 i≥j如果 i<j -
上三角矩阵:
行优先: k = { ( i − 1 ) ( 2 n − i + 2 ) 2 + j − i , 如果 i ≤ j n ( n + 1 ) 2 , 如果 i > j 列优先: k = { j ( j − 1 ) 2 + i − 1 , 如果 i ≤ j n ( n + 1 ) 2 , 如果 i > j 行优先: k = \left\{ \begin{array}{ll} \frac{(i-1)(2n-i+2)}{2}+j-i, & \text{如果 } i \leq j \\ \frac{n(n+1)}{2}, & \text{如果 } i > j \end{array} \right.\\ 列优先: k = \left\{ \begin{array}{ll} \frac{j(j-1)}{2}+i-1, & \text{如果 } i \leq j \\ \frac{n(n+1)}{2}, & \text{如果 } i > j \end{array} \right. 行优先:k={2(i−1)(2n−i+2)+j−i,2n(n+1),如果 i≤j如果 i>j列优先:k={2j(j−1)+i−1,2n(n+1),如果 i≤j如果 i>j
-
-
三对角矩阵:
k = 3 ( i − 1 ) + ( j − i ) = 2 i + j − 3 k=3(i-1)+(j-i)=2i+j-3 k=3(i−1)+(j−i)=2i+j−3 -
稀疏矩阵:
当矩阵中非零元素个数远少于为零元素个数时,可直接使用三元组存储非零元素及其对应下标。当然也需要存储矩阵的行数和列数。
但会这导致矩阵失去随机存取的特性,变为只读形式。