@toc


⌛前言⌛

学习了数据结构第一章时间空间复杂度计算,这篇应该近几周的最后一次更新了开学得军训两周,两周之后再继续学习。

【数据结构】——时间复杂度、空间复杂度


⛵一、 什么是数据结构?

数据结构 (Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。


⚽二、什么是算法?

算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单 来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。


⛲三、什么是时间复杂度和空间复杂度

1.算法效率

算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间,在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度

2.时间复杂度

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

3.空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。


✨四、时间复杂度

1.时间复杂度表示

因为在不同的硬件电路中,同一个程序编译花费的时间是不同的,因此,计算时间复杂度不能直接计算程序运行花费的时间,而算法中的基本操作的执行次数,就是算法的时间复杂度。时间复杂度用大写字母O加上( )表示,即O( );

2.大O的渐进表示法

推导大O阶方法:

  1. 用常数1取代运行时间中的所有加法常数。
  2. 在修改后的运行次数函数中,只保留最高阶项。
  3. 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

请计算下列代码的时间复杂度:

//例1
void fun1(int n)
{
    int count = 0;
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < n; j++)
        {
        ++count;
        }
    }

    for (int i = 0; i < 2 * n; i++)
    {
        ++count;
    }

    int M = 10;
    while (M--)
    {
        ++count;
    }
    printf("%d", count);
}

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

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

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

实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
因此这个程序的时间复杂度可以表示为:
$$
O(N^2)
$$


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

最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)

例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到

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


练习题

题目1:

//例1
void fun2(int n)
{
    int count = 0;
    for (int i = 0; i < 2 * n; i++)
    {
        ++count;
    }
    int M = 10;
    while (M--)
    {
        ++count;
    }
    printf("%d", count);
}

此时算法的执行次数是2N+10次,通过推导大O的渐进表示法 时间复杂度是O(N)


题目2

//例2
void fun3(int N,int M)
{
    int count = 0;
    for (int i = 0; i < N; i++)
    {
        ++count;
    }
    for (int i = 0; i < M; i++)
    {
        ++count;
    }
    printf("%d", count);
}

此时算法的执行次数是M+N次,有两个未知数,时间复杂度是O(M+N);
如果给了条件:

  1. M远大于N,时间复杂度是O(M);
  2. M,N大小差不多,时间复杂度是O(M)或O(N);

题目3:

//例3
void fun3(int N, int M)
{
    int count = 0;
    for (int i = 0; i < 100; i++)
    {
        ++count;
    }
    printf("%d", count);
}

执行次数为100次 为是常数,所以时间复杂度为O( 1 )


题目4

//例4
const char* strchr(const char* str, char character)
{
    while (*str != '\0')
    {
        if (*str == character)
            return str;

        ++str;
    }
    return NULL;
}

sttrchr是在一个字符串中查找一个字符,查找次数最好O(1),最坏O(2),根据大O的渐进表示法所以时间复杂度为O(N)


题目5

//例5 冒泡排序
void BUbbleSort(int* a, int n)
{
    assert(a);
    for (size_t end = n; end > 0; --end)
    {
        int exchange = 0;
        for (size_t i = n; i > end; ++i)
        {
            if (a[i - 1] > a[i])
            {
                Swap(&a[i - 1], &a[i]);
                exchange = 1;
            }
        }
        if (exchange == 0)
            break;
    }
}

因为比较的数字是n-1,n-2,n-3…3,2,1反过来就是1,2,3,…,n-3,n-2,n-1规律就是一个等差数列根据等差数列求和公式(n^2+n)/2,

那么根据大O的渐进表示法就是O(N ^ 2)


题目6

//例6
//有序数组二分查找
int BinarySearch(int* a, int n, int x)
{
    assert(a);
    int begin = 0;
    int end = n;
    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;
}

在一个有序的数组中一直二分直到找到要找的那个数字,那么n/2/2/2…=1,那么就可以变换为2^x=n,那么x就是log以2为底,n的次方,时间复杂度就是
$$
O(log2^N)
$$


题目7

//例7
//求阶乘递归
long long Factorial(size_t N)
{
    return N < 2 ? N : Factorial(N - 1) * N;
}

这题N是不是要递归到1才停止递归,那么就是递归N次。

这个题目求N阶层,那么算法的执行次数就是N,时间复杂度就是O(N)

递归复杂度=递归次数*每次递归函数次数


题目8:

//例8
//求斐波那契数列
long long Fibonacci(size_t N)
{
    return N < 2 ? N : Fibonacci(N - 1) + Fibonacci(N - 2);
}

大O的渐进表示法时间复杂度是O(2^N)

画图分析:

image-20210908154358864

非递归算法 O(n)=n

//非递归写法
int fabio_for(int n)
{
    int f1=1;
    int f2=1;
    int f3=1;

    for(int i=2;i<n;i++)
    {
    f3=f1+f2;
    f1=f2;
    f2=f3;
    }
    return f3;
}

⌚五.空间复杂度

1.空间复杂度表示

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,即在函数中有几个临时变量

2.空间复杂度经典题目

题目1

void BUbbleSort(int* a, int n)
{
    assert(a);
    for (size_t end = n; end > 0; --end)
    {
        int exchange = 0;
        for (size_t i = n; i > end; ++i)
        {
            if (a[i - 1] > a[i])
            {
                Swap(&a[i - 1], &a[i]);
                exchange = 1;
            }
        }
        if (exchange == 0)
            break;
    }
}

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


题目2


// 计算Fibonacci的空间复杂度?
long long* Fibonacci(size_t n)
{
    if (n == 0)
        return NULL;
    //动态开辟了n+1个空间
    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;
}

这里动态开辟了n+1个空间,所以空间复杂度是O(N)。


题目3:

//求阶乘递归
long long Factorial(size_t N)
{
    return N < 2 ? N : Factorial(N - 1) * N;
}

里递归每次调用一次函数,就会开辟一次栈帧,每个栈帧使用了常数个空间,所以空间复杂度为O(N)