Baeldung Pro – CS – NPI EA (类别 = Baeldung 关于计算机科学)
announcement - icon

通过超简洁的 Baeldung Pro 体验学习

>> 会员和 Baeldung Pro.

没有广告,深色模式,并免费获得 6 个月的 IntelliJ Idea Ultimate,供您入门。

1. 概述

算法复杂度是衡量算法相对于其输入大小所需的资源的一种度量。主要的两种复杂度类型是时间复杂度空间复杂度。此外,时间复杂度指的是算法执行的操作数量,而空间复杂度指的是算法消耗的内存量。此外,理解这些复杂度对于评估算法效率和增强软件系统至关重要。

在本教程中,我们将了解如何分析算法的复杂度。此外,我们还将讨论时间和空间复杂度,以及评估它们的方法。

2. 时间复杂度

算法的输入大小决定了时间复杂度。此外,它通常用大O记号表示。

2.1. 常数时间 O(1)

常数时间复杂度,也称为O(1),指的是无论输入大小如何,都花费相同时间的运算。此外,这些运算非常高效,不受较大数据集的影响。

2.2. 线性时间 O(n)

线性时间复杂度,也称为O(n),出现在算法中,该算法正好分析每个输入元素一次,例如线性搜索,虽然简单明了,但对于较大的数据集可能会变得效率低下。

2.3. 多项式时间 O(n^m)

嵌套循环导致算法中的多项式复杂度。此外,每个循环都会处理输入,随着输入大小的增长而增加执行时间。

2.4. 对数时间 O(log n)

采用分治算法的算法,例如二分搜索,具有对数复杂度。此外,这些算法会以每个步骤大幅减少问题的大小,使其适合大型数据集。

2.5. 影响时间复杂度的因素

输入数据量是影响时间复杂度的因素之一,因为它直接影响所需的运算次数
Product Sales Line Chart Graph
在此图表中,我们展示了常见时间复杂度的增长率。常数时间O(1) 无论输入大小如何都保持不变,但指数时间O(2^n) 会迅速增加,对于即使是很小的输入大小也变得不可行。最后,线性O(n)、二次O(n^2)和其他复杂度位于中间,其中对数O(log n) 对于更大的数据集而言特别高效。

3. 空间复杂度

空间复杂度衡量算法为其输入大小所需的内存量。

3.1. 常数空间复杂度

常数空间算法需要给定的内存量,而与输入大小无关。例如,交换两个变量需要O(1) 空间。

3.2. 线性空间复杂度

线性空间算法释放的内存与输入大小成比例。因此,它常见于保存或处理输入的所有元素的算法。

3.3. 对数空间复杂度

具有对数空间复杂度的算法经常出现在**递归技术中,其中递归深度决定了内存使用量。**

3.4. 二次空间复杂度

某些算法可能需要一个内存矩阵或类似的数据结构,具有二次空间复杂度,因此,**内存使用量与输入大小的平方成正比。**

4. 评估时间复杂度

评估算法的时间复杂度就是确定当输入大小增加时,算法执行所需的时间。

4.1. 分析循环

循环直接增加时间复杂度,并且是许多算法的重要组成部分。**因为每次迭代都会增加固定量的计算量,所以迭代 n 次的单个循环的时间复杂度为 O(n):**

def linear_loop(arr):
    for num in arr:
        print(num)

在此示例中,该函数遍历数组的每个元素,因为循环运行 n 次,其时间复杂度为 O(n)

此外,让我们看看嵌套循环以及如何评估它们

def quadratic_loop(arr):
    for i in arr:
        for j in arr:
            print(i, j)

在此示例中,外层循环执行 n 次,内层循环在每次迭代时执行 n 次,这导致 O(n^2) 的复杂度。

以下是一个具有 O(n^3) 复杂度的多项式时间算法的进一步示例

def cubic_loop(arr):
    for i in arr:  
        for j in arr:  
            for k in arr: 
                print(i, j, k)

在此示例中,由于三个嵌套循环每个循环迭代 n 次,总计 n × n × n = n^3,因此复杂度为 O(n^3)

总而言之,复杂度随着嵌套循环的数量增加。当两个循环都迭代 n 次时,双重循环具有 O(n^2) 复杂度。同样,三重嵌套循环给出 O(n^3)。此外,**确定嵌套层数并观察迭代次数如何随输入大小变化是识别循环相关复杂度的关键步骤。**

4.2. 评估递归调用

递归算法的复杂度可能因每次调用的问题大小如何演变而异。此外,如果输入大小减少固定量(例如,减半),则复杂度为 O(log n),例如二分搜索等算法。

def binary_search(arr, target, low, high):
    if low > high:
        return -1
    mid = (low + high)
    if arr[mid] == target:
        return mid
    elif arr[mid] < target:
        return binary_search(arr, target, mid + 1, high)  
    else:
        return binary_search(arr, target, low, mid - 1)

