我们学习算法前,都会接受一个概念:算法复杂度。但是很多人不知道为什么需要关注算法复杂度,同样类似的问题是,很多人也不知道为什么需要学习算法,觉得算法离应用太远。
算法复杂度有三个概念组成:时间复杂度、空间复杂度、稳定性。三者最为重要的是“时间复杂度”,因为随着存储硬件空间越来越大,一般情况不需要关注空间复杂度;而稳定性,只需要知道算法是否稳定,在需要算法稳定性的场景下去做优化即可。
重点来说一下时间复杂度:
- 什么是时间复杂度
常见的时间复杂度有:O(1)常数型;O(log n)对数型,O(n)线性型,O(nlog n)线性对数型,O(n2)平方型,O(n3)立方型,O(nk)k次方型,O(2n)指数型。
上图为不同类型的函数的增长趋势图,随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log n)<Ο(n)<Ο(nlog n)<Ο(n2)<Ο(n3)<…<Ο(2^n)<Ο(n!)。
- 为什么需要关注时间复杂度
这个问题,普林斯顿算法课的教授说得比较深入浅出,我摘录如下:
这门课中我们反复强调得一个主题,就是平方量级得时间太慢了,对于大型问题,我们不能接受需要平方时间得算法,原因在于它们无法成比例适应大规模问题,当计算机变得更快更大,平方时间算法实际上变得更慢。
我们来看看原因:
当今,计算机每秒能够进行几十亿次操作,这些计算机得主内存中有几十亿项,这表示你可以在大约一秒钟得时间内访问主内存所有的项,这是一个相当奇妙的事情:在大约一秒钟的时间内访问主内存所有的项,这是一个相当奇妙的事实。这个粗略的标准已经被保持了50到60年:计算机内存变得越来越大,但计算能力也变得越来越快,所以访问内存中得每一项只需要几秒钟。当计算机得内存只有几千个字得时候,这是成立的;现在计算机的内存有几十亿个字甚至更多,这依然是成立的。
现在我们假设,有一台拥有了那么巨大内存的计算机,我们可以处理巨大的问题,所以我们能够拥有几十亿个对象,并且希望能对它们进行几十亿个操作,但是对于那个快速查询算法,需要10的18次方个操作,或者说需要访问数组这么多次,或者说需要访问这么多次内存。如果你计算一下,这需要30多年的计算时间。很显然在现在的计算机上,处理这样的问题是不现实的,原因就在于问题出在二次方时间算法不能成比例适用于新的技术。也许你拥有一台比现在快10倍的新计算机,但你可能遇到一个10倍大的问题,那么使用平方时间算法处理这个问题,花的时间就要多10倍,这就是我们通过设计更高效的算法,来解决这样的问题努力避免的情形。
对上面内容总结一下,通过优化算法复杂度,让算法能使用于计算巨量数据的场景,让算法的计算时间不会由于计算对象的增大而指数级地增加。
- 一个应用的例子:kafuka
kafuka的其中一个设计目标是 : 时间复杂度为O(1)的磁盘访问能力。
关于这个能力,kafuka官方文档是这样写的
在消息系统中使用持久化数据通常是具有关联的BTree或其他随机访问的数据结构,以维护消息的元数据。BTree是最通用的数据结构,可以在消息系统中支持各种各样的语义。BTree的操作时间复杂度是O(log N)。通常O(log N)被认为是固定时间的,但是在磁盘操作中却不是。每个磁盘一次只能执行一个seek,所以并行度受到限制。因此即使少量的磁盘搜索也会导致非常高的开销。由于操作系统将快速的缓存操作和非常慢的磁盘操作相结合,所以观察到树结构的操作通常是超线性的,因为数据随固定缓存增加。
直观的,持久化队列可以像日志的解决方案一样,简单的读取和追加数据到文件的结尾。这个结构的优势是所有的操作都是O(1)的,并且读取是可以并行不会阻塞的。这具有明显的性能优势,因为性能与数据大小完全分离,可以使用低速的TB级SATA驱动器。虽然这些驱动器的搜索性能不佳,但是对于大量读写而言,他们的性能是可以接受的,并且价格是三分之一容量是原来的三倍。
要理解为什么读写操作都是O(1),必须理解kafka的设计思想,kafka设计了topic概念,一个topic包含一个或者多个partition,partition有分散到各个brokes集群中,其实一个partition包含一个或者多个segment文件,一个segment包含两个文件一个是数据文件,专门来存储数据的,一个是索引文件,方便搜索。以上是基础知识,为了更好的说明想看下官方的描述图。
\
\
先说写操作吧,写操作是直接追加到一个segment文件的某尾,所以他的时间复杂度是O(1)好理解。那么Consumer读取呢?
Kafka会为每一个Consumer Group保留一些metadata信息——当前消费的消息的position,也即offset。这个offset由Consumer控制。正常情况下Consumer会在消费完一条消息后递增该offset。当然,Consumer也可将offset设成一个较小的值,重新消费一些消息。
\
从上图可以看到,消费者可以跟进offset找到对应的所需要的数据对应segment的位置,但是这个不应该是O(n)吗?他是怎么做到常数级别的呢?所以kafka一定是对访问数据进行了一定的算法处理。
那这里的算法,他的算法复杂度就很重要了,是能不能完成设计目标的关键点。