# 算法复杂度

# O 的含义

我们一般使用大写O来表示算法的最坏情况下复杂度,我们一般它用来描述算法所需资源(如时间或者空间), 它使用数学符号来描述算法复杂度与输入大小之间的关系。

不同的数据结构对同一操作具有不同的时间复杂度。一个常见的例子是链表具有O(1)时间复杂度的插入和删除操作,而同样操作数组的时间复杂度是O(n)

# Ω(Omega) 和 Θ(Theta)

还有其他符号用于描述算法复杂度,如 Ω(Ωmega)和 Θ(Theta)。Ω 描述算法的最佳情况复杂度,而 Θ 描述算法的平均情况复杂度。

比如链表的插入和删除操作为Θ(1)O(1)。还有常用数组排序算法的时间复杂度,如快速排序的最佳复杂度为Ω(n log n),平均复杂度为Θ(n log n)和最坏复杂度为O(n^2)

# 如何计算复杂度

这里只考虑时间复杂度,不考虑空间复杂度

要计算一个算法的复杂度,首先需要确定算法中最慢的部分,也就是瓶颈,这部分复杂度就是整个算法的复杂度。

通常来说,可以通过检查算法中所有的循环和递归操作,并计算它们运行的次数来确定瓶颈。每个循环或递归贡献一个计数,最终,我们取这些计数中最大的那一个作为算法复杂度。例如,如果算法中有两个循环,一个是 O(n),另一个是 O(m),那么算法的复杂度就是 O(max(n, m))。 其中 n 和 m 分别代表数据的大小。

# 计算方法

算法复杂度的计算方法主要包括以下几种:

  1. 常数法则: 仅考虑最多的循环次数。
  2. 加法法则: 将算法中多个独立部分的时间复杂度相加。
  3. 乘法法则: 对于嵌套循环, 其时间复杂度是外层循环次数和内层循环次数之积。
  4. 大 O 记号法则: 只考虑算法中最高阶复杂度的项。

在计算复杂度的时候,应该尽量忽略常数,如果该算法中包含多个独立部分,则将它们的复杂度相加。

对于嵌套循环, 我们需要计算外层循环和内层循环的乘积.

# 举例说明

  1. 举一个常见的例子,O(1),它表示常数时间复杂度,意味着算法所需时间与输入大小无关,并且是最高效的。 比如:读取一个数组中的单个元素。因为访问数组中的单个元素是一个简单的操作,所以不需要遍历整个数组,只需要知道元素所在的位置即可。 因此读取一个数组中的单个元素的时间复杂度是 O(1)。具体实现如下:

    let arr = [1, 2, 3, 4, 5];
    console.log(arr[3]); // 4
    
  2. 另外,O(n!)表示阶乘时间复杂度,这是最低效的,因为随着输入增长,资源增长很快。 一个经典的例子是生成并打印全排列。

    全排列算法的基本思想是递归地生成所有可能的排列,并在生成每种排列时进行递归。这种算法的时间复杂度是O(n!),因为生成全排列需要进行n!次递归

    function permute(arr) {
      function helper(current, remaining) {
        if (remaining.length === 0) {
          console.log(current);
          return;
        }
        for (let i = 0; i < remaining.length; i++) {
          helper(
            current.concat([remaining[i]]),
            remaining.slice(0, i).concat(remaining.slice(i + 1))
          );
        }
      }
      helper([], arr);
    }
    
  3. 举一个 时间复杂度是 O(n^2) 的例子

    一个经典的例子是冒泡排序算法,冒泡排序的基本思想是重复地遍历序列,比较相邻的两个元素,如果它们的顺序错误就把它们交换过来。这个算法的时间复杂度是 O(n^2),因为它需要进行 n 次外层循环和 n-1 次内层循环,总循环次数是 n*(n-1)这样就是 O(n^2)

    function bubbleSort(arr) {
      for (let i = 0; i < arr.length; i++) {
        for (let j = 0; j < arr.length - i - 1; j++) {
          if (arr[j] > arr[j + 1]) {
            [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
          }
        }
      }
      return arr;
    }
    
  4. 举一个O(2^n)的例子 O(2^n)是指随着数据规模增大,算法运行时间的增长速度是指数级的。 一个经典的例子是求解背包问题的递归算法。 背包问题是这样的:给定n个物品和一个容量为V的背包,求如何让背包装入物品,使得装入物品的总价值最大。

    function knapsack(weight, value, n, V) {
        if (n == 0 || V == 0) {
            return 0;
        }
        if (weight[n - 1] > V) {
            return knapsack(weight, value, n - 1, V);
        }
        return Math.max(
            value[n - 1] + knapsack(weight, value, n - 1, V - weight[n - 1]),
            knapsack(weight, value, n - 1, V)
        );
    }
    

    这个递归算法的时间复杂度是O(2^n),因为每次调用都会让问题的规模翻倍,而算法会重复计