Day1 时间复杂度

一 概念

在 C++ 中,时间复杂度是衡量算法运行时间随输入规模增长的趋势的关键指标,用于评估算法的效率。它通过 大 O 表示法(Big O Notation) 描述,关注的是输入规模 n 趋近于无穷大时,算法时间增长的主导因素(忽略常数和低阶项)。

  • 输入规模(n):算法处理的数据量(如数组长度、链表节点数、矩阵维度等)。
  • 大 O 表示法:表示算法时间复杂度的上界,例如 O(n) 表示时间与输入规模 n 成线性关系。
  • 核心原则:只保留最高阶项,忽略常数因子和低阶项(如 O(2n + 3) 简化为 O(n)O(n² + n) 简化为 O(n²))。

二 常见时间复杂度类型 

1.O (1):常数时间

无论输入规模多大,算法执行时间恒定(与 n 无关)。

典型场景:

  • 数组 /vector 的随机访问(通过下标 [i])。
  • 哈希表(unordered_map)的插入、查找(平均情况)。

示例:访问数组元素 

#include <vector>

int get_element(const std::vector<int>& vec, int index) {
    return vec[index];  // 随机访问,时间复杂度 O(1)
}

2. O (n):线性时间

时间与输入规模 n 成正比,需逐个处理每个元素。
典型场景

  • 线性枚举(遍历数组、链表等)。
  • 未排序数组的顺序查找。

示例:遍历 vector 求和

#include <vector>

int sum_vector(const std::vector<int>& vec) {
    int sum = 0;
    for (int num : vec) {  // 遍历所有元素,时间复杂度 O(n)
        sum += num;
    }
    return sum;
}

3. O (logn):对数时间

时间随输入规模的对数增长,通常出现在分治算法中(如二分查找)。
典型场景

  • 有序数组的二分查找(std::lower_bound)。
  • 平衡二叉搜索树(如 std::setstd::map)的插入、查找。

示例:二分查找(C++ 标准库实现)

#include <vector>
#include <algorithm>  // for std::lower_bound

// 查找目标值的位置(返回迭代器)
auto binary_search(const std::vector<int>& vec, int target) {
    return std::lower_bound(vec.begin(), vec.end(), target);
    // 时间复杂度 O(logn)(数组已排序)
}

4. O (nlogn):线性对数时间

时间增长趋势为 n × logn,常见于高效的排序算法。
典型场景

  • 快速排序、归并排序(平均情况)。
  • std::sort(C++ 标准库排序,基于快速排序 / 堆排序)。

示例:使用 std::sort 排序

#include <vector>
#include <algorithm>

void sort_vector(std::vector<int>& vec) {
    std::sort(vec.begin(), vec.end());  // 平均时间复杂度 O(nlogn)
}

5.O (n²):平方时间

时间与输入规模的平方成正比,常见于双重循环的暴力算法。
典型场景

  • 冒泡排序、选择排序(最坏 / 平均情况)。
  • 二维数组的遍历(如矩阵元素逐个处理)。

示例:冒泡排序

#include <vector>

void bubble_sort(std::vector<int>& vec) {
    int n = vec.size();
    for (int i = 0; i < n - 1; ++i) {
        for (int j = 0; j < n - i - 1; ++j) {  // 双重循环,时间复杂度 O(n²)
            if (vec[j] > vec[j + 1]) {
                std::swap(vec[j], vec[j + 1]);
            }
        }
    }
}

6. 其他复杂度

  • O(2ⁿ):指数时间(如斐波那契数列的递归实现,存在大量重复计算)。
  • O(n!):阶乘时间(如全排列生成)。

三、时间复杂度的分析方法 

1. 单循环:O (n)

单个循环遍历 n 次,时间复杂度为 O(n)

for (int i = 0; i < n; ++i) {
    // 操作(时间复杂度 O(1))
}
// 总时间复杂度:O(n)

2. 双重循环:O (n²)

外层循环 n 次,内层循环 n 次,总次数为 n × n = n²

for (int i = 0; i < n; ++i) {
    for (int j = 0; j < n; ++j) {
        // 操作(O(1))
    }
}
// 总时间复杂度:O(n²)

