数据结构空间复杂度

空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 ,空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数,空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法, 注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定

复制代码
int ArraySum(int* a, size_t n)
{
    assert(a);
    int sum = 0;
    for (size_t i = 0; i < n; ++i)
    {
        sum += a[i];
    }
    return sum;
}

函数中开辟的额外空间仅为2 个常数级临时变量:整型变量sum、循环变量i;

无动态内存申请(无malloc/calloc)、无递归调用(无栈帧开销),所有额外空间的大小与数据规模 n 无关,固定不变;

按规则,常数级的额外空间复杂度为O(1)

复制代码
void BubbleSort(int* a, int n)
{
    assert(a);
    for (size_t end = n; end > 0; --end)
        {
            int exchange = 0;
            for (size_t i = 1; i < end; ++i)
                {
                    if (a[i-1] > a[i])
                    {
                        Swap(&a[i-1], &a[i]);
                        exchange = 1;
                    }
                }    
            if (exchange == 0)
            break;
        }
}

逐一看代码中开辟的辅助变量 / 空间,判断其是否与n相关:

循环变量:size_t/end、size_t、i------都是单个整型变量,数量固定,与n无关;

优化标记:int exchange------单个整型变量,用于标记本轮是否发生交换,数量固定,与n无关;

交换函数Swap:冒泡排序的交换逻辑是原地交换 (直接操作原数组的两个元素),无论Swap是用临时变量实现(int temp=*a;*a=*b;*b=temp;)还是异或实现,都只用到单个临时变量,依然是常数级空间,与n无关。

关键结论 :这段代码中所有额外开辟的辅助空间,都是固定的常数级变量,没有动态申请数组、链表等随n变化的空间,也无递归栈开销

因此,这段冒泡排序的空间复杂度为 O(1) ,冒泡排序也属于原地排序算法

复制代码
long long* Fibonacci(size_t n)
{
    if(n==0)
        return NULL;
    long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));
    fibArray[0] = 0;
    fibArray[1] = 1;
    for (int i = 2; i <= n ; ++i)
        {
            fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
        }
    return fibArray;
}

先逐一看代码中额外开辟的所有空间 ,区分结果存储空间辅助空间

动态数组fibArray (结果存储空间)

函数通过malloc动态开辟了(n+1)个long long类型的连续空间,用于存储前n+1个斐波那契数(fibArray[0]~fibArray[n]),这个空间的大小随 n 线性增长 ,且是输出结果的必要空间------函数要返回所有斐波那契数,必须开辟该空间,无法用常数空间替代。

辅助变量

代码中仅用到循环变量int i这一个额外的临时变量,还有形参size_t n,都是单个整型 / 无符号整型变量 ,数量固定、占用空间恒定,与n无关,属于常数级辅助空间

无其他开销:迭代实现无递归栈帧开销,无额外动态内存申请,所有操作都是原地计算并存入数组

统计函数实际额外开辟的总空间(包含结果存储的动态数组)------这是开发中关注的重点,因为调用者使用该函数时,malloc的空间是函数主动开辟的额外内存,需要后续手动释放,

此时总额外空间随n线性增长,空间复杂度为O(n)(这是这个函数的核心空间复杂度结论)

辅助空间(排除输出结果的必要存储空间)------算法分析中若聚焦计算逻辑本身的空间开销,会忽略必须的结果存储。此时纯辅助空间只有常数级的循环变量i,空间复杂度为O(1)

复制代码
long long Fac(size_t N)
{
    if(N == 0)
        return 1;

    return Fac(N-1)*N;
}

调用Fac(N)会触发线性链式的递归调用,直到Fac(0),完整的调用链是:

Fac(N)→Fac(N-1)→Fac(N-2)→...→Fac(1)→Fac(0)

从Fac(N)到Fac(0),逐层开辟栈帧,共开辟 N+1个栈帧(包含基线条件Fac(0)的栈帧);

触发Fac(0)返回1后,才会从Fac(0)到Fac(N)逐层释放栈帧,栈帧不会提前释放。

