在这一章,我们将讨论:
如何估计一个程序所需要的时间。
如何将一个程序的运行时间从天降低到秒。
粗心的使用递归的后果。
将一个数自乘得到其幂以及计算两个数的最大公约数的非常有效的算法。
数学基础
定义
我们将使用下面四个定义:
如果存在正常数c和n0使得当N >= n0时T(N) =< cf(N), 则记为T(N) = O(f(N))
如果存在正常数c和n0使得当N >= n0时T(N) >= cg(N), 则记为T(N) = Omega(f(N))
T(N) = (h(N))当且仅当T(N) = O(h(N))和T(N) = (h(N))
如果对所有的常数c存在n0使得当N > n0时T(N)< cp(N),则记为T(N) = o(P(N))
这些定义的目的是要在函数之间建立一种相对应的级别。我们将比较它们的相对增长率(relative rate of growth)
我们来看这四个定义:
当N较小的时候,1000N是要大于N^2的,但N^2的增长率则更快,所以总有一个数n0,使得N^2 > 1000N, 在我们的例子中,T(N) = 1000N, f(N) = N^2.
如果我们用传统的不等式来比较增长率,
第一个定义就是说T(N)的增长率小于等于f(N)的增长率,
第二个定义T(N)= Omega(g(N))就是说T(N)的增长率大于等于g(N)的增长率
第三个定义T(N) = (h(N))是说T(N)的增长率等于h(N)的增长率。
第四个定义T(N) = o(P(N))是说T(N)的增长率小于P(N)的增长率,它不同于O,因为O
我们举一个例子,一组数从a1到an,然后按照需求进行排序,排序后每个数字只出现一次,使得a1'< a2'< a3'……。我们先用插值排序来计算它的所需时间
void InsertSort (int *arr, int size)
{
int fOut, loc, temp;
for (fOut = 1;fOut < size; fOut++)
if(arr[fOut] < arr[fOut-1])
{
temp = arr[fOut];
loc = fOut;
do
{
arr[loc] = arr[loc - 1];
loc--;
}while(loc > 0 && arr[loc - 1] > temp);
arr[loc] = temp;
}
}
假设给定数组:8,2,4,9,3,6
则第一次排序:2,8,4,9,3,6
则第二次排序:2,4,8,9,3,6
则第三次排序:2,4,8,9,3,6
则第四次排序:2,3,4,8,9,6
则第四次排序:2,3,4,6,8,9
一般来说我们想知道算法运行时间的上限,这样方便用户进行更好的操作。算法的运行时间取决于好多因素,其中一个因素是输入本身,另一个因素是数据规模。通常我们处理输入的方式是将输入参数化,我们会把运行时间看作对待排列数据规模的。
所以我们T(n)定义为输入规模为n时的最长运行时间。
『我们有时候也讨论程序所需要的平均时间,这里T(n)就成了输入规模n之下所有可能输入的期望时间,什么是期望时间呢?每种输入运行的时间乘以那种输入出现的概率就是期望时间。』
算法运行时间的上限还取决于计算机的运行速度。所以当我们比较算法时,我们比较的是算法间的相对速度。忽略掉那些依赖于机器的常量,不去检验实际运行时间,而关注运行时间的增长。
所以说在不同的机器上跑同样的算法,最长情况时间总是不一样的,我们得想方设法去除机器性能的影响,而只是去单单分析算法本身的优劣,以便于在具体量化算法性能的时候又能去除机器因素的影响,因为我们讲算法分析而不是计算机系统分析。于是有人发明了Θ符号。对于这个符号我们来看几个例子,再看定义
Θ(N)
我们需要掌握几个渐进符号
-
Θ:Θ符号掌握起来很简单,你要做的就是写个公式,抛弃他的低阶项,忽略前面的常数因子。
例如:如果公式为 $$a x ^2 + b x + c = f(x)$$
那么有$$Θ(x^2)= f(x)$$如果n趋向于无穷大的时候,总会有$$thea(x) < thea(x^2)$$
也就是说它相当于取出f(x)的最高次项然后去掉其常数因子,然后放入Θ()中,而放入Θ()中又表示了什么呢?为了弄清楚这个问题,我们来看其定义:
Θ(g(n)) = f(n) : 存在某个常数 c1, c2, 和n0,使得当 n > = n0时,有 0 < = c1 g(n) < = f(n) < = c2 g(n)
从后面半句开始,
存在 n > n0, 0 < = c1 * g(n)
这里保证了g(n)是一个增函数。接着有 $$c1 g(n) <= f(n) <= c2 g(n)$$
在脑海里可以想象出来函数图像 [ c1 g(n),c2 g(n) ]这个区间实际上是摇摆的曲线.如果把g(n)看做是y = x这样的正比例函数,就是一个扇形是吧?一根曲线摇摆着,扫出一个扇形来。而f(n)就是这组成扇形的众多曲线中的一根!这里也就是说,无论g(n)是什么,总能有一个常数因子使得 $$c1 * f(n) =g(n).$$
再返回来原来的题目中去理解:
$$a x ^2 + b x + c = f(x)$$
那么有
$$Θ(x^2)= f(x)$$
这里Θ(x^2)表示,在x趋于无穷大这个过程中,有一个常数c使得 $$c n ^2 = a x ^2 + b * x + c = f(x)$$
也就是:同!阶!数!(见高数上).
这样,我们用Θ()来描述先程序的性能n^2 + n变成了Θ(n ^ 2),这里表明:
在n趋于无穷大的过程中,总有一个常数因子使得
$$c * n^2 = n ^2 + n$$
这个常数因子c在上文中说到,由机器和其它非算法因素来决定。也就是说,我们用Θ(n ^ 2)来表示一个算法的性能的时候,我们就完全忽略了机器,只研究算法本身,它好像是说:无论你在什么机器上跑这个算法,只要输入规模足够大,你的性能都是n ^ 2的整数倍,至于是多少倍,由机器决定的,反正在同样的机器上,这个倍数是相等了。
回到插值排序计算最坏情况时间:
对于每一个J的取值,循环会做多少次操作呢? 在渐近上,这等于j乘上某个常数,所以应该是thea(j)
看看循环次数计算一下时间 $$T(n)=\sum_{j=1}^nthea(j) = thea(n^2)$$
(因为(求和:fOut从1到size)就等于1+2+3+……算数级数求和)
插值排序对于n很小的时候很快,但对于n很大的时候就时间很长了
所以我们引入一种新的排序方式做对比——归并排序
归并排序我们做的就是递归地对A[1到n/2]这部分,以及A[n/2+1到n]这部分排序。
所以我们的输入是分为两部分的。第三步,我们把排序好的两个表归并,对于归并排序,每一步我们只需要关注两组数中元素大小,
所以对于总数为N的输入,时间是$$T(n)=2T(n/2)+thea(n)=thea(n*lgn)$$
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void Merge(int *R,int low,int m,int high); // 这个函数用来将两个排好序的数组进行合并
void MergeSort(int R[],int low,int high); //这个函数用来将问题细分
int main(void)
{
int i;
int a[10];
int low = 0,high = 9;
printf("请输入10个正整数:\n");
for (i = 0; i < 10; i++)
{
rescan:
if(scanf("%d", &a[i]) != 1)
{
fflush(stdin);
printf("非法输入!\n");
goto rescan;
}
//输入10个数,并判断是否为正整数
}
MergeSort(a,low,high);
for(i = low;i <= high;i++)
printf("%d ",a[i]);
printf("\n");
return 0;
}
void Merge(int *R,int low,int m,int high)
{
int i = low,j = m + 1,p = 0;
int *R1; // 动态分配内存,分给左右2个数组m和high(他这里倒不如用Left和Right更清楚)
R1=(int *)malloc((high - low + 1) *sizeof(int));
if (!R1) return; //只要有一个数组到达了尾部就要跳出
// 进行合并
while(i <= m && j <= high)
R1[p++] = (R[i] <= R[j]) ? R[i++] : R[j++]; // 把较小的那个数据放到结果数组里, 同时移动指针
while(i<=m)
R1[p++]=R[i++]; //如果 m 还有元素,把剩下的数据直接放到结果数组
while(j<=high)
R1[p++]=R[j++]; //如果 high 还有元素,把剩下的数据直接放到结果数组
for(p=0,i=low;i<=high;p++,i++)
R[i]=R1[p]; //把结果数组 复制 到 数组R 里
}
void MergeSort(int R[],int low,int high)
{
int mid;
if(low<high)
{
// 归并的基本思想
mid=(low+high)/2;
MergeSort(R,low,mid);// 排左边
MergeSort(R,mid+1,high);// 排右边
Merge(R,low,mid,high);// 合并
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。