学习算法复杂度

算法复杂度学习文档

目录

  1. 什么是算法复杂度
  2. 时间复杂度
  3. 空间复杂度
  4. 常见复杂度对比
  5. Java代码示例
  6. 如何分析复杂度
  7. 实用技巧

什么是算法复杂度

算法复杂度用来衡量算法执行所需资源(时间和空间)随输入规模增长的变化趋势。

使用大O符号(Big O Notation)表示,描述的是最坏情况下的上界。

核心原则:只关注增长最快的项,忽略常数和低阶项。


时间复杂度

O(1) — 常数时间

操作次数固定,与输入规模无关。

// 直接访问数组元素
static int getFirst(int[] arr) {
    return arr[0];  // 无论数组多大,只执行一次
}

典型场景:数组索引访问、哈希表查找、简单赋值


O(log n) — 对数时间

数据规模每次按固定比例缩减(通常是减半)。

// 二分查找:每次范围减半
static int binarySearch(int[] arr, int target) {
    int left = 0, right = arr.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (arr[mid] == target) return mid;
        else if (arr[mid] < target) left = mid + 1;
        else right = mid - 1;  // 范围减半
    }
    return -1;
}

判断方法:循环中数据规模每次除以某个常数(如2)。

假设 n=1024,只需要约 log₂(1024)=10 次就能找到。

典型场景:二分查找、平衡二叉树操作


O(n) — 线性时间

执行次数与输入规模成正比。

// 查找最大值:遍历所有元素
static int findMax(int[] arr) {
    int max = arr[0];
    for (int x : arr) {  // 循环 n 次
        if (x > max) {
            max = x;
        }
    }
    return max;
}

判断方法:单层循环,遍历所有 n 个元素。

典型场景:遍历数组、线性查找


O(n log n) — 线性对数时间

分治算法的典型复杂度,每层 O(n),共 log n 层。

// 归并排序:分治 + 合并
static void mergeSort(int[] arr, int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        mergeSort(arr, left, mid);       // 递归左半:T(n/2)
        mergeSort(arr, mid + 1, right);  // 递归右半:T(n/2)
        merge(arr, left, mid, right);    // 合并:O(n)
    }
}

// 递归式:T(n) = 2T(n/2) + O(n)
// 用主定理或递归树可得 O(n log n)

判断方法:分治算法,将问题分成若干子问题,然后合并结果。

典型场景:快速排序、归并排序、堆排序


O(n²) — 平方时间

双重嵌套循环,外层 n × 内层 n。

// 冒泡排序:双重循环
static void bubbleSort(int[] arr) {
    int n = arr.length;
    for (int i = 0; i < n; i++) {          // 外层 n 次
        for (int j = 0; j < n - 1; j++) {  // 内层 n 次
            if (arr[j] > arr[j + 1]) {
                // 交换元素
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

判断方法:嵌套循环,层数决定幂次。

典型场景:冒泡排序、选择排序、插入排序、双重循环遍历


O(n³) — 立方时间

三重嵌套循环。

// 三重循环示例
static void tripleLoop(int n) {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            for (int k = 0; k < n; k++) {
                // 执行 n³ 次
            }
        }
    }
}

典型场景:Floyd-Warshall算法、三重循环暴力求解


O(2ⁿ) — 指数时间

递归树呈指数增长,每个节点产生多个子节点。

// 斐波那契数列:递归树指数增长
static int fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);  // 每次分裂成2个
}

判断方法:递归产生多个相同规模的子问题。

典型场景:递归求解子集、汉诺塔、未优化的斐波那契


O(n!) — 阶乘时间

全排列,每个位置有 n, n-1, n-2... 种选择。

// 全排列
static void permutation(String str, String prefix) {
    if (str.length() == 0) {
        System.out.println(prefix);
        return;
    }
    for (int i = 0; i < str.length(); i++) {
        String rem = str.substring(0, i) + str.substring(i + 1);
        permutation(rem, prefix + str.charAt(i));
    }
}

典型场景:全排列、旅行商问题(暴力求解)


空间复杂度

空间复杂度衡量算法执行过程中额外使用的内存空间。

