C#算法设计与分析详解_第1页
C#算法设计与分析详解_第2页
C#算法设计与分析详解_第3页
C#算法设计与分析详解_第4页
C#算法设计与分析详解_第5页
已阅读5页,还剩7页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第C#算法设计与分析详解目录1.什么是科学方法??1.观察2.将问题规模和运行时间的关系量化2.数学模型近似近似运行时间成本模型总结3.增长数量级的分类4.倍率实验5.注意事项6.处理对于输入的依赖7.内存1.对象2.链表3.数组4.字符串对象作为程序员,开发完一段代码,实现了某个功能时,有必要知道:

我的程序需要多长时间?

是什么导致我的程序消耗很多内存?

比如,统计或者处理了一大批数据。影响这些问题的因素很多,例如,电脑的性能,数据的性质(值类型和引用类型的区别),使用的算法。想要为这些基础问题提供答案需要通过科学方法。

1.什么是科学方法??

它是科学家为获取自然界知识所使用的一系列大家认同的方法。

1.细致地观察真实世界的特点,通常还要精确的测量2.根据观察结果提出假设模型3.根据模型预测未来的事件4.继续观察并核实预测的准确性5.如此反复直到确认预测和观察一致

这里我们不需要深究它,我们会使用数学分析(科学方法的一种)为算法成本建立模型,并使用实验数据验证这些模型。

科学方法的一条关键原则是可重现的,可证伪的。

1.观察

怎么定量测量程序的运行时间?

各种工具,谷歌浏览器...,计时器Stopwatch

我们一般只需要近似值就可以了。

对大多数程序的第一个定量观察就是计算性任务的困难程度可以用问题的规模来衡量。什么是问题的规模?可以是输入的大小或是某个命令行参数的值(开发预估时间)。

另一个定量观察是运行时长和输入本身相对无关,它主要取决于问题模型。就是说,同样大小的输入量,程序运行时间是差不多的。如果换一批同样大小的数据,运行时长差很多,就需要控制运行时间对输入的敏感度(比如把实验数据存起来)。

2.将问题规模和运行时间的关系量化

1.生成实验数据2.数据分析

根据问题规模和运行时长的数据绘制图表

猜想

举例ThreeSum实验

publicstaticintThreeSum(int[]a)

intN=a.Length;

intcnt=0;

for(vari=0;ii++)

for(varj=i+1;jj++)

for(vark=j+1;kk++)

if(a[i]+a[j]+a[k]==0)

cnt++;

returncnt;

}

lg(T(N))=3lgN+lga--a是常数

T(N)=aN^3

这里猜想的时候用到冥次法则:T(N)=aN^b

2.数学模型

虽然有很多复杂的因素影响着程序的运行时间,但一个程序的运行的总时间主要有关的两点是:

1.执行每条语句的时长(取决于计算机,编译器和操作系统)2.执行每条语句的频率(取决于程序本身和输入)

计算执行每条语句的时长可以通过各种工具测出。

重点是判断执行每条语句的执行频率,有的语句很好判断,比如一些赋值语句;有些需要深入分析,比如ThreeSum实验中if语句的执行次数为N(N-1)(N-2)/6次(主要是要了解立方推导公式)。而且有些语句的执行次数取决于输入的数据,例如计算和为0的三元组的数量的语句(0~N^3/6)。

近似

频率分析可能会产生复杂冗长的数学表达式,例如:

N(N-1)(N-2)/6=N^3/6-N^2/2+N/3

我们使用波浪号逼近法,在其中我们丢弃使公式复杂化的低阶项。我们写〜f(N)表示当N增长时除以f(N)接近1的任何函数。我们写g(N)〜f(N)表示当N增长时g(N)/f(N)接近1。

所以N^3/6-N^2/2+N/3的近似函数是N^3/6,增长数量级是N^3。

近似运行时间

大部分情况下,执行最频繁的语句决定了程序执行的总时间-内循环,ThreeSum实验中的内循环就是第三层循环和if语句。大部分程序的运行时间都只取决于某一小部分。

性质(猜想):ThreeSum的运行时间~aN^3(a是常数),增长数量级是N^3。

成本模型

定义了所研究的算法的基本操作。

可以用一个成本模型来评估算法的性质。在这个成本模型下,我们可以用数字说明算法而非某个性质。

例如,ThreeSum的成本模型是数组的访问次数(无论读写)。

性质:该算法访问了~N^3/6个整数三元组中的所有3个整数。

通过明确成本模型使给定程序所需的运行时间的增长数量级和程序算法真正成本的增长数量级相同。

总结

大多数程序,得到运行时间的数学模型所需的步骤:

1.确定输入模型,定义问题的模型(该任务的困难程度)2.识别内循环3.根据内循环中的操作确定成本模型4.对于给定的输入,判断这些操作的执行频率

3.增长数量级的分类

我们使用一些结构原语(普通语句,条件,循环,嵌套和方法调用)来实现算法,因此,成本增长的数量级通常是问题规模N的几个函数之一。

4.倍率实验

步骤:

1.循环执行ThreeSum方法,并且每次N=2*N(2是比例,可以调整)

2.打印每次执行ThreeSum方法的运行时长和上一次的比

3.直到该比值趋近于2^b(b是常数)

publicstaticvoidRatioTest()

Randomrd=newRandom();

Stopwatchtimer=newStopwatch();

int[]a=newint[125];

for(vari=0;i125;i++)

a[i]=rd.Next(-1000,1000);

timer.Start();

varres=ThreeSum(a);

timer.Stop();

varprev=timer.ElapsedMilliseconds;

