时间复杂度和空间复杂度计算+尾递归

本文详细解析了算法的时间复杂度和空间复杂度的概念及其计算规则,并通过具体实例对比了递归和迭代的不同表现形式,如二分查找、斐波那契数列及尾递归的应用与优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、时间复杂度

(1)定义:
时间复杂度实际就是一个函数,该函数计算的是执行基本操作的次数。
注意:时间复杂度表示的是次数而不是时间。

一个算法运算结果分三种情况
最坏情况:任意输入规模的最大运行次数(上界)。
平均情况:任意输入规模的期望运行次数。
最好情况:任意输入规模的最小运行次数,通常最好情况不会出现(下界)。

而时间复杂度采用的是最坏的情况,因为:
①一个算法的最坏情况的运行时间是在任意输入下的运行时间上界
②对于某些算法,最坏的情况出现的较为频繁
③大体上看,平均情况与最坏情况一样差
因此:一般情况下使用O渐进表示法来计算算法的时间复杂度。

时间复杂度之大O渐进表示法
一个算法语句总的执行次数是关于问题规模N的某个函数,记为f(N),N称为问题的规模。语句总的执行次数记为T(N),当N不断变化时,T(N)也在变化,算法执行次数的增长速率和f(N)的增长速率相同。则有T(N) =O(f(N)),称O(f(n))为时间复杂度的O渐进表法。

(2)计算规则:
①用常数1取代运行时间中的所有加法常数
②在修改后的运行次数函数中,只保留最高阶项
③如果最高阶项系数存在且不是1,则去除与这个项相乘的常数
例如:O(2n^2 + 2*n + 1) = n^2
根据规则②只保留最高阶项2n^2,再根据规则③去掉系数2,最后结果是n^2。

递归算法的时间复杂度:递归总次数 * 每次递归次数

常见的算法时间复杂度由小到大依次为:
Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n^2)<Ο(n^3)<…<Ο(2^n)<Ο(n!)

这里写图片描述


二、空间复杂度

(1)定义:
不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数,与问题的规模没有关系(简单理解就是算法执行时创建的变量(包括临时变量)个数)

(2)计算规则
①忽略常数,用O(1)表示
②递归算法的空间复杂度=递归深度N*每次递归所要的辅助空间
③对于单线程来说,递归有运行时堆栈,求的是递归最深的那一次压栈所耗费的空间的个数,因为递归最深的那一次所耗费的空间足以容纳它所有递归过程。递归是要返回上一层的,所以它所需要的空间不是一直累加起来的 。


栗子1:求下面代码的空间复杂度

int Sum(int N)
{
    int count = 0;
    for(int i = 1; i <= N; ++i)
        count += i;
    return count;
}

空间复杂度:O(n)=1 这里N是个常数。


栗子2:

void fun(int n)
{
    int arr[10];
    if(n<0)
    {
        return 1;
    }
    else
    {
        return fun(n-1);
    }
}

空间复杂度:O(n*10)=O(n)
每次调用fun函数就会创建一个10个元素的整型数组,调用次数为n


三、实战分析
1.二分查找

//递归实现>
int binary_search(int *arr, int left, int right, int key)
{
    if (left <= right)
    {
        int mid = left + ((right - left) >> 1);
        if (key > arr[mid])
        {
            return binary_search(arr, mid+1, right, key);
        }
        else if (key < arr[mid])
        {
            return binary_search(arr, left, mid-1, key);
        }
        else
        {
            return mid;
        }
    }
    else
    {
        return -1;
    }
}

分析:
在这里查找我们按照最坏的情况来计算
①第一次在长度为 N/(2^0) 的数组中查找元素;
②第二次在长度为 N/(2^1) 的数组中查找元素;
③第三次在长度为 N/(2^2) 的数组中查找元素;
………
@第 n次在长度为 N/(2^n) 的数组中查找元素;
所以递归的次数 n = log2(N)
时间复杂度:O( log2(N) )
空间复杂度:O( log2(N) )


//迭代法>
int binary_search(int *arr, int lenth, int key)
{
    assert(arr != NULL);

    int left = 0;
    int right = lenth - 1;
    int mid = 0;

    while (left <= right)
    {
        mid = left + ((right - left) >> 1);
        if (key > arr[mid])
        {
            left = mid + 1;
        }
        else if (key < arr[mid])
        {
            right = mid - 1;
        }
        else
        {
            return mid;
        }
    }
    return -1;
}