2.推导空间复杂度

栈帧的最大数量:N+1(调用栈的深度为N+1);

单个栈帧的空间复杂度:O(1)(仅保存形参N、返回地址等,固定大小,与N无关);

总空间复杂度=栈帧最大数量×单个栈帧空间=(N+1)×O(1)

根据空间复杂度的规则,最终空间复杂度为O(N)

复制代码
long long Fib(size_t N)
{
    if (N < 3) return 1;
    return Fib(N-1) + Fib(N-2);
}

空间复杂度由调用栈的最大深度决定,与总递归调用次数无关

程序采用深度优先遍历 执行递归:先将Fib(N-1)的调用栈执行到底(直到Fib(2)/Fib(1)),再回溯计算Fib(N-2),栈帧会复用,不会同时开辟所有调用的栈帧

调用栈的最大深度为 N-1(从Fib(N)到Fib(1)的链式深度),每个栈帧为常数级O(1)空间;

总空间开销 = 栈帧最大深度 × 单个栈帧空间 = (N−1)×O(1),忽略常数项,故为O(N)

复制代码
int** CreateMatrix(size_t n)
{
    if (n == 0) return NULL;
    int** mat = (int**)malloc(n * sizeof(int*));
    for (size_t i = 0; i < n; ++i)
    {
        mat[i] = (int*)malloc(n * sizeof(int));
    }
    return mat;
}

函数动态开辟了n 阶方阵的连续空间,总开辟的内存单元数为:行指针数组n个+每行n个 int × n行=n+n^2;

按空间复杂度规则,保留最高阶项,忽略低阶项和常数项,n2是最高阶项,主导空间增长趋势;

额外临时变量仅循环变量i(常数级O(1)),对整体复杂度无影响;空间复杂度为 O(n2)

复制代码
int* ReverseArray1(int* a, size_t n)
{
    if (n == 0) return NULL;
    int* ret = (int*)malloc(n * sizeof(int));
    for (size_t i = 0; i < n; ++i)
    {
        ret[i] = a[n-1-i];
    }
    return ret;
}
void ReverseArray2(int* a, size_t n)
{
    assert(a);
    size_t left = 0, right = n-1;
    while (left < right)
    {
        Swap(&a[left], &a[right]);
        left++;
        right--;
    }
}

非原地反转

动态开辟了大小为 n 的int 类型新数组ret,用于存储反转后的结果,该空间随n线性增长;

额外临时变量仅循环变量i(常数级O(1))

总空间开销由动态数组主导,空间复杂度为O(1)

原地反转

开辟的额外空间仅为2个常数级临时变量:左指针 left 、右指针right

Swap 交换(单个临时变量,常数级), 无动态内存、无递归,所有操作直接在原数组上完成**,** 额外空间与n无关;空间复杂度为 O(1)

相关推荐
一路往蓝-Anbo1 小时前
第 4 篇:策略模式 (Strategy) —— 算法的热插拔艺术
网络·驱动开发·stm32·嵌入式硬件·算法·系统架构·策略模式
她说..1 小时前
验签实现方案整理(签名验证+防篡改+防重放)
java·经验分享·spring boot·java-ee·bladex
定偶1 小时前
C语言操作MYSQL
c语言·mysql·adb
不染尘.1 小时前
二分算法(优化)
开发语言·c++·算法
不吃橘子的橘猫1 小时前
Verilog HDL基础(概念+模块)
开发语言·学习·算法·fpga开发·verilog
爱吃山竹的大肚肚1 小时前
异步导出方案
java·spring boot·后端·spring·中间件
苦藤新鸡1 小时前
49.二叉树的最大路径和
数据结构·算法·深度优先
源代码•宸1 小时前
Leetcode—144. 二叉树的前序遍历【简单】
经验分享·算法·leetcode·面试·职场和发展·golang·dfs
梵刹古音1 小时前
【C语言】 定义变量
c语言·开发语言·嵌入式