Skip to content

大 O

大 O 复杂度分析(Big-O Complexity Analysis)是计算机科学中用于描述算法效率的标准方式。它关注的是输入规模增大时,算法运行时间(或空间)的增长趋势,而不关心常数因子和低阶项。通过大 O 表示法,我们可以理解一个算法在最坏情况下的表现。

1. 什么是大 O 复杂度?

大 O 复杂度是一种数学符号,用于描述算法运行时间或空间随着输入规模增加的增长趋势。它表示的是输入大小 ( n ) 时,算法执行的最多操作数量的上界(最坏情况),忽略常数项和低阶项。

常见的复杂度有:

  • O(1): 常数时间
  • O(log n): 对数时间
  • O(n): 线性时间
  • O(n log n): 线性对数时间
  • O(n^2): 平方时间
  • O(2^n): 指数时间
  • O(n!): 阶乘时间

2. 常见的大 O 复杂度

下面是一些常见的大 O 复杂度及其描述:

复杂度类型描述示例算法
O(1)常数时间复杂度,执行时间不随输入规模增加而增加。访问数组元素、字典查找
O(log n)对数时间复杂度,随着输入规模增加,执行时间增加的速度较慢。二分查找、平衡二叉树查找
O(n)线性时间复杂度,执行时间随着输入规模的增加而线性增加。简单循环遍历数组或链表
O(n log n)线性对数时间复杂度,大多数高效排序算法的时间复杂度。快速排序、归并排序
O(n^2)平方时间复杂度,通常出现在双重嵌套循环中。冒泡排序、插入排序、选择排序
O(2^n)指数时间复杂度,算法的时间增长速度极快,通常出现于暴力解法。汉诺塔问题、背包问题的暴力解法
O(n!)阶乘时间复杂度,通常出现在穷举所有排列的算法中。排列问题、旅行商问题

3. 常见算法的时间复杂度分析

O(1) - 常数时间复杂度

无论输入大小如何,算法的执行时间都是常量。常见于直接返回值或者访问固定位置的数据。

示例

go
func getFirstElement(arr []int) int {
    return arr[0]  // 访问第一个元素
}

时间复杂度:O(1),因为无论 arr 的长度如何,执行时间都是常量。

O(n) - 线性时间复杂度

算法的执行时间与输入大小 n 成正比。通常出现在遍历数组或链表时。

示例

go
func linearSearch(arr []int, target int) int {
    for i := 0; i < len(arr); i++ {
        if arr[i] == target {
            return i
        }
    }
    return -1
}

时间复杂度:O(n),因为在最坏情况下,需要遍历整个数组。

O(log n) - 对数时间复杂度

对数时间复杂度通常出现在二分查找等算法中,每次操作都能将问题规模减少一半。

示例

go
func binarySearch(arr []int, target int) int {
    left, right := 0, len(arr)-1
    for left <= right {
        mid := (left + right) / 2
        if arr[mid] == target {
            return mid
        } else if arr[mid] < target {
            left = mid + 1
        } else {
            right = mid - 1
        }
    }
    return -1
}

时间复杂度:O(log n),因为每次都会将搜索区间减半。

O(n log n) - 线性对数时间复杂度

这是许多高效排序算法的时间复杂度,如快速排序和归并排序。

示例:快速排序

go
func quickSort(arr []int) []int {
    if len(arr) <= 1 {
        return arr
    }
    pivot := arr[0]
    less, greater := []int{}, []int{}
    for _, num := range arr[1:] {
        if num <= pivot {
            less = append(less, num)
        } else {
            greater = append(greater, num)
        }
    }
    return append(append(quickSort(less), pivot), quickSort(greater)...)
}

时间复杂度:O(n log n),最坏情况下是 O(n^2)(当基准选得不好时),但平均情况下是 O(n log n)

O(n^2) - 平方时间复杂度

平方时间复杂度通常出现在双重嵌套循环中,例如冒泡排序、插入排序等。

示例:冒泡排序

go
func bubbleSort(arr []int) []int {
    for i := 0; i < len(arr); i++ {
        for j := 0; j < len(arr)-1-i; j++ {
            if arr[j] > arr[j+1] {
                arr[j], arr[j+1] = arr[j+1], arr[j]
            }
        }
    }
    return arr
}

时间复杂度:O(n^2),因为有两个嵌套循环,内外循环各遍历数组。

O(2^n) - 指数时间复杂度

指数时间复杂度通常出现在暴力求解算法中。随着输入规模的增加,算法的执行时间会呈指数级增长。

示例:计算斐波那契数列的暴力递归

go
func fibonacci(n int) int {
    if n <= 1 {
        return n
    }
    return fibonacci(n-1) + fibonacci(n-2)
}

时间复杂度:O(2^n),因为每次调用会再调用两个递归,导致重复计算。

O(n!) - 阶乘时间复杂度

阶乘时间复杂度通常出现在求解排列组合问题时,例如旅行商问题的暴力解法。

示例:全排列

go
func permute(nums []int) [][]int {
    var result [][]int
    var backtrack func(path []int, remaining []int)
    backtrack = func(path []int, remaining []int) {
        if len(remaining) == 0 {
            result = append(result, append([]int(nil), path...))
            return
        }
        for i := 0; i < len(remaining); i++ {
            backtrack(append(path, remaining[i]), append(append([]int(nil), remaining[:i]...), remaining[i+1:]...))
        }
    }
    backtrack([]int{}, nums)
    return result
}

时间复杂度:O(n!),因为算法需要生成所有的排列。


4. 如何进行大 O 复杂度分析

  1. 找到主要的操作步骤:通常是循环、递归或频繁调用的函数。
  2. 计算操作次数:分析循环或递归的次数,推导时间复杂度。
  3. 忽略低阶项:只关心增长最快的项,比如 O(n^2 + n) 就是 O(n^2)
  4. 考虑最坏情况:分析算法在最坏输入下的行为。

总结

  • 大 O 复杂度分析帮助我们了解算法的性能,尤其是随着输入规模的增加,算法的运行时间或空间使用情况。
  • 常见的大 O 复杂度包括 O(1)、O(n)、O(log n)、O(n log n)、O(n^2) 等,选择适当的算法可以显著提高程序效率。
  • 了解和分析算法复杂度对于设计高效算法、优化代码和提高系统性能至关重要。