算法基础
Posted 秋风Haut
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法基础相关的知识,希望对你有一定的参考价值。
这周我的任务是看完算法导论的第二章。这一章前言介绍了这章的主要讲述重点:伪代码、设计和分析算法的基本框架、怎样正确地分析算法的运行时间。并以归并排序为例子讲述这种方法的运用。
伪代码跳过。
插入排序虽然会写代码,也debug过,但还是描述一下,插入排序的做法与排序扑克牌非常相似,排序扑克牌时,所有的操作的数据分为三部分:已排序的、未排序的和正在排序的。
通常我们的操作是,从未排序的牌堆中取出一张牌,这张牌就是正在排序的,将这张牌与已排好序的牌从头到尾的比较,直到找到xi
for j=2 to a.length
key=a[j]
//insert a[j] into the sorted sequence a[1...j-1]
i=j-1;
while i > 0 and a[i] > key
a[i+1] = a[i];
i=i-1;
a[i+1]= key;
以上是对未排序的牌堆中的每一张牌都进行同一个操作,就是将该牌与已排序的牌从尾至头依次比较,当前牌小于已排序的牌时,将已排序的牌向后移一个位置,只有当当前牌大于已排序的牌时,才将当前牌放到已排序牌堆的当前位置。这其实相当于一个操作,每次当当前牌小于已排序的牌时,交换两张牌的位置。直到当前牌大于已排序的牌。然后退出循环,取出未排序牌堆中下一张牌。进行同样操作。(这一段描述比较模糊)
按照伪代码,实现代码如下:
public static void insertSort(int[] array,int length)
for(int i=1;i<array.length;i++)
for(int j=i-1;j>=0;j--)
if(array[i] < array[j])
change(array,i,j);
书中还讲到了循环不变式:
循环不变式主要用来帮助我们理解算法的正确性。对于循环不变式,我们需要证明三条性质(在我看来,这也是算法需要保证的三点):
初始化:循环的第一次迭代之前,它为真。
保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真
终止:在循环终止时,
不变式为我们提供一个有用的性质,该性质有助于证明算法是正确的。
2.2 分析算法
如上表格所示,这一节对我的最大启示就是,算法其实,是很清晰又严谨的。
它会计算每步代码计算的次数(ci 时间在这一节的前面部分已经简化成了每步代码的执行时间是定值)。
这样一来:插入排序的时间复杂度就是这些语句的执行时间总和:
对于while语句之前的代码,毫无疑问对于数组的每个元素都会执行,差别只在while的语句块。
对于最好的情况,是该数组已经排好序,那么while语句块只进行判断而内部语句块没有执行,则该算法的执行总时间为:
c1*n + c2*(n-1) + 0 (n -1 ) + c4(n-1)+c5 * (n-1) + c8 * (n-1)=(c1+c2+c4+c5+c8)n-(c2+c4+c5+c8)
该式子是关于n的线性函数。因此算法的执行时间与输入规模是成正比。即可以说该算法的时间复杂度是O(n)
而在最坏情况下,该数组被逆序排序。那么对于数组从第2个元素开始的每个元素,都会执行while语句块,则while语句块的执行时间分析如下:
对于语句4,数组中从第2个元素开始,都将会执行,由于数组已逆序排序,因此,对于n-1个元素,while语句块都将会执行。代入求和公式,将得出while语句块的执行时间为
c5 * (n-1) + c6 * ((n-1)(n-1+1)/2) + c7 * ((n-1)(n-1+1)/2)
由于格式限制,不再具体写出公式,不过很容易看出,语句5 语句6的执行总时间都与n成二次关系。
写出来总结果就是:
T=( c5/2 + c6/2 + c7/2)n^2 + (c1+c2+c4+c5/2-c6/2-c7/2 + c8)n - (c2+c4+c5+c8)
以上,ci的时间在具体物理机器上,均视为常量。
以上,可以清晰的得出插入排序算法的时间复杂度:最好时为O(n),最坏时为O(n^2)
在书中指出对最坏情况与平均情况的分析,书中说,大多数情况下,我们致力于求解算法的最坏情况的时间复杂度,有三个理由:
1、算法的最坏运行时间给出了任何输入的运行时间的一个上界。
2、对某些算法,最坏情况经常出现,比如在数据库检索中,经常出现没有要查找的元素的情况,这种情况下,最坏情况总是经常发生
3、很多时间,平均情况与最坏情况一样差。
事实上这一节看完,对于真正理解的算法,都是可以非常严谨地计算时间和空间复杂度的。
2.3设计算法
2.3节讲解了设计算法的分治思想。
使用分治模式设计算法时一般分为三个步骤:
1、分解;将原问题分解为若干子问题,这些子问题是原问题的规模较小的实例
2、解决子问题的解合并成原问题的解
设计算法这一节非常重要。
在书中,开头有一句话:我们可以选择使用的算法设计技术有很多。插入排序使用了增量方法:在排序子数组A[1..j-1]后,将单个元素A[j]插入子数组的适当位置,产生排序好的子数组A[1..j]。
这句话有几点很有趣:
一、算法设计事实上也是一门技术。与编程语言一样,没有什么神秘的。
二、插入排序使用了增量方法。
我们可以从中,理解出一点意思,算法并不是牛人学者们凭空想出来的,而是在一定的思想的指导下,与实际问题相结合为解决问题而设计出的解决方案。
这一节,将会使用分治思想,设计一种新的排序算法,即归并排序。
分治思想在设计时,会涉及三个步骤:
一、分解 将原问题分解为与原问题类似同结构的子问题
二、解决 依次将子问题解决
三、合并 将子问题的解合并成为原问题的解。
分解:将n个元素的排序,分解为n/2个元素的排序
解决:将n/2个元素进行排序,一般情况下,会一直分解到数组只剩下1或2个元素,可以直接排序。才不再分解
合并:将n/2个元素的排序结果进行合并,得到原数组n个元素的排序结果。毕。
仔细思考归并排序,事实上,该排序算法会对数组元素进行疯狂的遍历,可证明,归并排序,会对数组进行6NlogN次访问
对分治算法的分析:
书中金句:当一个算法包含对其自身的递归调用时,我们往往可以用递归方程或递归式来描述其运行时间,该方程根据在较小输入上的运行时间来描述在规模为n的问题上的总运行时间。
递归式的得出:
书中的过程比较长,但是讲的很清楚又严谨。
如前所术,我们假设T(n)是规模为n的一个问题的运行时间。若问题规模足够小,如对某个常量 c,n<=c,则直接求解需要常量时间,我们将其写作O(1)。假说把原问题分解成a个子问题,每个子问题的规模是原问题的1/b。(对归并排序,a和b都为2,然而我们将看到在许多分治算法中,a!= b )为了求解一个规模为n/b的子问题,需要 T(n/b)的时间,所以需要aT(n/b)的时间来求解a个子问题。如果分解问题成子问题需要时间D(n),合并子问题的解成原问题的解需要时间C(n),那么得到 递归式:
对于该递归式,书中使用采用构建递归树的方式证明了其数量级。但事实上,在《算法》中,则是使用迭代的方法,证明了其数量级,我认为两种方法都好。递归树的证明很直观,迭代法证明是高中就学的技巧,因此也非常容易理解。
以下是构建递归树的过程:
为方便起见,假设n刚好是2的幂,图的(a)部分图示了T(n),它在(b)部分被扩展成一棵描绘递归式的等价树。项cn是树根(在递归的顶层引起的代价),根的两根子树是两个较小的递归式T(n/2),(c)部分图示了通过扩展T(n/2)再推进一步的过程。
接着,我们将这棵树的所有代价相加。顶层具有总代价cn,下一层具有总代价c(n/2)+c(n/2)=cn,下一层的下一层具有总代价c(n/4)+c(n/4)+c(n/4)+c(n/4)=cn,等等。一般来说,顶层之下的第i层具有2^i个结点,每个结点的代价是c(n/2^i),因此,每层的代价之和,依旧是cn。直到最后一层,该层共有n个结点,每个结点的代价是c,该层代价之和依旧是cn。
递归树的总层数:
可以从推断得知,递归式中n=1时,递归树只有树根,层数为1。因为lg1=0,因此lgn+1给出了正确的层数。作为归纳假设,现在假设具有2^i个叶的递归树的层数为lg2^i+1=i+1。因为我们假设输入规模是2的幂,因此下一个要考虑的是输入规模是2^(i+1),从树的结构可知,具有2^(i+1)个叶子的树要比具有2^i的树多一层,因此,其总层数就是(i+1)+1=lg2^(i+1) + 1。
因此递归式的总代价,就是每层代价乘以递归树的层数,即:cn * lgn+1= cnlgn + cn,忽略低阶项和常量c,便 得出归并排序算法的时间复杂度是O(nlgn)
迭代方式 的证明,以高中知识即可证明。此处略。
以上是关于算法基础的主要内容,如果未能解决你的问题,请参考以下文章