1、时间复杂度和空间复杂度的概念

时间复杂度与空间复杂度是用来分析一个算法的效率的。

算法效率分析分为两种:

  1. 时间效率,时间效率被称为时间复杂度时间复杂度主要衡量的是一个算法的运行速度
  2. 空间效率,而空间效率被称作空间复杂度空间复杂度主要衡量一个算法所需要的额外空间

在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

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

2、大O的渐进表示方法和实例:

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

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

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

空间复杂度一般只有两种情况:
创建了常数个变量:O(1)
创建了N个变量:O(N)

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

实例1:

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^+2N+10 当N不断变大时,表达式的值也不断变大,而对表达式的结果影响最大的一项就是这个表达式中阶数最高的那一项。时间复杂度:O(N^2^)**

实例2:

void Func(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谁大,所以我们谁都无法省略。

实例3:

void my_strchr(char* str,char c)
{
    while (*str != '\0')
    {
        if (*str == c)
        {
            return str;
        }
        str++;
    }
}

​ 这是一个简单的字符串查找函数,并没有一个变量值来描述我们需要进行循环的次数,而觉得我们循环次数的是被查找字符串的长度,在时间复杂度的计算中,我们通常假设数组或字符串的长度为N,还有一个问题是这个算法中即使我们知道了字符串的长度但是我们执行循环的次数也是不一定的,因为我们不知道什么时候能够在字符串中找到我们寻找的元素,可能我们在第一个位置就找到了,也有可能我们要遍历整个字符串在最后一个元素的位置才能找到,出现这种情况时默认时间复杂度要以最坏的情况为准,即O(N)。

实例4:

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

​ 冒泡排序的函数,我们知道在进行冒泡排序时,我们首先要进行N次排序,然后在每次排序时,我们又要遍历这个数组,但是我们每进行一次排序,在下一次排序时我们就可以少遍历一个元素,所以我们可以得到*实际的运算次数F(N)=N+(N-1)+(N-2)……+2+1。这是一个等差数列,结果化简为F(N)=N(N-1)/2,所以时间复杂度为O(N²)**。

实例5:

int BinarySearch(int* a, int n, int x) 
{
    assert(a);
    int left = 0;
    int right = n - 1;
    while (left < right) 
    {
        int mid = left + ((right - left) >> 1);
        if (a[mid] < x)
            left = mid + 1;
        else if (a[mid] > x)
            right = mid;
        else
            return mid;
    }
    return -1;
}

​ 在进行二分查找时,我们每次都取数组的中间值,然后再根据中间值与查找值的大小来确定我们要查找的元素在那一半,然后在对找到的哪一半数组进行重复的操作,直到找到我们需要的元素,使用二分查找时,最坏的情况是我们把数组除的只剩最后一个元素,这时表达式为N÷2÷2÷2÷2…÷2÷2=1我们把这个式子换算一下为:N=1×2×2×2…×2×2我们每相乘一次,就进行了一次基本操作,所以上式中我们一共进行了log₂N次,所以时间复杂度为O(log₂N)。计算机一般写成O(logN)在算法分析 中表示是底数为2,对数为N

实例6:

long long Fibonacci(size_t N) 
{
    return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);
}

​ 递归函数,当我们输入N时,函数会进行两调用,然后不断地调用,但是我们可以把这一块空缺的区域看作一个常数,假设它是满的,那么我们执行调用的总次数为F(N)=2⁰+2¹+2²+…+2^(N-1)^ = 2^(N-1)^,所以该算法的时间按复杂度为:O(2^N^)
由以上的计算我们就可以发现用递归来算斐波那契数的算法的时间复杂度太高了,也就说明了这个算法的低效。

常见的时间复杂度基本就这几个:
O(1),O(N),O(N+M),O(logN),O(N^2^),O(2^N^),O(N!)
image.png
如图时间复杂度对比,随着Elemnets(元素)的增加走势越平缓的复杂度的算法越高效,牛逼。反之低效。