数据结构与算法之时间复杂度和空间复杂度(C语言版)

简介: 数据结构与算法之时间复杂度和空间复杂度(C语言版)

1. 时间复杂度

1.1 概念

简而言之,算法中的基本操作的执行次数,叫做算法的时间复杂度。也就是说,我这个程序执行了多少次,时间复杂度就是多少。


比如下面这段代码的执行次数:

void Func1(int N)
{
    int count = 0;
    for (int i = 0; i < N ; ++ i)
    {
        for (int j = 0; j < N ; ++ j)
        {
            ++count;
        }
    }
    for (int k = 0; k < 2 * N ; ++ k)
    {
        ++count;
    }
    int M = 10;
    while (M--)
    {
        ++count;
    }
    printf("%d\n", count);
}


Func1执行的基本操作次数:

F(N) = N^2 + 2*N + 10


在这里两层for循环的次数是N^2,第二个for循环的次数是2*N,while循环的次数是10

所以这个算法中的基本操作次数就是 N^2 + 2*N + 10。


那我们的时间复杂度就是这个吗,其实不是的。实际上我们在计算时间复杂度的时候,我们并不一定要计算精确的执行次数,而只需要大概执行次数。


这又是为什么呢?


当N = 10的时候,F(N) = 130


当 N = 100 的时候,F(N) = 10210


当 N = 1000 的时候,F(N) = 1002010


我们发现当N趋于无穷大的时候,对F(N)影响最大的是N^2,这就跟我们在数学里找极限一样,抓大头,找影响最大的一项,用影响最大的一项来表示我们的时间复杂度


这种表示方法我们称作大O的渐进表示法。

1.2大O的渐进表示法

 

大O符号(Big O notation):用于描述函数渐进行为的数学符号。



基本执行次数用大O阶方法表示的规则:

1. 如果执行次数中出现加法常数(无论多大,只要是常数),用1来代替。


2. 如果执行次数是多项式,执行次数只保留最高阶项(最高次项)


3. 如果最高阶项存在且不是1,舍去系数。


4.经过1,2,3操作得到的结果就是大O阶表示。


所以我们上面的F(N) = N^2 + 2*N + 10 用大O阶表示就是 O(N^2)。

1.3 最好,平均,最坏情况


有些算法的时间复杂度是存在最好,平均,最坏情况的。

1.最好情况:基本操作次数的最小值

2.平均情况:期望的基本操作次数

3.最坏情况:基本操作次数的最大值

但是在实际中我们都是用最坏情况来表示时间复杂度


1.4 时间复杂度例题

1.4.1 例题1

void Func2(int N)
{
    int count = 0;
    for (int k = 0; k < 2 * N ; ++ k)
    {
        ++count;
    }
    int M = 10;
    while (M--)
    {
        ++count;
    }
    printf("%d\n", count);
}

答案:O(N)

基本执行次数是(2 * N + 10),用大O阶表示为O(N)

1.4.2 例题2

void Func3(int N, int M)
{
    int count = 0;
    for (int k = 0; k < M; ++ k)
    {
        ++count;
    }
    for (int k = 0; k < N ; ++ k)
    {
        ++count;
    }
    printf("%d\n", count);
}

答案:O(M+N)

基本执行次数是M+N次,由于有两个未知数M和N,所以大O阶表示为O(M+N)

1.4.3 例题3

void Func4(int N)
{
    int count = 0;
    for (int k = 0; k < 100; ++ k)
    {
        ++count;
    }
    printf("%d\n", count);
}

答案:O(1)

基本执行次数是100次,用大O阶表示为O(1)

1.4.4 例题4

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;
    }
}

答案:O(N^2)


基本操作次数是(n-1)+(n-2)+(n-3)+....+3+2+1 = ((n + 1) * n) / 2


最好情况就是遍历一次就排序成功,基本操作次数是n-1,大O阶表示是O(N)


最坏情况就是全部遍历完,基本操作次数是((n + 1) * n) / 2,大O阶表示为O(N^2)


时间复杂度一般看最坏情况,为O(N^2)


大O表示为O(N^2)

1.4.5 例题5

int BinarySearch(int* a, int n, int x)
{
    assert(a);
    int begin = 0;
    int end = n-1;
    // [begin, end]:begin和end是左闭右闭区间,因此有=号
    while (begin <= end)
    {
        int mid = begin + ((end-begin)>>1);
        if (a[mid] < x)
            begin = mid+1;
        else if (a[mid] > x)
            end = mid-1;
        else
            return mid;
    }
    return -1;
}

答案:O(log N)


这个算法是二分查找


最好情况是查找一次,为O(1)


最坏情况是begin = end 了,只剩了一个元素,我们可以设循环的次数是x,一次循环我们是砍掉了一半的数组元素,那到最后没有元素了,说明n / 2^x = 1  


所以x = log n(以2为底)。时间复杂度的大O阶表示就是O(logN)。


我们规定以2为底的对数函数写成 log N  

1.4.6 例题6

long long Fac(size_t N)
{
    if(0 == N)
        return 1;
    return Fac(N-1)*N;
}

答案:O(N)

基本操作次数:这个函数一共递归了N次,时间复杂度就是O(N)

1.4.7 例题7

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

答案:O(2^N)

基本操作次数:这个函数递归了1+2+4+8+... 是一个不完整的等比数列,在N<3之后不会递归,但是不影响整体的趋势,可以忽略不计,这个等比数列的和是2^n - 1

所以大O阶表示为O(2^n)


2.空间复杂度

2.1概念

空间复杂度指的是临时占用存储空间大小的量度,需要注意的是空间复杂度并不是程序占了多少个字节的空间,因为没有什么太大意义

