分治算法的时间复杂度分析

来源:互联网 发布:java获取上下文路径 编辑:程序博客网 时间:2024/05/16 17:35
递归调用时, 最关键的就是递归调用栈的深度. 我们也可以理解为分治算法中, 被分成的段数。 也就是步骤中的第1步中所分成的子序列的个数。 假设这个递归调用深度我们用 D(n) 来表示。另外一个就是在每次处理一个具体子序列时,所用的时间复杂度,我们用C来表示。则最终的这个函数的时间复杂度为:  C * D(n)   (注:教科书上是:αT(n/b) + D(n) + C(n), 这个问题我们后面再讲)。 讲到这里, 肯定很多人都想到了快速排序算法, 这是一个典型的递归函数. 在这里用表示如下QuickSort(int low, high; int * p){// 得到分界点nPosnPos = GetDividedPosition(low, high, p);// 对分界点左边排序QuickSort(low, nPos-1, p);// 对分界点右边排序QuickSort(nPos+1, high, p);}在这里, 获取分界点函数 GetDividePostion它的效率是n的,即给定一个数组,只需要一次遍历就可以得到结果,因此它是一个Ѳ(n)的效率。那D(n)是多少呢?从函数中我们可以分析出,每次它都是把序列分成两部分, 在最优的情况下,即每次都分成两等份,那它的问题子序列个数就是 Log2N 个,那在最好情况下的时间复杂度就可以理解为 Ѳ(NLog2N)我们可以讨论一个它的最差情况是什么样的。在最差情况下,待排序序列是从大到小的,而要排成从小到大的,如果按照每次取基准点为数组的第一个值的话,每次得到的nPos为序列的第一个值,快速排序中形成的子序列第一次划分为 1, n-1, 第二次为 1, n-2  , 第三次 1, n-3, 递归的层高就是n-1层,效率就是 N * (N-1)  =  Ѳ(N2), 因此,在快速排序中获取基准点时, 一般都要改进下.有一种改进方法就是每是取的参照点是取p[low], p[High], p[(Low+High)/2],  取这三个值中的中间值,这样分隔开的子序列就会更逼近使两个子序列个数相等, 更逼近NLog2N。下面我们再来看一个例子:汉诺塔。递归函数一般这样写:void hanoi(int n,char one ,char two,char three)     if(n==1)   move(one,three);    else{      hanoi(n-1,one,three,two);  //  需要递归n-1次的移动      move(one,three);           //  只需要移动1次      hanoi(n-1,two,one,three);  //  需要 递归n-1次移动    }   }   它时间效率又是怎么样的呢?   从这里看,第一次的移动接口是一个常数,我们就可以认为是Ѳ(1)了,那它的递归层数是多少呢?   很明显,每一次的调用都是减1的,且要移动两次,如上面标识我们可以得出递推公式:            Cn+1 = 2Cn + 1 , 当n为1时, C1 = 1   我们通过变换求下Cn的通项:        Cn+1 = 2Cn +1     => (Cn+1 +1) = 2(Cn + 1)     => (Cn+1 +1)/(Cn + 1) = 2   根据等比数列的通项公式an = a1 * qn-1, C1 = 1     => Cn + 1  = (C1 +1)2n-2     => Cn  =  2n-1  - 1 。  即  Ѳ (2n)。很大啊~~~~~      或者这样证明:    Cn = 1 + 2Cn       = 1 + 2 + 22Cn-2       = 1 + 2 + 22 + 23 + ... + 2n-1C1         这个为 首项为1, 末项为2n-1C1 比例系数为2的等比数列求和.       = 1(1-2n-1)/(1-2) = 2n-1 - 1******************************************************************************************    以上是一种简化的求解递归函数时,分析其时间复杂度的方法。在这里,为防止误人子弟,我还是把官方的公式再简单解释下:    在分治算法中的三个步骤中, 我们假设分解和合并过程所用的时间分别为D(n), C(n), 设T(n)为处理一个规模为n的序列所消耗的时间为子序列个数,每一个子序列是原序列的1/b,α为把每个问题分解成α个子问题, 则所消耗的时间为    T(n) =    Ѳ(1)    如果n<=c (是n中一个可以直接求解的规模c。在上面两例中c都为1)              αT(n/b) + D(n) + C(n) 否则  在上面的例子中, 我们同样可以用这个公式来套下试试.  在快速排序中,α 是为2的, b也为2, 则分解(就是取参照点,可以认为是1), 合并(把数组合并,为n), 因此D(n) + C(n) 是一个线性时间Ѳ(n).  这样时间就变成了T(n) = 2T(n/2) + Ѳ(n).  下面有点复杂了, 在每个层上的时间复杂度为: 第在一层上是cn(c为比较一次时所用的时间), 在第二层上时数组被分成了两部分, 每部分为 n/2, 则在第二层上时间为 c * n/2 + c* n/2 = cn, 同样在第三层上, 被分成了四部分, 时间为c*n/4 + c*n/4 + c*n/4 + c*n/4 = cn. 层高一共是按刚才说的是Log2n层,每一层上都是cn, 所以共消耗时间 cn * Log2n; 则总时间:  cn * Log2n + cn = cn(1+Log2n)  即 Ѳ(nLog2n).******************************************************************************************   总结下, 我的总结的方法  C * D(n)  是抓住了递归算法中的最关键的地方αT(n/b)而得出的结论. 因为大O法只需要关注次数最高的部分就可以了, 可以简化你的分析过程.   对于快速排序, C: 一个子项的处理时间Ѳ(n), D(n): 递归层数, 树高, Log2n, 所以最终的结果是:Ѳ(nLog2n). 对于汉诺塔, C: 一个子项的处理时间 Ѳ(1), D(n): 递归层数, 树高, 2n-1, 故最终结果是: Ѳ(2n). 很快就得出了结论.  下面我们来做一个具体实例. 来自己设计一个算法, 并评价下时间复杂度. 问题是这样的, 如下图: 浅谈分治算法的时间复杂度分析  图1 其具体数据如下所示: 浅谈分治算法的时间复杂度分析  图2      题目是: 当给你任意一TitleID值 x, 根据TitleID <-- ItemID <-- ItemDetail 关系, 找出所有ItemDetailID值集合. 用代码写, 不用SQL. 假设主键与外键都已经是排过序的.假设 Title表记录条目数量为M, Item表条目数量为N, 而Detail表记录数量为K, 且K >= 10N,  N >= 4M(只是表述一个数量关系, 没有其它意思. 就是想表达Detail表记录比较多, 而且还挺多的). Item表中的数据只挂在Title表中的叶子节点下.   比如, 当TitleID为5(北京)时,其在Item中明细为4(东城区),5(西城区),6(海淀区), 则ItemDetailID的集合应该是: 6(中关村),7(上地)。如果TitleID为2(中国)时,则结果就是: 1,2,3,4,5,6,7.     处理这个问题我们两种方案:1. 先遍历ItemDetail表, 把Level为A的找出来, 对于每条符合的记录再判断是否属于选中的TitleID值x. 符合, 则把ItemID值记录下来  代码写法大概为:  void GetDetailItemIDsByTitleID(const X: Integer)  {  while 对于每个条ItemDetail中的记录 detailRecord, 则:{ (0)       在记录 detailRecord 中:         根据ItemID值 nItemID 在Item中找到TitleID值 x1; (1)          if x1==X 则: 把detailRecord.ID加到结果中 (2)         else            在Title表中根据PID递归判断x1是不是属于x的孩子,              是, 则加把detailRecord.ID加到结果中   (3)     }  }     在这里我们假设Title表中的树高(PID层次)为d, 由于主键和外键是排过序的, 则计算结果如下: (0): Detail表记录为K, 故为K;(1):在Item表中根据 nItem值找到x1的开销为Log2N;(2): 常数, 不考虑(3): 的递归层次为d, Title表记录数为M, 则开销为d * Log2M; 最后的结果:  K * (Log2N + d * Log2M) + c. c为把nItem加到结果中的时间, 不能把ID加重了, 我们用一个常量表示. 时间复杂度我们可以表示为: Ѳ(K(Log2N + d * Log2M)) 2. 先对Title递归, 对于每一个具体的记录,找出其明细, 代码大概如下void GetDetailItemIDsByTitleID(const X: Integer){  if 如果X所指的标题为节子节点 (1)    直接处理标题ID为X的明细(X) (2)  else {   while 对于所有的当前标题的明细 iTitleChildRecord记录 do (3)     GetDetailItemIDsByTitleID(iTitleChildRecord.ID) (4)  } }(1): 判断为叶子节点,看PID有没有为X的就可以了, Log2M;(2): 直接处理明细,则从子目表中定位到TitleID记录Log2N, 假设平均一个标题下明细Item个数为y, 则为: Log2N + yLog2K(3): Log2M(4): 层数为d, 假设平均一个标题下子标题数为z, 则: d * z最后:  d*z(Log2N + yLog2K) + 2Log2M 我们来比较下:  1. K * (Log2N + d * Log2M)  2. d*z(Log2N + yItemLog2K) + 2Log2M1中我们只看 k * d * Log2M, 2中我们只看d * z * y * Log2K 这样, 我们只需要关注  k * Log2M 和 z(标题下子标题个数) * y(一条标题下条目个数) * Log2K 即可了. 之前的关系 K >= 10N,  N >= 4M, 你应该知道结论了!   我举的这个例子, 在我们的项目中有经验数据,比如z 一般在 10以内(用我们的话说就是某一章下的节一般不过超过10个), y(一章节下挂的子目一般最多在50个).M最多为2000, 而K 一般都在在6万以上.在这种环境下, 第一种方案结果: 60000 * 11 = 66万, 第二种方案: 10 * 50 * 16 = 8000=0.8万     以上主要是讲解如何对一个比较复杂的算法进行时间复杂度分析, 比较算法的优劣. 关于上面的例子, 大家看看有什么高招, 看还有没有更好的办法? 学习ing !!!     ******** 补充关于最后一个例子我认为目前最好的算法 2009年11月17日9:09:06 ************思路是把标题上的当前TitleID和子标题的ID放在Hash中, 然后再在Detail表中进行一次遍历. 这样函数的效率可以认为是线性的, 实现如下: void GetDetailItemIDsByTitleID(const X: Integer){  创建一个哈希表oTitleHash, 是以TitleID为键Key  创建一个ItemID与TitleID对应的Hash: oItemHash, Key为ItemID, Value为TitleID  // 通过一个递归函数对oTitleHash进行填充, 把X及其孩子填充到哈希表中  RecursiveFillHashIDs(oTitleHash, X); (1)  // 遍历Item表, 对于TitleID存在于oTitleHash中的记录, 添加到oItemHash中  for 对于Item表中的每一条记录 iItem do {  (2)    //如果 iItem这记录的titleID存在于oTitleHash中, 则添加    if oTitleHash.FindNode(iItem.TitleID) != Null then (3)       oItemHash.Add(iItem.ID, iItem.TitleID);  }  for 对于ItemDetail中的每一条记录 iRecord do { (4)    // 从iRecord记录中得到ItemID值 : nItemID 在oItemHash表中查找是否存在    if oItemHash.FindNode(nItemID) != NULL then (5)      把nItemID添加到结果集中  }}  (1): 所消耗时间为递归层次 * 平均每个标题下的孩子数: d * z (2): 遍历所有的Item表记录: n (3): 哈希操作, 平均效率: 1 (4): 遍历ItemDetail表中的记录: k (5): 哈希操作: 1 故最后结果是: d*z + n * 1 + k*1 = d*z + n + k 按经验数据, d深度一般为4, z 为10个左右, k 为6万, n 为几百, 最终的结果就是6万左右, 取决于k的大小. 因此可以简单的认为效率为:   Ѳ(k), 是线性的.

0 0
原创粉丝点击