for(varN=250;true;N=2*N)

a=newint[N];

for(vari=0;ii++)

a[i]=rd.Next(-1000,1000);

timer.Start();

var_res=ThreeSum(a);

timer.Stop();

vartime=timer.ElapsedMilliseconds;

varratio=(decimal)time/prev;

//Console.WriteLine(a.Length+"\t"+time+"\t"+ratio);

Console.WriteLine(ratio);

prev=time;

//Thread.Sleep(1000);

}

根据冥次法则公式可以推导出该比例是以2为底的对数。并且可以得出增长数量级的近似模型(N^b):

a为比例数,c为极限比值,b为该算法增长数量级的指数。这里b=3。

这个实验对于比值没有极限的算法无效。

该方法可以简单地预测程序地性能并判断它们的运行时间大致的增长数量级。

使用该方法可以评估解决大型问题的可行性,比如可以预估一个大型问题的程序运行时间。同时也可以评估使用更快的计算机所运行的时间。

5.注意事项

在对程序的性能进行分析时,得到不一致或者有误导性的结果的原因有很多:

1.大常数

在去近似时,我们一般会忽略低阶项,但如果低阶项的系数很大时(例如10^3或10^6),该近似就是错误的。所以我们要对可能的大常数敏感。

2.非决定性的内循环

内循环是决定性因素的假设并不总是正确的。错误的成本模型可能无法得到真正的内循环,问题的规模也许没有大到对指令的执行频率的首项大大超过其他低阶项并可以忽略他们的程度。有些程序在内循环之外也有大量指令需要考虑。换句话说,成本模型需要改进。

3.指令时间

由于大多数计算机系统都会使用缓存技术,所以每条执行所需的时间并不是总是相同。

4.系统因素

如果计算机同时运行很多程序,会产生争夺资源的情况,这会影响实验结果。

5.对输入的强烈依赖

在研究程序的运行时间的增长数量级时,我假设运行时间和输入相对无关。当这个条件不满足时,会得到不一致或者错误的结果。例如,ThreeSum返回的不是三个整数为0的对数,而是是否存在。如果前三个整数就满足,该程序的运行时间的增长数量级为常数;如果输入不含有这样的三个整数,程序的运行时间的增长量级为立方级别。

6.多个问题参量

ThreeSum性能分析是仅需要一个参量的函数来衡量程序的性能,参量一般是输入的规模。但是,多个参量也是有可能的。例如白名单(一个整数列表M个,一个白名单整数列表N个,返回整数列表中有多少个整数在白名单中存在),运行时间一般和MlogN成正比。

6.处理对于输入的依赖

输入模型:我们可以仔细模拟要处理的输入的种类。这种方法具有挑战性,因为该模型可能是不现实的。

最坏情况下的性能保证:不管输入什么,程序的运行时间都小于一定的范围(取决于输入大小)。这种保守的方法可能适用于运行核反应堆或心脏起搏器或汽车制动器的软件。

随机算法:提供性能保证的一种方法是引入随机性,例如快速排序和哈希。每次您运行算法时,都会花费不同的时间。这些保证不是绝对的,但是它们无效的机会小于您的计算机被闪电击中的机会。因此,这种保证在实践中与最坏情况的保证一样有用。

操作序列:对于许多应用程序,算法输入可能不仅是数据,还包括客户端执行的操作顺序。

均摊分析:提供性能保证的另一种方法是通过记录所有操作的总成本并除以操作总数来将成本均摊。

7.内存

计算程序对内存的使用情况和运行时间一样重要。计算机中电路的很大一部分作用就是帮助应用程序保存一些值并在使用时取出。保存的值越多,需要的电路越多,需要的内存也越多。

.net内存分配系统已经帮我们解决了内存问题。

.net使用8位(64位)表示字节,每个基本类型需要的内存:

1.对象

一个对象所使用的内存,需要将所有实例变量使用内存与对象本身的开销(一般是16字节,这些开销包括一个指向对象的类的引用,垃圾回收信息和同步信息)以及4个填充字节相加。

当我们说一个引用所占的内存时,指的是它所指向的对象所占的内存。对象的引用通常是一个内存地址,因此使用8个字节的内存(在64位计算机上)。

2.链表

嵌套的非静态类需要额外的8字节。例如,如果Node类是嵌套类,基于链表的栈中一个Node对象需要40字节(16字节的对象开销,两个对象引用各需8字节,还有8字节的额外开销。为什么不需要填充字节)。

因为一个Integer对象需要24字节,所以一个含有N个整数的基于链表的表需要32+64N字节(Stack对象开销16字节,引用类型实例变量8字节,int类型实例变量4字节,4个填充字节,每个元素64字节(一个Node对象40字节和一个Integer对象24字节))

3.数组

C#中数组是引用类型,一般都会因为记录长度需要额外的内存。一个基础类型的数组一般需要24字节的头信息(16字节的对象开销,4字节用于保存长度以及4填充字节)再加上保存值所需的内存。例如一个含有N个int值的数组需要24+4N(会被填充为8的倍数)

4.字符串对象

String的标准实现含有4个实例变量:一个指向字符数组的引用(8字节)和三个int值(各4字节)。第一个int值描述的是字符数组中的偏移量,第二个int值是字符串的长度。第三个值是一个散列值,它在某些情况下可以节省计算。总共需要40字节,这是除字符数组之外字符串所需的内存空间,加上字符数组的话是40+24+2N=64+2N。

但是,字符数组常常是在多个字符串之间共享的。因为String对象是不可变的,这种设计使String的实现能够在多个String对象中都含有相同的字符数组时节

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论