数据结构——复杂度讲解

作者:几冬雪来

时间:2023年2月19日

内容:数据结构——复杂度讲解

 

目录

前言: 

复杂度讲解: 

1.算法效率: 

2.时间复杂度:

 结尾:


前言: 

从今天开始我们就要开始从C语言向C++过渡了,那么在正式学习C++之前,我们要先学习一些新内容为我们以后学习C++做铺垫,现在我们就来开始学习我们的数据结构的复杂度讲解。 

复杂度讲解: 

数据结构在我们现实中和我们的数学有一点相识,数学也有数学的复杂度,解题时间的多少,解题过程的多少,这些都可以被我们称为复杂度。当然,在我们的数据内容书写的过程中也存在复杂度的问题,那么我们复杂度讲解需要什么东西来衡量?这个东西就是我们的——算法效率

1.算法效率: 

 在我们上面讲解到了,我们的复杂度是由我们的算法效率来衡量的,但是我们的算法效率也可以被称为一个结合体,实际上我们的算法效率是我们的时间效率和空间效率的结合。也就是我们写代码所需要使用的时间和空间。

那么怎么看出一个代码的好坏呢?

首先我们的代码的运行效率并不止和我们的代码的好坏有关,在不同的环境下我们的代码运行所需要的时间也可能不同。因此在在不同的环境下,我们环境下我们不能直接比较出一个算法的好坏。

在不同的环境下,同样的算法运行时间都可能不相同,如果是不同的算法的话就更不用说了。因此基于不一样的原因,程序员在比较性能的时候往往不比较时间,而是比较代码大概执行的次数

2.时间复杂度:

例如我们下面这道题:

这个代码的执行次数我们可以轻松的算出来,在第一个嵌套循环中我们每次循环执行N次,一共有N次循环,所以执行N^2次在第二个循环中我们可以轻而易举的看出执行了2*N次最后一次因为我们的M有具体得到值,所以执行10次。 

所以我们一共执行了—— F(N)= N ^ 2 + 2 * N + 10 次 

这里也可以看出来我们执行的次数其实是一个带有未知数的函数式。所以我们的算法的时间复杂度是一个函数,这个函数不是我们C语言中的函数,而是我们数学中带未知数的函数。这个函数可以帮我们计算这个函数执行多少次。

但是如果要求复杂度要精准的计算到它会运行多少次,那么这样也是一件麻烦的事情,既然我们的需要得到的执行次数是一个大概的值,那么我们能不能将我们的表达式进行简化呢?那当然是可以的,这里我们计算的就是一个量级。

通过计算我们可以看出来,当N的值在不断的增大的时候,我们后面两项对我们结果的影响会不断的变小。 在这个表达式中,N^2对我们表达式的影响是最大的。当N的值趋近于无限大的时候,我们后两项的影响就已经是可有可无的了,这个时候我们就可以将我们的表达式简化为。

大概次数:F(N) = N ^ 2

所以在计算计算复杂度的过程中我们就运用到了一个新的知识——大O的渐进表示法。 

我们的这个表示法实际上就是去掉那些对结果影响不大的项。 这里我们就再举几个例子让大家更加熟悉一下。

在这个代码的时间复杂度的计算中就可能会引发争议有的人认为这个代码的时间复杂度的O(N),也有的人认为这个代码的时间复杂度的O(2*N)。那么这个代码的时间复杂度是哪一个?

我们想从这个代码的准确的运行次数下手:

代码运行次数为:F(N) = 2 * N + 10 次

随着我们N的值的增大,我们代码中的2和10的影响逐渐可以忽略不计,因此计算这个代码的时间复杂度的时候,我们的系数2也要去掉,只要这个系数是一个确定的常数我们就要去掉。 所以这个代码的时间复杂度实际上是O(N)

接下来我们再来一道题。

在这一个代码中,我们无法识别我们的M和N谁大谁小,因此我们这一个代码的时间复杂度实际上是O(M + N)。 但是如果我们这里的知道我们的M远大于N的话我们的时间复杂度就可以写为O(M),相同的如果是N远大于M的话,我们的时间复杂度就是O(N)如果我们前提是M等于N的话,我们的时间复杂度可以写为O(M)或者O(N)

 注:这里我们必须是远大于,只是简单的大于是不行的。 

了解完了上面的代码后,我们再来看看这个代码的时间复杂度是多少?  

有人可能说是O(100)也有人说是O(0)。那么实际上我们这个代码的时间复杂度是O(1) 这里的1并不是指我们的代码只用运行一次,这个1在这里的含义是常数,也就是我们代码我们会进行常数次运行

如果大家觉得什么的这些代码的计算复杂度自己都认识的差不多了,那么下来我们写一个代码来引出一点新知识。

