一、绪论
一个程序应该包含两个方面的内容:
- 对数据的描述:数据结构(data structure)
- 对操作的描述:算法(algorithm)
计算机科学家沃斯提出一个公式:数据结构 + 算法 = 程序
完整的程序设计应该是:数据结构 + 算法 + 程序设计方法 + 语言工具
广义地说,为解决一个问题而采取的方法和步骤,称为“算法”。
计算机算法可以分为两大类别:
- 数值运算算法:求数值解,例如求方程的根、函数的定积分等。
- 非数值计算:如图书检索、人事管理、行车调度等。
算法的表示方法:自然语言、传统流程图、伪代码、PAD图。
1.1 数据结构的基本概念
数据: 数据是信息的载体,是描述客观事物属性的数、字符及所有能输入到计算机中并被计算机程序识别和处理的符号集合。数据是计算机程序加工的原料。
数据元素: 数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。一个数据元素可由若干数据项组成,数据项是构成元素的不可分割的最小单位。
数据对象: 数据对象是具有相同性质的数据元素的集合,是数据的一个子集。
数据类型: 数据类型是一个值的集合和定义在此集合上的一组操作的总称。原子类型 – 值不可再分割的数据类型;结构类型 – 值可以再分解为若干成分(分量)的数据类型;抽象数据类型 – 抽象数据组织及与之相关的操作。
数据结构: 相互之间存在一种或多种特定关系的数据元素的集合。在任何问题中,数据元素都不是孤立存在的,它们之间存在某种关系,这种数据元素相互之间的关系称为结构。
数据结构包括三方面的内容:逻辑结构、存储结构和数据的运算。
数据的逻辑结构和存储结构是密不可分的两个方面,一个算法的设计取决于所选定的逻辑结构,而算法的实现依赖于所采用的存储结构。
抽象数据类型(ADT)描述了数据的逻辑结构和抽象运算,通常用(数据对象,数据关系,基本操作集)这样的三元组来表示,从而构成一个完整的数据结构定义。
逻辑结构指数据元素之间的逻辑关系,即从逻辑关系上描述数据,与数据的存储无关,独立于计算机。
逻辑结构分为线性结构和非线性结构。
线性结构:一般线性表、栈和队列、串、数组、广义表等。
非线性结构:集合、一般树、二叉树、图状结构或网状结构等。
存储结构指数据结构在计算机中的表示(映像),也称物理结构。包括数据元素的表示和关系的表示。
数据的存储结构是用计算机语言实现的逻辑结构,它依赖于计算机语言。
-
顺序存储:逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
优点是可以实现随机存取,每个元素占用最少的存储空间;
缺点是只能使用相邻的一整块存储单元,可能产生较多的外部碎片。
注: 内存碎片分为:内部碎片和外部碎片 【内部碎片】 内部碎片就是已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间; 内部碎片是处于(操作系统分配的用于装载某一进程的内存)区域内部或页面内部的存储块。占有这些区域或页面的进程并不使用这个存储块。而在进程占有这块存储块时,系统无法利用它。直到进程释放它,或进程结束时,系统才有可能利用这个存储块。 单道连续分配只有内部碎片。多道固定连续分配既有内部碎片,又有外部碎片。 【外部碎片】 外部碎片指的是还没有被分配出去(不属于任何进程),但由于太小了无法分配给申请内存空间的新进程的内存空闲区域。 外部碎片是处于任何两个已分配区域或页面之间的空闲存储块。这些存储块的总和可以满足当前申请的长度要求,但是由于它们的地址不连续或其他原因,使得系统无法满足当前申请。 产生外部碎片的一种情况: 举个例子,在内存上,分配三个操作系统分配的用于装载进程的内存区域A、B和C。假设,三个内存区域都是相连的。故而三个内存区域不会产生外部碎片。现在假设B对应的进程执行完毕了操作系统随即收回了B,这个时候A和C中间就有一块空闲区域了。
-
链式存储:不要求逻辑上相邻的元素在物理位置上也相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。
优点是不会出现碎片现象,能充分利用所有存储单元;
缺点是每个元素因存储指针而占用额外的存储空间,且只能实现顺序存取。
-
索引存储:在存储元素信息的同时,建立附加的索引表。索引表中的每项称为索引项,一般形式为(关键字,地址)。
优点是检索速度快;
缺点是附加的索引表额外占用存储空间,增加和删除数据时也要修改索引表,会花费较多的时间。
-
散列存储:(哈希-Hash-存储)根据元素的关键字直接计算出该元素的存储地址。
优点是检索、增加和删除节点的操作都很快;
缺点是散列函数不好,则可能出现元素存储单元的冲突,解决冲突会增加时间和空间的开销。
小补充:
循环队列是用顺序表表示的队列,是一种数据结构。
顺序表、哈希表、单链表是三种不同的数据结构,既描述逻辑结构,又描述存储结构和数据运算。
有序表指关键字有序的线性表,是逻辑结构。仅描述元素之间的逻辑关系。既可链式存储,又可顺序存储。
栈是一种抽象数据类型,可以采用顺序存储或链式存储,只表示逻辑结构。
数据的运算
施加在数据上的运算包括运算的定义和实现。
运算的定义是针对逻辑结构的,指出运算的功能;
运算的实现是实现针对存储结构的,指出运算的具体操作步骤。
1.2 算法和算法评价
算法是对特定问题求解步骤的一种描述,是智力呢个的有限序列,每条指令表示一个或多个操作。
算法具有5个重要特性:有穷性,确定性,可行性,输入,输出。
有穷性指必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成。
确定性指每条指令必须有确切的含义,对于相同的输入只能得出相同的输出。
可行性指算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现。
一个“好“算法应考虑的目标:
- 正确性。
- 可读性。
- 健壮性。输入非法数据时,能适当做出反应或进行处理,而不会产生莫名其妙的输出结果。
- 效率与低存储量需求。效率指算法执行的时间,存储量指算法执行过程中所需要的最大存储空间。两者与问题的规模有关。
时间复杂度
一个语句的频度指该语句在算法中被执行的次数。算法中所有语句的频度之和记为 T ( n ) T(n) T(n),它是该算法问题规模n的函数,时间复杂度主要分析 T ( n ) T(n) T(n)的数量级。
算法中基本运算的频度与 T ( n ) T(n) T(n)同数量级,因此通常采用算法中基本运算的频度 f ( n ) f(n) f(n)来分析算法的时间复杂度。
因此,算法的时间复杂度记为 T ( n ) = O ( f ( n ) ) T(n) = O(f(n)) T(n)=O(f(n))。
O的含义是 T ( n ) T(n) T(n)的数量级。
一般是考虑在最坏情况下的时间复杂度,以保证算法的运行时间不会比它更长。
常见渐进复杂度为:
O ( 1 ) < O ( l o g 2 n ) < O ( n ) < O ( n l o g 2 n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)<O(log_2 n)<O(n)<O(n log_2 n)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n) O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
空间复杂度
空间复杂度 S ( n ) S(n) S(n)定义为该算法所耗费的存储空间,它是问题规模n的函数。
一个程序在执行时除需要存储空间来存放本身所用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间。
若输入数据所占空间只取决于问题本身,和算法无关,则只需分析除输入和程序之外的额外空间。
算法原地工作是指所需的辅助空间为常量,即O(1)。
【例题】(计算时间复杂度):
一个算法所需的时间由下述递归方程表示,试求出该算法的时间复杂度的级别:
T ( n ) = { 1 , n = 1 2 T ( n / 2 ) + n , n > 1 T(n) = \left\{ \begin{aligned} &1,&n=1\\ &2T(n/2)+n,&n>1\end{aligned} \right. T(n)={1,2T(n/2)+n,n=1n>1
式中,n是问题规模,设n是2的整数次幂。
设 n = 2 k ( k ≥ 0 ) n=2^k(k \geq 0) n=2k(k≥0)
根据题目定义有 T ( 2 k ) = 2 T ( 2 k − 1 ) + 2 k = 2 2 T ( 2 k − 2 ) + 2 ∗ 2 k T(2^k) = 2T(2^{k-1})+2^k=2^2T(2^{k-2})+2*2^k T(2k)=2T(2k−1)+2k=22T(2k−2)+2∗2k
由此可得一般递推公式 T ( 2 k ) = 2 i T ( 2 k − i ) + i ∗ 2 k T(2^k)=2^i T(2^{k-i})+i*2^k T(2k)=2iT(2k−i)+i∗2k
进而得到 T ( 2 k ) = 2 k T ( 2 0 ) + k ∗ 2 k = ( k + 1 ) 2 k T(2^k) = 2^kT(2^0)+k*2^k=(k+1)2^k T(2k)=2kT(20)+k∗2k=(k+1)2k
即 T ( n ) = 2 l o g 2 n + n ∗ l o g 2 n = n + n l o g 2 n T(n)=2^{log_2 n}+n*log_2 n=n+nlog_2 n T(n)=2log2n+n∗log2n=n+nlog2n
因此,时间复杂度为 O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n)。