• 初识《时间复杂度和空间复杂度》


    目录

    前言:

    关于数据结构与算法

    1.什么是数据结构?

    2.什么是算法?

    3.数据结构和算法的重要性

    算法效率是什么?

    1.怎样衡量一个算法的好坏呢?

    2.算法的复杂度是个什么?

    时间复杂度

    1.时间复杂度的概念

    2.大O的渐进表示法

    3.举例说明(动手实践)

    例1:

     例2:

    例3:

    例4:

    例5:

    例6:

    例7:

    例8:

    总结:

    空间复杂度

    举例说明

    例1

    例2

    例3 

    总结


    前言:

    从本文开始,我们已经结束了之前对于C语言的初步学习,接下来我们就要运用我们学习过的C语言知识来实现问题的解决,因此我们不得不引入一大类————《数据结构与算法》。

    接下来的我们将会进行大量刷题与知识理解,在这里我们一定不能松懈,一定要去动手解决自己不熟悉的部分。

    关于数据结构与算法

    1.什么是数据结构

    数据结构是指在计算机中组织和存储数据的方式,它是计算机科学的一个基本概念。数据结构涉及到的是数据如何在内存中进行存储和访问,以及对数据进行操作和处理的方法。在计算机科学中,数据结构广泛应用于算法设计和实现,数据库管理系统,编译器和操作系统等领域。常见的数据结构有数组、链表、树、图等。掌握数据结构对于编写高效、优化的程序和解决复杂问题至关重要。

    2.什么是算法?

    简单来说,算法是一组解决问题的步骤和规则。在计算机科学中,算法是用来解决计算问题的有限指令集合,即一种可用来求解某一问题的明确定义的方法。算法包括准确的定义、输入、输出、有限性、确切性和有效性等特征。在计算机程序设计中,算法通常被用来解决各种问题,如搜索、排序、优化、数据压缩等。算法的设计和实现是计算机科学和编程中非常重要的组成部分。

    3.数据结构和算法的重要性

    数据结构和算法是计算机科学中非常重要的概念,它们可以帮助我们更高效地解决实际问题。具体来说,以下是数据结构和算法的重要性:

    1. 提高代码质量:使用正确的数据结构和算法可以提高代码的可读性、可维护性,减少代码的复杂度。

    2. 优化程序性能:通过选择合适的数据结构和算法,程序可以更快、更高效地执行。

    3. 解决实际问题:数据结构和算法是解决实际问题的基础,例如搜索、排序、最短路径等问题。

    4. 掌握基本编程技能:学习数据结构和算法是掌握基本编程技能的一部分,对于从事编程相关工作的人员而言是必须的。

    5. 促进思维能力:学习数据结构和算法可以提高我们的逻辑思维能力,培养我们的分析和解决问题的能力。

    综上所述,学习数据结构和算法对于计算机科学的学习和实践都非常重要。

    算法效率是什么?

    1.怎样衡量一个算法的好坏呢?

    衡量一个算法的好坏有多种方法,以下是一些常见的:

    1. 时间复杂度:算法执行所需的时间是衡量其好坏的重要因素之一。时间复杂度是指算法执行所需的时间随着输入数据规模增加而增加的速度。一般来说,时间复杂度越低,算法的执行效率就越高。

    2. 空间复杂度:同样是衡量算法好坏的重要因素之一,空间复杂度是指算法执行所需的内存空间随着输入数据规模增加而增加的速度。一般来说,空间复杂度越低,算法的内存利用率越高。

    3. 精度和准确性:算法的输出结果应该与期望的结果一致,并且应该尽可能精确。这是针对特定问题的衡量标准,不同问题需要不同的衡量方式。

    4. 可读性和可维护性:算法代码应该易于理解和修改,并且应该易于维护。代码的可读性和可维护性有助于提高软件的可靠性和可用性。

    5. 可扩展性:算法应该易于扩展,以便在需要时可以轻松地进行修改和更新。可扩展性是在需求变化时重要的因素。

    综上所述,衡量算法的好坏需要综合考虑多个因素,根据实际情况选择相应的衡量标准。

    2.算法的复杂度是个什么?

     算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般 是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。

    时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算 机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。

    但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

    时间复杂度

    1.时间复杂度的概念

    在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知 道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个 分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。

    即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

    1. void Func1(int N)
    2. {
    3. int count = 0;
    4. for (int i = 0; i < N; ++i)
    5. {
    6. for (int j = 0; j < N; ++j)
    7. {
    8. ++count;
    9. }
    10. }
    11. for (int k = 0; k < 2 * N; ++k)
    12. {
    13. ++count;
    14. }
    15. int M = 10;
    16. while (M--)
    17. {
    18. ++count;
    19. }
    20. printf("%d\n", count);
    21. }

     现在先来尝试计算机算以上代码的时间复杂度吧。

     

    实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这 里我们使用大O的渐进表示法。 

    2.大O的渐进表示法

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

    推导大O阶方法: 1、用常数1取代运行时间中的所有加法常数。

    2、在修改后的运行次数函数中,只保留最高阶项。

    3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

    使用大O的渐进表示法以后,Func1的时间复杂度为:

    O(N^2)

    通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

    另外有些算法的时间复杂度存在最好、平均和最坏情况:

    最坏情况:任意输入规模的最大运行次数(上界)

    平均情况:任意输入规模的期望运行次数

    最好情况:任意输入规模的最小运行次数(下界)

    例如:在一个长度为N数组中搜索一个数据x

    最好情况:1次找到

    最坏情况:N次找到

    平均情况:N/2次找到

    在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)

    3.举例说明(动手实践)

    例1:

    1. // 计算Func2的时间复杂度?
    2. void Func2(int N)
    3. {
    4. int count = 0;
    5. for (int k = 0; k < 2 * N; ++k)
    6. {
    7. ++count;
    8. }
    9. int M = 10;
    10. while (M--)
    11. {
    12. ++count;
    13. }
    14. printf("%d\n", count);
    15. }

    这里的时间复杂度应当为O(2*N + M)

    但是由大O的渐进表示方法,我们直接取

    O(N)即可。

    我们在后续做题时,应当先想出算法思路,再进行时间复杂度的计算,通过判断最坏情况,并选出时间复杂度最小的算法进行编写。

    这里更要注意的是,我们在计算时间复杂度的时候,切忌不要先看代码,而是应当先去思考和想象,因为时间复杂度是一个抽象的概念,我们应当想编写代码的思路,而非代码本身!!

     例2:

    1. // 计算Func3的时间复杂度?
    2. void Func3(int N, int M)
    3. {
    4. int count = 0;
    5. for (int k = 0; k < M; ++k)
    6. {
    7. ++count;
    8. }
    9. for (int k = 0; k < N; ++k)
    10. {
    11. ++count;
    12. }
    13. printf("%d\n", count);
    14. }

    通过我上面所讲的,这里不难看出时间复杂度应当为O(N+M),即

    O(N)

     

    例3:

    1. // 计算Func4的时间复杂度?
    2. void Func4(int N)
    3. {
    4. int count = 0;
    5. for (int k = 0; k < 100; ++k)
    6. {
    7. ++count;
    8. }
    9. printf("%d\n", count);
    10. }

    基本操作执行了10次,通过推导大O阶方法,时间复杂度为

     O(1)

     

    例4:

    1. // 计算strchr的时间复杂度?
    2. const char * strchr(const char * str, int character)
    3. {
    4. while (*str)
    5. {
    6. if (*str == character)
    7. {
    8. return str;
    9. }
    10. ++str;
    11. }
    12. }

     实例4基本操作执行最好1次,最坏N次,时间复杂度一般看最坏,时间复杂度为

    O(N)

    例5:

    1. void BubbleSort(int* a, int n)
    2. {
    3. assert(a);
    4. for (size_t end = n; end > 0; --end)
    5. {
    6. int exchange = 0;
    7. for (size_t i = 1; i < end; ++i)
    8. {
    9. if (a[i - 1] > a[i])
    10. {
    11. Swap(&a[i - 1], &a[i]);
    12. exchange = 1;
    13. }
    14. }
    15. if (exchange == 0)
    16. break;
    17. }
    18. }

    此例为冒泡排序,通过之前对数组的学习我们可以知道冒泡排序是效率最低,时间复杂度最高的算法,因为它要走n趟,并且在每一趟又要完遍历完n - i - 1个元素。

    如果忘记了冒泡排序的实现,可以参考我之前写的blog

    关于数组名-CSDN博客

    如果你很熟练的话,通过我上述的描述你就知道该时间复杂度为O(N^2)了。

    但是对于刚开始来学习的呢,有以下解释:

    实例5基本操作执行最好N次,最坏执行了(N*(N+1))/2次,通过推导大O阶方法+时间复杂度一般看最坏,时间复杂度为 O(N^2) 

     最坏的情况是怎么算出来的?

    这就要回顾我们在高中学习到对等差数列的求和公式。

    即:

    (首项 + 尾项)*(项数)/ 2

    什么意思呢?

    最坏的情况莫过于冒牌排序一个逆序数组,因为他对每个数字都进行了排序。

    如此我们可以简化为:

     

    将各个长度相加,不就是等差求和了吗,如此一来我们就理解了为什么是要等差求和。

    所以最后的答案就是

    O(N^2) 

    例6:

    1. // 计算BinarySearch的时间复杂度?
    2. int BinarySearch(int* a, int n, int x)
    3. {
    4. assert(a);
    5. int begin = 0;
    6. int end = n - 1;
    7. // [begin, end]:begin和end是左闭右闭区间,因此有=号
    8. while (begin <= end)
    9. {
    10. int mid = begin + ((end - begin) >> 1);
    11. if (a[mid] < x)
    12. begin = mid + 1;
    13. else if (a[mid] > x)
    14. end = mid - 1;
    15. else
    16. return mid;
    17. }
    18. return -1;
    19. }

    此例为二分查找的代码,答案为O(logN)

    对这道题我们可以用折纸再展开的方式来理解。

    在这里我们先从代码的角度先讲解一下为什么是logN。

    首先,我们要分析最坏的情况。

    最坏的情况莫过于在数组经历二分查找后的最后一个元素为我们要查找的数。

    所以我们就有以下的图:

     所以最终的答案是

    O(logN)

    我们在这里对2会进行省略。

    虽然二分查找的时间复杂度很低,但是我们使用该算法之前要对其进行排序。

    例7:

    1. // 计算阶乘递归Fac的时间复杂度?
    2. long long Fac(size_t N)
    3. {
    4. if (0 == N)
    5. return 1;
    6. return Fac(N - 1)*N;
    7. }

    例7通过计算分析发现基本操作递归了N次,时间复杂度为

    O(N)

    这个不难理解,我在这里不过多赘述。

    例8:

    1. // 计算斐波那契递归Fib的时间复杂度?
    2. long long Fib(size_t N)
    3. {
    4. if (N < 3)
    5. return 1;
    6. return Fib(N - 1) + Fib(N - 2);
    7. }

     对于斐波那契数列的递归实现,其实和细胞分裂很像,一生二,二生四,四生八等等等,这种以指数形式增加的。

    所以通过思想来看,不难看出时间复杂度为

    O(2^N)

    总结:

    以上就是关于时间复杂度的一些例题,仅通过这些无法较好的彻底学会使用时间复杂度,后续我们在刷题的同时,也会适当的对时间复杂度进行讲解。 

    要记住,首先看思路再看代码!

    空间复杂度

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

    由于空间复杂度的需求不是特别的重视,我们在以后的代码编写中,经常会实现牺牲空间换取时间的操作,这些都已是家常便饭的操作。

    接下来我们就来分析一下三种代码的空间复杂度。

    举例说明

    例1

    1. // 计算BubbleSort的空间复杂度?
    2. void BubbleSort(int* a, int n)
    3. {
    4. assert(a);
    5. for (size_t end = n; end > 0; --end)
    6. {
    7. int exchange = 0;
    8. for (size_t i = 1; i < end; ++i)
    9. {
    10. if (a[i - 1] > a[i])
    11. {
    12. Swap(&a[i - 1], &a[i]);
    13. exchange = 1;
    14. }
    15. }
    16. if (exchange == 0)
    17. break;
    18. }
    19. }

    实例1使用了常数个额外空间,所以空间复杂度为

    O(1)

    例2

    1. long long* Fibonacci(size_t n)
    2. {
    3. if (n == 0)
    4. return NULL;
    5. long long * fibArray = (long long *)malloc((n + 1) * sizeof(long long));
    6. fibArray[0] = 0;
    7. fibArray[1] = 1;
    8. for (int i = 2; i <= n; ++i)
    9. {
    10. fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
    11. }
    12. return fibArray;
    13. }

    实例2动态开辟了N个空间,空间复杂度为

    O(N)
     

    例3 

    1. // 计算阶乘递归Fac的空间复杂度?
    2. long long Fac(size_t N)
    3. {
    4. if(N == 0)
    5. return 1;
    6. return Fac(N-1)*N;
    7. }

    实例3递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。空间复杂度为

    O(N)

    总结

    时间复杂度是算法执行所需时间的度量,通常使用大 O 符号表示。它表示当输入规模变大时,算法执行时间增长的快慢程度。时间复杂度越小,算法执行速度越快。

    空间复杂度是算法执行所需内存的度量,也通常使用大 O 符号表示。它表示当输入规模变大时,算法所需内存增长的快慢程度。空间复杂度越小,算法所需内存越少。

    在设计算法时,需要充分考虑时间和空间复杂度。如果一个算法时间复杂度较小,但空间复杂度过高,可能会导致程序在执行时占用过多的内存而造成程序崩溃。因此,需要在时间复杂度和空间复杂度之间取得一个平衡,使得算法在执行时既能保持较快的速度,又能占用尽可能少的内存。

     这部分内容会比较抽象,但是我们需要多多练习,数据结构和算法的内容是这样的抽象的,所以我们更不应该放弃。

    这些部分是人发现的算法,既然是人那么我也可以,现在想不明白不代表明天不明白,只要肯坚持就不会有想不出来的。

    记住,

    坐而言不如起而行!

    Action speak louder than words!

    以下是这篇文字所需要的代码:

    Time_complexity_and_Space_complexity_CSDN/Time_complexity_and_Space_complexity_CSDN/test_10_27.c · 无双/Data structures amd algorithms - Gitee.com

  • 相关阅读:
    嵌入式系统-开关机测试笔记
    判断某点是否在三角形内(Python)
    Java编程练习题:Demo96 - Demo105(多维数组)
    李宏毅-21-hw3:对11种食物进行分类-CNN
    ARM---实现1-100求和任务
    总结二:linux面经
    基于java+ssm+shiro的出租房管理平台
    【vscode+clangd】clangd不起作用的解决方案、compile_commands.json文件为空的解决方案
    Java内存溢出故障案例及Linux内存机制探究
    【English】十大词性之连词
  • 原文地址:https://blog.csdn.net/weixin_72917087/article/details/134080909