算法系列之常用算法之一----分治算法
一、基本概念
在计算机科学中,分治法是一种很重要的算法。分治算法,字面上的解释是“分而治之”,分治算法主要是三点:
1.将一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题----“分”
2.将最后子问题可以简单的直接求解----“治”
3.将所有子问题的解合并起来就是原问题打得解----“合”
这三点是分治算法的主要特点,只要是符合这三个特点的问题都可以使用分治算法进行解决(注意用词,是”用”,至于好不好就是另外一回事了)
二、分治法的特征
分治法所能解决的问题一般具有以下几个特征:
1) 该问题的规模缩小到一定的程度就可以容易地解决
2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
3) 利用该问题分解出的子问题的解可以合并为该问题的解;
4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
三、为什么用分治法?怎么正确使用分治法?
为什么用分治算法?我们使用一种算法的原因大部分情况下都是为了”快“,只有在少数情况下,在程序已经足够”快“的前提下,我们才会牺牲一部分的”快“,去保全一些开发因素(比如,程序的可维护性等等),那么分治算法为什么快?我们在用这个算法之前必需理解清楚这个问题。
分治算法的思想就是将一个问题规模比较大的问题划分为几个相同逻辑性质(或者直接理解为类似)的问题规模变小的子问题。我们可以从这里入手。
举个超级简单的例子:
假如有一个存在n个元素的int型数组,我们需要求该数组的和。
可能有些人想不想就是一个分治算法,将这个问题分为两个子问题,然后每个子问题再分为两个子问题,当子问题的规模为只有两个数时进行相加。。。
然而,这种办法是使用了分治算法,可是效率比直接遍历一遍相加得到的效率还要低的多.
为什么?因为分治算法本身不适合这种单次遍历就可以搞定的简单问题。你们在阅读一遍分治算法的思想:分治算法的思想就是将一个问题规模比较大的问题划分为几个相同逻辑性质的问题规模变小的子问题,那么这个定义存在一个隐含的前提,当问题规模比较大时,该问题解决起来要成倍的困难!
我们可以举这样一个简单的例子:
我们对一个存在n个元素的数组,使用简单排序进行排序时:
当n=1时,无需比较
当n=2时,我们需要1次比较
当n=3时,我们需要3次比较
当n=4时,我们需要6次比较
当n的数值比较大时,我们需要比较的次数越来越多将会是一个巨大的数字。
而对于前面的求和的例子:
当n=1时,无需相加
当n=2时,我们需要1次相加
当n=3时,我们需要2次相加
当n=4时,我们需要3次相加
仔细观察这组数据,是否发现了什么?
对于求和的例子来说,该问题的计算量与问题规模成正比,在相同的条件下,我们根本无须使用分治算法,因为即使这个问题规模变大,他的解决问题的难易程度没有丝毫改变,它所付出的,只不过是增大了问题规模后所必须付出的计算量,概括起来就是线性增长的问题规模导致了线性增长的计算量。
而对于排序的例子,当问题规模变大时,计算量的增大是成幂次型增长的,概括起来就是线性增长的问题规模导致了幂次型计算量的增长。使得问题规模大的问题解决起来更加困难。
综合起来概括,在问题规模与计算量成正比的算法中,分治算法不是最好的解法,并且有可能是效率极其底下的算法。如果存在某个问题,线性增长的问题规模可能带动计算量的非线性增长,并且符合分治算法的三个特征,那么分治算法是一个很不错的选择。
四、实例解析
1.二分查找
实例分析:也许有人看到这里认为二分查找算法与我前面的推论是矛盾的,但其实并不矛盾。不矛盾的原因在于二分查找算法是存在前提条件的。二分查找的数组必须是有序状态的,二分查找是根据它的规则特性人们找到的一种取巧的方法。对于一个无序的查找方法,我的结论依然有效。
代码来一波:
2.棋盘覆盖
问题描述: 一个2^k×2^k 个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。
实例分析:每次都对分割后的四个小方块进行判断,判断特殊方格是否在里面。这里的判断的方法是每次先记录下整个大方块的左上角(top left coner)方格的行列坐标,然后再与特殊方格坐标进行比较,就可以知道特殊方格是否在该块中。如果特殊方块在里面,这直接递归下去求即可,如果不在,这根据分割的四个方块的不同位置,把右下角、左下角、右上角或者左上角的方格标记为特殊方块,然后继续递归。在递归函数里,还要有一个变量s来记录边的方格数,每次对方块进行划分时,边的方格数都会减半,这个变量是为了方便判断特殊方格的位置。其次还要有一个变nCount来记录L型骨牌的数量。
代码秀:
运行结果:
3.归并排序/合并排序
实例分析:咳咳,大名鼎鼎的合并排序不用我多说吧。。。缺点是需要多占一些内存充当缓冲区。。
上代码:
4.快速排序
运行结果:有了合并排序怎么可以没有快速排序咧
代码: