学习自《数据结构与算法 Python语言描述》机械工业出版社。记录一些要点,以供加深记忆和日后查阅。
1. 算法和算法分析
1.1 问题、实例和算法
三个基本概念:
- 问题:需要解决的一个具体需求。
- 实例:以上问题的一个具体例子。
- 算法:一种计算过程的严格描述。
算法的性质:
- 有穷性:算法的描述由有限条语句构成,对任何问题在有限的时间内获得解答。
- 可行性:语句严格、简单明确,可以被机械执行。
- 确定性:对于确定的输入产生确定的输出。
- 明确的输入/输出。
算法的描述:
- 自然语言描述:容易阅读,冗长繁琐,易有歧义。
- 数学公式描述:更加简洁严格。
- 形式化记法描述:如图灵机模型。
- 编程语言描述:简洁清晰,涉及语言细节,不易移植。
- 伪代码描述:不拘泥于具体语言形式。
算法思想(设计模式):
- 枚举法:在解决简单问题时十分有效。
- 贪心法:由部分解逐步扩充得到整体的解。
- 分治法:把复杂问题分解为相对简单的子问题。
- 回溯法:通过探索的方式求解,当后面的求解步骤无法继续时,退回到前面的步骤,另行选择求解路径。
- 动态规划法:在前面的步骤中积累信息,在后续步骤中使用已知信息。
- 分支限界法:在搜索的过程中逐步缩小求解的范围。
1.2 算法的代价及其度量
算法分析就是很对一个具体算法,设法确定一种函数关系,以问题实例的某种规模n为参量,反映出这个算法在处理该规模实例时所需要付出的时间或空间代价。
- 实例的规模采用什么来度量?方阵的行数、元素个数还是维度
- 完成某一算法平均需要多少时间,最长需要多少时间?
对于抽象的算法,通常无法做出精确度量。在这种情况下只能退而求其次,设法估计算法复杂性的量级。对于算法的时间和空间性质,最重要的是其量级和趋势,这些是代价的主要部分,而代价函数的常量因子可以忽略不计。
大O记法:对于单调的整数函数f,如果存在一个整数函数g和实常数c>0,使得对于充分大的n总有f(n)<=c*g(n),就说函数g是f的一个渐进函数,记为f(n)=O(g(n))。由此可以描述算法的时间复杂度和空间复杂度。
常用渐进复杂度函数:
O(1), O(log n), O(n), O(n log n), O(n^2), O(n^3), O(2^n)
算法的复杂度反过来决定了算法的可用性。如果算法的复杂度较低,就可能用于解决很大的实例,而复杂度很高的算法只能用于很小的实例。
1.3 算法分析
考虑最基本的循环程序,其中只有顺序组合、条件分支和循环结构。分析这种算法只需要几条基本计算规则:
(1)基本操作,认为其时间复杂度为O(1)。如果是函数调用,应将其时间复杂度代入,参与整体时间复杂度的计算。
(2)加法规则(顺序复合)。如果算法是两个或多个部分的顺序复合,其复杂度是这两部分或多个部分之和。
T(n)=T1(n)+T2(n)=O(T1(n)+T2(n))=O(max(T1(n),T2(n)))
由于忽略了常量因子,加法等价于求最大值。
(3)乘法规则(循环结构)。如果算法是一个循环,循环体将执行T1(n)次,每次执行需要T2(n)时间,那么
T(n)=T1(n)*T2(n)=O(T1(n))*O(T2(n))=O(T1(n)*T2(n))
(4)取最大规则(分支结构)。如果算法是条件分支,两个分支的时间复杂性分别为T1(n)和T2(n),则有
T(n)=O(max(T1(n),T2(n)))
1.4 Python程序的计算代价
1.4.1 时间开销
- 基本算术运算和逻辑运算是常量时间的。
- 组合对象的操作有些是常量时间的,有些不是。
- 复制和切片操作通常需要线性时间;
- list和tuple的元素访问和元素赋值是常量时间的;
- dict的操作情况比较复杂。
- 字符串也应当看作组合对象,其许多操作不是常量时间的。
- 创建对象也需要付出时间和空间,其代价都与对象的大小有关。
- 构造新结构。构造新的空结构(空表、空集合等)是常量时间操作,而构造一个包含n个元素的结构则需要O(n)时间。
- 关于list:表元素访问和元素修改是常量时间操作,但一般的加入/删除元素操作是O(n)的操作。
- 关于字典:一般效率较高,但偶尔也会出现效率低的情况。
- 在表的最后加入和删除元素的效率高,而在中间位置插入和删除元素的效率低,应优先选择前者。
1.4.2 空间开销
在程序里使用任何类型的对象,都需要付出空间的代价。
相对而言,表和元组是比较简单的结构,而集合和字典需要支持快速查询等操作,其结构更加复杂。
- python中各种组合数据对象都没有预设的最大元素个数,这些结构可以根据元素个数的增长自动扩充空间。
- 组合对象的实际空间开销在存续期内可能变大,但通常不会自动缩小(即使后来元素个数变少了)。
在程序开发时,不但要选择好的算法,还要考虑每一步的良好实现。
Python等高级程序语言存在一些“效率陷阱”,可能会大量浪费计算机的时间和空间。
2. 数据结构
用计算机解决问题,可以看作实现某种信息表示的转换。
需要处理的信息越复杂,处理计算过程越复杂,良好的数据组织结构就越重要。
2.1 数据结构及其分类
2.1.1 信息、数据和数据结构
信息:任何存在着的事物,其形态和运动都蕴含着信息。
数据:计算机程序能够处理的符号形式的总和,即编码的信息。
数据结构:研究数据之间的关联和组合的形式,总结其中的规律,发掘特别值得注意的有用结构,研究这些结构的性质,进而研究如何在计算机里实现这些有用的数据结构,以支持响应组合数据的高效使用,支持处理它们的高效算法。
2.1.2 抽象定义与重要类别
一般定义:一个具体的数据结构就是一个二元组
D=(E,R)
E是数据结构D的元素集合,R是D的元素之间的某种关系。
典型数据结构:
- 集合:元素间没有明确关系,R为空。
- 序列结构:数据元素间有一个明确的先后关系,存在一个排位在最前的元素,除了最后的元素外,每个元素都一个唯一的后继元素。R为线性顺序关系。这种结构也称为线性结构。还有一些变形,如环形结构。
- 层次结构:数据元素属于一些不同的层次,一个上层元素可以关联一个或多个下层元素。
- 树形结构:层次结构中最简单的一种。只有一个最上层元素,称为根,,除根之外的每个元素,都有且仅有一个上层元素与之关联。
- 图结构:数据元素之间可以有任意复杂的互相联系。广义,包含上面几种,上面几种可以看作图的受限形式。
另一类数据结构:并没有对其元素的相互关系提出任何结构性的规定,而是要求其在某种计算中实现有用的功能,最基本的有数据元素的存储和访问。
功能性数据结构:栈、队列、优先队列、字典等。
2.2 计算机内存对象表示
内存是CPU可以直接访问的数据存储设备,保存在外存里的数据必须先装入内存,而后CPU才能使用它们。
内存单元具有唯一编号,成为单元地址。全部可用地址为从0开始的一个连续的正整数区间。
在程序执行中,对内存单元的访问都通过地址进行。64位计算机一次可以存取8个字节的数据。
基于地址访问内存单元是一个O(1)操作,与单元的位置或整个内存的大小无关。
在计算机内存里表示数据元素之间的联系,只有两种基本技术:
- 顺序表示:利用数据元素的存储位置隐式表示。显然序列数据类型中的元素线性关系可以用这种方式表示。
- 把数据元素之间的联系也看作一种数据,显式地保存在内存中。用这种方式可以表示数据元素之间任意复杂的关系,因此这种技术的功能更强大。
2.3 Python对象和数据结构
Python语言的实现基于一套精心设计的链接结构,变量与其值对象的关联通过链接的方式实现,对象之间的联系同样通过链接。
Python程序内部有一个存储管理系统,负责管理可用内存,为各种对象安排存储,支持灵活有效的内存使用。程序中要求建立对象时,管理系统就会为其安排存储,某些对象不再有用时则回收其占用的存储。管理系统屏蔽了具体内存使用的细节,大大减少了编程人员的负担。