C++ cmath log2函数的时间复杂度,和它的计算方式是什么?

C++ cmath log2函数的时间复杂度,和它的计算方式是什么?,第1张

当我们说"时间复杂度"的时候,一般涉及一个和待处理元素个数有关的计算绝大部分数学函数不涉及待处理元素个数,都不用时间复杂度来衡量
具体实现没有看过,但是能想到计算方式基本上都是泰勒公式为主吧,

时间复杂度为O(log_2 n)
设循环次数为x, j=2^(x-1), j 满足:j<=n, 也就是 2^(x-1)<= n,
两边取对数得:x-1<=log_2 n,x=log_2 n + 1,那么计算复杂度为O(log_2 n)

作为一名“程序猿”,大家应该都听过这么一句话:程序=数据结构+算法

这句话是由瑞士计算机科学家尼古拉斯·沃斯(Niklaus Wirth)在 1984 年获得图灵奖时说的一句话,这位大佬还以这句话为名出了一本书《Algorithms + Data Structures=Programs》,从此这句话就成为了大家耳熟能详的一句名言。

随着时间的推移,不管这句话是不是非常准确,但至少能说明数据结构与算法对程序来说是非常核心的基础,如果我们想要写出更多优秀优雅的代码,那么数据结构与算法是必须要掌握好的。

很多人可能觉得,我不会算法,代码一样写得很"溜",算法这东西似乎用处不大。现在互联网的发达,我们想要什么几乎都可以在网上找到现成的,各种框架功能十分强大,似乎看起来确实不用算法也可以写出“好代码”。然而假如我们不懂算法,比如项目中用到了排序,我们如何评估代码的执行效率?再比如最常用的 ArrayList 和 LinkedList ,我们该如何选择,又比如说我们需要去集合中找某一个数,又该如何写出性能优秀的代码呢?

同样的代码,如何判断谁的代码是优秀的代码?可读性,可扩展性,健壮性可能都可以用来判定,然而这些东西我觉得并不能直接体现出你代码的优秀,因为对用户而言,访问你的代码响应速度快那就是优秀的代码,相反,动辄响应几秒甚至更长时间的接口,恐怕就算你可读性再好,再健壮也称不上是好代码。

所以说一段代码是否优秀,最直接的判断标准就是性能,而如果要写出高性能的代码,那么就必须要了解算法,而且抛开这个因素,但凡不想一辈子都写 CRUD 代码的,也需要去了解算法,我们使用的很多框架和中间件底层都有数据结构和算法的身影,学好算法对我们源码阅读时理解其设计思想也是大有裨益的。

要说功利性的目的,那就是面试,目前很多大厂的面试,算法基本必面,所以想进大厂的话,咱们也得好好学学算法。

提到算法,很多人的第一反应就是太难学了,学不会,或者说经常是看完就忘了,但是其实对于我们一个普通的开发者而言,因为并不需要我们去发明算法,我们需要的仅仅只是去灵活的运用算法,所以并不需要非常扎实的数据基础,当然基本的数学常识还是要有的。

如果说需要去发明设计一款算法,那就要去推导去证明算法的可行性,这种是需要具有非常扎实的数学基础的,一般人确实无法做到,然而我们普通程序员口中提到算法无非是二分查找法,哈希算法等,高级一点的就还有回溯,贪心,动态规划等等,这些所谓的算法都是已经有现成的公式了,我们要做的无非就是理解它,然后灵活的运用它。这就和我们以前学习数学公式一样,给你一个公式,然后你去做题,做题的过程其实就是去灵活地运用这个公式。

算法也是同理,都是有特定方法和特定思路的,我们也并不需要去推导证明这种方式为什么可行,所以学习算法没有其他诀窍,就是先理解思路,然后多练,等熟练了,自然就可以灵活运用了,也不会说学了立刻就忘了。学完就忘无非两个原因,一是没理解,二是没有练习巩固。

数据结构与算法经常是放在一起讲,这两者是没办法独立的,因为算法是为了达到某种目的的一种实现方式,而数据结构是一种载体,也就是说算法必须依赖数据结构这种载体,否则就是空谈。换句话说:数据结构是为算法服务的,而算法又需要作用在特定的数据结构之上。