复杂度 典型场景
O(1) 原地排序、固定变量
O(log n) 递归调用栈(二分、快排)
O(n) 辅助数组、线性递归
O(n²) 二维数组、邻接矩阵
// O(1) 空间:原地排序
static void swap(int[] arr, int i, int j) {
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

// O(n) 空间:辅助数组
static int[] copyArray(int[] arr) {
    int[] copy = new int[arr.length];  // 额外分配 n 个空间
    for (int i = 0; i < arr.length; i++) {
        copy[i] = arr[i];
    }
    return copy;
}

// O(log n) 空间:递归调用栈
static int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);  // 递归深度 log n 或 n
}

常见复杂度对比

当 n = 1000 时:

复杂度 操作次数 可接受性
O(1) 1 极佳
O(log n) ~10 极佳
O(n) 1,000 良好
O(n log n) ~10,000 良好
O(n²) 1,000,000 小规模可用
O(n³) 1,000,000,000 仅小规模
O(2ⁿ) 约 10³⁰¹ 不可行
O(n!) 远超宇宙原子数 不可行

Java代码示例

完整的可运行代码见 AlgorithmComplexityDemo.java

运行方法

javac AlgorithmComplexityDemo.java
java AlgorithmComplexityDemo

输出示例

========== 算法复杂度演示 ==========

--- O(1) 常数时间 ---
数组第一个元素: 5
操作次数: 1

--- O(n) 线性时间 ---
数组最大值: 9
遍历次数: 6

--- O(log n) 对数时间 ---
二分查找 7 的索引: 3
查找次数: 2

--- O(n²) 平方时间 ---
冒泡排序结果: 1 2 3 5 8 9
比较次数: 36

--- O(n log n) 线性对数时间 ---
归并排序结果: 1 2 3 5 8 9
递归层数: 3

--- O(2ⁿ) 指数时间 ---
斐波那契第10项: 55
递归调用次数: 177

========== 复杂度对比表 ==========
数据规模 n=1000 时的操作次数:
O(1):        1
O(log n):    10
O(n):        1,000
O(n log n):  10,000
O(n²):       1,000,000
O(2ⁿ):       约 10^301 (不可行)

如何分析复杂度

步骤

  1. 找循环 — 循环是复杂度的主要来源
  2. 看嵌套 — 嵌套层数决定幂次
  3. 看变化 — 变量是线性增长还是对数增长
  4. 取最大 — 多个部分取复杂度最高的项

多个独立循环

void example(int[] arr) {
    for (int x : arr) {        // O(n)
        System.out.println(x);
    }
    for (int x : arr) {        // O(n)
        System.out.println(x);
    }
    // 总复杂度 = O(n) + O(n) = O(2n) = O(n)
    // 常数省略,仍是 O(n)
}

不同规模参数

void process(int m, int n) {
    for (int i = 0; i < m; i++) {      // m 次
        for (int j = 0; j < n; j++) {  // n 次
            System.out.println(i, j);
        }
    }
    // 复杂度:O(m × n)
    // 不能简写,因为 m 和 n 是独立的
}

实用技巧

快速判断口诀

代码特征 复杂度
无循环,直接计算 O(1)
单层循环,遍历 n O(n)
两层嵌套循环 O(n²)
三层嵌套循环 O(n³)
循环变量 i *= 2 或 i //= 2 O(log n)
递归分成2份 + 合并 O(n log n)
递归产生多个子问题 可能是指数级

优化方向

  • O(n²) → O(n log n):使用分治或更优算法
  • O(n) → O(log n):使用哈希表或二分查找
  • O(2ⁿ) → O(n²):使用动态规划

总结

掌握算法复杂度的分析能力,可以帮助我们:

  1. 选择合适的数据结构 — 不同操作的复杂度不同
  2. 优化算法性能 — 识别瓶颈,针对性优化
  3. 预估系统容量 — 根据数据规模选择合适的算法
  4. 面试准备 — 算法面试的核心考察点

记住:先保证正确性,再考虑优化。过早优化是万恶之源。

posted @ 2026-05-19 10:57  SeiunSky  阅读(21)  评论(0)    收藏  举报