> 文章列表 > 【算法】时间和空间复杂度

【算法】时间和空间复杂度

【算法】时间和空间复杂度

文章目录

  • 前言
  • 一、时间复杂度
  • 二、空间复杂度
  • 三、常见的案例和示例
    • 1. 线性查找(Linear Search)
    • 2. 快速排序(Quick Sort)
    • 3.动态规划(Dynamic Programming)
    • 4.图搜索(Graph Search)
    • 5.哈希表(Hash Table)
  • 总结

前言

当我们谈论算法的效率时,我们关注的两个主要方面是时间复杂度空间复杂度

时间复杂度 描述的是算法在执行过程中所需要的时间资源。它通常用大O符号来表示,表示算法的运行时间随输入规模增大而变化的趋势。比如,O(1) 表示常数时间复杂度,O(n)表示线性时间复杂度,O(n^2)表示平方时间复杂度,等等。时间复杂度越低,算法的执行速度越快。

空间复杂度 描述的是算法在执行过程中所需要的内存资源。它也通常用大O符号来表示,表示算法的内存占用随输入规模增大而变化的趋势。比如,O(1)表示常数空间复杂度,O(n)表示线性空间复杂度,O(n^2)表示平方空间复杂度,等等。空间复杂度越低,算法所占用的内存越少。


为了更好地理解时间和空间复杂度的概念,下面我们通过Java语言来给出一些简单的例子。

一、时间复杂度

常见的表示如下:

  • O(1):常数时间复杂度,表示算法的运行时间不随输入规模变化而变化,例如数组的访问、常数次数的数学运算等
  • O(log n):对数时间复杂度,表示算法的运行时间随着输入规模 n 的增加而以对数形式增长,例如二分查找、平衡二叉搜索树等。
  • O(n):线性时间复杂度,表示算法的运行时间随着输入规模 n 的增加而以线性形式增长,例如顺序查找、线性搜索等。
  • O(n^2):平方时间复杂度,表示算法的运行时间随着输入规模 n 的增加而以平方形式增长,例如嵌套循环、选择排序等。
  • O(n^k):多项式时间复杂度,表示算法的运行时间随着输入规模 n 的增加而以 k 次方的形式增长,其中 k 是常数。
  • O(2^n):指数时间复杂度,表示算法的运行时间随着输入规模 n 的增加而以指数形式增长,例如穷举法、递归法等。

// O(1)的时间复杂度,常数时间复杂度
public void printFirstElement(int[] arr) {System.out.println(arr[0]);
}

上面的代码中,无论输入参数a和b的大小如何,都只进行一次加法操作,因此运行时间不会随输入规模的增加而增加,所以时间复杂度为O(1)


// O(n)的时间复杂度,线性时间复杂度
public void printAllElements(int[] arr) {for (int i = 0; i < arr.length; i++) {System.out.println(arr[i]);}
}

上面的代码中,需要对一个长度为n的数组进行遍历,并比较每个元素与当前最大值的大小关系,因此运行时间随输入数组的长度n线性增长,所以时间复杂度为O(n)。


// O(n^2)的时间复杂度,平方时间复杂度
public void printAllPairs(int[] arr) {for (int i = 0; i < arr.length; i++) {for (int j = 0; j < arr.length; j++) {System.out.println(arr[i] + ", " + arr[j]);}}
}

上面的代码中,嵌套了两层循环,需要对数组中的每一对元素进行输出,因此运行时间随输入数组的长度n的平方增长,所以时间复杂度为O(n^2)。


二、空间复杂度

常见的表示如下:

O(1):常数空间复杂度,表示算法的空间需求不随输入规模变化而变化,例如常数个变量的存储。
O(n):线性空间复杂度,表示算法的空间需求随着输入规模 n 的增加而以线性形式增长,例如数组、链表等。
O(n^2):平方空间复杂度,表示算法的空间需求随着输入规模 n 的增加而以平方形式增长,例如二维数组等。

没有提到 O(logn),是因为空间复杂度很少用于描述对数级别的空间需求。通常情况下,空间复杂度主要关注算法在运行时所需的额外存储空间,例如数组、变量、数据结构等。而对数级别的空间需求很少出现,因为它通常只涉及到一些常数级别的辅助变量


// O(1)的空间复杂度,常数空间复杂度
public int add(int a, int b) {return a + b;
}

