1 概论
1 引子
- 数据结构(Data Structure)的定义:计算机中存储、组织数据的方式。通常情况下,精心选择的数据结构可以带来最优效率的算法。
- 数据结构在计算机内部的存储:1、逻辑结构;2、物理存储结构
- 矩阵的抽象数据类型: AM∗N=(aij) ,由MxN个三元集 <a,i,j> <script type="math/tex" id="MathJax-Element-2"> </script>组成
ElementType GetEntry(Matrix A, int i, int j): //返回矩阵A的第i行第j列元素
- 为什么要抽象?
抽象可以过滤掉细节,只研究事物的某一个特定的角度,从而触及到问题的本质。
- 例子1:如何在书架上摆放图书?看规模!摆放书需要考虑:1、如何插入一本新书;2、如何查找到某一本指定的书。
- 1.随便放:新书插入很方便,但是找到某一本书很难!
- 2.按书名拼音字母顺序:二分查找!但是新书插入很难
- 3.划分区域,每个区域摆放一类图书,按照拼音顺序拜访:查找和插入都方便一些。但是事先需要给出很大的空间,否则可能导致没地方放。另外,类别分的粗和细又是一个问题。
- 例子2:实现PrintN函数:打印1~N的全部正整数
//循环实现
void PrintN(int N){
int i;
for(i = 1; i <= N; i++){
printf("%d\n", i);
}
}
//递归实现
void PrintN(int N)
{
if (N){
PrintN(N-1);
printf("%d\n", N);
}
}
对于递归实现,当N = 100000时,递归程序崩溃。因为递归函数对计算机空间占用很大。这说明,解决问题方法的效率和空间的利用率有关。
- 例子3:计算给定多项式 f(x)=a0+a1x+…+anxn 在点x处的值
double f(int n, double a[], double x){
int i;
double p = a[0];
for(i = 1; i <= n; i++){
p += (a[i] * pow(x,i));
}
renturn p;
}
或者: f(x)=a0+x(a1+x(…))
double f(int n, double a[], double x){
int i;
double p = a[n];
for(i = n; i > 0; i--){
p = a[i-1] + x*p;
}
renturn p;
}
第二个函数更好,计算效率更高。因此解决问题的效率和算法有关。
- clock()函数:捕捉程序开始运行到clock()被调用时耗费的时间。单位:clock tick, CLK_TCK:机器时钟每秒所走的时钟打点数。
#include<stdio.h>
#include<time.h>
clock_t starrt, stop; //clock_t是clock()函数返回的变量类型
double duration; //以秒为单位
int main(){
start = clock();
MyFunction();
stop = clock();
duration = ( (double)(stop - start) ) / CLK_TCK;
return 0;
}
2 算法
- 算法(Algorithm)的定义:一个有限的指令集,接受一些输入并产生输出,一定在有限的步骤之后终止。其中每一条指令有充分目标没有歧义,在计算机的处理范围内,不依赖于任何一种计算机语言以及具体的实现手段。
- 衡量算法好坏的指标:
- 1 空间复杂度S(n):占用存储单元的长度
- 2 时间复杂度T(n):执行耗费时间的长度
//递归实现
void PrintN(int N)
{
if (N){
PrintN(N-1);
printf("%d\n", N);
}
}
例如在递归算法中,假设我们需要输出PrintN(100000),那么函数首先需要调用PrintN(99999),于是100000需要存储在内存中,同样的,99999、99998……、1都需要存储,直到PrintN(0)可以输出。所以
S(N)=C⋅N
而在循环算法中,只涉及到临时变量,所以不管N多大,占用的空间始终是固定的常量。
在求多项式的值的算法中,由于计算机对于加减法的计算效率要远高于乘除法,pow(x,i)函数中执行了i次乘法,于是乘法的总次数为
(n2+n)/2
。而第二个算法中,一共执行了n次乘法。因此
T1(n)=C1n2+C2n
,
C1,C2
分别为乘除法和加减法的复杂度。
T2(n)=Cn
什么是好的算法?
- 最坏情况复杂度: Tworst(n)
- 平均复杂度: Tavg(n) ——最关心的问题
复杂度的渐进表示:
- T(n)=O(f(n)) 表示存在常数 C>0,n0>0 使得当 n≥n0 时, T(n)≤Cf(n)
- T(n)=Ω(g(n)) 表示存在常数 C>0,n0>0 使得当 n≥n0 时, T(n)≥Cg(n)
- T(n)=Θ(h(n)) 表示同时有 T(n)=O(h(n)) 和 T(n)=Θ(h(n))
太大的上界和太小的下界是无意义的。
- 常见的算法时间复杂度由小到大依次为: O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<…<O(2n)<O(n!)
- 若两段算法分别有复杂度
T1(n)=O(f1(n))
、
T2(n)=O(f2(n))
,那么
- T1(n)+T2(n)=max(O(f1(n))+O(f2(n)))
- T1(n)∗T2(n)=O(f1(n)∗f2(n))
- 若 T(n) 是关于n的k阶多项式,那么 T(n)=Θ(nk)
- for循环的时间复杂度等于循环次数乘循环体代码的复杂度。
- if-else总体复杂度为三者中最大
3 最大子列和问题
最大子列和问题:给定N个整数的序列 {A1,A2,…,AN} ,求函数 f(i,j)=max{0,∑ijAk} 的最大值。
- 算法1:暴力搜索, T(N)=O(N3)
int MaxSubseqSum1( int A[], int N){
int ThisSum, MaxSum = 0;
int i, j, k;
for(i = 0; i < N; i++){
for( j = i; j < N; j++){
ThisSum = 0;
for(k = i; k < j; k++)
ThisSum += A[k];
if(ThisSum > MaxSum)
MaxSum = ThisSum;
}
}
return MaxSum;
}
- 算法2: T(n)=O(n2)
int MaxSubseqSum2( int A[], int N){
int ThisSum, MaxSum = 0;
int i, j;
for(i = 0; i < N; i++){
ThisSum = 0;
for( j = i; j < N; j++){ //对于相同的i,不同的j,只在j-1次循环的基础上累加一项即可
ThisSum += A[j];
if (ThisSum > MaxSum)
MaxSum = ThisSum;
}
}
return MaxSum;
}
- 算法3:分而治之,将序列在中间切分,左边有一个最大子列和,右边有一个最大子列和,跨越中间线的有一个最大子列和,取三者中最大的即可。
T(n)=O(nlogn)
- 时间复杂度分析: T(1)=O(1) ,左右两边问题为 T(N/2) ,中间为 O(N) ,所以 T(N)=2T(N/2)+O(N) ,因此 T(N)=O(N)+O(NlogN)=O(NlogN)
- 空间复杂度分析:左右两部分空间复杂度为 S(N/2) ,跨边界空间复杂度为 O(N) 。故整个空间复杂度 S(N)=2S(N/2)+O(N)=O(NlogN)
int Max3( int A, int B, int C )
{ /* 返回3个整数中的最大值 */
return A > B ? A > C ? A : C : B > C ? B : C;
}
int DivideAndConquer( int List[], int left, int right )
{ /* 分治法求List[left]到List[right]的最大子列和 */
int MaxLeftSum, MaxRightSum; /* 存放左右子问题的解 */
int MaxLeftBorderSum, MaxRightBorderSum; /*存放跨分界线的结果*/
int LeftBorderSum, RightBorderSum;
int center, i;
if( left == right ) { /* 递归的终止条件,子列只有1个数字 */
if( List[left] > 0 ) return List[left];
else return 0;
}
/* 下面是"分"的过程 */
center = ( left + right ) / 2; /* 找到中分点 */
/* 递归求得两边子列的最大和 */
MaxLeftSum = DivideAndConquer( List, left, center );
MaxRightSum = DivideAndConquer( List, center+1, right );
/* 下面求跨分界线的最大子列和 */
MaxLeftBorderSum = 0; LeftBorderSum = 0;
for( i=center; i>=left; i-- ) { /* 从中线向左扫描 */
LeftBorderSum += List[i];
if( LeftBorderSum > MaxLeftBorderSum )
MaxLeftBorderSum = LeftBorderSum;
} /* 左边扫描结束 */
MaxRightBorderSum = 0; RightBorderSum = 0;
for( i=center+1; i<=right; i++ ) { /* 从中线向右扫描 */
RightBorderSum += List[i];
if( RightBorderSum > MaxRightBorderSum )
MaxRightBorderSum = RightBorderSum;
} /* 右边扫描结束 */
/* 下面返回"治"的结果 */
return Max3( MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum );
}
int MaxSubseqSum3( int List[], int N )
{ /* 保持与前2种算法相同的函数接口 */
return DivideAndConquer( List, 0, N-1 );
}
- 算法4:在线处理, T(N)=O(N) 。“在线”:每输入一个数据就进行即时处理,在任何一个地方终止输入,算法都能给出正确的解。
int MaxSubseqSum4( int A[], int N){
int ThisSum = 0, MaxSum = 0;
int i;
for(i = 0; i < N; i++){
ThisSum += A[i];
if (ThisSum > MaxSum)
MaxSum = ThisSum;
else if(ThisSum < 0) //如果当前子列和为负,不可能通过累加后面的数达到最大。
ThisSum = 0;
}
return MaxSum;
}