算法复杂度简介

算法的时间复杂度和空间复杂度合称为算法的复杂度。

1.时间复杂度

(1)时间频度 一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
(2)时间复杂度 在刚才提到的时间频度中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。 一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
     时间频度不同,但时间复杂度可能相同。如:T(n)=n2+3n+4与T(n)=4n2+2n+1它们的频度不同,但时间复杂度相同,都为O(n2)。
    按数量级递增排列,常见的时间复杂度有:常数阶O(1),对数阶O(log2n),线性阶O(n), 线性对数阶O(nlog2n),平方阶O(n2),立方阶O(n3),..., k次方阶O(nk),指数阶O(2n)。随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
 (3)最坏时间复杂度和平均时间复杂度  最坏情况下的时间复杂度称最坏时间复杂度。一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比任何更长。
     在最坏情况下的时间复杂度为T(n)=0(n),它表示对于任何输入实例,该算法的运行时间不可能大于0(n)。 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,算法的期望运行时间。
    指数阶0(2n),显然,时间复杂度为指数阶0(2n)的算法效率极低,当n值稍大时就无法应用。
(4)求时间复杂度
【1】如果算法的执行时间不随着问题规模n的增加而增长,即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。
x=91; y=100;
while(y>0) if(x>100) {x=x-10;y--;} else x++;
解答: T(n)=O(1),
这个程序看起来有点吓人,总共循环运行了1100次,但是我们看到n没有?
没。这段程序的运行是和n无关的,
就算它再循环一万年,我们也不管他,只是一个常数阶的函数
 
【2】当有若干个循环语句时,算法的时间复杂度是由嵌套层数最多的循环语句中最内层语句的频度f(n)决定的。
 x=1; 
for(i=1;i<=n;i++) 
        for(j=1;j<=i;j++)
           for(k=1;k<=j;k++)
               x++;   
该程序段中频度最大的语句是(5),内循环的执行次数虽然与问题规模n没有直接关系,但是却与外层循环的变量取值有关,而最外层循环的次数直接与n有关,因此可以从内层循环向外层分析语句(5)的执行次数:  则该程序段的时间复杂度为T(n)=O(n3/6+低次项)=O(n3)
 
【3】算法的时间复杂度不仅仅依赖于问题的规模,还与输入实例的初始状态有关。
在数值A[0..n-1]中查找给定值K的算法大致如下:   
i=n-1;            
while(i>=0&&(A[i]!=k))       
      i--;        
return i;        
此算法中的语句(3)的频度不仅与问题规模n有关,还与输入实例中A的各元素取值及K的取值有关: ①若A中没有与K相等的元素,则语句(3)的频度f(n)=n; ②若A的最后一个元素等于K,则语句(3)的频度f(n)是常数0。
(5)时间复杂度评价性能 
有两个算法A1和A2求解同一问题,时间复杂度分别是T1(n)=100n2,T2(n)=5n3。(1)当输入量n<20时,有T1(n)>T2(n),后者花费的时间较少。(2)随着问题规模n的增大,两个算法的时间开销之比5n3/100n2=n/20亦随着增大。即当问题规模较大时,算法A1比算法A2要有效地多。它们的渐近时间复杂度O(n2)和O(n3)从宏观上评价了这两个算法在时间方面的质量。在算法分析时,往往对算法的时间复杂度和渐近时间复杂度不予区分,而经常是将渐近时间复杂度T(n)=O(f(n))简称为时间复杂度,其中的f(n)一般是算法中频度最大的语句频度。
 
2.空间复杂度
一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分。  
(1)固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
(2)可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。
一个算法所需的存储空间用f(n)表示。S(n)=O(f(n))  其中n为问题的规模,S(n)表示空间复杂度。

转载于:https://www.cnblogs.com/insomniazz/p/7837364.html