一个算法到底好不好,我们如何去评价?前面我们提到了,你的代码好不好,最直观的就是看响应速度,算法也一样,同样实现一个目的(比如说排序),谁的算法速度快,我们就可以认为谁的算法更优,如果说两种算法实现的速度差不多,那么我们还可以去评价算法所占用的空间,谁占用的空间少,那么就可以认为谁的算法更优,这就是算法的基础:时间复杂度和空间复杂度。

学习算法之前,我们必须要学会如何分析时间复杂度和空间复杂度(也就是“快”和“省”),否则自己写出来的算法自己都不知道算法的效率。

接触过算法的都知道,算法的时间复杂度是用大写的“O”来表示的,比如: O(1) , O(n) , O(logn) , O(nlogn) , O(n²) 等等。

变量指的是变量,也就是一段代码的执行时间是随着变量的变化而变化的,而不变指的是常量,也就是不论我的变量如何改变,执行时间都不会改变。

接下来我们就实际的来分析下常用时间复杂度的例子来练习一下。

0(1) 复杂度算法也称之为常数阶算法。这里的 1 是用来代指常量,也就是说这个算法的效率是固定的,无论你的数据量如何变化,效率都一样,这种复杂度也是最优的一种算法。

上面的示例中不论有多少行代码,时间复杂度都是属于常数阶段。换言之:只要代码不存在 循环 递归 等循环类调用,不论代码有多少行,其复杂度都是常数阶。

O(n) 复杂度算法也称之为线性阶段。比如下面这个示例我们应该怎么分析复杂度呢?

前面常量阶没分析是因为常量阶比较容易理解,接下来我们就以线性阶这个为例子来分析下具体是怎么得到的。

我们假设每一行代码的执行时间是 T ,那么上面这段代码的执行复杂度是多少呢?

答案很明显,那就是 T+nT ,也就是 (n+1)T ,而在算法中有一个原则,那就是常量可以被忽略,所以就得到了 nT ,换成大 O 表示法就是 O(n) 。

这只是一个简略的计算过程,大家也不用较真说每行代码执行时间可能不一样之类的,也不要较真说 for 循环占用了一行,下面的大括号也占用了一行,如果要较真这个,那我建议可以去想一下 1=1 为什么等于 2 。

算法中的复杂度反应的只是一个趋势,这里 O(n) 反应的就是一个趋势,也就是说随着 n 的变化,算法的执行时间是会降低的。

知道了上面的线性阶,那么平方阶就很好理解了,双层循环就是平方阶,同理,三次循环就是立方阶, k 次循环就是 k 次方阶。

O(logn) 也称之为对数阶,对数阶也很常见,像二分查找,二叉树之类的问题中会见到比较多的对数阶复杂度,但是对数阶也是比较难理解的一种算法复杂度。

下面我们还是来看一个例子:

这段代码又该如何分析复杂度呢?这段代码最关键的就是要分析出 while 循环中到底循环了多少次,我们观察这个循环,发现 i 并不是逐一递增,而是不断地翻倍: 1->2->4->8->16->32->64 一直到等于 n 为什么才会结束,所以我们得到了这样的一个公式: 2^x=n 。

也就是说我们只要计算出 x 的值,就得到了循环次数,而根据高中的数学知识我们可以得到 x=log2n ( 2 在下面,是底数,试了几种方法都打不出来,放弃了),所以根据上面线性阶的分析方法,我们省略常量,就得到了示例中的算法复杂度为 O(log2n) 。

同样的分析方式,下面的例子,我们可以很快地分析出复杂度就为 O(log3n) :

上面得到的 log3n 我们可以再做进一步的转换: log3n=log32 log2n ,而 log32 (注意这几个地方的情况 3 是底数,在下面) 是一个常量,常量可以省略,所以也就得到了: O(log3n)=O(log2n) 。同样的道理,不论底数是多少,其实最终都可以转化成和 O(log2n) 相等,正因为如此,为了方便,我们算法中通常就会省略底数,直接写作 O(logn) 。

上面的数学公式大家如果忘了或者看不懂也没关系,只要记住不论对数的底数是多少,我们都算作 O(logn) ,而对于一个算法的复杂度是否是对数阶,还有一个简易的判断方法: 当循环中下标以指定倍数形式衰减,那么这就是一个对数阶

如果理解了上面的对数阶,那么这种线性对数阶就非常好理解了,只需要在对数阶的算法中再嵌一层循环就是线性对数阶:

分析了前面这些最常用的时间复杂度,其实我们可以得到以下规律:

