数据结构之复杂度

复杂度

 

纲要:

  • 算法效率
  • 时间复杂度
    • 概念
    • 大O的渐进表示法
    • 示例
  • 空间复杂度
    • 概念
    • 示例

 


  在我们学习完C语言之后,我们就要蹦着向更高处走了,所以今天,我们来到了数据结构。

  下面呢,就正式开启数据结构的大门!

 

 

一.算法效率

  算法效率分析分为两种:

    1.时间效率

      时间效率又叫做时间复杂度,它衡量的主要是一个算法的运行速度。

    2.空间效率

  空间效率又叫做空间复杂度,它衡量的主要是一个算法所需要的额外空间。在计算机发展的早期,因为科技水平有限,往往计算机的容量很少,但如今科技急速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

二.时间复杂度

   1.概念

  在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是同一个算法在不同性能的机器上也会有不同的差异,所以才有了时间复杂度这个分析方式。

  因一个算法所花费的时间与其中语句的执行次数成正比例,所以,算法中的基本操作的执行次数,为算法的时间复杂度。

 

  比如所如下代码,它的执行次数为多少呢?

#include <stdio.h>

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

  显而易见,它的执行次数为 F=N*N+2*N+10;

  往往我们在计算时间复杂度的时候,我们写的是一个概数,为什么呢?

  我们不妨设想一下,对于上面的执行次数函数,当N趋向于无穷大时,那个10对于它的影响微乎其微。如果是一个高阶函数,这个函数的大小往往取决于表达式中次数最高的项,所以我们的时间复杂度也采取这种思想。

  我们不妨来测试一下:

  

#include <stdio.h>
#include <time.h>

void Func1(int N)
{
    int start = clock();
    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;
    }
    int end = clock();
    printf("%d\n", end - start);//单位为毫秒
}

int main()
{
    Func1(0);//0
    Func1(100);//0
    Func1(10000);//386
    return 0;
}

  我们发现,差异主要是在最高次上面。所以接下来我们来介绍大O的渐进表示法。

   2.大O的渐进表示法

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

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

    推导大O阶方法:

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

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

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

  所以,使用大O的渐进表示法以后,Func1的时间复杂度为:O(N^2);

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

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

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

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

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

  例如:

//描述:传递一个数组,给定一个数,查找该数组中是否含有这个数
int FindNum(int* arr,int N,int search_num)
{
    int i = 0;
    for(i=0;i<len;i++)
    {
        if(search_num == arr[i])
            return 1;//找到则返回 1
    }
    return 0;//找不到则返回 0
}

  它的最好情况为 1 ,即只执行一次就找到了

  平均情况:N/2

  最差情况: N ,遍历了整个数组。

  那么,对于这个算法,它的时间复杂度是多少呢?答案是: O(N)

  在计算时间复杂度时,我们往往取得是它的最差情况。

 

   3.示例

// 1.计算Func2的时间复杂度?
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.计算Func3的时间复杂度?
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(N+M) ,因为在这个式子中,我们并不知道M和N的大小情况,如果N>>M,那这题的时间复杂度就是O(N)

 

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

  对于它,执行次数为一个确定的常数:100,所以它的时间复杂度为 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基本操作执行最好N次,最坏执行了(N*(N+1)/2次,通过推导大O阶方法+时间复杂度一般看最坏,所以时间复杂度为 O(N^2)

 

// 5.计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
    assert(a);
    int begin = 0;
    int end = n-1;
    while (begin < end)
    {
        int mid = begin + ((end-begin)>>1);
        if (a[mid] < x)
            begin = mid+1;
        else if (a[mid] > x)
            end = mid;
        else
            return mid;
    }
    return -1;
}

  实例5基本操作执行最好1次,最坏O(logN)次,时间复杂度为 O(logN)(我们可以通过折纸查找的方式理解logN是怎么计算出来的)

 

 

 

  注:

    logN在算法分析中表示是底数为2,对数为N。有些地方会写成lgN。

 

// 6.计算阶乘递归Factorial的时间复杂度?
long long Factorial(size_t N)
{
    return N < 2 ? N : Factorial(N-1)*N;
}

  对于递归的时间复杂度我们怎么算呢? 

  递归的时间复杂度  = 递归次数*每次递归中的次数

  对于上题,一共递归了N次,每次递归中只有一次,所以时间复杂度为O(N)

 

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

  在这个例中,一共递归了N次,而在递归中的次数为2^N-1,所以最后的时间复杂度为:O(N^2)

 

 

   注:

    如果有一个递归有N层,且每一层中又有N次,那么它最后的时间复杂度为 O(N^2)

 

三.空间复杂度

   1.概念

  空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少 bytes的空间,因为这个也没太大意义,

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

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

   2.示例

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

  我们要注意在计算时,我们计算的是算法需要的额外空间,不考虑数组输入的空间。并且我们要注意,空间是可以复用的,比如所某个算法需要创建一个变量并不断的为这个变量赋值,这时这个空间复杂度为O(1);

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

 

//2. 计算生成的Fibonacci数组的空间复杂度?
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动态开辟了N个空间来放N个斐波那契数,所以空间复杂度为 O(N)

 

//3. 计算阶乘递归Factorial的空间复杂度?
long long Factorial(size_t N)
{
    return N < 2 ? N : Factorial(N-1)*N;
}

  递归算法的空间复杂度通常情况下为 递归的深度 (递归的次数)

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

 

 

那么在最后我们再来看一张图:

 

  这就说明有时候  不同的时间复杂度在N小的时候都还接近,当N越来越大时差距越来越多!

 

 

 

 

 

 

 

 

 

 

 

 


 

|——————————————————–

关于复杂度的知识到这便结束了

因笔者水平有限,若有错误,还望指正