一、数据结构概述
1. 概念
-
数据是能够输入计算机且能够被计算机处理的各种符号的集合,它既包括数值类型的数据(例如整数、浮点数等),又包括非数值型的数据(文字、图像、声音)等。
-
数据元素是数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理(类似于高级语言中的对象)。
-
数据项是构成数据元素的不可分割的最小单位(类似于高级语言定义的类中的数据成员)。
-
数据对象是性质相同的数据元素的集合,是数据的一个子集。
数据结构是数据元素相互之间的关系,是相互之间存在一种或多种特定关系的数据元素的集合。一个数据结构包含了以下三个方面的内容:
- 数据元素之间的逻辑关系,也就是逻辑结构(就是给人看的、能画出来的)。
- 数据元素及其关系在计算机内存中的表示,成为数据的存储结构(就是给计算机看的、具体实现相关的)。
- 数据的运算和实现,即对数据元素可以施加的操作以及这些操作在相应存储结构上的实现(就是数据结构对外提供的操作接口)。
2. 逻辑结构的种类
逻辑结构可以分为线性结构和非线性结构:
-
线性结构:有且只有一个开始节点和一个终端节点,并且所有节点都最多只有一个直接前驱和直接后继:
-
非线性结构:一个节点可能有多个直接前驱和直接后继。
3. 存储结构的分类
存储结构可以分为顺序存储结构、链式存储结构、索引存储结构和散列存储结构:
- 顺序存储结构:用一组连续的内存单元依次存储数据元素,数据元素之间的逻辑关系由元素存储位置来表示。
- 链式存储结构:用一组任意的存储单元存储数据元素,数据元素之间的逻辑关系用指针来表示。
- 索引存储结构:在存储节点信息的同时,还建立附加的索引信息。索引表中的每一项称作一个索引项。
- 稠密索引:每个节点在索引表中都有一个索引项。
- 稀疏索引:一组节点在索引表中只对应一个索引项。
- 散列存储结构:根据节点的关键字直接计算出该节点的存储地址。
二、算法概述
算法效率通过以下两个方面来考虑:
- 时间效率:指的是算法所耗费的时间;
- 空间效率:指的是算法执行过程中所耗费的存储空间。
时间效率与空间效率有时是矛盾的,二者往往不可兼得。
1. 算法时间效率的度量
算法时间效率可以用依据该算法编制的程序在计算机上执行时消耗的时间来度量。
-
事后统计:将算法实现,测算其时间和空间开销。
-
事前分析:对算法所消耗的资源进行理论估算。
由于事后统计法需要花费大量的时间编写程序,并且其运行结果很大程度上取决于具体的测试平台,所以事前分析法往往是更有效的评估方法,具体方法是:
T
=
∑
i
=
1
n
f
i
t
i
T = \sum_{i=1}^n f_it_i
T=i=1∑nfiti
其中T
表示算法的运行时间,f
表示每种语句的执行频率,t
表示该种语句执行一次所需的时间。
由于每条语句执行的时间与算法无关,所以我们可以假定执行每条指令所需的时间均为单位时间,此时上式就可简化为每种语句的执行频率之和,即:
T
=
∑
i
=
1
n
f
i
T = \sum_{i=1}^n f_i
T=i=1∑nfi
为了便于比较不同算法的时间效率,我们仅比较它们的数量级。定义算法时间复杂度的渐进表示如下:
∃
f
(
n
)
,
s
.
t
lim
n
→
∞
T
(
n
)
f
(
n
)
=
k
≠
0
\exists f(n),s.t \lim_{n\to\infty} \frac{T(n)}{f(n)} = k \neq 0
∃f(n),s.tn→∞limf(n)T(n)=k=0
则称T(n) = O(f(n))
为算法的时间复杂度。算法时间复杂度表示随着问题规模n
的增大,算法执行时间的增长率与f(n)
的增长率相同。例如,若有算法的耗费时间为:T(n)=2n^3+3n^2+2n+1
,则有:
∃
f
(
n
)
=
n
3
,
s
.
t
lim
n
→
∞
T
(
n
)
n
3
=
2
≠
0
\exists f(n)=n^3, s.t \lim_{n\to\infty}\frac{T(n)}{n^3} = 2 \neq 0
∃f(n)=n3,s.tn→∞limn3T(n)=2=0
所以T(n)=O(n^3)
,即此算法的时间复杂度为O(n^3)
。由此可见,算法的时间复杂度实际上是借助于无穷大来定义的,并且这个值越小,表示算法执行得越快。
有的情况下,算法中基本操作重复执行的次数还与输入数据集的特性有关。例如,如果冒泡排序算法的输入为一个有序数组,那么算法可以直接退出,时间复杂度为O(1)
;如果输入为一个逆序数组,那么算法需要交换所有元素才能保证数组有序,时间复杂度为O(n^2)
。因此,算法的时间复杂度又分为:
- 最坏时间复杂度:指在最坏的情况下,算法的时间复杂度。
- 最好时间复杂度:指在最好的情况下,算法的时间复杂度。
- 平均时间复杂度:指在所有可能的输入实例在等概率出现的情况下,算法的期望运行时间。
一般情况下总是会考虑最坏时间复杂度,这样可以保证算法的运行时间不会比这种情况更长。
当算法的输入规模n
很大时,不同时间复杂度算法的执行时间差距非常大。下面的表格列出了一些常见时间复杂度的算法的执行时间随着输入规模增加的增长率:
n n n | O ( 1 ) O(1) O(1) | O ( log 2 n ) O(\log_2{n}) O(log2n) | f ( n ) f(n) f(n) | O ( n log 2 n ) O(n\log_2{n}) O(nlog2n) | f ( n 2 ) f(n^2) f(n2) | f ( n 3 ) f(n^3) f(n3) | f ( 2 n ) f(2^n) f(2n) | f ( n ! ) f(n!) f(n!) |
---|---|---|---|---|---|---|---|---|
1 | 1 | 0 | 1 | 0 | 1 | 1 | 2 | 1 |
2 | 1 | 1 | 2 | 2 | 4 | 8 | 4 | 2 |
4 | 1 | 2 | 4 | 8 | 16 | 64 | 16 | 24 |
8 | 1 | 3 | 8 | 24 | 64 | 512 | 256 | 40320 |
16 | 1 | 4 | 16 | 64 | 256 | 4096 | 65536 | 2.0923E+13 |
32 | 1 | 5 | 32 | 160 | 1024 | 32768 | 4.295E+9 | 2.6313E+35 |
2. 算法空间效率的度量
算法的空间效率使用空间复杂度来度量。空间复杂度就是算法运行所需的存储空间的度量,记作:
S
(
n
)
=
O
(
f
(
n
)
)
S(n) = O(f(n))
S(n)=O(f(n))
算法要占据的空间包括:
- 算法本身要占据的空间,例如输入输出、指令、常数、变量等。
- 算法要使用的辅助空间。