【数据结构与算法】(1):如何评估算法性能?

🤡博客主页:醉竺 

🥰本文专栏:《数据结构与算法》

😽欢迎关注:欢迎大家点赞评论+关注,祝您学有所成!


 ✨✨💜💛想要学习更多数据结构与算法点击专栏链接查看💛💜✨✨ 


目录

一.算法的基本概念

1. 算法

2. 算法的特性

二.算法效率的度量

2.1 理解数量级(理解)

 2.2 时间复杂度(理解)

 2.3 大O的渐进表示法(掌握)

 2.4 时间复杂度举例 


一.算法的基本概念

1. 算法

算法可以理解为由基本运算及规定的运算顺序所构成的完整的解题步骤, 或者看作按照要求设计好的有限的确切的计算序列。


2. 算法的特性

一个算法应该具有以下5个重要的特征

(1)有穷性

        一个算法必须保证执行有限步之后结束。

(2)确定性

        算法的每个步骤必须有明确的含义。

(3)可行性
        算法中的所有操作都必须通过已经实现的基本操作进行运算,并在有限次内实现,且人们用笔和纸做有限次运算后也可完成。

(4) 输入
        一个算法有0个或多个输入,以刻画运算对象的初始情况。所谓0个输入是指算法本身确定了初始条件
(5)输出
        一个算法有一一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的。

算法的5个特征:有穷性、确定性、可行性、输入、输出

通常,设计一个“好”的算法应考虑达到以下目标:

  • 正确性。算法应能够正确地解决求解问题。
  • 可读性。算法应具有良好的可读性,以帮助人们理解
  • 健壮性。输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果。
  • 高效率与低存储量需求。效率是指算法执行的时间,存储量需求是指算法执行过程中所40需要的最大存储空间,这两者都与问题的规模有关。

好算法的特点:正确性、可读性、健壮性、高效率与低质量存储


二.算法效率的度量

算法效率的度量是通过“时间复杂度”和“空间复杂度”来描述的。(更关注时间复杂度)!

 算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。 时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

注:学习时空复杂度之前必须明白什么是数量级!只有深刻理解这个才能更好的计算时间复杂度 


2.1 理解数量级(理解)

语句频度:一条语句的重复执行次数称作语句频度。

09bde5e572514632bed79c69f4b7a375.png

所以用语句频度之和:f(n),来描述算法的时间总消耗。

接下来思考一个重要的问题:

n趋向于无限大时它们谁增长的最快?谁的函数值最大?

1. n^2 和 n,    2. n^2 和 nlogn,  3. n^2 和 8n,4. n^2 和 nlogn + 7n ....

(下面用简单的高中知识和一个小结论来解答这个问题)

结论:n趋向于无穷大时:1 < log2^n < n < nlog2^n < n^2 < n^3 < 2^n <n! < n^n 

4c7e2f73bbaa4809899a7a4b1893f2bd.png

下面的这张图片也是重点! 

37d94781e60946febd1a3a40d7ce0135.png

对于上面的问题,结合高中知识以及上张图片有: 

n趋向于无穷大时则有:n^2>n,  n^2 > nlogn,  n^2 > 8n,4. n^2 > nlogn + 7n。

时间复杂度跟上面的结论和高中知识有啥关系呢?

答:从上面可知,当n趋向于无穷大的时候,只需要比较函数表达式中数量级的大小就能判断出来频度之和的大小,即使数量级小的那一方再加上一些小数量级也不会影响最后的比较结果。 

因此应用到时间复杂度上:一个算法其语句运算次数的数量级越大,代表的时间复杂度也就大,同时比较两个算法时间复杂度的时候,只需比较两者自身最大的数量级即可,其余较小的数量级不用参与比较可以直接舍去。(大O阶表示法) 


 2.2 时间复杂度(理解)

        时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,并不是真的计算一个算法运行的时间!而是看一个算法中语句执行的次数是否达到某个数量级,或者说达到了某种规模如果达到了这个数量级或规模,那么这个数量级或规模就说是这个算法的时间复杂度。(接着往下看会越来越明了~,回头再体会下这句话)

        为什么要用执行次数来判断一个算法的时间复杂度呢?因为一个算法所花费的时间与其中语句的执行次数成正比例,例如:同一个问题用两种算法都能解决,不用想也是哪一个算法执行的次数越多,算法的效率越差。

总结:算法中基本操作的执行次数,作为算法的时间复杂度的度量。我们的目标就是找到算法基本操作的执行次数达到了哪种量级或规模) 

说了那么多接下来看看例子吧!看例子之前先学一下大O的渐进表示法,学会这个对时间复杂度的数量级或者规模就有了深刻的认识!


 2.3 大O的渐进表示法(掌握)

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

推导大O阶方法:(重中之重!!)

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

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

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

常见的大O阶有:

9ae60dff09014645b167bf6f68aa25db.jpeg

 f3e6f846659341d8a23e38fe2246ba0d.png

除了上面的大O表示法,和2.1节的一个结论,还有一个看似复杂其实很简单但又重要的大O计算规则: 

加法规则—— O( f(n) )+O( g(n) ) = O( max( f(n), g(n) ) )  

解释:两个数量级相加的时候,两者中较大的数量级为最终的数量级。例如:O(n^2)+O(n) = O(n^2),数量级小的省略。(我和马云的身家加一起排名福布斯富豪榜前三,然而我并没有产生多大的作用...)

乘法规则—— O( f(n) )*O( g(n) ) = O( f(n)*g(n) )

解释:两个数量级相乘的时候,两个数量级的乘积

下面举个小例子:

案例1:请计算一下Func1中++count语句总共执行了多少次?