上面的代码中,只需要有两个整数型的变量来保存输入参数和返回值,不论输入参数的大小如何,所需的内存空间都是固定的,因此空间复杂度为O(1)。


// O(n)的空间复杂度,线性空间复杂度
public int[] createArray(int n) {int[] arr = new int[n];for (int i = 0; i < n; i++) {arr[i] = i;}return arr;
}

上面的代码中,需要创建一个大小为n的整数型数组,并将数组中的元素初始化为0到n-1的值,因此需要占用n个整数型的内存空间,所以空间复杂度为O(n)


// O(n^2)的空间复杂度,平方空间复杂度
public void printAllPairs(int[] arr) {for (int i = 0; i < arr.length; i++) {for (int j = 0; j < arr.length; j++) {System.out.println(arr[i] + ", " + arr[j]);}}
}

上面的代码中,虽然没有显式地申请额外的内存空间,但每次输出一对元素的组合时,都需要创建一个包含这两个元素的字符串对象,因此需要占用 n^2 个字符串对象的内存空间,所以空间复杂度为O(n^2)。

三、常见的案例和示例

1. 线性查找(Linear Search)

时间复杂度:O(n)
空间复杂度:O(1)

线性查找是一种简单的查找算法,从数组或列表中逐一遍历元素,直到找到目标元素或遍历完整个数组。在最坏的情况下,目标元素可能在最后一个位置,需要遍历整个数组,因此时间复杂度为O(n)。由于只需要使用常数个变量来存储当前查找的元素和一些辅助信息,所以空间复杂度为O(1)

2. 快速排序(Quick Sort)

时间复杂度:平均情况下O(logn),最坏情况下O(n^2)
空间复杂度:O(logn)

快速排序是一种常用的排序算法,通过选择一个基准元素将数组划分为两部分,然后分别对这两部分进行递归排序。在平均情况下,每次划分都将数组分为大致相等的两部分,需要进行logn次划分,因此时间复杂度为O(nlogn)。但在最坏的情况下,如果选择的基准元素总是导致数组的划分极不均匀,例如已经有序的数组,就会导致每次只能划分出一个元素和n-1个元素,需要进行n次划分,从而时间复杂度变为O(n^2)。空间复杂度取决于递归调用的深度,最坏情况下为O(n),平均情况下为O(logn)。

3.动态规划(Dynamic Programming)

时间复杂度:根据具体问题而定
空间复杂度:根据具体问题而定

动态规划是一种解决优化问题的算法,通过将问题划分为重叠子问题并使用记忆化的方式避免重复计算。动态规划的时间复杂度和空间复杂度通常取决于具体问题的特性和解决方法。例如,对于斐波那契数列问题,使用递归方式计算会导致指数级的时间复杂度和指数级的空间复杂度,但通过使用动态规划并存储中间计算结果,可以将时间复杂度优化到线性级别O(n),空间复杂度优化到O(1)。

4.图搜索(Graph Search)

时间复杂度:根据具体算法和图的规模而定
空间复杂度:根据具体算法和图的规模而定

图搜索是一类解决图相关问题的算法,例如深度优先搜索(DFS)和广度优先搜索(BFS)。其时间复杂度和空间复杂度通常取决于图的规模和具体的搜索算法。例如,在无权图中,DFS和BFS的时间复杂度通常为O(V+E),其中V表示顶点数,E表示边数。而在有权图中,涉及到权重的计算和比较,时间复杂度可能会有所不同。

5.哈希表(Hash Table)

时间复杂度:平均情况下O(1),最坏情况下O(n)
空间复杂度:O(n)

哈希表是一种用于实现查找、插入和删除等操作的数据结构,具有高效的平均查找时间。在哈希表中,通过将关键字映射到数组索引来实现快速的查找操作。在理想情况下,每个关键字都会被均匀地映射到数组中的不同位置,从而保证平均情况下的O(1)时间复杂度。但在最坏的情况下,如果所有关键字都被映射到了同一个位置,就会导致查找操作变得非常缓慢,时间复杂度变为O(n)。空间复杂度取决于哈希表中存储的元素个数,通常为O(n)。

总结

通过以上的例子,我们可以看到,时间和空间复杂度是评估算法效率的重要指标。理解算法的时间和空间复杂度有助于我们在设计和分析算法时,能够更好地选择合适的算法以满足实际需求。