假设我们给一个字符串:"abcdefg",这里如果我们要查找字符a的话,我们只需要查找一次。如果我们要查找g的话,我们要将整个字符串都查找一遍才能找到。 

这里就涉及到了我们时间复杂度的一个比较深入的问题。

那么在我们的数据结构中,我们到底是取最好情况,最坏情况还是平均情况? 这里就关系到我们的预期管理。所以在我们的数据结构中,我们会选择我们的最坏情况

类似我们这个冒泡排序。我们的时间复杂度最好是O(N)最坏是O(N^2)。那有人问这里我们的时间复杂度为什么不是O(1)呢,因为在冒泡排序没有给前提的情况下我们并不知道它是有序函数无序

因此我们最少都要比较N-1次,如果不存在前一个数大于后一个数的情况,这个时候我们就可以确定我们的冒泡排序是有序的。这个时候我们这里的exchange的值就为0,我们就可以直接break

但是如果我们这里是无序的,我们就要算我们最坏的情况了,也就是:

这个式子在我们的数学中被我们称为等差数列,现在我们对我们的等差数列求和。求和后的结果是

(N(N - 1))/2。当N趋于无限大的时候除2可以忽略不计,所以我们这个表达式时间复杂度的最坏情况是O(N^2)

但是如果我们将代码中的所有exchange都去除掉,这样保留下来的代码无论是最坏还是最好情况,我们的时间复杂度都是O(N^2)。 

在这之前,我们可能是通过数循环来确定我们的时间复杂度的大小,但是在我们求解时间复杂度的时候,数循环并不是万能的方法,有些题目数循环并不能解决问题。 

类似我们这里的二分查找的代码,最好的情况当然是第一次查找就找到这个时候我们的时间复杂度就是O(1)。 但是我们这里需要知道的是它的最坏情况,那么我们的二分查找的时间复杂度最坏是什么?当我们的区间缩放到一个值的时候,这个时候就是我们二分查找的最坏情况。

 

在二分查找中,我们折半查找多少次,就除以多少个2,直到最后剩下一个值为止。这里假设我们二分查找运行了X次,我们的N是我们元素得到个数,那么我们就可以将我们的二分查找的方法写成一条式子——2 ^ X = N

 

在我们计算时间复杂度的过程中,这个式子是一个十分方便的存在。我们画一个图你就可以理解为什么了。

 

从图中可以看出如果要从100万中寻找一个值,如果是我们普通查找的话要一个一个进行查找,那也就是进行100万次的查找。如果从100万中查找一个值,我们用我们的二分查找的话,我们这里只需要查找20次就行了

虽然我们的二分查找的方法十分的方便,但是我们在写代码的时候并不经常用到这种方法,这是为什么?因为如果我们要进行二分查找的话,我们需要一个前提,那就是数组必须有序,如果数组是无序的我们就不能用二分查找了。

所以这里我们就要先了解一下我们以后要学习的一个内容——红黑树。这个在以后我们就会学习到。同时在数据结构中我们也要在意我们的书写。

 

注:式子只有底数是2的才可以简写,底数不为2的不能进行简写。 

同样的我们的函数递归也有它的时间复杂度,我们将它写出来看看。

这里有人可能说时间复杂度是O(1),但是实际上并不是这样的,因为这里是我们的递归函数,所以会存在递归的操作。因此这里我们的时间复杂度其实是O(N)。 

再下来我们将我们的递归函数书写成为斐波那契函数,然后大家再来求一求它的时间复杂度是什么?

 

这里大家可以通过看代码得出不一样的结果,有的人认为是O(N),有的人认为是O(N^2)。 但是我们前面说过,计算时间复杂度如果单单是看循环次数其实有时候是不准确的对比起看循环次数,有的时候我们更应该去通过画图来计算,就例如这道斐波那契函数。

 

这一个式子就类似我们数学中的等比数列,最后我们计算出来的量级就是我们的O(2^N)。虽然在斐波那契数列中,我们在进行到很后面的时候,有一些式子会提前结束,但是当我们的N趋于无限的时候,这些提前结束的式子也可以忽略不计。 

但是这种算法并没有什么用,为什么?我们用图来说明一下。

我们通常都会使用一些更简便的方法来计算我们这一题表达式的结果。 

 结尾:

因为学习进度的原因,作者本人正在学习C语言进阶和数据结构的知识,所以以后可能会经常出现C语言进阶或者数据结构的博客,C语言初阶可能会大幅度的减少,但是如果有时间的话,我还是会将C语言初阶的博客给全部写完的。这是一篇数据结构的博客,我也是第一次了解数据结构,可能不太熟悉,所以博客只能尽我最大的努力写好,希望对各位有所帮助。

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值