所以空间复杂度指的是新创建的变量个数,也是额外开辟的空间。

也就是说你为了完成一个算法,必须开的空间不算空间复杂度里,但是你为了解决算法,你又去开辟的空间才算空间复杂度。


空间复杂度计算规则和时间复杂度基本一致,用大O阶渐渐表示法。


注意⚠️⚠️⚠️:


1. 计算空间复杂度时,一般不需要考虑函数的形式参数。空间复杂度主要关注的是算法执行过程中所占用的额外空间,而函数的形式参数在函数调用时会被压入调用栈中,属于函数调用过程中的内存分配,并不计入空间复杂度的计算。


2. 递归算法在每次递归调用时需要维护函数调用栈,而函数调用栈会占用额外的内存空间,所以其空间复杂度为递归所使用的堆栈空间的大小。

2.2 空间复杂度例题

2.2.1 例题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;
    }
}


这个冒泡排序临时创建的变量分别是 end , exchange 和 i  一共三个,是常数个。

所以空间复杂度用大O阶表示为O(1)

2.2.2 例题2

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这个数组,是动态开辟的一个数组,开辟了n+1个空间,还有个i变量,一共是n+2个变量。

用大O阶表示为O(n)

2.2.3 例题3

long long Fac(size_t N)
{
    if(N == 0)
        return 1;
    return Fac(N-1)*N;
}


首先我们要明确的是这是一个函数递归调用


我们函数递归一共要递归N次,共创建新的栈空间N个

所以空间复杂度为O(N)

2.2.4 例题4

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

此时的空间复杂度是多少呢? O(2^N)? 还是O(N)?

我们画图来说明



我们来看这个函数的递归调用并不是同时进行的,而是先调用左边的,左边调用完了,最下面Fib(2)往回销毁空间之后才去调用Fib(1),也就是在这个时候才开辟Fib(1)的空间

蓝色箭头代表从Fib(3)到Fib(2)先开辟Fib(2)的栈空间,当调用结束的时候,这段空间就要销毁,于是就有了红色的箭头代表销毁Fib(2)的空间,销毁之后我们才开始调用Fib(1),绿色箭头代表调用Fib(1),此时就要开辟Fib(1)的空间,要知道的是我们刚刚销毁一个栈空间,现在又要开辟一个栈空间,所以空间是被重复利用的,也就是黄色的箭头,Fib(1)的空间跟Fib(2)的空间是同一块空间。


所以我们真正开辟的空间只有从Fib(N)到Fib(2)这一段,其他的调用函数,都是在重复利用栈空间的过程。


所以空间复杂度是O(N)


总结:时间是累积的,一去不复返

           空间是可以重复利用的。  

3.常见复杂度的对比

常数阶 O(1) 5201314
线性阶 O(N) 3N+1
平方阶 O(N^2) 2N^2 + 3N + 1
对数阶 O(logN) 3log(2)N + 2
NlogN阶 O(NlogN) 2N+3Nlog(2)N + 1
立方阶 O(N^3) 3N^3+2N^2+N+1
指数阶 O(2^N) 2^N
O(1)  <  O(log n)  <  O(n)  <  O(nlogn)  <  O(n^2)  <  O(n^3)  <  O(2^n)  <  O(n!)  <  O(n^n)
相关文章
|
27天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
123 9
|
2月前
|
存储 算法 C语言
通义灵码在考研C语言和数据结构中的应用实践 1-5
通义灵码在考研C语言和数据结构中的应用实践,体验通义灵码的强大思路。《趣学C语言和数据结构100例》精选了五个经典问题及其解决方案,包括求最大公约数和最小公倍数、统计字符类型、求特殊数列和、计算阶乘和双阶乘、以及求斐波那契数列的前20项和。通过这些实例,帮助读者掌握C语言的基本语法和常用算法,提升编程能力。
70 4
|
2月前
|
机器学习/深度学习 缓存 算法
Python算法设计中的时间复杂度与空间复杂度,你真的理解对了吗?
【10月更文挑战第4天】在Python编程中,算法的设计与优化至关重要,尤其在数据处理、科学计算及机器学习领域。本文探讨了评估算法性能的核心指标——时间复杂度和空间复杂度。通过详细解释两者的概念,并提供快速排序和字符串反转的示例代码,帮助读者深入理解这些概念。同时,文章还讨论了如何在实际应用中平衡时间和空间复杂度,以实现最优性能。
69 6
|
26天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
63 16
|
26天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
83 8
|
28天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
54 4
|
2月前
|
机器学习/深度学习 存储 缓存
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
文章主要介绍了排序算法的分类、时间复杂度的概念和计算方法,以及常见的时间复杂度级别,并简单提及了空间复杂度。
33 1
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
|
1月前
|
存储 C语言
【数据结构】顺序表(c语言实现)(附源码)
本文介绍了线性表和顺序表的基本概念及其实现。线性表是一种有限序列,常见的线性表有顺序表、链表、栈、队列等。顺序表是一种基于连续内存地址存储数据的数据结构,其底层逻辑是数组。文章详细讲解了静态顺序表和动态顺序表的区别,并重点介绍了动态顺序表的实现,包括初始化、销毁、打印、增删查改等操作。最后,文章总结了顺序表的时间复杂度和局限性,并预告了后续关于链表的内容。
61 3
|
1月前
|
存储 算法 C语言
C语言数据结构(2)
【10月更文挑战第21天】
|
2月前
|
存储 算法 C语言
【趣学C语言和数据结构100例】
《趣学C语言和数据结构100例》精选5个编程问题,涵盖求最大公约数与最小公倍数、字符统计、特殊序列求和及阶乘计算等,通过实例讲解C语言基础与算法思维,适合初学者实践学习。
78 1