• 【数据结构初阶】一. 复杂度讲解


    =========================================================================

    相关代码gitee自取

    C语言学习日记: 加油努力 (gitee.com)

     =========================================================================

    接上期

    学C的第三十四天【程序环境和预处理】_高高的胖子的博客-CSDN博客

     =========================================================================

                         

    1 . 算法效率

    (1). 什么是数据结构:

                   

    数据结构(Data Structure)是计算机存储组织数据的方式

    相互之间存在一种或多种特定关系的数据元素的集合

                         


                        

    (2). 什么是算法:

                    

    算法(Algorithm)就是定义良好的计算过程

    取一个或一组的值为输入,并产生出一个或一组值作为输出

    简单来说算法就是一系列的计算步骤用来将输入数据转化成输出结果

                         


                        

    (3). 算法的复杂度:

                         

    算法编写成可执行程序后运行时需要耗费时间资源空间(内存)资源

    因此衡量一个算法的好坏,一般是时间空间两个维度来衡量的,

    时间复杂度空间复杂度

                          

    时间复杂度主要衡量一个算法的运行快慢

    空间复杂度主要衡量一个算法运行所需要的额外空间

    计算机发展的早期计算机的存储容量很小。所以对空间复杂度很是在乎

    但是经过计算机行业的迅速发展计算机的存储容量已经达到了很高的程度

    所以我们如今已经不需要再特别关注一个算法的空间复杂度

                   

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                       

    2 . 时间复杂度

    (1). 时间复杂度的概念:

                   

    计算机科学中算法的时间复杂度是一个函数,它定量描述了该算法的运行时间

    一个算法执行所耗费的时间,从理论上说,是不能算出来的,

    只有把你的程序放在机器上跑起来才能知道

    但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦

    所以才有了时间复杂度这个分析方式

                

    一个算法所花费的时间其中语句的执行次数成正比例

    算法中的基本操作的执行次数,为算法的时间复杂度

                           

    即:

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

                 

    图例:Func1执行的基本操作次数

                     

    上图得到的Func1执行的基本次数为:

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

    但实际我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数

    只需要大概执行次数,那么这里我们使用大O的渐进表示法

                         


                        

    (2). 大O的渐进表示法:

              

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

                

    推导大O阶方法

    1、常数1取代运行时间中的所有加法常数

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

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

                

    使用大O的渐进表示法以后,

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

    第一步:+10 变为 +1

    第二步:保留最高阶项 N^2

    第三步:最高项相乘常数为1不用去除

                

    所以Func1的时间复杂度O(N^2)

    N = 10             F(N) = 100        

    N = 100           F(N) = 10000    

    N = 1000         F(N) = 1000000

    通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项

    简洁明了的表示出了执行次数

    大O的渐进表示法本质计算的是算法属于哪个量级

               

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

    (可查看下方案例四)

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

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

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

                    

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

    最坏情况N次找到

    平均情况N/2次找到

    最好情况1次找到

    实际操作下一般情况关注的是算法的最坏运行情况

    所以数组中搜索数据时间复杂度为O(N)

                         


                        

    (3). 常见时间复杂度计算案例:

              

    案例一:

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

                   

                   

    案例二:

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

                   

                   

    案例三:

    1. //示例三:
    2. //计算Func4的时间复杂度:
    3. void Func4(int N)
    4. {
    5. int count = 0;
    6. for (int k = 0; k < 100; ++k)
    7. {
    8. ++count;
    9. }
    10. printf("%d\n", count + N);
    11. }
    图示:“cpu技术太强了”

                   

                   

    案例四:

    1. //示例四:
    2. //计算strchr的时间复杂度:
    3. const char* strchr(const char* str, int character);
    4. //strchr库函数:在str字符数组中查找一个字符
    图示:

                   

                   

    案例五:

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

                   

                   

    案例六:

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

                   

                   

    案例七:

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

                   

                   

    案例八:

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

                         


                        

    (4). 常见时间复杂度对比

                 

    一般算法常见的复杂度如下表:

    5201314O(1)常数阶
    3n + 4O(n)线性阶
    3n^2 + 4n + 5O(n^2)平方阶
    3log(2)n + 4O(logn)对数阶
    2n + 3nlog(2)n + 14O(nlogn)nlogn阶
    n^3 + 2n^2 + 4n + 6O(n^3)立方阶
    2^nO(2^n)指数阶

             

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                 

    3 . 空间复杂度

    (1). 空间复杂度的概念:

                         

    空间复杂度是一个数学表达式

    对一个算法在运行过程中额外临时占用存储空间大小的量度

                    

    空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,

    所以空间复杂度算的是变量的个数

    空间复杂度计算规则基本跟时间复杂度类似也使用大O渐进表示法

                       

    注意:

    函数运行时所需要的栈空间(存储参数局部变量、一些寄存器信息等)

    编译期间已经确定好了

    因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定

                         


                        

    (2). 常见空间复杂度计算案例:

               

    案例一:

    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. {
    18. break;
    19. }
    20. }
    21. }
    图示:

               

               

    案例二:

    1. //计算Fibonacci的空间复杂度:
    2. //返回斐波那契数列的前n项
    3. long long* Fibonacci(size_t n)
    4. {
    5. if (n==0)
    6. {
    7. return NULL;
    8. }
    9. long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long));
    10. fibArray[0] = 0;
    11. fibArray[1] = 1;
    12. for (int i = 2; i <= n; ++i)
    13. {
    14. fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
    15. }
    16. return fibArray;
    17. }
    图示:

               

               

    案例三:

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

             

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                 

    4 . 复杂度的oj练习

    (1). 时间复杂度练习:消失的数字

                       

    对应链接:

    面试题 17.04. 消失的数字 - 力扣(LeetCode)

                  

    题目:

               

    解决思路一:使用等差数列公式

                 

    假设数组nums包含从0到n的所有整数

    那么就可以使用 0+N等差公式 计算出一个结果

    该结果等于 0~n的各数相加总和

    用这个结果 减去 数组中的值

    结果就是消失的数字的值

                  

    图示:

    对应代码:
    1. int missingNumber(int* nums, int numsSize){
    2. int N = numsSize;
    3. int ret = N*(N+1)/2;
    4. for(int i = 0; i < N; i++)
    5. {
    6. ret -= nums[i];
    7. }
    8. return ret;
    9. }

                  

    解决思路二:异或法

                 

    用 0 异或 完整的0~N各值

    再用该异或的结果异或 nums数组少一个值

    因为异或后相同为0相异为1

    此时两对值中相同的值就会异或为0

    nums少的一个值异或后就会得到该值

                  

    图示:

    对应代码:
    1. int missingNumber(int* nums, int numsSize){
    2. int N = numsSize;
    3. int x = 0; //用来保存异或后的结果
    4. for(int i = 0; i <= N; ++i)
    5. {
    6. x ^= i;
    7. }
    8. for(int i = 0; i < N; ++i)
    9. {
    10. x ^= nums[i];
    11. }
    12. return x;
    13. }

                          


                        

    (2). 空间复杂度练习:轮转数组

                       

    对应链接:

    189. 轮转数组 - 力扣(LeetCode)

                   

    题目:要求时间复杂度为O(N),空间复杂度为为O(1)

               

    解决思路一:整体右旋

                 

    原数组分为两部分

    假设需要右旋k个数字

    以原数组末尾k个数字为一组剩下其他数字为一组

    两组进行调换,即可实现

                  

    图示:

    对应代码:
    1. void rotate(int* nums, int numsSize, int k){
    2. //用空间换时间:
    3. int n = numsSize; //数组长度
    4. int* tmp = (int*)malloc(sizeof(int)*n);
    5. k %= n; //确保要右旋个数小于数组大小
    6. //直接使用memcpy函数进行调换:
    7. memcpy(tmp, nums+n-k, sizeof(int)*k); //把后k个值移到前面
    8. // tmp : 起始位置
    9. // nums+n-k : 数组nums后k个值的起始位置
    10. // sizeof(int)*k :拷贝k个int大小的数据
    11. memcpy(tmp+k, nums, sizeof(int)*(n-k)); //把后k个值移到前面
    12. // tmp+k : 拷贝到tmp+k的位置,因为上面把后k个值放在了前面
    13. // nums : 数组nums开始位置
    14. // sizeof(int)*(n-k) :拷贝(n-k)个int大小的数据
    15. //再赋给数组nums:
    16. memcpy(nums, tmp, sizeof(int)*n);
    17. //释放开辟的动态空间:
    18. free(tmp);
    19. }

               

    解决思路二:逆置

                 

    将原数组的前 n-k 个数逆置

    后 k 个数也逆置

    最后再整体逆置,即可实现

                  

    图示:

    对应代码:
    1. //逆置函数:
    2. void reverse(int* a, int left, int right)
    3. {
    4. while(left < right)
    5. {
    6. int tmp = a[left];
    7. a[left] = a[right];
    8. a[right] = tmp;
    9. ++left;
    10. --right;
    11. }
    12. }
    13. void rotate(int* nums, int numsSize, int k){
    14. k %= numsSize;
    15. //逆置前 n-k 个数:
    16. reverse(nums, 0, numsSize-k-1);
    17. //逆置后 k 个数:
    18. reverse(nums, numsSize-k, numsSize-1);
    19. //最后整体逆置:
    20. reverse(nums, 0, numsSize-1);
    21. }
  • 相关阅读:
    【Java技术专题】「编译器专题」深入分析探究“静态编译器”(JAVA\IDEA\ECJ编译器)是否可以实现代码优化?
    不需要标注数据的语义分割!ETH&鲁汶大学提出MaskDistill,用Transformer来进行无监督语义分割,SOTA!...
    Typescript给定一个由key值组成的数组keys,返回由数组项作为key值组成的对象
    团建游戏大全
    线上展厅多元运用
    最全总结:这几种 Python 命令行参数化的方式真香啊
    JAVA毕业设计-酒店管理系统-计算机源码+lw文档+系统+调试部署+数据库
    【黑马-SpringCloud技术栈】【05】Nacos配置中心_搭建Nacos集群
    深入理解联邦学习——联邦学习的分类
    QT6不支持QDesktopWidget包含头文件报错Qt 获取设备屏幕大小
  • 原文地址:https://blog.csdn.net/weixin_63176266/article/details/132613140