本文转载自算法市, java全栈技术,不靠谱的猫
算法(Algorithm):是对特定问题求解方法或步骤的一种描述。一个算法可以用多种方法描述,主要有:
注:算法和程序是两个不同的概念。一个计算机程序是对一个算法使用某种程序设计语言的具体实现。
算法一般具有以下五个特性:
评价一个好的算法有以下几个标准:
算法中基本操作重复执行的次数是问题规模n的某个函数,其时间量度记作:T(n)=O(f(n)),称作算法的渐近时间复杂度(Asymptotic Time complexity),简称时间复杂度。一般地,常用最深层循环内的语句中的原操作的执行频度(重复执行的次数)来表示。
表示时间复杂度的阶有:
例:两个n阶方阵的乘法
两个n阶方阵的乘法
由于是一个三重循环,每个循环从1到n,则总次数:n * n * n=n^3,时间复杂度为T(n)=O(n^3)。
基本操作执行次数
关于代码的基本操作执行次数,我们用四个生活中的场景来做一下比喻:
场景1. 给小灰一条长10寸的面包,小灰每3天吃掉1寸,那么吃掉整个面包需要几天?
答案自然是 3 X 10 = 30天。
如果面包的长度是 N 寸呢?
此时吃掉整个面包,需要 3 X n = 3n 天。
如果用一个函数来表达这个相对时间,可以记作 T(n) = 3n。
场景2. 给小灰一条长16寸的面包,小灰每5天吃掉面包剩余长度的一半,第一次吃掉8寸,第二次吃掉4寸,第三次吃掉2寸......那么小灰把面包吃得只剩下1寸,需要多少天呢?
这个问题翻译一下,就是数字16不断地除以2,除几次以后的结果等于1?这里要涉及到数学当中的对数,以2位底,16的对数,可以简写为log16。
因此,把面包吃得只剩下1寸,需要 5 X log16 = 5 X 4 = 20 天。
如果面包的长度是 N 寸呢?
需要 5 X logn = 5logn天,记作 T(n) = 5logn。
场景3. 给小灰一条长10寸的面包和一个鸡腿,小灰每2天吃掉一个鸡腿。那么小灰吃掉整个鸡腿需要多少天呢?
答案自然是2天。因为只说是吃掉鸡腿,和10寸的面包没有关系 。
如果面包的长度是 N 寸呢?
无论面包有多长,吃掉鸡腿的时间仍然是2天,记作 T(n) = 2。
场景4. 给小灰一条长10寸的面包,小灰吃掉第一个一寸需要1天时间,吃掉第二个一寸需要2天时间,吃掉第三个一寸需要3天时间.....每多吃一寸,所花的时间也多一天。那么小灰吃掉整个面包需要多少天呢?
答案是从1累加到10的总和,也就是55天。
如果面包的长度是 N 寸呢?
此时吃掉整个面包,需要 1+2+3+......+ n-1 + n = (1+n)*n/2 = 0.5n^2 + 0.5n。
记作 T(n) = 0.5n^2 + 0.5n。
上面所讲的是吃东西所花费的相对时间,这一思想同样适用于对程序基本操作执行次数的统计。刚才的四个场景,分别对应了程序中最常见的四种执行方式:
场景1, T(n) = 3n,执行次数是线性的。
场景2, T(n) = 5logn,执行次数是对数的。
场景3,T(n) = 2,执行次数是常量的。
场景4,T(n) = 0.5n^2 + 0.5n,执行次数是一个多项式
渐进时间复杂度
有了基本操作执行次数的函数 T(n),是否就可以分析和比较一段代码的运行时间了呢?还是有一定的困难。
比如算法A的相对时间是T(n)= 100n,算法B的相对时间是T(n)= 5n^2,这两个到底谁的运行时间更长一些?这就要看n的取值了。
所以,这时候有了渐进时间复杂度(asymptotic time complectiy)的概念,官方的定义如下:
若存在函数 f(n),使得当n趋近于无穷大时,T(n)/ f(n)的极限值为不等于零的常数,则称 f(n)是T(n)的同数量级函数。
记作 T(n)= O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
渐进时间复杂度用大写O来表示,所以也被称为大O表示法。
如何推导出时间复杂度呢?有如下几个原则:
让我们回头看看刚才的四个场景。
场景1:
T(n) = 3n
最高阶项为3n,省去系数3,转化的时间复杂度为:
T(n) = O(n)
场景2:
T(n) = 5logn
最高阶项为5logn,省去系数5,转化的时间复杂度为:
T(n) = O(logn)
场景3:
T(n) = 2
只有常数量级,转化的时间复杂度为:
T(n) = O(1)
场景4:
T(n) = 0.5n^2 + 0.5n
最高阶项为0.5n^2,省去系数0.5,转化的时间复杂度为:
T(n) = O(n^2)
这四种时间复杂度究竟谁用时更长,谁节省时间呢?稍微思考一下就可以得出结论:
O(1)< O(logn)< O(n)< O(n^2)
在编程的世界中有着各种各样的算法,除了上述的四个场景,还有许多不同形式的时间复杂度,比如:
O(nlogn), O(n^3), O(m*n),O(2^n),O(n!)
其关系为:
O(1) < O(logn) < O(n) < O(n*logn) < O(n^2) < O(n^3)
指数时间的关系为:
O(2^n) < O(n!) < O(n^n)
当n取得很大时,指数时间算法和多项式时间算法在所需时间上非常悬殊。因此,只要有人能将现有指数时间算法中的任何一个算法化简为多项式时间算法,那就取得了一个伟大的成就。
对于不同运行时间的快速图形表示以及它们之间的比较,大O cheatsheet也非常有用。
时间复杂度的巨大差异
我们来举过一个栗子:
算法A的相对时间规模是T(n)= 100n,时间复杂度是O(n)
算法B的相对时间规模是T(n)= 5n^2,时间复杂度是O(n^2),
算法A运行在小灰家里的老旧电脑上,算法B运行在某台超级计算机上,运行速度是老旧电脑的100倍。
那么,随着输入规模 n 的增长,两种算法谁运行更快呢?
从表格中可以看出,当n的值很小的时候,算法A的运行用时要远大于算法B;当n的值达到1000左右,算法A和算法B的运行时间已经接近;当n的值越来越大,达到十万、百万时,算法A的优势开始显现,算法B则越来越慢,差距越来越明显。
这就是不同时间复杂度带来的差距。
空间复杂度(Space complexity):是指算法编写成程序后,在计算机中运行时所需存储空间大小的度量。记作:S(n) = O(f(n)),其中:n为问题的规模或大小。
存储空间一般包括三个方面:
一般地,算法的空间复杂度指的是辅助空间。如:
递归:递归是指函数调用自身。也许递归的典型例子是实现阶乘函数:
def factorial(n):
if n < 1: #base case
return 1
else: #recursive case
return n * factorial(n-1)
Divide and Conquer(D&C):一种解决问题的递归方法,D&C(1)确定问题的最简单情况(又名基本情况,the base case),(2)减少问题,直到现在成为基本情况。
也就是说,一个复杂的问题被分解成更简单的子问题。这些子问题得到了解决,然后将它们的解决方案组合起来,以解决原来更大的问题。
搜索和排序算法可能是首先要了解的最重要的算法。
这在前面的电话簿示例中进行了描述,最糟糕的情况是要求您在找到感兴趣的名称之前搜索电话簿中的所有名称。通常,简单搜索具有O(n)时间。所需的最长时间与列表中的元素数量线性相关。
让我们坚持使用电话簿示例。我们仍然有兴趣在电话簿中找到某人的名字,但这次我们将尝试提高效率。我们不是单调乏味地浏览电话簿中的每个名字,而是从电话簿的中间开始,然后从那里开始。
假设我们的目标名称以P开头。我们打开大致位于字母表中间的M s。我们知道M比字母表中的P早,所以我们可以消除从A到M的部分。现在我们可以看一下电话簿的后半部分(N到Z),将该部分分成中间(到Ts)),并与我们的目标进行比较。T在字母表中比P稍晚。然后我们知道消除后半部分(T到Z)。我们专注于N到S.现在,把它分成两半,依此类推,直到找到我们感兴趣的名字。
通常,在二分搜索中,您可以获取已排序(这很重要)的数据并找到中点。每次,您将目标与中间值进行比较。如果目标值与中间值相同,那么您的工作就完成了。否则,您知道根据比较消除列表的哪一半。您继续分割,直到找到目标或数据集不能再减半。
二分搜索图与数字列表
由于二分搜索涉及数据集减半,因此Big O时间为O(log n)。因此,它比简单搜索更快,特别是当您的数据集增长时(算法的增长不是线性的而是对数的,因此相对于O(n)的线性运行时间,它变得更慢)。
另外,二分搜索可以递归写入,但不被视为D&C算法。尽管较大的输入确实被分解为子集,但如果它们不包含感兴趣的值,则忽略这些子集。不为这些子集生成解决方案,以便它们可以组合以解决更大的输入。
分类
就像搜索算法的简单搜索一样,选择排序可能是对数据进行排序的最直接,“强力”方式。实际上,您将遍历列表中的每个元素,并按所需顺序将每个元素附加到新列表中。例如,如果您有兴趣对从最大到最小的数字列表进行排序,您会:
对于选择排序,你必须遍历列表中的每个项目(这需要n次,就像进行简单搜索一样)并且你必须这样做n次(不只是一次,因为你必须继续回到原始列表,用于查找要添加到新列表的下一个项目)。因此,这需要O(ñ ²)时间。
快速排序与选择排序有何不同?如果我们使用数字列表,就像以前一样:
快速排序图与数字列表
快速排序是D&C算法的一个例子,因为它将原始列表分成越来越小的有序列表。然后将这些较小的、有序的列表组合起来,得到一个较大的、有序的列表。
Quicksort是独一无二的,因为它的速度取决于pivot选择。在最坏的情况,可能需要O(ñ ²)的时间,这是因为选择排序慢。但是,如果pivot在列表中始终是一个随机元素,则quicksort 平均在O(n log n)时间内运行。
假设我们仍在使用我们的数字列表。对于合并排序算法,列表将分解为其各个元素。然后从这些元素创建有序对(左边的数字越小)。然后将这些有序对分组为有序的四个组,并且这将继续,直到创建最终合并的排序列表。
与quicksort一样,mergesort是一种D&C算法,因为输入列表在被组合以生成较大的原始列表的有序版本之前被分解和排序。
Mergesort在O(n log n)时间运行,因为整个列表迭代减半(O(log n))并且这是针对n个项目完成的。
算法和数据结构的知识对数据科学家很有用,因为我们的解决方案不可避免地要用代码编写。因此,理解我们数据的结构以及如何从算法的角度思考非常重要。