”数据结构是数据对象,以及存在于该对象的实例和组成实例的数据元素之间的各种联系。这些联系可以通过定义相应的函数来给出。“ Sartaj Sahni《数据结构、算法与应用》
”数据结构是ADT(抽象数据类型Abstract Data Type)的物理实现“ Clifford A.Shaffer 《数据结构与算法分析》
”数据结构(data structure)是计算机中存储、组织数据的方式。通常情况下,精心选择的数据结构可以带来最优效率的算法。“ 中文维基百科
例1:如何在书架上摆放图书?
数据如何组织和规模有关系。
图书的摆放要使得两个操作方便实现,操作1:新书怎么插入。操作2:怎么找到某本指定的书。
方法1:随便放。操作1:哪里有空放哪,一步到位。操作2:难以实现。
方法2:按照书名的拼音字母顺序排放。操作1:难以实现,比如新进一本《阿Q正传》,则要将之后的书一本一本往后错位,直到前面留出空当为止。操作2:二分查找。
方法3:把书架划分成几块区域,每块区域指定摆放某种类别的书,每种类别内,按照书名的字母拼音顺序排放。操作1:先定类别,二分查找确定位置,移出空位。操作2:先定类别,再二分查找。
基于方法3,问题:空间如何分配?类别应该分多细?
上述例子说明,解决问题方法的效率,跟数据的组织方式有关。
答:先分大类,大类可以再细分小类,基于合适的数量时,再利用拼音字母顺序摆放。也可以利用计算机建立索引目录,对所有书按类别,按出版时间,按作者等以不同方式进行编号,可以通过不同的检索条件,快速查找到图书。
例2:写程序实现一个函数PrintN,使得传入一个正整数为N的参数后,能顺序打印从1到N的全部正整数
循环实现:
因为递归程序对空间占用很大,上述程序就是因为递归程序用完了能用的空间,非正常终止了。
上述例子说明,解决问题方法的效率,跟空间的利用效率有关。
多项式:f(x) = a0 + a1*x + a2*x^2 + ……+ an-1*x^(n-1) + an*x^n
最直接的算法:
我们开始比较两种函数的运行时间:
C语言提供了clock():捕捉从程序开始运行到clock()被调用时所耗费的时间。这个时间单位是clock tick,即”时钟打点“。常数CLK_TCK:机器时钟每秒所走的时钟打点数。
常用的计算函数运行时间的模板:
所以我们需要让被测函数重复运行多次,使得测出的总的时钟打点间隔充分长,最后计算被测函数平均每次运行的时间。
1.1.4抽象数据类型
数据结构是数据对象在计算机中的组织方式,数据对象与一系列加在其上的操作相关联,完成这些操作的方法就是算法。
数据对象的逻辑结构:
以例1摆放图书作例,图书按拼音顺序一本接着一本,一个编号对应一本书,即线性结构。
先把图书分类,某个类一个编号,一个编号对应多本书,一对多的逻辑结构,即树。
如果统计一本书有几个人买,买这本书的人还买了什么书,一本书对应很多人,一个人对应很多书,多对多的一个关系网,关系网对应着图。
数据对象的物理存储结构:
这些逻辑结构在机器内存的放法。例如是用数组来存放还是用链表来存放。
抽象数据类型(Abstract Data Type):
数据类型:数据对象集, 数据集合相关联的操作集。
抽象:描述数据类型的方法不依赖于具体实现。与存放数据的机器无关,与数据存储的物理结构无关,与实现操作的算法和编程语言均无关。只描述数据对象集和相关操作集”是什么“,并不涉及”如何做到“的问题。
例4:”矩阵“的抽象数据类型定义
类型名称:矩阵
数据对象集:一个M*N的矩阵A(M*N)=(aij)(i=1,……,M;j=1,……,N)由M*N个三元组构成,其中a是矩阵元素的值,i是元素所在行号,j是元素所在列号。
抽象数据类型描述时不关心a是int还是float,对元素值进行操作时也不关注元素类型。矩阵如何存储,是用一维数组、二维数组还是十字链表我们也不用关注。函数的具体实现、用什么语言也不用管。这就是抽象。
答:把复杂问题简单化、系统化,可以解决类似的一系列问题,不考虑具体的实现,从全局来看待问题。
算法(algorithm):一个有限指令集;接受一些输入(有些情况不需要输入);产生输出;一定在有限步骤后终止;每一条指令必须有充分明确的目标,不可以有歧义,在计算机能处理的范围内,描述应不依赖于任何一种计算机语言以及具体的实现手段。
例1:选择排序算法的伪码描述
void SelectionSort(int List[],int N)
{//将N个整数List[0]…List[N-1]进行非递减排序
for(i = 0; i < N;i++)
{
MinPosition = ScanForMin(List,i,N-1);
//从List[i]到List[N-1]中找最小元,并将其位置赋给MinPosition;
Swap(List[i],List[MinPosition]);
//将未排序部分的最小元换到有序部分的最后位置;
}
}
这段伪码描述比较抽象,并不知道List是数组还是链表,Swap是函数实现还是宏实现,这些具体实现我们在描述算法时是不关心。
空间复杂度S(n)——根据算法写成的程序在执行时占用存储单元的长度。这个长度往往与输入数据的规模有关。空间复杂度过高的算法可能导致使用的内存超限,造成程序非正常中断。
时间复杂度T(n)——根据算法写成的程序在执行时耗费时间的长度。这个长度往往也与输入数据的规模有关。时间复杂度过高的低效算法可能导致我们在有生之年都等不到运行结果。
比如之前1.1.2的例2写PrintN函数的递归版本,每次调用PrintN,都会占用一块空间,因为是递归调用,要一直调用到PrintN(0)才会开始释放之前的空间,所以N过大可能就会导致内存超限,使程序非正常中断。如下图所示,其占用空间的大小随着N的增大而线性增长。
而之前的循环版本占用的空间不论N如何变化,是不变的。
再回顾之前1.1.3的例3,因为机器运算加减法的速度比乘除法的速度快很多,程序的时间复杂度一般看函数做了多少次乘除法。例3第一个版本,每次循环需要做i次的乘法,乘法的总次数为(1+2+……n)=(n^2+n)/2,而例3第二个版本每次循环只做了一次乘法,总共n次乘法。所以第一个版本的时间复杂度T(n)=C1*n^2 + C2*n,第二个版本的时间复杂度T(n) = C * n。所以当n充分大时,n^2的值远远大于n,所以当n很大时,第二个版本会比第一个版本运行快很多。
在分析一般算法的效率时,我们经常关注下面两种复杂度,最坏情况复杂度Tworst(n),平均复杂度Tavg(n),Tavg(n) <= Tworst(n)。我们平时比较喜欢分析最坏情况复杂度。
查找算法中的“二分法”是这样定义的:
给定N个从小到大排好序的整数序列List[],以及某待查找整数X,我们的目标是找到X在List中的下标。即若有List[i]=X,则返回i;否则返回-1表示没有找到。
二分法是先找到序列的中点List[M],与X进行比较,若相等则返回中点下标;否则,若List[M]>X,则在左边的子系列中查找X;若List[M] 试写出算法的伪码描述,并分析最坏、最好情况下的时间、空间复杂度。 空间复杂度S(n) = C 用图形直观表示: 作为一个专业的程序员,设计算法时看到复杂度为n^2的,最好能想办法把它降为nlogn,效率会巨大提高。 复杂度分析小窍门: 算法1:算出所有的子列和,从中找出最大的一个: 算法2: 算法3:分而治之 分而治之的思想:把复杂的问题切分成小的块,分头去解决它们,最后将结果合并。 应用在最大子列和问题上,即将存储数据的数组看成左右两部分,分别求出两部分的最大子列和以及贯穿中间线的最大子列和,三个部分中的最大部分即我们要求的最大子列和。 具体来说,这个问题分两部分: 由于递归而产生的空间复杂度是多少? 算法的整体空间复杂度一共是多少? 2.数组存储所需的空间C1*n和递归所需的空间C2*log2(n) = O(n) 算法4:在线处理 在PTA上发布的编程题“最大子列和问题”给了非常宽松的时间上限,让大家可以至少把算法2、3、4分别尝试一下。 算法2运行结果: 算法3运行结果: 算法4运行结果: 01-复杂度2 Maximum Subsequence Sum (25分)
Given a sequence of K integers { N1, N2, ..., NK }. A continuous subsequence is defined to be { Ni, Ni+1, ..., Nj } where 1≤i≤j≤K. The Maximum Subsequence is the continuous subsequence which has the largest sum of its elements. For example, given sequence { -2, 11, -4, 13, -5, -2 }, its maximum subsequence is { 11, -4, 13 } with the largest sum being 20. Now you are supposed to find the largest sum, together with the first and the last numbers of the maximum subsequence. Input Specification: Each input file contains one test case. Each case occupies two lines. The first line contains a positive integer K (≤10000). The second line contains K numbers, separated by a space. Output Specification: For each test case, output in one line the largest sum, together with the first and the last numbers of the maximum subsequence. The numbers must be separated by one space, but there must be no extra space at the end of a line. In case that the maximum subsequence is not unique, output the one with the smallest indices i and j (as shown by the sample case). If all the K numbers are negative, then its maximum sum is defined to be 0, and you are supposed to output the first and the last numbers of the whole sequence. Sample Input: Sample Output:
1.2.3复杂度的渐进表示
复杂度的渐进表示法:T(n) = O(f(n)) ,表示存在常数C>0,n0>0,使得当n>= n0时有T(n) <= C * f(n),即为上界。T(n) = Ω(g(n)),表示存在常数C>0,n0>0,使得当n>=n0时有T(n) >= C *g(n),即为下界。T(n) = θ(h(n))表示同时有T(n) = O(h(n)) 和T(n) = Ω(h(n))。我们应该要找能找到的最小的上界和最大的下界。下图是一些不同复杂度的函数:
1.3应用实例:最大子列和问题
1.3.1应用实例-算法1&2
1.3.2应用实例-算法3
讨论1.6算法3的空间复杂度是多少?
答:1.递归1次,左边2/n,右边2/n;……递归k次,左边1,右边1,即2^k = n,k = log2(n),S(n) = O(logn)
1.3.3应用实例-算法4
时间复杂度T(N) = O(N),已经是可以想象的最快最好的算法了。但别人理解起来可能会有困难。在线处理指的是每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都能正确给出当前的解。
int MaxSubseqSum4(int A[],int N)
{
int ThisSum,MaxSum;
int i;
ThisSum = MaxSum = 0;
for(i = 0;i < N;i++)
{
ThisSum += A[i]; //向右累加
if(ThisSum > MaxSum)
MaxSum = ThisSum; //发现更大和则更新当前结果
else if(ThisSum < 0) //如果当前子列和为负
ThisSum = 0;//则不可能使后面的部分和增大,抛弃之
}
return MaxSum;
}
讨论1.7 晒运行结果
课后作业
10
-10 1 2 3 4 -5 -23 3 7 -21
我的代码:
10 1 4
#include