基本概念和术语
- 数据
数据是信息的载体,是描述客观事物属性的数,字符以及所有能输入到计算机中并被计算机程序识别和处理的符号的集合。
- 数据元素
数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。一个数据元素可由若干个数据项组成,数据项是构成数据元素的不可分割的最小单位。例如,学生记录就是一个数据元素,它由学号、姓名、性别等数据项组成。
- 数据对象
数据对象是具有相同性质的数据元素的集合,是数据的一个子集。例如,整数数据对象是集合 \(N = \lbrace 0, \pm 1, \pm 2, \cdots \rbrace\)
- 数据类型
数据类型是一个值的集合和定义在此集合上一组操作的总称。
(1)原子类型:其值不可再分的数据类型。
(2)结构类型:其值可以再分解为若干成分的数据类型。
(3)抽象数据类型:抽象数据组织和与之相关的操作。
- 抽象数据类型
抽象数据类型是指一个数学模型以及定义在该模型上的一组操作。抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关,即不论其内部结构如何变化,只要它的数学特性不变,都有不影响其外部的使用。通常用(数据对象、数据关系、基本操作集)这样的三元组来表示抽象数据类型。
- 数据结构
在任何问题中,数据元素都不是孤立存在的,而是在它们之间存在着某种关系,这种数据元素相互之间的关系称为结构。数据结构是相互之间存在的一种或多种特定关系的数据元素集合。数据结构包括三方面的内容:逻辑结构、存储结构和数据的运算。数据的逻辑结构和存储结构时密不可分的两个方面,一个算法的设计取决于所选定的逻辑结构,而算法的实现依赖于所采用的存储结构。
数据的逻辑结构
逻辑结构是指数据元素之间的逻辑关系,即从逻辑关系上描述数据。它与数据的存储无关,是独立于计算机的。
划分方法一
(1)线性结构
有且仅有一个开始和一个终端结点,并且所有结点都最多只有一个直接前趋和一个后继。
例如:线性表、栈、队列、串
(2)非线性结构
一个结点可能有多个直接前趋和直接后继。
例如:树、图
划分方法二
集合
- 数据元素间除“同属于一个集合”外,无其它关系
线性结构
- 一个对一个,如线性表、栈、队列
树形结构
- 一个对多个,如树
图形结构
- 多个对多个,如图
数据的存储结构
存储结构是指数据结构在计算机中的表示(又称映像),也称物理结构。它包括数据元素的表示和关系的表示。数据的存储结构是逻辑结构用计算机语言的实现,它依赖于计算机语言。数据的存储结构主要有:顺序存储、链式存储、索引存储和散列存储。
- (1)顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元里,元素之间的关系有存储单元的邻接关系来体现。其优点是可以实现随机存取,每个元素占用最少的存储空间;缺点是只能使用相邻的一整块存储单元,因此可能产生较多的外部碎片。
- (2)链接存储:不要求逻辑上相邻的元素在物理位置上也相邻,借助指示元素存储地址的指针表示元素之间的逻辑关系。其优点是不会出现碎片现象,充分利用所有存储单元;缺点是每个元素因存储指针而占用额外的存储空间,并且只能实现顺序存储。
- (3)索引存储:在存储元素信息的同时,还建立附加的索引表。索引表中的每一项称为索引项,索引项的一般形式是:(关键字,地址)。其优点是检索速度快;缺点是增加了附加的索引表,会占用较多的存储空间。另外,在增加和删除数据时要修改索引表,因而会花费较多额时间。
- (4)散列结构:根据元素的关键字直接计算出该元素的存储地址,又称为 Hash 存储。其优点是检索、增加和删除结点的操作都很快;缺点是如果散列函数不好可能出现元素存储单元的冲突,而解决冲突会增加时间和空间开销。
注:
数据的逻辑结构是以面向实际问题的角度出发的,只采用抽象表达方式,独立于存储结构,数据的存储方式有多种不同的选择;而数据的存储结构是逻辑结构在计算机上的映射,它不能独立于逻辑结构而存在。数据结构包括三要素,缺一不可。
算法的特性
算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每一条指令表示一个或多个操作。此外,一个算法还具有下列 5 个重要特性。
(1)有穷性: 一个算法必须总是(对任何合法的输入值)在执行有穷步之后结束,且每一步都可在有穷时间内完成。
(2)确定性: 算法中每一条指令必须有确切的含义,读者理解时不会产生二义性。即对于相对的输入只能得出相同的输出。
(3)可行性: 一个算法是可行的,即算法中描述的操作都是可以通过已经实现的基本运算执行有限次来实现的。
(4)输入 : 一个算法有零个或者多个的输入,这些输入取自于每个特定的对象的集合。
(5)输出 : 一个算法有一个或者多个输出,这些输出是同输入有着某种特定关系的量。
通常设计一个“好”的算法应考虑达到以下目标。
(1)正确性:算法应当能够正确地解决求解问题。
(2)可读性:算法应当具有良好的可读性,以助于人民理解。
(3)健壮性:当输入非法数据时,算法也能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。
(4)效率与低存储量需求:效率是指算法执行的时间,存储量需求是指算法执行过程中所需要的最大存储空间,
这两者都与问题的规模有关。
算法效率的度量
算法效率的度量是通过时间复杂度和空间复杂度来描述的。
1、时间复杂度
一个语句的频度是指该语句在算法中被重复执行的次数。算法中所有语句的频度之和记作 \(T(n)\),它是该算法问题规模 n 的函数,时间复杂度主要分析 \(T(n)\) 的数量级。算法中的基本运算的频度与\(T(n)\)同数量级,所以通常采用算法中基本运算的频度 \(f(n)\) 来分析算法的时间复杂度。因此,算法的时间复杂度记为:\(T(n)=O(f(n))\)
上式中 O 的含义是 \(T(n)\) 的数量级,其严格的数学定义是:若 \(T(n)\) 和 \(f(n)\) 是定义在正整数集合上的两个函数,则存在正常数 \(C\) 和 \(n_0\) ,使得\(n>=n_0\)时,都满足 \(\color{red}{0<=T(n)<=C*f(n)}\) 。其中 \(f(n)\) 是 \(T(n)\) 的一个渐近函数。
算法的时间复杂度不仅依赖于问题的规模 n,也取决于待输入数据的性质。
例如 在数组 \(A[0, \cdots, n-1]\) 中,查找定值 k 的算法大致如下:
i = n - 1;
while(i>=0 && (A[i] != k))i--;
return i;
此算法中的语句(3)(基本运算)的频度不仅与问题规模有关,还与输入实例中 A 的各元素取值及 k 的取值有关:
(1)若 A 中没有与 k 相等的元素,则语句(3)的频度\(f(n) =n\)。
(2)若 A 中的最有一个元素等于 k,则语句(3)的频度\(f(n)\) 是常数 0。
最坏时间复杂度是指在最坏情况下,算法的时间复杂度。
平均时间复杂度是指所有可能输入实例在等概率出现的情况下,算法的期望运行时间。
最好时间复杂度是指在最好情况下,算法的时间复杂度。
一般总是考虑在最坏情况下的时间复杂度,以保证算法的运行时间不会比它更长。
在分析一个程序的时间复杂性时,有以下两条规则:
- 加法规则
\(T(n)=T1(n)+T2(n)=O(f(n))+O(g(x))=O(max(f(n),g(n)))\)
- 乘法规则
\(T(n)=T1(n) \times T2(n)=O(f(n)) \times O(g(x))=O(f(n) \times g(n))\)
常见的渐近时间复杂度
- 常量复杂度 \(O(1)\)
- 对数复杂度 \(O(logn)\)
- 线性复杂度 \(O(n)\)
- 平方复杂度 \(O(n^2)\)
- 指数复杂度 \(O(2^n)\)
基本的复杂度如上,基于以上的表达式可以有很多的组合,其中 \(logn\) 默认情况下等同于 \(log_{2}n\)
大小关系:
\(O(1) < O(log_2n) < O(n) < O(nlog_2n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)\)
2、空间复杂度
算法的空间复杂度\(S(n)\)定义为该算法所耗费的存储空间,它是问题的规模\(n\)的函数。渐进空间复杂度简称为空间复杂度,记作\(S(n)=O(g(n))\)。
一个上机程序除了需要存储空间来存放本身所用指令、常数、变量和输入数据外,也需要一些对数据进行操作的工作单位和存储一些为实现计算所需信息的辅助空间,若输入数据所占空间只取决于问题本身,和算法无关,组只需分析输入和程序之外的额外空间了。
算法原地工作是指算法所需辅助空间是常量,即\(O(1)\)。
算法复杂的意义
算法复杂度的分级相当于(高等数学)的无穷大的阶,反映了在规模\(n\)趋于无穷大的过程中,算法代价增长的速度。算法的复杂度越高,其实施的代价随着规模增大而增长的速度越快。
\(example\)
\(斐波那契数列的第n项\)
- 递归算法
def fib(n):if n < 2:return 1else:return fib(n-1) + fib(n-2)
将参数\(n\)看问题实例的规模,不难看出,计算\(F_n\)的时间代价(考虑求加法操作的次数)大致等于计算\(F_{n-1}\)和\(F_{n-2}\) 的时间代价之和。这一情况说明,计算\(F_n\)的时间代价大致等比于斐波那契数\(F_n\)的值。根据已有的结论:
\[ \mathop {\lim }\limits_{n \to \infty } {F_n} = {(\frac{{\sqrt 5 + 1}}{2})^n}=1.618^n \]
可以看到计算\(F_n\)的时间代价按\(n\)值的指数增长。
- 递推算法
def fib(n):f1 = f2 = 1for k in range(1,n):f1, f2 = f2, f2 + f1return f2
用这个算法计算\(F_n\)的值,循环的工作只做一次,循环需要做\(n-1\)次。每次循环中只执行了几个简单动作,总的工作量(基本操作执行次数)与\(n\)值呈现某种线性关系。
这个例子说明,解决同一问题的不同算法,其计算复杂度的差异很大,甚至具有截然不同的性质。通过分析算法复杂度,可以帮助使用者选择适用的算法;也可能发现已知算法的缺陷,促使人们设法开发更好的算法 。
Python 内置类型性能分析
timeit 模块
timeit 模块可以用来测试一小段 Python 代码的执行速度。
class timeit.Timer(stmt='pass', setup='pass', timer=<timer function>)
Timer 是测量小段代码执行速度的类。
stmt 参数是要测试的代码语句(statment);
setup 参数是运行代码时需要的设置;
timer 参数是一个定时器函数,与平台有关。
timeit.Timer.timeit(number=1000000)
Timer 类中测试语句执行速度的对象方法。number 参数是测试代码时的测试次数,默认为 1000000 次。方法返回执行代码的平均耗时,一个 float 类型的秒数。
list 的操作测试
def test1():l = []for i in range(1000):l = l + [i]
def test2():l = []for i in range(1000):l.append(i)
def test3():l = [i for i in range(1000)]
def test4():l = list(range(1000))from timeit import Timert1 = Timer("test1()", "from __main__ import test1")
print("concat ",t1.timeit(number=1000), "seconds")
t2 = Timer("test2()", "from __main__ import test2")
print("append ",t2.timeit(number=1000), "seconds")
t3 = Timer("test3()", "from __main__ import test3")
print("comprehension ",t3.timeit(number=1000), "seconds")
t4 = Timer("test4()", "from __main__ import test4")
print("list range ",t4.timeit(number=1000), "seconds")# ('concat ', 1.7890608310699463, 'seconds')
# ('append ', 0.13796091079711914, 'seconds')
# ('comprehension ', 0.05671119689941406, 'seconds')
# ('list range ', 0.014147043228149414, 'seconds')
pop 操作测试
x = range(2000000)
pop_zero = Timer("x.pop(0)","from __main__ import x")pop_zero = Timer("x.pop(0)","from __main__ import x")pop_zero = Timer("x.pop(0)","from __main__ import x")pop_zero = Timer("x.pop(0)","from __main__ import x")pop_zero = Timer("x.pop(0)","from __main__ import x")
print("pop_zero ",pop_zero.timeit(number=1000), "seconds")
x = range(2000000)
pop_end = Timer("x.pop()","from __main__ import x")
print("pop_end ",pop_end.timeit(number=1000), "seconds")# ('pop_zero ', 1.9101738929748535, 'seconds')
# ('pop_end ', 0.00023603439331054688, 'seconds')
测试 pop 操作:从结果可以看出,pop 最后一个元素的效率远远高于 pop 第一个元素
list 内置操作的时间复杂度
\[ \begin{array}{lc} Op & O \ Efficiency \\ indexx[\ ] & O(1) \\ index \ assignment & O(1) \\ append & O(1) \\ pop() & O(1) \\ pop(i) & O(n) \\ insert(i, item) & O(n) \\ del \ operator & O(n) \\ iteration & O(n) \\ contain(in) & O(n) \\ get \ slice[x:y] & O(k) \\ del \ slice & O(n) \\ set \ slice & O(n+k) \\ reverse & O(n) \\ concatenate & O(k) \\ sort & O(nlogn) \\ multiply & O(nk) \\ \end{array} \]
从上可以看出 list 大概是顺序表实现。
dict 内置操作的时间复杂度
\[ \begin{array}{lc} Op & O \ Efficiency \\ copy & O(1) \\ get \ item & O(1) \\ set \ item & O(1) \\ del \ item & O(1) \\ contains(in) & O(1) \\ iteration & O(n) \\ \end{array} \]