从插入排序一窥时间复杂度的计算方法

本文深入探讨了算法的时间复杂度分析,以插入排序算法为例,详细解释了如何计算算法的运行时间,并讨论了最坏情况与平均情况分析的重要性。

为什么需要分析时间复杂度

通常在运行一段代码之前,我们需要预测其需要的资源。虽然有时我们主要关心像内存、网络带宽或者计算机硬件这类资源,但是通常我们想度量的是计算时间。
接下来我们以插入排序算法为切入点一窥时间复杂度的计算方法。

时间复杂度分析

一般来说,算法需要的时间于输入的规模同步增长,所以通常把一个程序的运行时间描述成其输入规模的函数。为此,我们必须先给出术语运行时间输入规模

输入规模通常依赖于研究的问题。比如,对于排序问题来说,最自然的量度是需要排序元素的数量。又比如对于最短路算法而言,其输入是一个图,则输入规模可以用该图中的顶点数及边数来描述。

一个算法在特定输入上的运行时间是指执行的基本操作数或步数。首先我们假设执行一行代码需要常量时间。常量时间是指:无论输入规模如何变化,执行这一行代码的时间都不受输入规模的影响。我们记第 i 行代码的执行时间为 CiC_iCi

在用插入排序举例之前,我们先看下该算法的基本思想:每步将一个待排序的元素,按其值的大小插入前面已经排序的序列中适当位置上,直到全部元素插入完为止。以升序排序为例,具体代码如下:

template<typename Array>
void InsertionSort(Array &data) {                       
    for(size_t i = 1, n = data.size(); i < n; i++) {  // 1           
        auto key = data[i];                           // 2  
        auto j = i-1;                                 // 3
        while(j >= 0 && data[j] > key) {              // 4
            data[j+1] = data[j];                      // 5
            j--;                                      // 6
        }
        data[j+1] = key;                              // 7
    }
}

我们设上述代码的 for 循环中七行代码的执行时间分别为:
C0,C1,C2,C3,C4,C5,C6C_0, C_1, C_2, C_3, C_4, C_5, C_6C0,C1,C2,C3,C4,C5,C6
设枚举到第 i 个元素时,第四行代码的执行次数为 TiT_iTi
每行代码的执行次数可表示为:
n,n−1,n−1,∑i=2nTi,∑i=2n(Ti−1),∑i=2n(Ti−1),n−1n, n-1, n-1, \sum_{i=2}^nT_i, \sum_{i=2}^n(T_i-1), \sum_{i=2}^n(T_i-1),n-1n,n1,n1,i=2nTi,i=2n(Ti1),i=2n(Ti1),n1

具体对应关系如下:
每行的执行耗时和执行次数
该算法的运行时间是执行每条语句的运行时间之和。为计算在具有 n 个元素的输入上该算法的运行时间S(n),我们将代价和次数列对应元素之积求和,得:
S(n)=C0n+C1(n−1)+C2(n−1)+C3∑i=2nTi+C4∑i=2n(Ti−1)+C5∑i=2n(Ti−1)+C6(n−1)\begin{aligned} S(n)=&C_0n + C_1(n-1)+C_2(n-1)\\ &+C_3\sum_{i=2}^nT_i\\ &+C_4\sum_{i=2}^n(T_i-1)\\ &+C_5\sum_{i=2}^n(T_i-1)\\ &+C_6(n-1) \end{aligned} S(n)=C0n+C1(n1)+C2(n1)+C3i=2nTi+C4i=2n(Ti1)+C5i=2n(Ti1)+C6(n1)

即使对给定规模的输入,一个算法的运行时间也有可能依赖于给定输入的一些特点。例如,对于插入算法来说,若输入数组已排好序,则出现最佳情况。这时,对每个 i=1,2,3,...,n−1i = 1,2,3,...,n-1i=1,2,3,...,n1Ti=1Ti = 1Ti=1 ,该最佳情况的运行时间为
S(n)=C0n+C1(n−1)+C2(n−1)+C3(n−1)+C6(n−1)\begin{aligned} S(n) =&C_0n\\ &+ C_1(n-1)\\ &+C_2(n-1)\\ &+C_3(n-1)+C_6(n-1)\\ \end{aligned} S(n)=C0n+C1(n1)+C2(n1)+C3(n1)+C6(n1)
整理一下上述式子得:
S(n)=(C0+C1+C2+C3+C6)n−(C1+C2+C3+C6)\begin{aligned} S(n)= &(C_0+C_1+C_2+C_3+C_6)n\\ &-(C_1+C_2+C_3+C_6) \end{aligned}S(n)=(C0+C1+C2+C3+C6)n(C1+C2+C3+C6)
我们可以把该运行时间表示为an+ban+ban+b,其中常量 a 和 b 依赖于语句代价CiCiCi。因此,它是n的线性函数