分析:最大查找次数是log2 n,所以:时间复杂度是O(log2 n);
由于辅助空间是常数级别的所以:空间复杂度是O(1);


2.斐波那契

int fib_recursion(int n)
{
    if (n <= 2)
        return 1;
    else
    {
        return fib(n - 1) + fib(n - 2);
    }
}

分析:
循环的基本操作次数是n-1,辅助空间是n+1,所以:
时间复杂度O(2^n)
空间复杂度O(n)


//迭代法
int fib_iteration(int n)
{
    int a = 1;
    int b = 1;
    int c = 1;
    if (n<2)
    {
        return n;
    }
    while (n>2)
    {
        c = a + b;
        a = b;
        b = c;
        n--;
    }
    return c;
}

分析:
循环的基本次数是n-1,所用的辅助空间是常数级别的:
时间复杂度:O(n)
空间复杂度:O(1)


四、尾递归

1.定义:如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。
当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。
尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。

栗子3:

//这是尾递归
function f(x) {
   if (1 == x) 
       return 1;
   return f(x-1);
}
//这不是尾递归
function f(x) {
   if (1 == x) 
       return 1;
   return 1 + f(x-1);
}

后者不是尾递归,是因为该函数的最后一步操作是用1加上f(x-1)的返回结果,因此,最后一步操作不是调用自身。尾递归的判断标准是函数运行最后一步是否调用自身,而不是是否在函数的最后一行调用自身。


引用知乎大佬的解释:

 function story(){
    从前有座山,山上有座庙,庙里有个老和尚,一天老和尚对小和尚讲故事:story() // 尾递归,进入下一个函数不再需要上一个函数的环境了,得出结果以后直接返回。 
   } 
  function story(){   
    从前有座山,山上有座庙,庙里有个老和尚,一天老和尚对小和尚讲故事:story(),小和尚听了,找了块豆腐撞死了 // 非尾递归,下一个函数结束以后此函数还有后续,所以必须保存本身的环境以供处理返回值。    
}

2.尾递归的优势
尾递归和一般的递归不同在对内存的占用,普通递归创建stack累积而后计算收缩,尾递归只会占用恒量的内存(和迭代一样)

我们知道递归调用是通过栈来实现的,每调用一次函数,系统都将函数当前的变量、返回地址等信息保存为一个栈帧压入到栈中,那么一旦要处理的运算很大或者数据很多,有可能会导致很多函数调用或者很大的栈帧,这样不断的压栈,很容易导致栈的溢出。

我们回过头看一下尾递归的特性,函数在递归调用之前已经把所有的计算任务已经完毕了,他只要把得到的结果全交给子函数就可以了,无需保存什么,子函数其实可以不需要再去创建一个栈帧,直接把就着当前栈帧,把原先的数据覆盖即可。相对的,如果是普通的递归,函数在递归调用之前并没有完成全部计算,还需要调用递归函数完成后才能完成运算任务。