除了上面常用的复杂度之外,另外还有指数阶,阶层阶,根号阶等,这些接触的相对会较少,我们就不特意做分析了,如果大家感兴趣的话,可以自己去了解下。

前面我们分析的都是只有一段代码比较复杂的情况下得到的复杂度结果,那么假如我一个算法中,有多段代码都比较复杂呢?这时候复杂度该如何分析?

我们先看下面这个例子:

这个例子中有三个循环,首先第一个,是一个常量,那么根据前面的结论,不论这个常量是多大,都属于常量级,所以第一个循环中的复杂度为 O(1) ,第二个和第三个循环我们前面也分析过,复杂度分别为 O(n) 和 O(n²) 。

也就是这一段代码中有三段代码产生了三种不同复杂度,而且这三个复杂度可以很明显得到的大小关系为: O(1)<o(n)<o(n²) span=""> </o(n)<o(n²)> ,像这种在同一个算法中有明确大小关系的,我们就可以直接取最大值作为这个算法的复杂度,所以这个例子中算法的复杂度就是 O(n²) 。

接下来我们再来看一个例子:

这个例子我们同样对三段循环分别分析可以分别得到如下复杂度: O(1) , O(m) , O(n) 。这时候我们只能知道 O(1) 最小可以忽略,但是后面两个无法却无法确定大小,所以这时候我们需要取两段循环复杂度之和来作为算法的复杂度,所以可以得到这个例子的算法复杂度为: O(m+n) 。

上面分析的时间复杂度都是比较简单的,实际算法中可能会比示例中复杂的多,而且我们示例中只要是循环都是无脑循环,也就是一定从头循环到尾,然而实际中我们有时候并不需要从头循环到尾,可能中途就会结束循环,所以我们根据实际情况,又可以将时间复杂度从以下四个方面来进一步分析:

这四种类型的时间复杂度在这里只会介绍前面三种,因为第四种比较复杂,而且使用场景也非常有限,而且对于这四种复杂度的分析,大家也作为了解就可以,不敢兴趣的朋友们可以跳过这一小部分,因为在绝大部分情况我们只需要分析最坏复杂度就行,也就是假设循环全部执行完毕场景下的时间复杂度。

我们通过一个例子来理解下最好时间复杂度:

这个方法就是在一个指定数组中找到指定元素的下标,找不到就返回 -1 ,这个方法比较简单,应该比较好理解。

注意这个方法中的循环体,如果找到元素,那么就直接返回,这就会有一个现象,那就是我这个循环体到底会循环多少次是不确定的,可能是 1 次,也可能是 n (假设数组的长度) 次,所以假如我们要找的元素就在数组中的第一个位置,那么我循环一次就找到了,这个算法的复杂度就是 O(1) ,这就是最好情况时间复杂度。

理解了最好时间复杂度,那么最坏时间复杂度也很好理解了,那就是数组中不存在我要找到元素,或者说最后一个值才是我要找的元素,那么这样我就必须循环完整个数组,那么时间复杂度就是 O(n) ,这也就是最坏时间复杂度。

最好时间复杂度和最坏时间复杂度毕竟只有特殊情况才会发生,概率还是相对较小,所以我们很容易就想到我们也需要有一个平均时间复杂度。

我们简单的来分析一下,为了便于分析,我们假设一个元素在数组和不在数组中的概率都为 1/2 ,然后假如在数组在,那么又假设元素出现在每个位置的概率也是一样的,也就是每个位置出现元素的概率为: 1/n 。

所以最终得到的平均时间复杂度应该等于元素在数组中和元素不在数组中两种情况相加。

因为元素在数组中的概率为 1/2 ,然后在每个位置出现的概率也为 1/n 。假如元素出现在第一个位置,复杂度为 1(1/2n) ;假如元素出现在第二个位置,复杂度为 2 (1/2n) ,最终得到当前场景下时间复杂度为: 1(1/2n) + 2 (1/2n) + + n(1/2n) =(n+1)/4。

前面已经假定了元素不在数组中的概率为 1/2 ,所以当前场景下的时间复杂度为: n (1/2) ,因为元素不在数组中,那么这个算法必然会将整个循环执行完毕,也就循环是 n 次。

最后我们把两种情况的复杂度之和相加就得到了平均时间复杂度: (n+1)/4 + n/2 = (3n+1)/4 ,最终我们将常数类的系数忽略掉,就得到了平均时间复杂度为 O(n) 。