<think>嗯,用户想了解手摇算法算法复杂度分析,以及用C++实现有序序列合并。首先,我得确认这几个概念之间的关系。手摇算法通常指的是三次反转法,用于原地合并两个有序数组,这样可以节省空间,但时间复杂度可能略高。然后需要分析算法复杂度,包括时间复杂度和空间复杂度。最后,用户提到C++实现,所以可能需要给出代码示例。 首先,手摇算法的原理。回忆一下,三次反转法是将两个有序数组合并时,先找到第二个数组的起始位置,然后通过反转三个部分来实现合并。比如,假设数组A和数组B是相邻的两个有序数组,合并时先将A反转,再将B反转,最后整体反转,这样就能合并成一个有序数组。不过,可能我记错了,应该更准确地说,是找到合适的位置分割,然后通过三次反转来合并。比如,假设要合并arr[left..mid]和arr[mid+1..right],可以先将前半部分反转,后半部分反转,然后整体反转,这样合并后的数组就是有序的。不过具体的步骤可能需要再查证。 接下来,算法复杂度分析。手摇算法的优势在于原地合并,不需要额外空间,所以空间复杂度是O(1)。时间复杂度方面,每次反转操作的时间是O(n),三次反转的话,总时间还是O(n),但是可能合并两个总长度为n的数组,时间复杂度是O(n),而如果是整个归并排序中使用手摇算法的话,整体复杂度是否还是O(n log n)呢?这部分需要理清楚。 然后,有序序列合并的常规方法,比如使用双指针,逐个比较元素,放入新数组,这样的时间复杂度是O(n),空间复杂度O(n)。而手摇算法则是原地合并,空间O(1),但时间可能更高?或者同样O(n)?因为三次反转的时间总和是O(n)的,所以应该总时间还是O(n),只是常数因子可能更高,但节省了空间。 在C++中,实现手摇算法合并有序数组的步骤可能需要用到标准库的反转函数,或者自己实现反转函数。比如,假设有两个有序的vector,或者数组,如何应用三次反转来合并。可能需要先找到分割点,比如在合并arr1和arr2时,找到arr1中最后一个小于等于arr2第一个元素的位置,或者类似的方式,确保分割后的两个部分各自有序,然后进行三次反转。 举个例子,比如有两个有序数组:[1,3,5,7]和[2,4,6,8]。合并后的应该是[1,2,3,4,5,6,7,8]。使用手摇算法的话,可能需要找到正确的位置,比如原数组是连续的,假设在同一个数组中,中间位置是分割点。例如,原数组是[1,3,5,7,2,4,6,8],然后通过三次反转:先反转前半部分(7,5,3,1),然后反转后半部分(8,6,4,2),得到整体是[7,5,3,1,8,6,4,2],再整体反转得到[2,4,6,8,1,3,5,7],这显然不对。可能我的理解有误,正确的手摇算法步骤可能需要先找到正确的分割点,即第二个数组的起始位置,然后将两个子数组分别反转,再整体反转。 或者,正确的三次反转法应该是:例如,假设有两个相邻的有序子数组A和B,合并后的正确顺序需要将B的部分元素移到A的前面。例如,A是[5,7,9],B是[1,3,5],合并后应为[1,3,5,5,7,9]。此时,正确的分割点可能在A中最后一个比B第一个元素大的位置。比如,找到A中的元素9比B的1大,所以分割点在这里。然后反转A部分(变为9,7,5),反转B部分(变为5,3,1),然后整体反转得到1,3,5,5,7,9。这可能才是正确的步骤。所以关键在于找到两个数组的正确分割位置,然后进行三次反转。 那在C++中的实现步骤可能是这样的:首先,确定两个有序数组的位置,比如在一个大数组中的两个相邻区间。找到分割点mid,使得左边的元素都大于右边的元素,此时需要将这两个区间合并。使用三次反转:反转左半部分,反转右半部分,然后反转整个数组。或者,可能需要更精确地确定反转的范围。 然后,关于复杂度分析,三次反转每次都是O(k)的时间,其中k是反转的长度,总和是O(n),所以时间复杂度是O(n),空间复杂度是O(1)。 接下来,需要将这些步骤整理成中文,结构清晰的回答,包括算法原理、复杂度分析、C++实现示例。需要注意的是,手摇算法可能还有其他名称,比如原地归并算法,或者块交换算法。可能用户所说的“手摇算法”即为三次反转方法。 在代码实现部分,需要给出如何用三次反转合并两个有序数组。可能需要先找到合适的分割点,即第二个数组的起始位置。例如,在数组arr中,已知low、mid、high,其中arr[low..mid]和arr[mid+1..high]都是有序的,现在要合并这两个部分。首先找到mid+1的位置,然后找到第一个在左半部分大于右半部分的元素的位置,这可能需要进行二分查找。例如,找到左半部分最后一个元素大于右半部分第一个元素的位置,然后分割成两个部分。这部分可能需要详细说明。 例如,假设左半部分是升序,右半部分也是升序,但左半的最大值大于右半的最小值。这时候需要找到左半中第一个比右半第一个元素大的位置,设为i。然后左半部分是low到i-1,右半是i到mid,而原来的右半部分是mid+1到high。或者可能需要重新调整分割点。这部分可能需要更深入的算法步骤。 或者,可能更简单的情况是,假设两个有序数组已经存在于同一个数组中,且左半部分和右半部分都是有序的,但整体无序。这时候使用三次反转来合并。例如,左半是升序,右半也是升序,但左半的最大值大于右半的最小值。此时,找到右半部分的起始位置mid+1,然后确定左半中需要移动到右边的元素的位置。这可能涉及到查找分割点,然后进行三次反转。 可能具体的实现步骤如下: 1. 找到两个有序数组的分割点,使得左半部分的元素都大于右半部分的元素。这一步可能需要使用二分查找来确定分割的位置。 2. 将左半部分反转,右半部分反转。 3. 将整个区间反转,得到合并后的有序数组。 例如,假设原数组是 [1,3,5,7,2,4,6,8],左半是0到3,右半是4到7。左半是升序,右半也是升序。但左半的最大值7大于右半的最小值2。需要合并成一个升序数组。此时,正确的三次反转应该是: - 反转左半部分:7,5,3,1 - 反转右半部分:8,6,4,2 此时数组变为 [7,5,3,1,8,6,4,2] 然后整体反转,得到 [2,4,6,8,1,3,5,7],这显然不对。这说明我的理解有误。可能正确的分割点不是中间位置,而是需要找到左半部分和右半部分中的正确分割点,使得反转后可以正确合并。 看来之前的例子可能不适用,或者我的步骤有误。这时候需要重新考虑手摇算法的正确步骤。查资料回忆,手摇算法(即三次反转)的正确应用场景是交换两个相邻的内存块。例如,假设内存中有块A和块B,需要将它们的顺序交换为B和A,可以通过三次反转实现:先反转A,再反转B,最后反转整个A+B区域。例如,A=[a,b,c], B=[d,e],反转A得到[c,b,a],反转B得到[e,d],整体反转得到[d,e,a,b,c],即B+A。这可能适用于块交换,但如何用于合并有序数组呢? 或者,可能手摇算法在归并排序中的应用,用于合并两个相邻的有序子数组,而不需要额外空间。这时候,正确的步骤可能需要找到两个子数组中的分割点,使得交换后合并。例如,假设左子数组和右子数组都是升序,但左子数组的末尾元素大于右子数组的第一个元素。此时,找到左子数组中第一个大于右子数组第一个元素的元素的位置i,和右子数组中最后一个小于左子数组i位置元素的位置j。然后,交换这两个子数组的位置,可以通过三次反转:反转i到mid,反转mid+1到j,然后反转i到j。这样,这两个子数组的位置就被交换,从而合并成一个有序的数组。 或者,可能更简单的例子:比如,合并两个升序的数组,左半部分和右半部分,但左半的最大值大于右半的最小值。这时候,正确的三次反转步骤需要确定如何分割这两个部分。例如,假设数组是[4,5,6,1,2,3],左半是[4,5,6],右半是[1,2,3]。要合并成[1,2,3,4,5,6]。此时,将左半反转得到[6,5,4],右半反转得到[3,2,1],整体数组是[6,5,4,3,2,1],然后整体反转得到[1,2,3,4,5,6],这样正确。所以在这种情况下,三次反转确实有效。但前提是两个子数组已经是需要交换位置的块,即右半部分应该整体出现在左半部分的前面。 所以,手摇算法在这种情况下有效,当需要交换两个相邻块的位置时。但合并两个有序数组的常规情况可能并不总是满足这一点。例如,如果两个数组是交叉的,而不是整个块需要交换,那么可能需要其他处理。 因此,手摇算法在合并有序数组时的应用可能有限,需要满足特定的条件。例如,在归并排序中,当两个相邻的子数组需要合并,并且其中一个子数组的所有元素都大于另一个子数组的所有元素时,可以通过三次反转来交换这两个块的位置,从而达到合并的目的。这种情况下,合并的时间复杂度是O(n),空间O(1)。 但实际应用中,归并排序中的合并通常需要更复杂的处理,因为两个子数组的元素可能是交错的。这时候,手摇算法可能并不直接适用,或者需要结合其他步骤。例如,先找到两个子数组中的适当分割点,将较大的元素移动到正确的位置,这可能需要更复杂的操作。 回到用户的问题,用户可能希望了解如何用三次反转法(手摇算法)合并两个有序数组,并分析其复杂度,以及在C++中的实现。 现在需要整理这些思路,给出清晰的步骤: 手摇算法(三次反转法)的合并步骤: 1. 确定两个有序子数组的范围,假设在同一个数组中,左子数组为arr[low..mid],右子数组为arr[mid+1..high],并且这两个子数组都是有序的。 2. 找到左子数组中第一个大于右子数组第一个元素的元素的位置i。 3. 找到右子数组中最后一个小于arr[i]的元素的位置j。 4. 此时,交换arr[i..mid]和arr[mid+1..j]这两个块的位置。通过三次反转实现: a. 反转arr[i..mid] b. 反转arr[mid+1..j] c. 反转arr[i..j] 这样,这两个块的位置被交换,合并后的数组在arr[low..high]范围内保持有序。 但这个过程可能需要更详细的说明,以及如何找到i和j的位置。例如,使用二分查找来找到i和j的位置。 时间复杂度分析: - 寻找分割点i和j的时间:使用二分查找,每次查找为O(log n),总时间为O(log n)。 - 三次反转的时间:总长度为n,时间复杂度O(n)。 因此,总的时间复杂度为O(n),空间复杂度O(1)。 C++实现可能需要实现反转函数,以及查找i和j的位置。例如,函数reverse(arr, start, end)用于反转数组中的某一部分。然后找到i和j的位置。 不过,这可能比较复杂。或者,手摇算法在合并两个已经有序且相邻的子数组时,可能需要先找到合适的位置,然后进行三次反转。例如,如果两个子数组是[1,3,5,7]和[2,4,6,8],在同一个数组中相邻,但顺序是左子数组的最大值7大于右子数组的最小值2,这时需要将右子数组整个移到左子数组前面。三次反转可以实现这一点。 例如,原数组是[1,3,5,7,2,4,6,8]。左子数组是0-3,右子数组是4-7。此时,左子数组反转后是7,5,3,1;右子数组反转后是8,6,4,2。整体数组变为[7,5,3,1,8,6,4,2],然后整体反转得到[2,4,6,8,1,3,5,7],这显然不是正确的合并结果。这说明这种情况下三次反转并不适用,除非子数组的顺序满足特定的条件。 这说明手摇算法可能并不适用于所有情况的有序数组合并,而是适用于需要交换两个相邻块的位置的情况。例如,当左子数组的所有元素都大于右子数组的所有元素时,三次反转可以交换这两个块的位置,从而达到合并的目的。这种情况下,合并后的数组即为右子数组+左子数组,自然有序。 因此,手摇算法在归并排序中的应用可能是在这种情况下,当两个相邻的子数组需要合并,并且左子数组整体大于右子数组时,可以直接交换块的位置,从而省去常规合并中的额外空间。 综上,用户的问题可能需要分情况讨论: 1. 手摇算法(三次反转)的原理和应用场景。 2. 算法复杂度分析:时间O(n),空间O(1)。 3. 在C++中的实现,需要处理特定条件下的合并,例如相邻子数组的交换。 因此,回答的结构可能如下: - 手摇算法简介 - 算法步骤 - 复杂度分析 - C++代码示例 - 应用场景限制 现在需要将这些整理成结构化的中文回答,确保正确性。</think>### 手摇算法有序序列合并详解(C++实现) #### 一、手摇算法原理 **手摇算法**(又称三次反转法)是一种原地合并有序序列的算法,核心思想是通过三次反转操作交换相邻内存块的位置。适用于以下场景: - 两个有序子数组**相邻存储** - 左子数组所有元素 > 右子数组所有元素 #### 二、算法步骤 假设数组`arr[low..high]`包含两个相邻有序子数组`arr[low..mid]`和`arr[mid+1..high]`,且左子数组全部元素大于右子数组: 1. **反转左子数组**:`reverse(arr, low, mid)` 2. **反转右子数组**:`reverse(arr, mid+1, high)` 3. **整体反转**:`reverse(arr, low, high)` #### 三、复杂度分析 | 操作 | 时间复杂度 | 空间复杂度 | |------------|------------|------------| | 三次反转 | $O(n)$ | $O(1)$ | | 二分查找 | $O(\log n)$| - | | **总计** | $O(n)$ | $O(1)$ | #### 四、C++代码实现 ```cpp #include <algorithm> #include <vector> // 三次反转合并函数 void rotate_merge(std::vector<int>& arr, int low, int mid, int high) { // 反转左半部分 std::reverse(arr.begin() + low, arr.begin() + mid + 1); // 反转右半部分 std::reverse(arr.begin() + mid + 1, arr.begin() + high + 1); // 整体反转 std::reverse(arr.begin() + low, arr.begin() + high + 1); } // 示例用法 int main() { std::vector<int> arr = {7, 8, 9, 1, 2, 3}; // 左子数组[7,8,9], 右子数组[1,2,3] rotate_merge(arr, 0, 2, 5); // 输出结果: 1 2 3 7 8 9 return 0; } ``` #### 五、常规合并 vs 手摇算法 | 方法 | 时间复杂度 | 空间复杂度 | 适用场景 | |--------------|------------|------------|------------------------| | 双指针合并 | $O(n)$ | $O(n)$ | 通用数组合并 | | 手摇算法 | $O(n)$ | $O(1)$ | 相邻块且左>右的合并 | #### 六、扩展应用 1. **归并排序优化**:在归并排序中,当检测到`arr[mid] > arr[mid+1]`时触发手摇算法 2. **内存受限场景**:嵌入式开发等需要严格节省内存的场景 #### 七、算法限制 - 仅适用于**左子数组完全大于右子数组**的特定情况 - 通用合并仍需传统双指针法(代码示例): ```cpp std::vector<int> merge(const std::vector<int>& a, const std::vector<int>& b) { std::vector<int> res; int i = 0, j = 0; while (i < a.size() && j < b.size()) { if (a[i] < b[j]) res.push_back(a[i++]); else res.push_back(b[j++]); } while (i < a.size()) res.push_back(a[i++]); while (j < b.size()) res.push_back(b[j++]); return res; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值