数据结构基本概念
关于算法效率的例子计算多项式的值
写程序计算给定多项式在给定点x处的值
法一:
double f( int n, double a[], double x )
{/* 计算阶数为n,系数为a[0]...a[n]的多项式在x点的值 */
int i;
double p = a[0];
for ( i=1; i<=n; i++ )
p += a[i] * pow(x, i);
return p;
}
法二:
double f( int n, double a[], double x )
{/* 计算阶数为n,系数为a[0]...a[n]的多项式在x点的值 */
int i;
double p = a[n];
for ( i=n; i>0; i-- )
p = a[i-1] + x * p;
return p;
}
法一相比于法二慢很多
验证一下两种方法运行的时间
介绍clock函数的用法可以计算函数运行的时间。
clock():捕捉从程序开始运行到clock()被调用时所耗费的时间。这个
时间单位是clock tick,即“时钟打点”。
常数CLK_TCK(或CLOCKS_PER_SEC):机器时钟每秒所走的时钟打点数。
#include <stdio.h>
#include <time.h>
clock_t start, stop; /* clock_t是clock()函数返回的变量类型 */
double duration; /* 记录被测函数运行时间,以秒为单位 */
int main ()
{ /* 不在测试范围内的准备工作写在clock()调用之前*/
start = clock(); /* 开始计时 */
MyFunction(); /* 把被测函数加在这里,使用时这个函数必须被替换 */
stop = clock(); /* 停止计时 */
duration = ((double)(stop - start))/CLK_TCK; /* 计算运行时间 (CLK_TCK是一个常数)*/
/* 注意CLK_TCK是机器时钟每秒所走的时钟打点数, */
/* 在某些IDE下也可能叫CLOCKS_PER_SEC。 */
/* 其他不在测试范围的处理写在后面,例如输出duration的值 */
return 0;
}
比较时间的代码如下
/* 给定9阶多项式 f(x)=1*x+2*(x^2)+...+9*(x^9) */
/* 用不同方法计算f(1.1)并且比较运行时间 */
#include <stdio.h>
#include <time.h>
#include <math.h>
clock_t start, stop;
double duration;
#define MAXN 10 /* 多项式最大项数,即多项式阶数+1 */
#define MAXK 1e7 /* 被测函数最大重复调用次数*/
/*函数只运行一次可能时间太快,达不到1个时间单位,所以要多运行几次*/
double f1( int n, double a[], double x )
{
int i;
double p = a[0];
for ( i=1; i<=n; i++ )
p += (a[i] * pow(x, i));
return p;
}
double f2( int n, double a[], double x )
{
int i;
double p = a[n];
for ( i=n; i>0; i-- )
p = a[i-1] + x*p;
return p;
}
void run( double (*f)( int, double*, double ), double a[], int case_n )
{/* 此函数用于测试被测函数(*f)的运行时间,并且根据case_n输出相应的结果 */
/* case_n是输出的函数编号:1代表函数f1;2代表函数f2 */
int i;
start = clock();
for ( i=0; i<MAXK; i++ ) /* 重复调用函数以获得充分多的时钟打点数*/
(*f)(MAXN-1, a, 1.1);
stop = clock();
duration = ((double)(stop - start))/CLK_TCK;
printf("ticks%d = %f\n", case_n, (double)(stop - start));
printf("duration%d = %6.2e\n", case_n, duration);
}
int main ()
{
int i;
double a[MAXN]; /* 存储多项式的系数*/
/* 为本题的多项式系数赋值,即a[i]=i */
for ( i=0; i<MAXN; i++ ) a[i] = (double)i;
run(f1, a, 1);
run(f2, a, 2);
return 0;
}
很明显的看出,两个函数的运行时间上差了一个数量级。
数据结构的最终定义
- 数据对象在计算机中的组织方式
- 逻辑结构
- 物理存储结构
- 数据对象必定与一系列加在其上的操作相关联
- 完成这些操作所用的方法就是算法
抽象数据类型
-
数据类型
- 数据对象集
- 数据集合相关联的操作集
-
抽象:描述数据类型的方法不依赖于具体实现
- 与存放数据的机器无关
- 与数据存储的物理结构无关
- 与实现操作的算法和编程语言均无关
只描述数据对象集和相关操作集“是什么”,并不涉及“如何做到”的问题。
“矩阵”的抽象数据类型定义
类型名称:矩阵(Matrix)
数据对象集:一个M × \times ×N的矩阵AMxN = (aij) (i=1, …, M; j=1, …, N )由M × \times ×N个三元组< a, i, j >构成,其中a是矩阵元素的值,i是元素所在的行号,j是元素所在的列号。
操作集:对于任意矩阵A、B、C ∈ \in ∈Matrix,以及整数i、j、M、N
- Matrix Create( int M, int N ):返回一个M × \times ×N的空矩阵;
- int GetMaxRow( Matrix A ):返回矩阵A的总行数;
- int GetMaxCol( Matrix A ):返回矩阵A的总列数;
- ElementType GetEntry( Matrix A, int i, int j ):返回矩阵A的第i行、第j列的元素;
- Matrix Add( Matrix A, Matrix B ):如果A和B的行、列数一致,则返回矩阵C=A+B ,否则返回错误标志;
- Matrix Multiply( Matrix A, Matrix B ):如果A的列数等于B的行数,则返回矩阵C=AB否则返回错误标志;
算法
定义
- 一个有限指令集
- 接受一些输入(有些情况下不需要输入)
- 产生输出
- 一定在有限步骤之后终止
- 每一条指令必须
- 有充分明确的目标,不可以有歧义
- 计算机能处理的范围之内
- 描述应不依赖于任何一种计算机语言以及具体的实现手段
例1 选择排序算法的伪码描述
void SelectionSort ( int List[], int N )
{ /* 将N个整数List[0]...List[N-1]进行非递减排序 */
int i;
for ( i=0; i<N; i++ ) {
/* 从List[i]到List[N-1]中找最小元,并将其位置赋给MinPosition */
MinPosition = ScanForMin( List, i, N-1 );
/* 将未排序部分的最小元换到有序部分的最后位置 */
Swap( List[i], List[MinPosition] );
}
}
抽象 ——
- List到底是数组还是链表(虽然看上去很像数组)?
- Swap用函数还是用宏去实现?
算法效率
- 空间复杂度S(n)
- 根据算法写成的程序在执行时占用存储空间的长度
- 时间复杂度 T(n)
- 根据算法写成的程序在执行时耗费的时间的长度
举例
void PrintN ( int N )
{/* 打印从1到N的全部正整数 */
if ( N > 0 ){
PrintN( N-1 );
printf("%d\n", N );
}
} 递归打印
通过图我们可以看到它的内存里面占用的空间数量,与我们原始的N的大小成正比的
空间复杂度表示为S(N)=C × \times ×N,当N非常大的时候,你的空间就爆掉了,就退出了。
在方法1中
for ( i=1; i<=n; i++ ) (1+2+…+n)
p += a[i] * pow(x, i); (n2+n)/2次乘法
所以T(n)=C1n2+C2n
在方法二中
for ( i=n; i>0; i-- )
p = a[i-1] + x * p; n次乘法
T(n)=C × \times ×n
在分析一般算法的效率时,我们经常关注下面两种复杂度
- 最坏情况复杂度Tworst(n)
- 平均复杂度Tavg(n)
Tavg(n) ≤ \leq ≤Tworst(n)
复杂度的渐进表示法
- T(n)=O(f(n))表示存在常数C>0,n0>0,使得当n ≥ \geq ≥n0时有T(n) ≤ \leq ≤C × \times ×f(n),即O(f(n))表示f(n)是T(n)的某种上界
- T(n)=Ω(g(n))表示存在常数C>0,n0>0,使得当n ≥ \geq ≥n0时有T(n) ≥ \geq ≥C × \times ×f(n),即O(f(n))表示f(n)是T(n)的某种下界
- T(n)=θ(h(n)) 表示同时有T(n)=O(h(n))和T(n)=Ω(h(n)),即θ(h(n))即是上界也是下界
复杂度分析小窍门
若有两段算法分别有复杂度T1(n) = O(f1(n))和T2(n) = O(f2(n)),则
- T1(n)+T2(n) = max(O(f1(n)),O(f2(n))) 将两个算法相加拼在一起时,他们的上界是两个上界中大的那个
- T1(n) × \times ×T2(n) =O(f1(n) × \times ×f2(n)) 将两个算法嵌套起来时,两个复杂度相乘,他们的上界就是他们上界的乘积
若T(n)是关于n的k阶多项式,那么T(n)=θ(nk)
一个for
循环的时间复杂度等于循环次数乘以循环体代码的复杂度
if-else
结构的复杂度取决于if
的条件判断复杂度和两个分支部分的复杂度,总体复杂度取三者中最大
应用实例
最大子列和问题
给定N个整数的序列{A1,A2, …,An},求函数f(i,j) = max{0, ∑ k = i j \sum_{k=i}^j ∑k=ijAk}的最大值。
算法1.
int MaxSubseqSum1( int List[], int N )
{
int i, j, k;
int ThisSum, MaxSum = 0;
for ( i=0; i<N; i++ ) { /* i是子列左端位置 */
for ( j=i; j<N; j++ ) { /* j是子列右端位置 */
ThisSum = 0; /* ThisSum是从List[i]到List[j]的子列和 */
for ( k=i; k<=j; k++ )
ThisSum += List[k];
if ( ThisSum > MaxSum ) /* 如果刚得到的这个子列和更大 */
MaxSum = ThisSum; /* 则更新结果 */
} /* j循环结束 */
} /* i循环结束 */
return MaxSum;
}
这个算法的时间复杂度为T(N)=O(N3)
算法2. 将算法一改进——>去掉了累加循环k
int MaxSubseqSum2( int List[], int N )
{
int i, j;
int ThisSum, MaxSum = 0;
for( i=0; i<N; i++ ) { /* i是子列左端位置 */
ThisSum = 0; /* ThisSum是从List[i]到List[j]的子列和 */
for( j=i; j<N; j++ ) { /* j是子列右端位置 */
/*对于相同的i,不同的j,只要在j-1次循环的基础上累加1项即可*/
ThisSum += List[j];
if( ThisSum > MaxSum ) /* 如果刚得到的这个子列和更大 */
MaxSum = ThisSum; /* 则更新结果 */
} /* j循环结束 */
} /* i循环结束 */
return MaxSum;
}
这个算法只有两重循环的嵌套,所以时间复杂度是T(N)=O(N2)
算法3.分治法
分治法就是把数列冲中间一分为二,然后递归的解决两边的问题,我们会分别得到左右两边的最大子列和,还有一种可能是跨越中间边界的最大子列和。找到三个子列和以后,那么最大子列和就在三个其中。
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;
/*
递归的终止条件,子列只有1个数字
当该数为正数时,最大子列和为其本身
当该数为负数时,最大子列和为 0
*/
if( left == right ) {
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.在线处理
“在线”的意思是指每输入一个数据就进行即时处理,在任何一个地方中止输入,算法都能正确给出当前的解。
int MaxSubseqSum4( int List[], int N )
{
int i;
int ThisSum, MaxSum;
ThisSum = MaxSum = 0;
for ( i=0; i<N; i++ ) {
ThisSum += List[i]; /* 向右累加 */
if ( ThisSum > MaxSum )
MaxSum = ThisSum; /* 发现更大和则更新当前结果 */
else if ( ThisSum < 0 ) /* 如果当前子列和为负 */
ThisSum = 0; /* 则不可能使后面的部分和增大,抛弃之 */
}
return MaxSum;
}
好处是只有一个for循环,它的时间复杂度为T(N)=O(N),但是缺点是他最后得到的结果并不是特别准确