时间复杂度和空间复杂度是什么
时间复杂度(Time Complexity)是描述算法运行时间长短的一个度量。空间复杂度(Space Complexity)是描述算法在运行过程中所需要的存储空间大小的一个度量。?
?
时间复杂度和空间复杂度是衡量算法性能的重要指标。在实际开发中,我们通常会选择时间复杂度和空间复杂度都较低的算法。?
?
时间复杂度可以用大O表示法来表示。大O表示法是用一个大写字母O来表示一个函数的增长率。例如,一个函数f(n)的增长率为O(n),表示当n趋于无穷大时,f(n)的增长率与n的增长率相同。?
?
空间复杂度也可以用大O表示法来表示。例如,一个函数f(n)的空间复杂度为O(n),表示当n趋于无穷大时,f(n)所需要的存储空间与n的增长率相同。?
?
在实际开发中,我们通常会选择时间复杂度和空间复杂度都较低的算法。例如,在排序算法中,我们通常会选择快速排序算法,而不是冒泡排序算法。这是因为快速排序算法的时间复杂度为O(nlogn),而冒泡排序算法的时间复杂度为O(n^2)。
?
常见的时间复杂度有哪些?
O(1):常数时间复杂度。表示算法运行时间与输入数据的大小无关。例如,求一个数的绝对值,无论这个数是多少,算法运行时间都是常数。?
例如:访问数组元素:如果我们有一个长度为n的数组,并且我们想要访问数组中的某个元素,那么无论n多大,访问该元素的时间复杂度都是O(1)。因为访问数组元素只需要一个固定的时间,与数组的大小无关。 |
算法实现:
/**
* 我们只处理数组中的第一个元素,无论数组中有多少个元素,
* 算法的运行时间是固定的,所以时间复杂度为O(1)。
*
* @param arr 数组
*/
public int constantTimeAlgorithm(int[] arr) {
int firstElement = arr[0];
// 在这里进行处理第一个元素的操作
// ...
return firstElement;
}
O(logn):对数时间复杂度。表示算法运行时间与输入数据的对数成正比。例如,二分查找算法。
你可以这样理解: 假设你有一个长度为n的数列,你想找到某个数在数列中的位置。如果用顺序查找,你需要遍历整个数列,时间复杂度为O(n)。如果用二分查找,你只需要遍历数列的一半,时间复杂度为O(logn)。 随着n的增大,O(n)的增长速度要比O(logn)快得多。例如,当n=100时,O(n)的值为100,而O(logn)的值为7。当n=1000时,O(n)的值为1000,而O(logn)的值为10。 因此,O(logn)的时间复杂度比O(n)的时间复杂度要好得多。 复习一下计算过程: 当计算 log?(100) 时,我们要找到一个数 x,使得 2 的 x 次方等于 100。换句话说,我们要求解以下方程: 2^x = 100 为了求解这个方程,我们可以使用对数的定义。根据定义,log?(100) 就是满足 2 的 x 次方等于 100 的 x 值。 因此,我们可以将方程改写为: x = log?(100) 现在,我们需要计算 log?(100) 的值。可以使用换底公式将其转化为常用对数或自然对数。换底公式如下: log?(100) = log?(100) / log?(2) 其中,x 可以是任意正数,我们可以选择常用对数(以 10 为底)或自然对数(以 e 为底)。这里我们选择常用对数。 所以,我们有: log?(100) = log??(100) / log??(2) 接下来,我们计算 log??(100) 和 log??(2) 的值: log??(100) ≈ 2? ? ?log??(2) ≈ 0.30103 将这些值代入公式中,我们可以计算出 log?(100) 的近似值: log?(100) ≈ log??(100) / log??(2) ≈ 2 / 0.30103 ≈ 6.6438561898 所以,log?(100) 的近似值为 6.6438561898。 |
算法实现:
/**
* 代码中的binarySearch方法实现了对有序数组进行二分查找的算法。
* 它将目标元素与数组的中间元素进行比较,若相等则返回中间元素的索引,若小于中间元素则在左子数组中继续查找,
* 若大于中间元素则在右子数组中继续查找。通过不断缩小查找范围,直到找到目标元素或发现不存在目标元素。
*
* @param arr 数组
* @param target 目标值
* @return int
*/
public int binarySearch(int[] arr, int target) {
int low = 0;
int high = arr.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1; // 如果找不到目标元素,则返回-1
}
O(n):线性时间复杂度。表示算法运行时间与输入数据的大小成正比。例如,冒泡排序算法。
你可以这样理解: 假设你有一个长度为n的数列,你想对这个数列进行排序。如果用冒泡排序,你需要遍历整个数列,然后将每个元素与它后面的元素进行比较,如果前一个元素比后一个元素大,就交换它们的位置。你需要重复这个过程,直到整个数列都被排序。 随着n的增大,O(n)的增长速度要比O(1)快得多。例如,当n=100时,O(n)的值为100,而O(1)的值为1。当n=1000时,O(n)的值为1000,而O(1)的值为1。 因此,O(n)的时间复杂度比O(1)的时间复杂度要差得多。 |
算法实现:
/**
* 这个方法计算整数数组的平均值
*
* 在这个算法中,我们遍历整个数组一次,所以时间复杂度是O(n),其中n是数组的长度。
* 这是计算数组平均值的最佳时间复杂度,因为我们至少需要查看数组中的每个元素一次。
*
* @param array 数组
*/
public static double calculateAverage(int[] array) {
int sum = 0;
for (int i = 0; i < array.length; i++) {
sum += array[i];
}
return (double) sum / array.length;
}
O(n^2):平方时间复杂度。表示算法运行时间与输入数据的平方成正比。例如,选择排序算法。
O(n^2) 是一个表示算法复杂度的概念。简单来说,当你运行一个O(n^2)的算法时,它的运行时间或步骤的数量会随着输入大小n的增加而增加。具体来说,这个算法的复杂度是指它的运行时间或步骤的数量与n的平方成正比。 举个例子,如果你有一个数组,你想计算每个元素与所有其他元素的组合,那么你需要对每个元素进行n次比较(n是数组的大小),总共需要进行n * n = n^2次比较。因此,这个算法的时间复杂度是O(n^2)。 总结一下,O(n^2) 表示当输入大小n增加时,算法的运行时间或步骤数量会以n的平方的速度增加。 |
代码实现:
/**
* 一个时间复杂度为O(n^2)的算法,常见的方法是使用嵌套循环。
* 下面代码实现了一个冒泡排序算法。它通过嵌套循环遍历数组,并比较相邻的元素。
* 如果前一个元素大于后一个元素,则交换它们的位置。通过多次遍历和交换,
* 较大的元素会逐渐向数组的末尾冒泡。
* 这个过程将重复执行n-1次,每次循环都需要比较n-i-1次。因此,总的比较次数为:
*
* n-1 + n-2 + ... + 1 = n * (n-1) / 2,
*
* 最终得到时间复杂度为O(n^2)。
*
* @param arr 数组
*/
public void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换arr[j]和arr[j + 1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
O(n^3):立方时间复杂度。表示算法运行时间与输入数据的立方成正比。
如果你有一个非常大的数组,并且你想计算每个元素与所有其他元素的组合的乘积,那么你需要对每个元素进行n次比较,然后再进行一次乘法操作。所以总共需要进行n * n = n^2次比较,以及n * n * n = n^3次乘法操作。因此,这个算法的时间复杂度是O(n^3)。 |
O(2^n):指数时间复杂度。表示算法运行时间与输入数据的指数成正比。例如,汉诺塔问题,斐波那契数列。
斐波那契数列:斐波那契数列是一个非常著名的数列,其中每个数字都是前两个数字的和。对于斐波那契数列的第n项,我们可以通过递归或迭代来计算。但是,由于递归的重复计算,其时间复杂度是O(2^n)。这是因为每次递归都会生成一个新的项,导致重复计算大量前面的项,因此总体时间复杂度是指数级的增长。 |
算法实现:
/**
* 这个代码中,generatePowerSet方法会生成一个数组的所有子集。
* 我们通过两个嵌套的循环来实现这一点。外部循环遍历2^n个可能的子集
* (因为每个元素都可以在子集中或不在子集中,所以总共有2^n个子集),
* 内部循环则根据当前子集的二进制表示来决定是否将数组中的元素添加到子集中。
* 如果二进制表示的某一位为1,那么就将对应的元素添加到子集中。
*
* @param array 数组
* @return List<List<Integer>>
*/
public static List<List<Integer>> generatePowerSet(int[] array) {
List<List<Integer>> powerSet = new ArrayList<>();
int n = array.length;
for (int i = 0; i < (1 << n); i++) {
List<Integer> subset = new ArrayList<>();
for (int j = 0; j < n; j++) {
if (((i >> j) & 1) == 1) {
subset.add(array[j]);
}
}
powerSet.add(subset);
}
return powerSet;
}
常见的空间复杂度有哪些?
算法的空间复杂度是评估算法在执行过程中所需额外存储空间的重要指标。以下是算法空间复杂度的一些常见类型:
这些空间复杂度类型可以用于评估算法在处理不同规模输入时所需的额外存储空间的大小。选择合适的算法和数据结构可以优化空间复杂度,以适应不同规模的需求。