【数据结构】 算法的时间复杂度和空间复杂度 (上)(附leetcode练习题)

☃️个人主页:fighting小泽
作者简介:目前正在学习C语言和数据结构
博客专栏:数据结构
️欢迎关注:评论点赞留言

文章目录

  • 1.算法效率
    • 1.1 如何衡量一个算法的好坏
    • 1.2 算法的复杂度
  • 2.时间复杂度
    • 2.1时间复杂度的概念
    • 2.2 大O的渐进表示法
    • 补充
    • 2.3 leetcode练习题
    • 方法1
    • 方法2(重点)
    • 方法3
  • 结尾

1.算法效率

1.1 如何衡量一个算法的好坏

如何衡量一个算法的好坏呢?比如对于以下斐波那契数列:

long long Fib(int N)
{
 if(N < 3)
 return 1;
 
 return Fib(N-1) + Fib(N-2);
}

斐波那契数列的递归实现方式非常简洁,但简洁一定好吗?那该如何衡量其好与坏呢?

1.2 算法的复杂度

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

注意:在不同的计算机上执行相同的代码也会因为计算机硬件的不同而花费不同的时间。
比如我们进行一个数量较多的冒泡排序,在 i3,1g内存的机器和 i9,16g内存的机器上花费的时间肯定不同

2.时间复杂度

2.1时间复杂度的概念

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

即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

请计算一下Func1中++count语句总共执行了多少次?
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);
}

Fun进行了一次N * N 的循环,一次 2*N 的循环和一次 10 次的循环
即 Fun=(N * N) + (2 * N) + 10

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

我们发现,随着N的增大,后面项对结果的影响越小,所以实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法

2.2 大O的渐进表示法

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
使用大O的渐进表示法以后,Func1的时间复杂度为:**O(N^2)

N = 10 F(N) = 100
N = 100 F(N) = 10000
N = 1000 F(N) = 1000000
通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

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

Fun2一共执行了2n+10次,所以它的时间复杂度是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);
}

Fun3一共执行了M+N次,由于我们不知道M和N的关系,可能M特别大,可能N特别大,也可能它俩一样大,M和N都会影响整个程序运行的时间,而M和N又是未知的,所以Fun3的时间复杂度是O(M+N)

练习3:

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

Fun4执行了100次,为常数次,所以它的时间复杂度为O(1)

练习4:

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

strchr就是用来从一个字符串中找某个特定的字符,可能一开始就找到了,也可能很久都没找到,但是我们在求时间复杂度的时候一般都是按照最坏打算来求的,也就是未知N次。它的时间复杂度为O(N)

补充

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

练习5:

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

在冒泡排序中会从第一个数开始不断的进行两个数的比较,然后不断的把大数排在后面,所以每次比较的次数会减一,所以冒泡排序整体进行的次数为一个等差数列,用等差数列公式即 N * (1+N) / 2。它的最高项为N ^ 2,所以冒泡排序的时间复杂度为O(N ^ 2).

练习6:


计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );
// 计算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;
 }
}
// 计算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;
}

二分查找就是每次除以2嘛,2 ^ x = n,则 x = log n 。时间复杂度就为O(logN)。

注意:当我们想在一个有序数列找一个特别大的数时,暴力查找可能要进行几百万或者几亿次查找,而二分查找只用几十次就可以了(有没有感觉二分查找很厉害)
注:qsort的时间复杂度为(N*logN),后面我们会讲到的

【数据结构】 算法的时间复杂度和空间复杂度 (上)(附leetcode练习题)_第1张图片
练习7:

 计算阶乘递归Fac的时间复杂度?
long long Fac(int N)
{
 if(0 == N)
    return 1;
 for(int i = 0;i < n; i++)
 {
   //....
 }
 
 return Fac(N-1)*N;
}

Fac一共进行N次递归,每次递归会进行N(当前的N)次循环,即进行1次,2次…N次循环,所以它的本质也是一个等差数列,时间复杂度为O(N^2)
【数据结构】 算法的时间复杂度和空间复杂度 (上)(附leetcode练习题)_第2张图片

2.3 leetcode练习题

数组nums包含从0到n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?力扣 ,消失的数字

方法1

我们可以把整个数组进行排序,然后遍历。如果下一个数不是上一个数+1,则上一个数+1就是消失的数,这个方法大家可以自己尝试一下,我们就不写了

方法2(重点)

异或法:首先将sums所有的数进行异或(^)得到number1,number1再异或从0~n的数字得到number2,则得到的number2就是最终消失的那个数

int missingNumber(int* nums, int numsSize){
    int i=0;
    int x=0;
    for(i=0;i<numsSize;i++)
    {
        x ^=nums[i];
    }
    for(i=0;i<numsSize+1;i++)
    {
        x ^= i;
    }
    return x;
}

方法3

等差数列求和法:我们可以通过等差数列求出0—n的值,然后减去数组的每一个元素,剩下的就是消失的数字

int missingNumber(int* nums, int numsSize)
{     
    int add_1 = 0;
    int add_2 = 0;
    add_1 = ( (0 + numsSize) * (numsSize + 1) ) / 2;
    for(int i = 0; i < numsSize; i++)
    {
         add_2 += nums[i];
    }
    return add_1 - add_2;
}

结尾

这些就是我给大家分享的关于算法的复杂度的知识啦,希望我们都能有所收获
先赞后看,养成习惯!!^ _ ^
码字不易,大家的支持就是我坚持下去的动力,点赞后不要忘了关注我哦!

如有错误,还请您批评改正(。ì _ í。)

你可能感兴趣的:(数据结构,c++,c语言,开发语言,数据结构,算法)