在此示例中,每次递归调用都会将输入大小减半。此外,对于大小为 n 的数组,最大调用次数为 log(n),给出 O(log n) 复杂度。

此外,让我们考虑其他类似的例子

def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n - 1)

在这里,阶乘函数进行 n 次递归调用,每次调用都会将问题大小减少 1。因此,这会导致 O(n) 的复杂度。

此外,诸如归并排序之类的分治算法,**分割和重组数据,其复杂度由递归关系决定,并且经常产生 O(n log n):**

def merge_sort(arr):
    if len(arr) > 1:
        mid = len(arr) 
        left = arr[:mid]
        right = arr[mid:]
        merge_sort(left)
        merge_sort(right)

在此示例中,我们将输入分成两半 log n,并以线性时间 O(n) 将它们组合起来。因此,整体复杂度等于 O(n log n)

4.3. 常数时间操作

常数时间操作的复杂度,例如简单的算术运算或数组中的单元素访问,为 O(1):

def constant_time_operations(arr):
    print(arr[0])
    print(len(arr))
    print(arr[0] + arr[1]) 

在此示例中,这些操作无论输入大小如何,都花费固定的时间。例如,访问列表的第一个条目、执行算术运算和确定数组的长度都是 O(1)

总而言之,这些操作易于识别,因为它们不依赖于输入大小。此外,**在小型操作中,它们支配了复杂度,但在大型算法中,与循环或递归相比,它们通常不重要。**

5. 评估空间复杂度

算法所需的内存量,作为输入大小的函数来衡量,即空间复杂度。

5.1. 固定和可变内存使用

常量、程序指令和静态变量是固定内存消耗的示例,它们贡献了 O(1) 复杂度,因为它们不会随输入大小改变:

def swap(a, b):
    temp = a
    a = b
    b = temp

在此示例中,swap 函数为变量 abtemp 分配了固定数量的内存。因此,空间复杂度为 O(1),因为内存使用不受输入大小的影响。

此外,数组和动态分配的结构是可变内存使用的示例,它们会随着输入大小而增加。例如,存储 n 个元素在数组中需要 O(n) 空间:

def copy_array(arr):
    copy = [0] * len(arr)
    for i in range(len(arr)):
        copy[i] = arr[i]
    return copy

在此示例中,copy_array 函数生成一个与输入列表 arr 大小相同的副本列表。此外,由于内存使用随着输入大小 n 的增加,空间复杂度为 O(n)

5.2. 分析辅助空间

辅助空间是算法所需的临时内存。此外,像归并排序这样的排序算法使用 O(n) 辅助空间,它组合了额外的数组

def merge_sort(arr):
    if len(arr) > 1:
        mid = len(arr)
        left = arr[:mid]
        right = arr[mid:]
        merge_sort(left)
        merge_sort(right)

在此示例中,归并排序利用额外的内存来存储分离的子数组(左侧和右侧)。因此,所需的总辅助空间与输入大小 n 成比例,因此空间复杂度为 O(n)

此外,让我们考虑其他排序算法

def quicksort(arr, low, high):
    if low < high:
        pivot_index = partition(arr, low, high)
        quicksort(arr, low, pivot_index - 1) 
        quicksort(arr, pivot_index + 1, high)

在此示例中,快速排序使用调用堆栈进行递归。因此,递归深度为对数 log n,辅助空间复杂度为 O(log n).

然而,快速排序和其他排序算法只需要 O(log n) 空间用于递归堆栈。此外,我们可以跟踪执行期间产生的临时结构来计算辅助空间。

5.3. 评估递归空间使用

递归算法在调用堆栈上的内存使用量与递归深度成正比。此外,例如,具有 n 层递归的递归算法需要 O(n) 堆栈空间:

def binary_search_recursive(arr, target, low, high):
    if low > high:
        return -1
    mid = (low + high)
    if arr[mid] == target:
        return mid
    elif arr[mid] < target:
        return binary_search_recursive(arr, target, mid + 1, high) 
    else:
        return binary_search_recursive(arr, target, low, mid - 1)

在此示例中,二分查找通过每次递归调用将问题大小减半。因此,最大的递归深度为 log(n),空间复杂度为 O(log n)

此外,让我们考虑其他类似的例子

def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n - 1)

在此示例中,阶乘函数执行 n 次递归调用,每次调用都会在调用堆栈上添加一层。因此,它产生 O(n) 的空间复杂度。

此外,如果递归按指数方式减小输入大小,则空间复杂度变为 O(log n)。因此,可以通过查看递归调用的深度和组织来计算所需的堆栈空间量。

6. 结论

分析算法复杂度对于开发高效系统至关重要。时间复杂度衡量执行时间的增加,而空间复杂度量化内存使用情况。

在本文中,我们讨论了时间和空间复杂度,解释了这两个概念以及查找算法的时间和空间复杂度的实际方法。

最后,掌握这些分析可以帮助我们开发成功平衡性能和资源利用率的算法。

© .