均摊时间复杂度的算法需要使用摊还分析法,计算方式相对有点复杂,而且使用场景很有限,本文就不做过多介绍了。

空间复杂度全称就是渐进空间复杂度,用来表示算法的存储空间与数据规模之间的增长关系。和时间复杂度一样,空间复杂度也是用大 O 进行表示。

其实学会了分析时间复杂度,那么空间复杂度的分析就简单了,主要就看我们在一个算法当中到底有没有使用到了额外的空间来进行存储数据,然后判断这个额外空间的大小会不会随着 n 的变化而变化,从而得到空间复杂度。

我们来看一个给数组赋值例子,假设这就是一个算法,我们可以来分析下这个算法的空间复杂度:

一开始定义了一个变量,这里需要空间,但是这是一个常量级的(不随 n 的变化而变化),然后再定义了一个数组,数组的长度为 n ,这里数组也需要占用空间,而且数组的空间是随着 n 的变化而变化的,其余代码没有占用额外空间,所以我们就可以认为上面示例中的空间复杂度为 O(n) 。

对于算法的空间复杂度也可以简单的进行总结一下:

本文主要讲述了为什么要学习算法,也简单减少了数据结构与算法之间的关系,随后主要介绍了算法中的入门知识:时间复杂度和空间复杂度。想要学好算法,必须要掌握如何分析一个算法的时间复杂度和空间复杂度,只有自己会分析这两个个衡量算法主要性能的标准,才能更好的写出性能优秀的算法,同时我们也讲到了最好时间复杂度,最坏时间复杂度,平均时间复杂度和均摊时间复杂度,不过这四种复杂度的计算方式大家作为了解即可,等实际确实需要使用到再来回顾也不迟。

时间复杂度是总运算次数表达式中受n的变化影响最大的那一项(不含系数)

时间复杂度就是输入规模n与运算次数T的关系

T(n) = T = O(h)

h 为T的最高阶

例如在一个长度为n的数组array中查找一个确定的值k

for(int i=0; i<n; i++) {
    if(array[i]==k) {
        return i;
    }
}

输入规模为n,关键语句就是最深层循环的 if 语句

运算次数(这里是平均查找次数):T=(1+n)/2

因此

T(n)=(1+n)/2=1/2+n/2=O(n)

上面的例子是与输入规模有关的

即:规模变大或变小,运算次数也变大或变小

因此还要计算最好和最坏的情况

分别是比较 1次 和 n次

时间复杂度为O(1)和O(n)

常见的时间复杂度,按数量级递增排列依次为:

常数阶O(1)、对数阶O(log2n)、线性阶O(n)、线性对数阶O(nlog2n)、平方阶O(n^2)、立方阶O(n^3)、k次方阶O(n^k)、指数阶O(2^n)、n^n

考试可能还会出递归的时间复杂度计算,这个不写了,看网上的方法:

递归算法的时间复杂度分析
在算法分析中,当一个算法中包含递归调用时,其时间复杂度的分析会转化为一个递归方程求解。实际上,这个问题是数学上求解渐近阶的问题,而递归方程的形式多种多样,其求解方法也是不一而足,比较常用的有以下四种方法:
   (1)代入法(Substitution Method)
   
   代入法的基本步骤是先推测递归方程的显式解,然后用数学归纳法来验证该解是否合理。
   
   (2)迭代法(Iteration Method)
   
   迭代法的基本步骤是迭代地展开递归方程的右端,使之成为一个非递归的和式,然后通过对和式的估计来达到对方程左端即方程的解的估计。
   
   (3)套用公式法(Master Method)
   
   这个方法针对形如“T(n) = aT(n/b) + f(n)”的递归方程。这种递归方程是分治法的时间复杂性所满足的递归关系,即一个规模为n的问题被分成规模均为n/b的a个子问题,递归地求解这a个子问题,然后通过对这a个子间题的解的综合,得到原问题的解。
   
   (4)差分方程法(Difference Formula Method)
   可以将某些递归方程看成差分方程,通过解差分方程的方法来解递归方程,然后对解作出渐近阶估计。
   
   下面就以上方法给出一些例子说明。
       
   一、代入法
   
   大整数乘法计算时间的递归方程为:T(n) = 4T(n/2) + O(n),其中T(1) = O(1),我们猜测一个解T(n) = O(n2 ),根据符号O的定义,对n>n0,有T(n) < cn2 - eO(2n)(注意,这里减去O(2n),因其是低阶项,不会影响到n足够大时的渐近性),把这个解代入递归方程,得到:
   
   T(n) = 4T(n/2) + O(n)
          ≤ 4c(n/2)2 - eO(2n/2)) + O(n)
          = cn2 - eO(n) + O(n)
          ≤ cn2
   
   其中,c为正常数,e取1,上式符合 T(n)≤cn2 的定义,则可认为O(n2 )是T(n)的一个解,再用数学归纳法加以证明。
   
   二、迭代法
   某算法的计算时间为:T(n) = 3T(n/4) + O(n),其中T(1) = O(1),迭代两次可将右端展开为:
   
   T(n) = 3T(n/4) + O(n)
        = O(n) + 3( O(n/4) + 3T(n/42 ) )
        = O(n) + 3( O(n/4) + 3( O(n/42 ) + 3T(n/43 ) ) )
       
   从上式可以看出,这是一个递归方程,我们可以写出迭代i次后的方程:
   
   T(n) = O(n) + 3( O(n/4) + 3( O(n/42 ) + + 3( n/4i + 3T(n/4i+1 ) ) ) )
   
   当n/4i+1 =1时,T(n/4i+1 )=1,则
   
   T(n) = n + (3/4) + (32 /42 )n + + (3i /4i )n + (3i+1 )T(1)
        < 4n + 3i+1
       
   而由n/4i+1 =1可知,i<log4 n,从而
   
   3i+1 ≤ 3log4 n+1 = 3log3 nlog4 3 +1 = 3nlog4 3
   
   代入得:
   
   T(n) < 4n + 3nlog4 3,即T(n) = O(n)。
   
   三、套用公式法
   
   这个方法为估计形如:
 T(n) = aT(n/b) + f(n)
 其中,a≥1和b≥1,均为常数,f(n)是一个确定的正函数。在f(n)的三类情况下,我们有T(n)的渐近估计式:
   1若对于某常数ε>0,有f(n) = O(nlogb a-ε ),则T(n) = O(nlogb a )
   
   2若f(n) = O(nlogb a ),则T(n) = O(nlogb a logn)
   
   3若f(n) = O(nlogb a+ε ),且对于某常数c>1和所有充分大的正整数n,有af(n/b)≤cf(n),则T(n)=O(f(n))。
   
   设T(n) = 4T(n/2) + n,则a = 4,b = 2,f(n) = n,计算得出nlogb a = nlog2 4 = n2 ,而f(n) = n = O(n2-ε ),此时ε= 1,根据第1种情况,我们得到T(n) = O(n2 )。
   
   这里涉及的三类情况,都是拿f(n)与nlogb a 作比较,而递归方程解的渐近阶由这两个函数中的较大者决定。在第一类情况下,函数nlogb a 较大,则T(n)=O(nlogb a );在第三类情况下,函数f(n)较大,则T(n)=O(f (n));在第二类情况下,两个函数一样大,则T(n)=O(nlogb a logn),即以n的对数作为因子乘上f(n)与T(n)的同阶。
   
   但上述三类情况并没有覆盖所有可能的f(n)。在第一类情况和第二类情况之间有一个间隙:f(n)小于但不是多项式地小于nlogb a ,第二类与第三类之间也存在这种情况,此时公式法不适用

1、顺序查找:

(1)最好情况:要查找的第一个就是。时间复杂度为:O(1)

(2)最坏情况:最后一个是要查找的元素。时间复杂度未:O(n)

(3)平均情况下就是:(n+1)/2。

所以总的来说时间复杂度为:O(n)

2、二分查找:O(log2n)->log以2为底n的对数

解释:2^t = n; t = log(2)n;

3、插值查找:O(log(2)(log(2)n))->log以2为底的(log以2为底的n的对数)的对数

4、斐波那契查找:O(log2n)->log以2为底n的对数

5、树表查找:

(1)二叉树:O(log2n)~O(n)之间

(2)红黑树:O(lgn)

(3)B和B+树:O(log2n)

6、分块查找:O(log2n)~O(n)之间

7、哈希查找:O(1)


欢迎分享,转载请注明来源:内存溢出

原文地址: https://www.outofmemory.cn/yw/13130850.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-06-08
下一篇 2023-06-08

发表评论

登录后才能评论

评论列表(0条)

保存