不知道小伙伴们有没有刷过力扣上的算法题,我在上研究生的时候,刷过了前40道题,上面的算法题,我觉得还挺难的,当你写完代码的时候,就可以提交自己写的代码到系统上,系统会给你写的代码计算时间复杂度和空间复杂度,并给出了你的排名。可能有的小伙伴不理解这个概念,今天我们来详细讲一下。
在编写和优化代码时,时间复杂度和空间复杂度是两个关键概念。它们帮助我们衡量算法的效率和资源使用情况,从而选择最优的解决方案。本文将详细探讨这两个概念,并通过具体的例子来说明它们在实际编程中的应用。
时间复杂度?
时间复杂度描述了一个算法的运行时间如何随着输入数据规模的增长而变化。它通常用大O符号表示,例如O(1)、O(n)、O(log n)等。以下是几种常见的时间复杂度类型及其含义:
O(1) - 常数时间复杂度:
不管输入数据的大小如何,算法的执行时间始终相同。常数时间复杂度表示算法的运行时间不受输入数据规模的影响。
例子:
public int GetFirstElement(int[] array)
{
return array[0];
}
解释:
这段代码总是返回数组的第一个元素,不管数组有多大,花费的时间都是一样的。这就像从一堆书里拿起最上面的一本书,不管书堆有多高,拿起一本书的时间始终不变。
示例图:
函数调用: 数组:
GetFirstElement(array) [10, 20, 30, 40, 50]
| 0 1 2 3 4
| ↑
返回 array[0] |
|
O(1) <--------------------+
注释:访问数组第一个元素的操作始终是常数时间,不受数组大小影响。
通过这种方式,我们可以清晰地展示O(1)时间复杂度的概念,即无论输入数据的大小如何,算法的执行时间始终相同。
O(n) - 线性时间复杂度:
算法的执行时间与输入数据规模成正比。当输入数据增大时,运行时间也线性增长。
**例子:**求数组每个元素之和
public int Sum(int[] array)
{
int sum = 0;
foreach (int item in array)
{
sum += item;
}
return sum;
}
解释:
这个函数需要遍历整个数组来计算所有元素的总和。数组越大,需要的时间就越长。就像你需要逐个数数一堆硬币,硬币越多,数完的时间就越长。
示例图
scss复制代码函数调用: 数组:
Sum(array) [10, 20, 30, 40, 50]
| 0 1 2 3 4
| ↑ ↑ ↑ ↑ ↑
遍历每个元素 <------------------ |
|
时间复杂度:O(n) <---------------+
注释:计算数组所有元素的总和需要遍历整个数组,运行时间与数组大小成正比。
O(log n) - 对数时间复杂度:
算法的执行时间随着输入数据规模的增长按对数比例增加。这通常出现在需要将数据集分成较小部分的算法中,例如二分查找。
**例子:**在有序数组中查找目标元素的索引
public int BinarySearch(int[] array, int target)
{
int left = 0;
int right = array.Length - 1;
while (left <= right)
{
int mid = left + (right - left) / 2;
if (array[mid] == target)
return mid;
if (array[mid] < target)
left = mid + 1;
else
right = mid - 1;
}
return -1;
}
解释:
这个函数使用二分查找法来寻找目标元素。每次迭代都会把搜索范围缩小一半。这就像在一本按字母顺序排列的字典里查一个词,每次翻开中间的页面,决定目标词在前半部分还是后半部分,然后重复这个过程。
示例图
scss复制代码函数调用: 数组:
BinarySearch(array, target) [10, 20, 30, 40, 50]
| 0 1 2 3 4
| 第一次比较:
| mid = 2 -> 30 == target ?
| 目标小于30,搜索范围缩小一半
| 新范围: [10, 20]
| 0 1
| 第二次比较:
| mid = 0 -> 10 == target ?
| 目标大于10,搜索范围缩小一半
| 新范围: [20]
| 1
| 第三次比较:
| mid = 1 -> 20 == target ?
| 找到目标,返回索引1
↑
|
|
O(log n) <----------------+
注释:每次迭代搜索范围缩小一半,因此执行时间按对数比例增加。
O(n^2) - 平方时间复杂度:
算法的执行时间与输入数据规模的平方成正比。这通常出现在嵌套循环中,每个循环遍历输入数据。
**例子:**将数组中的数据从小到大排序
public void BubbleSort(int[] array)
{
for (int i = 0; i < array.Length - 1; i++)
{
for (int j = 0; j < array.Length - i - 1; j++)
{
if (array[j] > array[j + 1])
{
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
解释:
这个函数使用冒泡排序算法对数组进行排序。它包含两个嵌套的循环,每个循环都会遍历数组。就像你要给一堆学生排座次,每次都要两两比较,整个过程需要很多次。
示例图
less复制代码函数调用: 数组:
BubbleSort(array) [30, 10, 40, 20, 50]
外层循环 i = 0:
| 第一次比较:30 > 10, 交换
| 新数组:[10, 30, 40, 20, 50]
内层循环 j = 0:
| 第二次比较:30 > 40, 不交换
| 新数组:[10, 30, 40, 20, 50]
| 第三次比较:40 > 20, 交换
| 新数组:[10, 30, 20, 40, 50]
| 第四次比较:40 > 50, 不交换
外层循环 i = 1:
| 第一次比较:10 > 30, 不交换
| 新数组:[10, 30, 20, 40, 50]
内层循环 j = 1:
| 第二次比较:30 > 20, 交换
| 新数组:[10, 20, 30, 40, 50]
| 第三次比较:30 > 40, 不交换
... (持续进行直到排序完成)
↑
|
|
O(n^2) <---------------------+
注释:每次外层循环遍历数组,内层循环也遍历数组,因此执行时间与数组大小的平方成正比。
O(2^n) - 指数时间复杂度:
算法的执行时间随着输入数据规模的增长按指数比例增加。这种复杂度通常出现在递归算法中,尤其是解决组合问题时。
例子:
public int Fibonacci(int n)
{
if (n <= 1)
return n;
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
解释:
这个函数计算斐波那契数列的第n个数字。每次调用函数时,它会递归调用自身两次,导致总的调用次数按指数增长。
示例图
scss复制代码Fibonacci(5)
|
-------------
| |
Fib(4) Fib(3)
| |
---- -----
| | | |
Fib(3) Fib(2) Fib(2) Fib(1)
| | | |
--- --- --- ---
| | | | | | |
Fib Fib Fib Fib Fib Fib
(2) (1) (1) (0) (1) (0)
|
...
注释:每次调用函数时,它会递归调用自身两次,导致总的调用次数按指数增长,时间复杂度为 O(2^n)。
空间复杂度?
空间复杂度描述了一个算法在运行时需要的内存空间如何随着输入数据规模的增长而变化。以下是几种常见的空间复杂度类型及其含义:
O(1) - 常数空间复杂度:
算法所需的内存空间不随输入数据规模的变化而变化。
例子:
public int GetFirstElement(int[] array)
{
return array[0];
}
解释:
这个函数只返回数组的第一个元素,不需要额外的内存空间。就像你只拿一张桌子放书,无论书堆多高,只用一张桌子就够了。
示例图
scss复制代码函数调用: 数组:
GetFirstElement(array) [10, 20, 30, 40, 50]
| 0 1 2 3 4
| ↑
返回 array[0] |
|
O(1) <---------------------+
注释:访问数组第一个元素的操作不需要额外的内存空间,内存使用量不随输入数据规模变化。
O(n) - 线性空间复杂度:
算法所需的内存空间与输入数据规模成正比。
例子:
public int[] CopyArray(int[] array)
{
int[] newArray = new int[array.Length];
for (int i = 0; i < array.Length; i++)
{
newArray[i] = array[i];
}
}
解释:
这个函数创建了一个和输入数组一样大的新数组。就像你需要一个新的箱子来装一堆硬币,硬币越多,箱子就得越大。
示例图
O(n^2) - 平方空间复杂度:
算法所需的内存空间与输入数据规模的平方成正比。
例子:
public int[,] CreateMatrix(int n)
{
int[,] matrix = new int[n, n];
return matrix;
}
解释:
这个函数创建了一个n x n的矩阵。随着n的增加,所需的内存空间按平方增长。
示例图
scss复制代码函数调用: 矩阵:
CreateMatrix(n) n = 2
| [1, 1]
| [1, 1]
| 矩阵大小:2 x 2
|
| n = 3
| [1, 1, 1]
| [1, 1, 1]
| [1, 1, 1]
| 矩阵大小:3 x 3
|
| n = 4
| [1, 1, 1, 1]
| [1, 1, 1, 1]
| [1, 1, 1, 1]
| [1, 1, 1, 1]
| 矩阵大小:4 x 4
↑
|
|
O(n^2) <---------------------+
注释:创建一个n x n的矩阵所需的内存空间随着n的平方增长。
O(2^n) - 指数空间复杂度:
算法所需的内存空间随着输入数据规模的增长按指数比例增加。
**例子:**字符串的所有子集
public List<string> GenerateSubsets(string str)
{
if (str.Length == 0)
{
return new List<string> { "" };
}
char first = str[0];
List<string> subsets = GenerateSubsets(str.Substring(1));
List<string> newSubsets = new List<string>();
foreach (string subset in subsets)
{
newSubsets.Add(first + subset);
newSubsets.Add(subset);
}
return newSubsets;
}
解释:
这个函数生成字符串的所有子集。随着字符串长度的增加,生成的子集数量按指数增长,需要的内存空间也按指数增长。
示例图
scss复制代码函数调用:
GenerateSubsets("abc")
|
---------------
| |
GenSub("bc") GenSub("bc")
| |
GenSub("c") GenSub("c")
| |
GenSub("") GenSub("")
| |
[""] [""]
生成的子集:
["", "c", "b", "bc", "a", "ac", "ab", "abc"]
空间复杂度:O(2^n)