快速理解代码编程的时间复杂度和空间复杂度

一、什么是时间复杂度和空间复杂度?

时间复杂度和空间复杂度是衡量算法性能的两个指标。

时间复杂度是指执行算法所需要的计算工作量。它通常表示为输入大小的函数,用大O符号(例如O(n), O(n^2))表示。大O符号描述了算法在最坏情况下的运行时间增长趋势,不包括系数和低阶项。

空间复杂度是指算法执行过程中需要的最大存储空间。它也用大O符号表示,并且也是关于输入大小的函数。空间复杂度不仅包括变量和数据结构占用的空间,还包括调用栈的空间(如果算法是递归的)。

什么是调用栈?

调用栈(Call Stack),也称为执行栈,是计算机程序执行函数调用时用来存储有关每次函数调用的信息的数据结构。每当程序执行到一个函数时,它会将该函数的返回地址和必要的参数放到调用栈上,这个过程称为“入栈”。函数执行完成后,会从栈上“出栈”,并返回控制权到函数被调用的地方。

调用栈是有容量限制的,如果递归或函数调用过深,超出了栈的容量,就会发生“栈溢出”错误。这是因为每次函数调用都会占用一定的栈空间,当空间被耗尽时,程序无法继续执行,通常这会导致程序异常终止。

二、如何计算复杂度

计算这两种复杂度通常需要:

1. 分析算法结构:识别算法中的基本操作,并估计这些操作的执行次数。
2. 计算基本操作的总和:对所有基本操作的执行次数求和,得到总的执行次数。
3. 考虑最坏情况:基于最坏情况下输入的特点,估计算法的时间和空间需求。
4. 使用大O符号简化:忽略常数因子和较小阶的项,只保留最大阶的项。

这是一种高层次的概述。对于具体的算法,这些计算可能会变得更加复杂,需要对算法的每一步都有深入理解。

三、举例说明

例子 1: 遍历数组

def print_array(arr):
    for element in arr:
        print(element)
  • 时间复杂度: O(n) - n 是数组 arr 的长度,因为需要遍历数组中的每个元素。
  • 空间复杂度: O(1) - 使用的空间不随输入大小变化,只用了一个额外的变量 element

例子 2: 矩阵相加

def add_matrices(a, b):
    num_rows, num_cols = len(a), len(a[0])
    result = [[0] * num_cols for _ in range(num_rows)]
    for i in range(num_rows):
        for j in range(num_cols):
            result[i][j] = a[i][j] + b[i][j]
    return result
  • 时间复杂度: O(m*n) - m 是行数,n 是列数。需要遍历两个矩阵的每个元素。
  • 空间复杂度: O(m*n) - 需要创建一个和输入矩阵大小相同的新矩阵来存储结果。

例子 3: 斐波那契数列(递归)

def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)
  • 时间复杂度: O(2^n) - 每一层递归会调用两次函数,导致呈指数级增长。
  • 空间复杂度: O(n) - 递归深度直接取决于 n,因此调用栈的大小是 n

例子4:快速排序

def quicksort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quicksort(left) + middle + quicksort(right)
  • 时间复杂度: O(n*logn) - 快速排序的平均情况下时间复杂度。在每次分割中,算法选择一个“枢轴”元素,然后将数组分成小于和大于这个枢轴的两部分。这个分割操作平均需要线性时间(O(n)),而递归树的深度大约是log(n)。
  • 空间复杂度: O(logn) - 递归栈的深度,在最好的情况下是log(n)。

在算法的时间复杂度分析中提到的 log(n),除非特别指定,通常底数是 2。这是因为算法分析中的很多问题,如二分搜索、二叉树操作等,都涉及到将数据集分成两半的操作,这些操作的次数可以用以 2 为底的对数来表示。

在快速排序的每次迭代中,我们选择一个枢轴元素,并根据它对数组进行分区:将小于枢轴的元素移动到枢轴的左边,将大于枢轴的元素移动到右边。这个分区操作需要遍历整个数组一次,因此其时间复杂度为 O(n)。

平均情况下,枢轴将数组分成大致相等的两部分,这样递归地应用快速排序到每部分又会产生两个更小的部分,每个部分的大小大约是原始数组大小的一半。因此,快速排序的平均时间复杂度是 O(n*logn),其中 n 是数组大小,logn 是因为每次分区都将问题规模减半,所以需要 logn 层递归(以2为底)。

扫描二维码关注公众号,回复: 17130546 查看本文章

例子5:冒泡排序

def bubble_sort(arr):
    n = len(arr)
    for i in range(n):
        for j in range(0, n-i-1):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
  • 时间复杂度: O(n^2) - 对于每个元素,冒泡排序都会遍历剩余的元素来进行比较和交换,因此需要平方级的时间。
  • 空间复杂度: O(1) - 冒泡排序是原地排序,不需要额外的存储空间。

冒泡排序的时间复杂度通常表示为 O(n^2),这是因为在大O记号中,我们关注的是算法运行时间随输入规模增长的最高阶项,常数因子和低阶项在这个记号中是不考虑的。虽然精确的比较和交换次数是(1/2)*n^2-(1/2)*n,但是在大O记号中,我们省略了这个 1/2 和 −(1/2)*n,因为它们对于大规模输入的增长趋势影响很小。所以,我们说冒泡排序的时间复杂度是 O(n^2)。

通过这些例子,我们可以看到,分析时间和空间复杂度通常需要考虑算法中操作的数量(对于时间复杂度)以及内存使用(对于空间复杂度),并且通常是关于输入大小的函数。

猜你喜欢

转载自blog.csdn.net/knightsinghua/article/details/134569372