若输入数组已反向排序,即按递减序列排好序,则导致最坏情况。这时,对于i=1,2,3,...n−1i=1,2,3,...n-1i=1,2,3,...n1Ti=i+1Ti=i+1Ti=i+1。将 TiTiTi 代入 S(n)S(n)S(n) 得:
S(n)=(C32+C42+C52)n2+(C0+C1+C2+C32−C42−C52−C6)n−(C1+C2+C3+C6)\begin{aligned} S(n) &=(\frac{C_3}{2}+\frac{C_4}{2}+\frac{C_5}{2})n^2\\ &+(C_0+C_1+C_2+\frac{C_3}{2}-\frac{C_4}{2}-\frac{C_5}{2}-C_6)n\\ &-(C_1+C_2+C_3+C_6) \end{aligned}S(n)=(2C3+2C4+2C5)n2+(C0+C1+C2+2C32C42C5C6)n(C1+C2+C3+C6)
我们可以把最坏情况运行时间表示为an2+bn+can^2+bn+can2+bn+c,其中常量a,b,ca,b,ca,b,c依赖于语句执行耗时CiC_iCi。因此,它是n的二次函数。

最坏情况与平均情况分析

在分析插入排序时,我们同时研究了最坏情况和最佳情况。然而我们往往集中于最坏情况运行时间,即规模为n的所有输入中,算法运行时间最长的情况。原因如下:

  • 一个算法的最坏情况运行时间给出了运行时间的上界。从而可以保证在任何情况下,算法的运行时间绝对不会超过这个上界。
  • 对于大多数算法来说,最佳情况出现的频率极低。
  • "平均情况"往往和最坏情况大致一样差。

增长量级

我们使用某些简化的抽象来使插入排序的分析更加容易。
首先,通过使用常量CiC_iCi表示每条语句的执行耗时以忽略每条语句的细节。
其次,我们进一步整合总体耗时的计算公式,使其表示为an2+bn+can^2+bn+can2+bn+c,进一步忽略了每条语句的执行耗时。
现在我们做出一种更简化的抽象:即我们真正感兴趣的运行时间的增长率或增长量级。所以我们只考虑公式中最重要的项,因为当 n 的值很大时,低阶项相对来说并不重要。我们也忽略最重要的项的常系数,因为对大的输入,在确定计算效率时常量因子不如增长率重要。对于插入排序,当我们忽略掉低阶项和最重要的项的常系数时,只剩下最重要的项中的因子n2n^2n2。我们记插入排序的时间复杂度为O(n2)O(n^2)O(n2)

如果一个算法的最坏情况运行时间具有比另一个算法更低的增长量级,那么我们通常认为前者比后者更有效。由于常量因子和低阶项,对于小的输入,运行时间具有较高增长量级的一个算法与运行时间具有较低增长量级的另一个算法相比,其可能需要较少时间。但是当输入足够大时,例如,一个 O(log2n)O(log_2^n)O(log2n)算法在最坏情况下比另一个O(n2)O(n^2)O(n2)的算法要运行的更快。

扫码关注,快乐加倍

### 插入排序算法的平均时间复杂度计算方法 插入排序种简单直观的排序算法,其核心思想是将数组分为已排序部分和未排序部分。每次从未排序部分取出个元素,插入到已排序部分的正确位置中[^1]。 #### 平均时间复杂度分析 在插入排序中,假设输入数组的长度为 \( n \),我们需要对每个元素进行插入操作。对于第 \( i \) 个元素(从 1 开始计数),需要将其与前面已经排好序的 \( i-1 \) 个元素进行比较,并可能进行移动。如果数组中的元素是随机分布的,则每个元素需要比较的次数可以视为 \( (i-1)/2 \) 次(因为平均情况下,新元素会插入到中间位置)。因此,总的比较次数可以表示为: \[ \text{总比较次数} = \sum_{i=1}^{n-1} \frac{i}{2} = \frac{1}{2} \sum_{i=1}^{n-1} i \] 根据数学公式,等差数列求和公式为 \( \sum_{i=1}^{k} i = \frac{k(k+1)}{2} \),所以有: \[ \text{总比较次数} = \frac{1}{2} \cdot \frac{(n-1)n}{2} = \frac{n(n-1)}{4} \] 由于时间复杂度通常只关注最高阶项,忽略低阶项和常数项,因此插入排序的平均时间复杂度为 \( O(n^2) \)。 #### 示例代码 以下是插入排序的实现代码,展示了其基本逻辑: ```java public class InsertionSort { public static void sort(int[] arr) { for (int i = 1; i < arr.length; i++) { int key = arr[i]; int j = i - 1; // 将当前元素插入到已排序部分的正确位置 while (j >= 0 && arr[j] > key) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = key; } } public static void main(String[] args) { int[] arr = {5, 2, 9, 1, 5, 6}; sort(arr); System.out.println(Arrays.toString(arr)); } } ``` 上述代码中,`while` 循环的执行次数取决于当前元素与之前元素的比较结果。在平均情况下,每次插入操作需要比较 \( (i-1)/2 \) 次[^1]。 #### 空间复杂度 插入排序种原地排序算法,除了输入数组外不需要额外的空间,因此其空间复杂度为 \( O(1) \)[^4]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值