当外层循环到第 i 次时,内层循环 i 次,总次数为 (1 + n)n / 2 = (n + n^2) / 2次。

for (int i = 0; i < n; ++i) {
    for (int j = 0; j <= i; ++j) {
        //操作(O(1))
    }
}
/*  i  0  1  2  3  ...  n-1
    j  1  1  2  3  ...  n
    
    总操作次数为等差数列:(1 + n)n / 2 = (n + n^2) / 2
    只保留最高阶项,忽略常数因子和低阶项,故时间复杂度O(n^2)
*/

3. 对数循环:O (logn)

每次循环将问题规模减半(如二分查找)。

int i = 1;
while (i < n) {
    i *= 2;  // 每次规模翻倍
}
// 循环次数为 log₂n,时间复杂度 O(logn)
int i = n * n;
while (i != 1) {
    i /= 2; //每次规模减少为原来的一半
}

/*
t(操作次数) 0       1           2            3     ...
i          n^2   n^2 / 2     n^2 / 4      n^2 / 8  ...

由上面规律可得:i = n^2 / 2^t
将该表达式与 i = 1 联立
得 t = 2log2(n)
所以时间复杂度为 O(log2(n)),记为 O(logn)
*/

4. 递归的时间复杂度

递归的时间复杂度需结合递归深度每层操作次数
示例:斐波那契数列的递归实现(低效版)

int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);  // 每次递归分裂为 2 个子问题
}
// 时间复杂度:O(2ⁿ)(存在大量重复计算)

四、时间复杂度的优化策略

1. 用哈希表优化查找(O (n) → O (1))

将线性查找(O (n))替换为哈希表(unordered_map)的键值查找(O (1)),以空间换时间。

示例:两数之和。在数组中找到两个元素值等于target,返回这两个元素组成的数组。

#include <vector>
#include <unordered_map>

std::vector<int> two_sum(const std::vector<int>& nums, int target) {
    std::unordered_map<int, int> hash;  // 哈希表存储值→索引
    for (int i = 0; i < nums.size(); ++i) {
        int complement = target - nums[i];
        if (hash.count(complement)) {  // 查找时间 O(1)
            return {hash[complement], i};
        }
        hash[nums[i]] = i;  // 插入时间 O(1)
    }
    return {};
}
// 原暴力解法时间复杂度 O(n²),优化后 O(n)

2. 用排序 + 二分查找优化(O (n²) → O (nlogn))

将双重循环的暴力匹配替换为排序后二分查找。
示例:查找数组中是否存在两数之和为目标值

#include <vector>
#include <algorithm>

bool has_two_sum(std::vector<int>& nums, int target) {
    std::sort(nums.begin(), nums.end());  // 排序 O(nlogn)
    for (int num : nums) {
        int complement = target - num;
        // 二分查找 complement,时间 O(logn)
        if (std::binary_search(nums.begin(), nums.end(), complement)) {
            return true;
        }
    }
    return false;
}
// 原暴力解法 O(n²),优化后 O(nlogn)

3. 避免重复计算(O (2ⁿ) → O (n))

通过动态规划或记忆化搜索,消除递归中的重复子问题。
示例:斐波那契数列的动态规划实现

#include <vector>

int fib(int n) {
    if (n <= 1) return n;
    std::vector<int> dp(n + 1);  // 存储已计算的结果
    dp[0] = 0;
    dp[1] = 1;
    for (int i = 2; i <= n; ++i) {
        dp[i] = dp[i - 1] + dp[i - 2];  // 避免重复计算,时间 O(n)
    }
    return dp[n];
}
// 原递归解法 O(2ⁿ),优化后 O(n)

五 总结

时间复杂度反映算法运行时间随输入规模增长的趋势,不同复杂度的增长速度从低到高排列如下:

O(1) < O(logn) < O(n) < O(nlogn) < O(n²)

时间复杂度是评估算法效率的核心指标。在 C++ 中,通过分析循环嵌套、递归深度或操作次数,可以确定算法的时间复杂度。实际编码时,应优先选择低时间复杂度的算法(如 O (nlogn) 优于 O (n²)),并利用哈希表、排序 + 二分等优化手段降低复杂度。同时,结合数据结构的特性(如unordered_map的 O (1) 查找),可以显著提升程序性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值