Python数据结构之集合概览
集合(collection),正如其名称所示,是可以作为概念性的单位来处理的一组零个或多个项。几乎软件的每一个重要部分都涉及集合的使用。尽管我们在计算机科学中所学的一些内容已经随着技术的变化逐渐消失,但组织集合的基本原理并没有变化。尽管集合在结构和用法上各不相同,但是,所有的集合都有着相同的基本作用,即帮助程序员有效地在程序中组织数据。
可以从两个视角来看待集合。集合的用户和客户关注它们在各种应用程序中能做些什么。集合的开发者和实现者关注它们作为通用资源的最佳性能。
本章将从这些集合的用户的角度给出不同类型的集合的概览。本章介绍了不同类型的集合、集合上常用的和可用的操作,以及常用的实现。
本文摘自《数据结构 python语言描述》
2.1 集合类型
你已经知道了,Python包含了几种内建的集合类型:字符串、列表、元组、集(set)和字典。字符串和列表可能是最常用和最基本的集合类型了。其他重要的集合类型还包括栈、队列、优先队列、二叉搜索树、堆、图、包(bag)和各种类型的有序集合。
集合可以是同构的,这意味着集合中的所有项必须具有相同的类型;也可以是异构的,这意味着这些项可以是不同的类型。尽管大多数的Python集合可以包含多种类型的对象,但是在很多编程语言中,集合是同构的。
集合通常是动态的而不是静态的,这意味着,它们可以随着问题的需要增加或缩小。此外,在程序的整个过程中,它们的内容是可以改变的。这一规则的一个例外是不可变集合,例如Python的字符串和元组。不可变集合的项是在其创建的过程中添加的,在此之后,就不能够再添加、删除或替换项了。
区分集合的另一个重要的特征是,它们的组织方式不同。本章介绍了集合的几个广泛分类的用法,包括线性集合、层级集合、图集合、无序集合和有序集合。
2.1.1 线性集合
线性集合中的项就像是排队的人一样,都是按照位置来有序的。除了第1项外,每个项都有一个唯一的前驱,并且除了最后一项,每个项都有一个唯一的后继。D2的前驱是D1,后继是D3,如图2.1所示。
图2.1 线性集合
线性集合的示例包括购物清单、一堆餐盘和ATM前等待的一队顾客。
2.1.2 层级集合
层级集合中的数据项,在结构中的顺序类似于一棵上下颠倒的树。除了最顶端的第一个数据项,每个数据项都只有一个前驱,称为其父亲,但是,可能有多个后继,称为其孩子。D3的前驱(父亲)是D1,其后继(孩子)是D4、D5和D6,如图2.2所示。
图2.2 层级集合
一个文件系统、一家公司的组织结构树,以及一本书的目录,都是层级集合的例子。
2.1.3 图集合
图集合也叫作图,这个集合中的每一项都可能有多个前驱和多个后继。如图2.3所示,连接到D3的所有元素,都被认为是其前驱和后继,并且它们也称为其邻居。
图2.3 图集合
城市之间的航线图和建筑物之间的电力连线图,都是图的例子。
2.1.4 无序集合
正如其名称所示,无序集合中的项没有特定的顺序,并且讨论一个项的前驱或后继也是没有意义的。图2.4展示了这样的一个结构。
图2.4 无序集合
一袋玻璃球就是无序集合的一个例子。尽管你可以将玻璃球放入到袋子之中,并且按照你想要的任何顺序从袋子中取出玻璃球,但玻璃球还是没有特定的顺序。
2.1.5 有序集合
有序集合在其项之上施加了一个自然的顺序。示例就是电话簿中的条目(20世纪的各种纸版的电话簿)和班级花名册中的条目。
为了施加一种自然的顺序,必须要有某种规则来比较各项,例如,itemi<= itemi+1,以便能够访问有序集合中的各个项。
有序列表是有序集合的一个最常见的例子,有序集合并不需要是线性的或者是按照位置来排序的。从客户的视角来看,集、包和字典可能都是有序的,即便它们的项并不是按照位置来访问的。层级集合的一种特殊的类型叫作二叉搜索树,它也是在其各项上施加了一种自然的顺序。
一个有序的集合,允许客户按照排好的顺序来访问其所有的项。一些操作,例如搜索,在有序的集合上可能也比在无序集合上更为高效。
2.1.6 集合类型的分类
记住了集合的主要类型,现在,我们可以将不同的常用集合类型分类了,如图2.5所示。这种分类的方法,将有助于我们组织在本书后面的各章中用来表示这些类型的Python类。
图2.5 集合类型的分类
注意,这个分类中的类型名称并不意味着集合的一个特定的实现,因为你稍后将会看到,一种特定类型的集合可能有多种实现。此外,其中的一些名称,例如“集合”和“线性集合”,指定了集合类型的分类,而不是指定了一个特殊的集合类型。然而,这些分类对于组织拥有相似性的特定类型的集合的特征和行为来说,还是很有用的。
2.2 集合上的操作
在一个集合上可以执行的操作,会根据所使用的集合类型的不同而有所不同,这些操作可以分为表2.1所示的几种类型。
表2.1 集合的操作的分类
操作分类 | 说明 |
---|---|
确定大小 | 使用Python的len函数来获取集合中当前项的数目 |
测试项的成员关系 | 使用Python的in运算符在集合中搜索一个给定的目标项。如果找到这个项,返回True;否则的话,返回False |
遍历集合 | 使用Python的for循环来访问集合中的每一个项。访问项的顺序取决于集合的类型 |
获取一个字符串表示 | 使用Python的str函数来获取集合的字符串表示 |
测试相等性 | 使用Python的==运算符来确定两个集合是否相等。如果两个集合具有相同的类型并且包含相同的项,那么它们是相等的。比较哪两对项的顺序,则取决于集合的类型 |
连接两个集合 | 使用Python的+运算符来获取和运算数相同类型的一个新的集合,并且这个新的集合中包含了两个运算数中的项 |
转换为另一种类型的集合 | 使用源集合中相同的项,来创建一个新的集合。克隆是类型转换的一种特例,其中,两个集合具有相同的类型 |
插入一项 | 给集合添加一项,可能在一个给定的位置添加 |
删除一项 | 从集合中删除一项,可能在一个给定的位置删除 |
替换一项 | 将删除和插入组合到一个操作之中 |
访问或获取一项 | 获取一项,可能从一个给定的位置获取 |
注意,这些操作中有几个和标准Python运算符、函数或控制语句相关,例如,in
、+,len
、str
和for
循环。我们已经熟悉了如何对Python字符串和列表使用它们。
对于Python中的插入、删除、替换或访问操作来说,并没有单独的名称。然而,有一些标准的变体。例如,pop
方法用于从Python列表中给定的位置删除项,或者从Python字典中删除给定键的值。remove
方法用于从Python集或列表中删除给定的项。随着在Python中尚未得到支持的新的集合类型被开发出来,针对它们的操作,我们将会尽全力使用标准的运算符、函数或方法名。
你可能不熟悉的一项集合操作是类型转换。我们已经了解了类型转换在输入数字时候的用法。在该环境中,我们从键盘接收数字的一个字符串,并对输入字符串应用int
或float
函数,将其转换为一个整数和浮点数(参见第1章了解详细情况)。
可以用相似的方式,把一种类型的集合转换为另一种类型的集合。例如,可以将Python字符串转换为Python列表,并且将Python列表转换为Python元组,如下面的代码所示:
>>> message = "Hi there!"
>>>lyst = list(message)
>>>lyst
[’H’, ’i’, ’’, ’t’, ’h’, ’e’, ’r’, ’e’, ’!’]
>>>toople tuple(lyst)
>>>toople
(’H’, ’i’, ’’, ’t’, ’h’, ’e’, ’r’, ’e’, ’!’)
>>>
复制代码
list
或tuple
函数的参数不需要是另一个集合,它可以是任何可迭代的对象。可迭代的对象允许程序员使用Python的for
循环来访问项的一个序列(是的,这听上去就像是一个集合,所有的集合也都是可迭代的对象)。例如,我们可以通过一个range
函数来创建一个列表,如下所示:
>>>lyst = list(range(1, 11, 2))
>>>lyst
[1, 3, 5, 7, 9]
>>>
复制代码
其他的函数,如用于字典的dict
函数,期待类型更加具体的可迭代对象作为其参数,例如(key, value)
的元组的列表。
通常,如果省略了参数,集合的类型转换函数会返回该类型的一个新的、空的集合。
克隆是一种特殊的类型转化,它将参数的一个完全的副本返回给转换函数。在这种情况下,参数的类型和转换函数的类型应该是相同的。例如,如下的代码段产生了列表的一个副本,然后,使用is
和==
运算符来比较两个列表。如果两个列表不是相同的对象,is
会返回False
。尽管两个列表是不同的对象,但是由于它们具有相同的类型并且拥有相同的结果(两个列表中的每一个位置上的每一对元素都是相同的),==
返回True
。
>>> lyst1 = [2, 4, 8]
>>> lyst2 = list(lyst1)
>>> lyst1 is lyst2
False
>>> lyst1 == lyst2
True
>>>
复制代码
这个示例中的两个列表不仅拥有相同的结构,而且它们还拥有相同的项。也就是说,list
函数对其参数列表进行了浅复制,这些项并没有在添加到新列表之前进行了自身的复制。
当这些项是不可变的时候(数字、字符串或Python元组),这一策略不会有问题。然而,当集合共享可变的项,将会引起副作用。为了防止这种情况发生,程序员可以通过编写在源集合上的一个for
循环来创建深复制,这会在把项添加到新的集合之前,显式地复制其项。
后续的各章将会采取为大多数集合类型提供类型转换函数的策略。这个函数接受一个可迭代的对象作为可选的参数,并且对访问的各项执行浅复制。
2.3 集合的实现
实际上,和最初负责实现集合的程序员相比,负责编写使用集合的程序的那些程序员对这些集合有着不同的视角。
使用集合的程序员需要知道如何实例化和使用每一种集合类型。从他们的视角来看,集合是以某种预定义的方式存储和访问数据项的一种手段,他们并不关心集合实现的细节。换句话说,从用户的视角来看,集合是一个抽象体,正因为此,在计算机科学中,集合也称为抽象数据类型(abstract data types,ADT)。ADT的用户只关心其接口,或者说关心该类型的对象所识别的一组操作。
另一方面,集合的开发者则关心能以可能的、最为高效的方式来实现集合的行为,其目标是为集合的用户提供最佳性能。通常可能有大量的实现。然而,很多的实现都会占用太多的空间,或者运行得太慢,以至于我们认为不值得这么做。而剩下的那些实现,则倾向于基于几种基本的方法来组织和访问计算机内存。第3章和第4章将会详细介绍这些方法。
一些编程语言,例如Python,针对每种可用的集合类型只提供了一种实现。而像Java等其他语言,则提供数种实现。例如,Java的java.util
包包含了列表的两个实现,分别是ArrayList
和LinkedList
。还有集和映射(就像Python的字典一样)各自的两个实现,分别是HashSet
、TreeSet
、HashMap
和TreeMap。Java程序员对于每一个实现都使用相同的接口(一组操作),但是能够根据各种实现的性能特征和其他标准,从实现中自由地选取。本书的一个主要的目标是,让Python程序员能够具备和Java程序员一样的选择权。本书同时还会介绍抽象数据类型,并指出它们在任何一种语言中不可用的实现。对于每一种类型的集合(线性的、层级的、图的、无序的和有序的)。你将会看到一种或多种抽象集合类型,以及一种或多种实现。
抽象的思路并不是讨论集合的时候所独有的。在计算机科学领域之内和之外的很多场合,抽象都是一个重要的原理。例如,当学习下落物体上的重力作用的时候,你可能会尝试搭建一个实验条件,其中,你可以忽略掉物体的颜色和味道(例如,砸到牛顿的脑袋上的苹果)等无关紧要的细节。当学习数学的时候,你不需要关心用什么数字来统计鱼钩或箭镞,而是要试图发现数字的抽象和持久不变的原理。房屋的装修设计规划,是实体的房屋的一个抽象,它使你能够关注结构性要素而不必被诸如橱柜的颜色这样无关紧要的细节所淹没。结构性要素是那些对整个房屋的整体外观很重要的细节,并不涉及房屋的主要部件之间的关系。
在计算机科学中,抽象用于忽略或隐藏那些不重要的细节。软件系统通常是一层一层地构建的,每一层都被使用它的上一层当作是一个抽象体来对待。没有了抽象,你将需要同时考虑软件设计的所有方面,这是一项不可能完成的任务。当然,你最终必须考虑细节,但是,你可以在一个较小的、可管理的环境中做这些事情。
在Python中,函数和方法是抽象的最小单位,类是下一个较大的单位,而模块是最大的单位。本书介绍了将抽象集合类型作为类以及模块中相关的一组类而实现。组织这些类的通用性技术,则涉及面向对象编程,我们将会在第5章和第6章中介绍。本书所介绍的集合类的完整列表,可参见附录A。
2.4 小结
- 集合是保存0个或多个其他对象的对象。集合拥有访问对象、插入对象、删除对象、确定集合的大小以及遍历或访问集合的对象的操作。
- 集合的5个主要类别是:线性集合、层次集合、图集合、无序集合和有序集合。
- 线性集合按照位置来排列其项,除了第一项,每一项都有唯一的一个前驱,除了最后一项,每一个项都有唯一的一个后继。
- 层次集合中的项都拥有唯一的前驱(只有一个例外,就是顶层的项),以及0个或多个后继。单个的称为根的项是没有前驱的。
- 图中的项拥有0个或多个后继,以及0个或多个前驱。
- 无序集合中的项没有特定的顺序。
- 集合是可迭代的,可以用一个
for
循环来访问包含在集合中的每一项。 - 抽象的数据类型是一组对象,以及这些对象上的操作。因此,集合是抽象数据类型。
- 数据结构是表示集合中包含的数据的一个对象。
2.5 复习题
1.线性集合的例子是( )。
a.集和树
b.列表和栈
2.无序集合的例子是( )。
a.队列和列表
b.集和字典
3.层级集合可以表示( )。
a.银行排队的顾客
b.城市之间的航线
4.图集合可以表示( )。
a.数与集合
b.城市之间的航线图
2.6 编程项目
1.在一个shell提示符窗口中,使用dir
和help
函数,研究Python的内建集合类型str
、list
、tuple
、set
和dict
。使用这两个函数的语法分别是dir(
和help(
。
2.为了和Python进行比较,浏览位于Java的java.util
包中的集合类型,参见http:// docs.oracle.com/javase/7/docs/api/。