• 数据结构——时间复杂度和算法复杂度


    目录

    时间复杂度

     计算下列函数的时间复杂度

    冒泡排序时间复杂度 

     大O的渐进表示法

    旋转数组 

    空间复杂度


    时间复杂度

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

     计算下列函数的时间复杂度

    1. // 请计算一下Func1中++count语句总共执行了多少次?
    2. void Func1(int N)
    3. {
    4. int count = 0;
    5. for (int i = 0; i < N ; ++ i)
    6. {
    7. for (int j = 0; j < N ; ++ j)
    8. {
    9. ++count;
    10. }
    11. }
    12. for (int k = 0; k < 2 * N ; ++ k)
    13. {
    14. ++count;
    15. }
    16. int M = 10;
    17. while (M--)
    18. {
    19. ++count;
    20. }

     时间复杂度函数式:N*N+2*N+10

    Func1 执行的基本操作次数 :


    N = 10 F(N) = 130
    N = 100 F(N) = 10210
    N = 1000 F(N) = 1002010

    这里计算清楚该函数在那个量级就行,不必求具体值,上式中后俩项对F(N)的影响很小,

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

    这里时间复杂度:O(N^2)

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

    F(N)=2*N+10,2和10对F(N)的影响不大,所以是O(N)

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

    F(N)=M+N,

    M远大于N,时间复杂度就是O(M)

    N远大于M,时间复杂度就是O(N)

    N和M差不多大,时间复杂度就是O(M)或O(N)

    N=M,时间复杂度O(2M)或O(2N)

    冒泡排序时间复杂度 

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

    N个元素, 冒泡排序第一轮比较N-1次

                                    第二轮N-2次

                                             .

                                             .

                                             .

                              最后一轮1次

    1+2+3……+N-1=N*(N-1)/2=F(N)

    对F(N)影响最大的一项是N^2/2,所以时间复杂度:O(N^2)

     大O的渐进表示法

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

    推导大O阶方法(量级的估算):
    1、用常数1取代运行时间中的所有加法常数。
    2、在修改后的运行次数函数中,只保留最高阶项
    (对结果产生决定性影响)。
    3、如果最高阶项系数存在且不是1,则去除与这个项目相乘的常数(参考上面的冒泡排序和M+N那道题)。得到的结果就是大O阶。

    1. void Func4(int N)
    2. {
    3. int count = 0;
    4. for (int k = 0; k < 100; ++ k)
    5. {
    6. ++count;
    7. }
    8. printf("%d\n", count);
    9. }

    如这道题,用常数1取代运行时间中的所有加法常数,这道题算法复杂度为O(1),O(1)不代表一次,代表常数次

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

    1. // 计算strchr的时间复杂度?
    2. const char * strchr ( const char * str, int character );

    strchar函数遍历一个字符串,找某一个字符

    另外有些算法的时间复杂度存在最好、平均和最坏情况:
    最坏情况:任意输入规模的最大运行次数(上界)
    平均情况:任意输入规模的期望运行次数
    最好情况:任意输入规模的最小运行次数(下界)
    例如:在一个长度为N数组中搜索一个数据x
    最好情况:1次找到
    最坏情况:N次找到
    平均情况:N/2次找到
    在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)

    这道题最好情况:O(1)

    平均情况:O(N/2)

    最坏情况:O(N)

    对于冒泡排序

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

    时间复杂度最坏:O(N^2),

    时间复杂度最好:O(N) 这种情况是数组排好序了,遍历了N个元素

    时间复杂度不能去数循环,而是要对程序进行分析

    这是希尔排序,有三层循环,不能把这个时间复杂度认为是O(N^3),这个排序要比冒泡排序快,这个排序平均下来O(N^1.3),因此我们不能通过循环去确定时间复杂度,要看算法的逻辑进行时间复杂度计算

    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(1)

    最坏的情况,当找完所有的数后没有找到某元素

    二分查找每次会缩放一半的区间,若区间大小为N,第一次在N/2个元素查找,第二次N/2/2,第三次N/2/2/2……=1,以此类推

    每查找一次,查找区间的个数减少一班,最后就剩一个值了

    假设查找了X次,N=2^X,X=log 2 N(以二为底,N的对数)

    X就是时间复杂度O(log 2 N)

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

    时间复杂度为:O(N)

    Fac(N)>Fac(N-1)>Fac(N-2)……F(0)

    这里总共要调用N次,这里递归了N次,每次是O(1)

    1. // 计算阶乘递归Fac的时间复杂度?
    2. long long Fac(size_t N)
    3. {
    4. int i=0;
    5. for(i=0;i
    6. printf("%d",i);
    7. if(0 == N)
    8. return 1;
    9. return Fac(N-1)*N;
    10. }

    对上题做修改后,时间复杂度变为O(N^2)

    N+N-1+N-2+N-3......+1

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

    2^0+2^1+2^2+2^3.... +2^(n-1)=2^N-1

     时间复杂度:O(2^N)

    我们可以看到左边缺了一块,说明少了一些调用次数,但是少的这些调用次数和2^N相比可以忽略不计 。

    优化斐波那契数列(把递归改循环)

    1. long long Fib(size_t N)
    2. {
    3. long long f1=1,f2=1,f3;
    4. for(size_t i=0;i<=N;i++)
    5. {
    6. f3=f2+f1;
    7. f1=f2;
    8. f2=f3;
    9. }
    10. return f3;
    11. }

    旋转数组 

    给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

    输入: nums = [1,2,3,4,5,6,7], k = 3
    输出: [5,6,7,1,2,3,4]
    解释:
    向右轮转 1 步: [7,1,2,3,4,5,6]
    向右轮转 2 步: [6,7,1,2,3,4,5]
    向右轮转 3 步: [5,6,7,1,2,3,4]

     方法1:

    1. void rotate(int* nums, int numsSize, int k) {
    2. int newArr[numsSize];
    3. for (int i = 0; i < numsSize; ++i) {
    4. newArr[(i + k) % numsSize] = nums[i];
    5. }
    6. for (int i = 0; i < numsSize; ++i) {
    7. nums[i] = newArr[i];
    8. }
    9. }

     方法二:

    以空间换时间

    新开辟一块空间把后K个拷贝到数组前面,前N-K个拷贝到数组后面 ,之后将TMP数组拷贝回原数组,然后释放tmp数组

    方法二:

     

     

     

    空间复杂度

    空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度

    空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。

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

    注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因 此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。

     

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

    冒泡循环空间复杂度:O(1),这里数组是创建好的不是临时占用的,这里只有三个变量end i 和exchang,每次循环当中end i exchang都各自占用一个空间,循环了N次,end只占了一个空间,i和exchange也一样,所以空间复杂度是O(1),

    旋转数组

    这个空间复杂度是O(N),因为中间有创建一个临时数组来接收旋转的结果 

    时间可以累计,空间可以重复利用

    如果是结构体,还是O(1),不用考虑里面的成员个数,把结构体当成一个整体

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

    空间复杂度为O(N),因为函数调用会涉及到栈帧的创建与销毁,每次在调用完一个函数之后,会把这块空间还给操作系统,这里对一个函数进行了调用,无论调用了多少次,函数所占的空间都一样,只不过是每次调用完之后会还给操作系统,每次调用的时候空间复杂度为O(1),调用N次就是O(N)

    不管调用多少次,函数仍然占据 

     

     

  • 相关阅读:
    传奇开服教程:传奇开服在哪些网站打广告?传奇发布站打广告技巧
    ValueAnimator的一些骚玩法
    【LMKD】十 lmkd进程查杀配置
    【Unity入门计划】2D游戏中遮挡问题的处理方法&伪透视
    windows cmd 常用操作命令
    ErrCode: 13102001没有找到名称为 ‘xxx‘ 的数据源
    通过Python Pandas分析数据上涨下跌趋势的方法:求离散数据的差分、导数
    【单片机毕业设计】【mcuclub-jk-003】基于单片机的非接触红外测温的设计
    “px、pt、ppi、dpi、dp、sp”全攻略
    flask自定义序列化
  • 原文地址:https://blog.csdn.net/weixin_49449676/article/details/125963893