为什么你写的代码运行慢?
分析程序运行的快慢是一项复杂的工作,主要考虑到运行程序的机器,代码的复杂度,以及其他因素等等。
脱离实际运行环境,计算机有一套科学的分析程序运行快慢的方法叫做:复杂度分析,接下来一起学习下吧
为什么需要分析复杂度?
当我们编写算法时,我们需要考虑很多因素,如算法的正确性、易读性和可维护性等。然而,我们还需要考虑算法在执行时所需的资源,如时间和空间。这就是为什么需要复杂度分析的原因。
复杂度分析是一种方法,用于评估算法的执行效率和资源消耗。它帮助我们了解算法的性能和优化的潜力。在编写和优化算法时,复杂度分析是不可或缺的。
例如,如果我们正在编写一个排序算法,我们可以使用复杂度分析来比较不同算法的执行效率。我们可以使用它来比较“冒泡排序”和“快速排序”的性能。我们可以使用它来确定哪个算法更适合我们的用例。
另一个例子是在内存受限的环境中编写算法。在这种情况下,我们需要考虑算法所需的空间。我们可以使用空间复杂度分析来确定算法在运行时所需的内存量。这可以帮助我们优化我们的算法,以便在受限的内存环境中运行。
复杂度分析是一种非常有用的工具,可以帮助我们了解算法的性能和优化的潜力。
在编写和优化算法时,复杂度分析是不可或缺的。
什么是时间复杂度分析?
时间复杂度分析是一种用于衡量算法执行时间的方法。它帮助我们确定算法在执行时所需的时间资源。时间复杂度是指算法所需的操作次数,随着输入大小的增加而增加的速率。
强调的是一个相对于输入数据规模大小,所需时间的趋势
在进行时间复杂度分析时,我们通常关注算法的最坏情况。这是因为算法的最坏情况给出了算法的最大执行时间。我们可以使用以下步骤来确定算法的时间复杂度:
- 确定算法的基本操作,例如赋值、比较、循环等。
- 确定每个基本操作的执行次数。
- 将每个基本操作的执行次数相加,以确定算法的总操作次数。
- 确定算法的最大执行次数,并将其表示为时间复杂度。
例如,考虑以下代码段:
function sum(n) {
let total = 0;
for (let i = 1; i <= n; i++) {
total += i;
}
return total;
}
在这个例子中,我们的基本操作是加法和循环。在每次循环中,我们执行一次加法操作。因此,我们的基本操作执行次数为 n。算法的总操作次数为 n,因为我们执行 n 次基本操作。因此,该算法的时间复杂度为 O(n)。
在实际情况中,我们经常需要比较不同算法的时间复杂度以确定哪个算法更适合我们的用例
一些常见的时间复杂度案例
现在我们来看一些常见的时间复杂度实例,并分析它们的执行效率。
- O(1):常数复杂度
常数复杂度表示算法的执行时间不随输入大小而改变。例如,以下代码段的时间复杂度为 O(1):
function multiply(a, b) {
return a * b;
}
这个算法的执行时间不受输入 a 和 b 的大小的影响,因此它具有常数复杂度。
- O(log n):对数复杂度
对数复杂度表示算法的执行时间随着输入大小的增加而增加,但增长速度很慢。例如,以下代码段的时间复杂度为 O(log n):
function binarySearch(arr, x) {
let start = 0;
let end = arr.length - 1;
while (start <= end) {
let mid = Math.floor((start + end) / 2);
if (arr[mid] === x) {
return mid;
} else if (arr[mid] < x) {
start = mid + 1;
} else {
end = mid - 1;
}
}
return -1;
}
这个算法使用二分查找来查找给定元素 x 是否在数组 arr 中。在每次迭代中,它将数组的一半舍入到最接近的整数。因此,它的时间复杂度为 O(log n)。
- O(n):线性复杂度
线性复杂度表示算法的执行时间随着输入大小的增加而线性增加。例如,以下代码段的时间复杂度为 O(n):
function findMax(arr) {
let max = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
这个算法使用一个循环来找到给定数组 arr 中的最大值。在每次迭代中,它执行一次比较操作。因此,它的时间复杂度为 O(n)。
- O(n log n):线性对数复杂度
线性对数复杂度表示算法的执行时间随着输入大小的增加而增加,但增长速度比 O(n) 慢。例如,以下代码段的时间复杂度为 O(n log n):
function mergeSort(arr) {
if (arr.length <= 1) {
return arr;
}
const mid = Math.floor(arr.length / 2);
const left = arr.slice(0, mid);
const right = arr.slice(mid);
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right) {
let result = [];
while (left.length && right.length) {
if (left[0] < right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
return result.concat(left, right);
}
这个算法使用归并排序来对给定数组 arr 进行排序。在每次迭代中,它将数组分成两半,并对每半进行递归排序。然后,它将两个已排序的半部分合并为一个已排序的数组。因此,它的时间复杂度为 O(n log n)。
- O(n²):平方复杂度
平方复杂度表示算法的执行时间随着输入大小的增加而平方增加。例如,以下代码段的时间复杂度为 O(n²):
function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
let tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
return arr;
}
这个算法使用冒泡排序对给定数组 arr 进行排序。在每次迭代中,它比较相邻的元素并交换它们,直到数组被完全排序。因此,它的时间复杂度为 O(n²)。
当我们选择算法时,我们需要考虑它的时间复杂度和输入的大小。
如果输入很小,那么一个平方复杂度的算法可能是最好的选择,因为简单直观。
但是,如果输入很大,就要考虑性能优化了,我们可能需要选择具有更快时间复杂度的算法,例如对数或线性对数复杂度。
空间复杂度
除了时间复杂度,空间复杂度也是衡量算法效率的一个指标。
空间复杂度表示算法所需的内存空间随着输入大小的增加而增加的速率。
空间复杂度也是相对于输入值规模变大以后,一种空间相对的变化
在进行空间复杂度分析时,我们通常关注算法所需的额外空间,而不是算法本身所使用的输入空间。我们可以使用以下步骤来确定算法的空间复杂度:
- 确定算法所需的额外空间,例如变量、数组、堆栈等。
- 确定每个额外空间的大小。
- 将每个额外空间的大小相加,以确定算法的总空间需求。
- 确定算法的最大空间需求,并将其表示为空间复杂度。
例如,考虑以下代码段:
function fibonacci(n) {
if (n <= 1) {
return n;
}
let prev = 0;
let curr = 1;
for (let i = 2; i <= n; i++) {
let next = prev + curr;
prev = curr;
curr = next;
}
return curr;
}
这个算法使用迭代的方式来计算斐波那契数列中第 n 个数。在每次迭代中,它使用 prev 和 curr 两个变量来计算下一个数。因此,它的额外空间需求为常数级别,即 O(1)。因此,它的空间复杂度为 O(1)。
另一个例子是归并排序算法:
function mergeSort(arr) {
if (arr.length <= 1) {
return arr;
}
const mid = Math.floor(arr.length / 2);
const left = arr.slice(0, mid);
const right = arr.slice(mid);
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right) {
// 所需空间
let result = [];
while (left.length && right.length) {
if (left[0] < right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
return result.concat(left, right);
}
这个算法使用递归的方式将一个数组分成两半,对每一半进行递归排序,然后将两个已排序的半部分合并为一个已排序的数组。在每个递归调用中,它需要额外的数组来存储左半部分和右半部分的排序结果。因此,它的额外空间需求为 O(n),其中 n 是输入数组的大小。因此,它的空间复杂度为 O(n)。
在进行算法分析时,我们需要综合考虑时间复杂度和空间复杂度。在某些情况下,我们可能需要权衡时间和空间的复杂度,以选择最适合的算法。
也就是说如果业务对于时间要求很高可以牺牲空间来解决问题,业务对于空间要求很高可以牺牲时间来解决问题。