牢记数据结构的时间复杂度 #P004#

虽然大多数情况下,Python程序都不会应用在计算密集型的场景。但是,作为一个合格的工程师,依然应该对Python内置数据类型的时间复杂度有一个基本的了解,才能够避免写出一些明显低效的代码。打个比方,我们都知道,在Python里面list是异构元素的集合,并且能够动态增长或收缩,可以通过索引和切片访问。那么,又有多少人知道,list是一个数组而不是一个链表呢。

1 时间复杂度的重要性

算法的时间复杂度是用来度量算法的运行时间,算法的空间复杂度用来度量程序占用的内存,这两个都是计算机系统中非常重要的概念。它们直接关乎程序的运行效率,其重要程度怎么强调都不为过。在这一小节中,我们将通过一个Python字符串连接的例子,来解释说明程序的时间复杂度和空间复杂度的重要性。

合格的Python工程师应该知道,Python中的字符串是不可变的。因为Python中的字符串是不可变的,在进行字符串操作时,Python每次操作都会产生一个新的字符串,新的字符串会占用一块独立的内存。因此,在操作字符串时,应该避免产生太多的中间结果。例如,下面就是一个典型的反面教材:

In [1]: fruits = ['orange', 'apple', 'banana', 'pear']

In [2]: statement = fruits[0]

In [3]: for item in fruits[1:]:
   ...:     statement = statement + ", " + item
   ...:

In [4]: print(statement)
orange, apple, banana, pear

在这个例子中,用for循环和字符串的"+"操作连接字符串,由于Python的字符串具有不可变性,因此,每次连接操作都会生成一个中间结果。对于这里的这个例子,图4.1中虚线的部分就是无用的中间结果,它们一产生就被销毁,白白浪费了程序的运行时间和计算机的内存。

image1

图4.1 "+"操作连接字符串

如果使用时间复杂度和空间复杂度来量化上面的代码,则它们的时间复杂度为O(nn),空间复杂度也为O(nn),这也是为什么代码执行效率低的原因。

显而易见的是,产生的中间结果越多,程序的性能就越差。因此,Python工程师应该牢记Python字符串的不可变性,在实际编程过程中,避免产生太多的中间结果。对于这个例子,正确的做法应该是使用字符串的join方法,如下所示:

In [5]: ", ".join(fruits)
Out[5]: 'orange, apple, banana, pear'

如果使用时间复杂度和空间复杂度来量化join函数,则它的时间复杂度为O(n),空间复杂度为O(n)。很显然,这段代码的执行效率比for循环连接的方式快很多很多。

2 List的时间复杂度

使用任何编程语言的工程师都知道,链表和数组都是最常使用的数据结构,并且它们有着显著的不同特性。图4.2是图形化表示的链表和数组。

image2

图 4.2链表与数组

数组是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想删除一个元素,同样需要移动大量元素去填掉被移动的元素。如果应用需要快速访问数据,很少或不插入和删除元素,就应该用数组。

链表恰好相反,链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。比如:上一个元素有个指针指到下一个元素,以此类推,直到最后一个元素。如果要访问链表中的某一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单了,只要修改元素中的指针就可以了。如果应用需要经常插入和删除元素就应该使用链表。

在大多数将数据结构的书籍中,数组都用Array表示,链表都用LinkedList表示。很自然的,不少工程师以为List就是LinkedList,是一个链表。Python中的List底层实现是一个数组,它的特性和数组一样,适合需要快速访问,不经常插入和删除元素的场景。

List的各个操作的时间复杂度如下:

image3

既然List是一个数组,那么,我们要使用链表的时候,应该使用什么数据结构呢?在写Python代码的时候,如果你要一个链表,你应该使用标准库collections中的deque,Python的标准库提供了collections.deque,deque的底层实现是一个双链表,能够满足需要经常插入和删除,访问比较少的场景。标准库里面有一个queue,看起来和deque有点像,它们是什么关系呢?这个问题留着读者自己回答。

3 Set与Dict的时间复杂度

如果问一个工程师,dict的底层实现是什么数据结构,大多数工程师都可以轻易的回答出是hash表。但是,如果问一个工程师,set的底层实现是什么数据结构,则很多工程师会一脸懵逼。

其实稍微思考一下,就很容易知道set的底层实现的。set的主要作用是去重,为了实现去重,就需要进行高效的按值查找,以确定该值是否已经在集合中。而查找操作是hash表的强项。所以,set的底层实现也是hash表。读者可以在脑海中想象一下,set的底层是一个只有key没有value的hash表。

hash表的示意图如下:

iamge3

图4.3 hash表的示意图

hash表各项操作的时间复杂度:

iamge4

我们已经介绍了List与Set,接下来看一个非常实际的例子:有两个目录,每个目录都有大量文件,求两个目录中都有的文件,此时,用Set比List快很多。因为,Set的底层实现是一个hash表,判断一个元素是否存在于某个集合中,List的时间复杂度为O(n),Set的时间复杂度为O(1),所以这里应该使用Set。

4 总结

算法和数据结构是一个非常复杂的话题,本身就是计算机系非常重要的一门课,并且有大量的讲算法和数据结构的书籍。在这篇文章中,笔者仅仅针对Python相关的一些特性,介绍了一小部分算法和数据结构相关的知识。仅仅是抛砖引玉,让读者在编写Python代码的过程中,也关注时间复杂度。读者应该非常清楚Python中各个常用数据结构的时间复杂度,并在实际写代码的过程中,充分利用不同数据结构的优势。只有这样,才能够写出高效的代码,而不是在程序运行慢的时候,将锅甩给Python语言。

作者介绍

赖明星,架构师、作家。现就职于腾讯,参与并主导下一代金融级数据库平台研发。有多年的 Python 开发经验和一线互联网实战经验,擅长 C、Python、Java、MySQL、Linux 等主流技术。国内知名的 Python 技术专家和 Python 技术的积极推广者,著有《Python Linux 系统管理与自动化运维》一书。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值