void Func1(int N)
{
    int count = 0;

    for (int i = 0; i < N ; ++ i)
    {
         for (int j = 0; j < N ; ++ j)
         {
             ++count; //N^2次
         }
    }

    for (int k = 0; k < 2 * N ; ++ k)
    {
         ++count; //2*N次
    }

    int M = 10;
    while (M--)
    {
         ++count; //这里的M为常数次,10次
    }
    printf("%d\n", count);
}

解:Func1 执行的基本操作次数 : f(n)=N^2 + 2*N + 10

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

使用大O的渐进表示法计算:首先常数10属于常数数量级0(1), 2*N数量级为O(N),N^2数量级为O(N^2)。只保留最高项N^2,(其他项作用太小,类比我跟马云财富总和的那个例子),所以 Func1的时间复杂度为:O(N^2)。

有的同学看到这里可能会有一个小疑惑?为什么语句 int count = 0; printf("%d\n", count);这样的语句不把它们的执行次数也加上?

解释如下: 

我们在计算基本操作的执行次数时一般找最内层的语句,因为它可以反应出算法的复杂度。例如:循环语句里面的最内层。而单独的一个表达式语句像声明、定义变量等等,这种不需要计算次数在内,是因为我们是计算执行次数的数量级,这些单独的表达式语句即使加起来,执行次数也只是一个常数,哪怕一千,一万,一百亿,它也只是一个常数!它的数量级也只是常数级O(1),只要表达式有数量及大于O(1),这个常数数量级也会被“舍去”。

即:大O的渐进表示法会去掉那些对结果影响不大的项,简洁明了的表示出了执行次数。 

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

 (这个时候我们应该按照那个标准呢?)

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

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

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

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

最好情况:1次找到

最坏情况:N次找到

平均情况:N/2次找到

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


 2.4 时间复杂度举例 

实例1:计算Func2的时间复杂度?

void Func2(int N)
{
     int count = 0;

     for (int k = 0; k < 2 * N ; ++ k)
     {
         ++count; //2*N次
     }

     int M = 10;
     while (M--)
     {
         ++count; //10次
     }

     printf("%d\n", count);
}

实例1: 由大O表示表示法可知(没记住的往上再看一下),时间复杂度为O(N)

实例2:计算Func3的时间复杂度? 

void Func3(int N, int M)
{
     int count = 0;

     for (int k = 0; k < M; ++ k)
     {
         ++count; //执行M次
     }

     for (int k = 0; k < N ; ++ k)
     {
         ++count; //执行N次
     }

     printf("%d\n", count);
}

 实例2: 由大O表示表示法可知,时间复杂度为O(N) + O(M)

 实例3: 计算Func4的时间复杂度?

void Func4(int N)
{
     int count = 0;

     for (int k = 0; k < 100; ++ k)
     {
         ++count; //执行100次
     }

     printf("%d\n", count);
}

实例3: 由大O表示表示法可知,时间复杂度为O(1)

实例4: 计算BubbleSort的时间复杂度?

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

实例4:时间复杂度为O(n^2)

冒泡排序:n个元素进行排序的话,那么需要进行n-1轮,每一轮元素需要比较n-1-i次,(i是第几轮数),所以总的比较次数是n-1,n-2,n-3,n-4,......1,所有加一块,用等差公式计算:项数 *(首项+尾项) / 2,即(n-1)*(n-1+1)/2 ---->(n-1)*n / 2 次数,所以时间复杂度为O(n^2)

实例5:计算BinarySearch的时间复杂度?

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

 实例5:BinarySearch的时间复杂度:O(log2^n)

f375c9dcf9d64ad68ba42071de094c64.png   

实例6:计算阶乘递归Fac的时间复杂度?

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

     return Fac(N-1)*N;
}

 实例6:阶乘递归Fac的时间复杂度:O(N)

该递归函数总共递归了N次,时间复杂度为O(N),每递归一次的时间复杂度为0(N),所以总的时间复杂度为O(N)。如下图

d05cc26ccb6b45e2a162cc340ea633f4.png

 实例7:计算结成递归Fac的时间复杂度?

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

    for (size_t i = 0; i < N; ++i)
    {
        //...
    }

        return Fac(N-1)*N
}

实例7:阶乘递归Fac的时间复杂度:O(N^2)

该递归函数总共递归了N次,时间复杂度为O(N),每递归一次的时间复杂度为0(N),所以总的时间复杂度为O(N^2)。如下图 

70dfa158381747dfac093f428501e059.png

实例8: 计算斐波那契数Fib的时间复杂度?

long long Fib(size_t N)
{
    if (N < 3)
        return 1;

    return Fib(N-1) + Fib(N-2)
}

实例8:斐波那契数Fib的时间复杂度:O(2^N)

解释如下图 

 d9d1ffcee96f43d2aa8f3917b05bbc81.png


 2.5 空间复杂度 

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

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

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

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


 2.6 空间复杂度举例 

实例1:计算BubbleSort的空间复杂度?

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

实例1:空间复杂度为O(1),灭有额外显示开辟空间

实例2:计算Fibonacci的空间复杂度?

// 返回斐波那契数列的前n项
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;
}

实例2:Fibonacci的空间复杂度为O(n),显示申请了n+1个额外空间,大O阶表示法,空间复杂度为O(n).

 实例3:计算阶乘递归Fac的空间复杂度?

// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
     if(N == 0)
         return 1;

     return Fac(N-1)*N;
}

0cc2a4a221c647169f3277a4534e53ac.png

 实例3:阶乘递归Fac的空间复杂度为:O(n)

 总结上述3个实例:

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

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

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

创作不易,看到这里了,希望你能动动手点个赞支持一下吧~让我更有动力更新!

  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

醉竺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值