<think>嗯,用户想了解时间复杂度空间复杂度的概念,以及它们的定义、计算方法比较。首先,我需要回顾一下这些基本概念,确保自己理解正确。 时间复杂度空间复杂度都是用来分析算法效率的。时间复杂度关注的是算法执行时间随输入规模增长的变化趋势,而空间复杂度则是算法在运行过程中临时占用的存储空间大小。这两个概念对于评估算法性能非常重要,尤其是在处理大数据量时。 根据引用[3],时间复杂度是通过计算语句总的执行次数T(n)关于问题规模n的函数,然后确定其数量级,用大O表示法来表示。比如,O(1)是常数阶,O(n)是线性阶,O(n²)是平方阶等等。那如何计算呢?比如,一个循环执行n次的代码,时间复杂度就是O(n),嵌套两层循环则是O(n²)。如果有多个循环结构并列,时间复杂度取最大的那个,比如O(n + n²)会简化为O(n²)。 空间复杂度方面,引用[5]中的例子是计算斐波那契数列,分配了一个n+1大小的数组,因此空间复杂度是O(n)。空间复杂度主要看变量、数据结构递归调用等占用的空间。比如递归深度为n,每次递归需要常数空间的话,空间复杂度就是O(n)。 比较时间与空间复杂度时,用户可能需要知道在什么情况下优先考虑哪一个。比如,在内存充足的情况下,可能更关注时间效率;而在内存受限时,如嵌入式系统,空间复杂度更重要。但通常两者需要权衡,找到一个平衡点。 用户的问题可能还涉及到如何具体计算不同算法的时间空间复杂度。比如快速排序的平均时间复杂度是O(n log n),最坏情况是O(n²),而空间复杂度平均是O(log n)因为递归栈的深度。这时候需要引用相关的例子计算方法。 可能用户对如何推导大O阶不太清楚,需要解释推导方法,如去掉低阶项常数系数,只保留最高阶项。例如,T(n) = 3n² + 2n + 5 的时间复杂度是O(n²)。 还需要注意常见误区,比如认为时间复杂度就是实际执行时间,而实际上它只是增长趋势的表示。实际执行时间还受硬件、编程语言等因素影响。 总结起来,回答的结构应该先分别介绍时间空间复杂度的定义,然后详细说明计算方法,包括常见例子,最后比较两者的重要性应用场景。同时,要确保引用正确的参考资料,如引用[3]关于大O表示法的解释,引用[5]关于空间复杂度的例子等。</think>### 时间复杂度空间复杂度总结 #### 一、定义与核心概念 1. **时间复杂度** 描述算法执行时间随输入规模$n$的增长趋势,用大O记法$O(f(n))$表示。核心是分析**基本操作执行次数的数量级**,忽略低阶项常数系数[^3]。 例如: - 循环$n$次的代码:时间复杂度为$O(n)$ - 嵌套两层循环:时间复杂度为$O(n^2)$ 2. **空间复杂度** 描述算法运行过程中临时占用的内存空间随$n$的增长趋势,同样用大O记法表示。关注**变量、数据结构、递归调用栈**等占用的空间[^5]。 例如: - 数组长度为$n$:空间复杂度为$O(n)$ - 递归深度为$n$:空间复杂度为$O(n)$ --- #### 二、计算方法 1. **时间复杂度计算步骤** - 统计基本操作执行次数$T(n)$ - 用大O阶推导法简化: - 去除常数项(如$T(n)=2n+3 \rightarrow O(n)$) - 保留最高阶项(如$T(n)=n^2 + n \rightarrow O(n^2)$)[^3] **常见场景示例**: - 单层循环:$O(n)$ ```python for i in range(n): # 执行n次 print(i) # O(1)操作 ``` - 双层嵌套循环:$O(n^2)$ ```python for i in range(n): # 执行n次 for j in range(n): # 执行n次 print(i + j) # O(1)操作 ``` 2. **空间复杂度计算步骤** - 分析算法中**额外分配的显式空间**(如数组、队列) - 递归调用需计算栈深度(如斐波那契递归为$O(n)$,快速排序平均为$O(\log n)$)[^5] **示例对比**: - 迭代计算斐波那契数列: ```c long long* Fibonacci(size_t n) { long long* fibArray = malloc((n+1) * sizeof(long long)); // 空间O(n) // ... } ``` 空间复杂度:$O(n)$[^5] - 递归计算阶乘: ```python def factorial(n): if n == 1: return 1 return n * factorial(n-1) # 递归深度n → 空间O(n) ``` --- #### 三、时间与空间复杂度的比较 | 维度 | 时间复杂度 | 空间复杂度 | |--------------|---------------------------|---------------------------| | **核心指标** | 时间增长趋势 | 内存占用增长趋势 | | **优先级** | 通常优先优化(时间敏感场景)| 内存受限时优先考虑(如嵌入式系统) | | **典型优化** | 减少循环嵌套、剪枝算法 | 复用变量、尾递归优化 | --- #### 四、常见复杂度对比 | 类型 | 表示 | 典型算法 | |------------|------------|------------------------| | 常数阶 | $O(1)$ | 数组随机访问 | | 对数阶 | $O(\log n)$| 二分查找 | | 线性阶 | $O(n)$ | 遍历数组 | | 线性对数阶 | $O(n \log n)$ | 快速排序、归并排序 | | 平方阶 | $O(n^2)$ | 冒泡排序 | | 指数阶 | $O(2^n)$ | 穷举算法 | --- #### 五、实际应用建议 1. **时间换空间**:在内存紧张时,通过增加计算时间减少内存占用(如流式处理数据)。 2. **空间换时间**:通过缓存、预计算等策略提升速度(如哈希表查询$O(1)$)[^1][^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值