- 程序员必会的40种算法
- (加)伊姆兰·艾哈迈德
- 4350字
- 2021-09-27 16:59:57
2.1 Python中的数据结构
在任何编程语言中,数据结构都用于存储和操作复杂的数据。在Python中,数据结构也是数据存储容器,用于以有效方式对数据进行管理、组织和查找。它们用于存储成组出现的数据元素,这些数据元素需要一起存储和处理,每一组这样的数据称为一个集合。在Python中,有五种不同的数据结构可以用来存储集合:
- 列表(list):有序的可变元素序列。
- 元组(tuple):有序的不可变元素序列。
- 集合(set):无序元素序列(其中元素不重复)。
- 字典(dictionary):无序的键值对序列。
- 数据帧(DataFrame):存储二维数据的二维结构。
下面我们在更详细地介绍它们。
2.1.1 列表
在Python中,列表是用来存储可变元素序列的主要数据结构。列表中存储的数据元素序列不必是同一数据类型。
要创建一个列表,数据元素需要用[ ]括起来,并且需要用逗号隔开。例如,下面的代码创建了一个含有四个数据元素的列表,其数据类型不完全相同:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/036-1.jpg?sign=1739040769-BiMCStezs3NDr03MmJUppuqgEDO9k5Uz-0-d5585aec39dcefab44c24f210270edd8)
在Python中,列表是一种创建一维可写数据结构的便捷方法,在算法的不同内部阶段都特别有用。
使用列表
数据结构关联的实用功能非常有用,因为这些功能可以用来管理列表中的数据。
我们看看如何使用列表:
- 列表索引:由于元素在列表中的位置是确定的,因此可以使用索引来获取某个特定位置的元素。下面的代码演示了这个概念:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/036-2.jpg?sign=1739040769-spJD8mAGEx15c0OFpszaGPNGXyMGksoj-0-ee8146ffcac711cefec72a720bbea2a0)
该代码创建的四元素列表如图2-1所示。
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/036-3.jpg?sign=1739040769-bclG6gH4m84rYrYpbYmgXgiRddMDLHn7-0-300cd6d6854297276f644ed58bc94f09)
图 2-1
注意,索引从0开始,因此第二个元素Green由索引1即bin_color[1]
检索。
- 列表切片:通过指定索引范围可以检索列表中的元素子集,这个过程叫作切片。下面的代码可以用来创建列表的一个切片:
-
注意,列表是Python中非常流行的一维数据结构之一。
在对列表进行切片时,其切片范围如下所示:包含第一个数字而不包含第二个数字。例如,
bin_colors[0:2]
将包括bin_color[0]
和bin_color[1]
,而不包括bin_color[2]
。在使用列表时应注意这一点,因为Python语言的一些用户抱怨这不是很直观。
我们看看下面的代码片段:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/037-2.jpg?sign=1739040769-sqeyIsuRH3JmSxmTe8lQv3M8ob9KzGnD-0-93a37cb721119dddde1a1eff91022d6e)
如果未指定起始索引,则意味着起始索引为列表的开始,如果未指定终止索引,则表示终止索引为列表的末尾,前面的代码实际上已经演示了这个概念。
- 负索引:在Python中,也有负索引,负索引从列表的末尾开始计数。下面的代码对此进行了演示:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/037-3.jpg?sign=1739040769-GCcFakVNmjyJdziPjx9OG55jShjiFokR-0-78964db94c5c1fe69688ab986d57a64f)
注意,如果我们想将参考点设置为最后一个元素而不是第一个元素,负索引特别有用。
- 嵌套:列表的每个元素可以是简单数据类型,也可以是复杂数据类型,这就允许在列表中进行嵌套。对于迭代和递归算法来说,这是非常重要的功能。
让我们来看看下面的代码,这是在一个列表中嵌套列表的例子:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/038-1.jpg?sign=1739040769-vF6zfcg4xStH4UPsfyAaGMzFrCwV5vi2-0-30951659bddfc1cfb7184076611b9d86)
- 迭代:Python允许使用
for
循环对列表中的每个元素进行迭代,这在下面的例子中进行了演示:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/038-2.jpg?sign=1739040769-ESabBl7riXuefwmtcLM3E7XSwyIR73Pp-0-38f7142f322923ac53a6f0da5c721c1d)
注意,前面的代码会遍历列表并打印每个元素。
lambda函数
在列表中可以使用大量的lambda函数。lambda函数在算法中特别重要,其提供了动态创建函数的能力。有时在文献中,lambda函数也被称为匿名函数。本小节将展示其用途:
- 过滤数据:为了过滤数据,需要先定义一个谓词,说明需要完成什么工作,它是输入一个参数并返回一个布尔值的函数。下面的代码演示了它的使用方法:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/038-3.jpg?sign=1739040769-HjpxaoGTt9vdHGgw2Q7CY2JDbny1DjQm-0-48d340762ba4d9826bc18dbcc31751dd)
在这段代码中,我们使用了lambda
函数来过滤一个列表,该函数指定了过滤标准。filter函数旨在依据定义的标准从序列中过滤掉不符合标准的元素。在Python中,filter函数通常与lambda
函数一起使用。除了列表之外,它还可以用来从元组或集合中过滤元素。对于前面展示的代码,定义的过滤标准是x>100
,这段代码将遍历列表中的所有元素,并过滤掉不符合这个标准的元素。
- 数据转换:
map()
函数可用于通过lambda函数进行数据转换。示例如下:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/038-4.jpg?sign=1739040769-HqP9a5dR8LYwl17rOyWrXePmL7nFqS89-0-daffb782021cfea9cc43a8ac2c48ce6f)
将map
函数和lambda
函数一起使用可以提供相当强大的功能。当与map
函数一起使用时,lambda
函数可以用来声明一个转换器,对给定序列的每个元素进行转换。在前面展示的代码中,转换器是取平方。因此,我们使用map
函数对列表中的每个元素求平方。
- 数据聚合:对于数据聚合,可以使用
reduce()
函数,该函数会循环运行定义的函数,对列表中每对元素值进行处理:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/039-1.jpg?sign=1739040769-IJMb5Hq7b9svXUIcgLuyIKWcKT3qLj9S-0-f855b314f7f843c9163f2343eaa976c0)
注意,reduce
函数需要定义一个数据聚合函数,前面代码中的数据聚合函数是doSum
,它定义了如何对给定列表中的各项元素进行聚合。聚合从最前面的两个元素开始,然后用聚合结果替换这两个元素。这样,列表元素会减少,该过程不断重复,直到最后得到一个聚合数字。doSum
函数中的x1
和x2
分别代表了每轮迭代中的两个数字,doSum
则代表了它们聚合的标准。
前面代码块所得结果是一个单值(即270)。
range函数
range
函数可以用来轻松地生成一个大的数字列表。它用作自动填充列表的数字序列。
range
函数使用起来很简单,使用时只需指定列表中想要的元素个数。在默认情况下,列表中的元素从0开始,并逐渐递增1:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/039-2.jpg?sign=1739040769-C3zd1vKsXYSCeYKvhataqLIT2dOvTg9J-0-a9e804970ebe600d22336f6ed8a91364)
我们还可以指定结束的数字(不包含)和步长(两个相邻元素之间的差值):
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/039-3.jpg?sign=1739040769-6a5Gngt28lTmD3hcPeGU6EHYWmoDGGiJ-0-8fa1fa12e95a8d646547a47556f80e37)
上面的range
函数给出从3到29的奇数(不包括结束数字,也就是29)。
列表的时间复杂度
列表的时间复杂度可以使用大O记号来表示,整理如下:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/040-1.jpg?sign=1739040769-ztqC0cMZArbbx895G1VJb2c2xQ7G9h2M-0-509bed402204e8e5af9bcebb5a8c9825)
注意,添加单个元素所需的时间与列表的规模无关,而表格中其他操作的复杂度则取决于列表的规模。列表的规模越大,性能受到的影响就越明显。
2.1.2 元组
第二个可以用于存储集合的数据结构是元组。与列表相反,元组是不可变的(只读)数据结构。元组由一些被( )包围的元素组成。
同列表一样,元组中的元素可以是不同类型的,元组也允许其元素使用复杂数据类型。因此,元组中也可以包含其他元组,这就提供了一种创建嵌套数据结构的方法。创建嵌套数据结构的能力在迭代和递归算法中特别有用。
下面的代码演示了如何创建元组:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/040-2.jpg?sign=1739040769-VPvDbSs8mxXM2bDY2GEjWCdhl5AsGPrJ-0-8a5bbf1495e6516c696a065796d48c2a)
在可能的情况下,出于性能考虑,应该优先使用不可变的数据结构(例如元组)而不是可变的数据结构(例如列表)。特别是在处理大数据时,不可变的数据结构比可变的数据结构快得多。这是因为,我们需要为列表具备改变数据元素的能力而付出代价。因此,应该仔细分析是否真的需要这种能力。如果将代码实现为只读的元组,则其速度会快很多。
注意,在前面的代码中,a[2]
指的是第三个元素,即一个元组(100,200,300)
。a[2][1]
指的是这个元组中的第二个元素,也就是200
。
元组的时间复杂度
元组的Append
函数的时间复杂度总结如下(使用大O记号):
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/041-1.jpg?sign=1739040769-Ewht1XwwC8NUJKNy5asWaR5HGVt1PR5G-0-65857405f07b64d9a159356c634094ea)
注意,Append
函数是在一个已经存在的元组末尾添加一个元素,其复杂度为O(1)。
注意,元组是不可变的数据类型,其中没有Append
函数。这里所说的Append
其实是创建了一个新的元组,具体见如下代码:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/041-2.jpg?sign=1739040769-ZBvwkPBgJrfOBYvtFDvueTMRwwQ3OFKn-0-4907641f4d5ed7ebb2a652b2f3db71a6)
可以看到,我们成功地将新元素添加到元组的末尾,但其实是创建了一个新的元组。
2.1.3 字典
以键值对的形式保存数据是非常重要的,尤其是在分布式算法中。在Python中,这些键值对的集合被存储为一个称为字典的数据结构。要创建一个字典,应该选择一个在整个数据处理过程中最适合识别数据的属性作为键。值可以是任何类型的元素,例如,数字或字符串。Python总是使用复杂的数据类型(如列表)作为值。如果用字典作为值的数据类型,则可以创建嵌套字典。
为了创建一个为各种变量分配颜色的简单字典,需要将键值对用{ }括起来。例如,下面的代码创建了一个由三个键值对组成的简单字典:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/041-3.jpg?sign=1739040769-JfOhJxma9AQQrtLJnHC7kmlHaQaNnWf0-0-1a31fa293e2a24d4286cdea2fbb54afe)
前面一段代码所创建的三个键值对也在图2-2中进行了说明。
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/042-1.jpg?sign=1739040769-KLlzbXaNDwVwiQgMOcdkRKeU4dZXmIOM-0-3d220b562f71b9b5e49c02e1dd58788f)
图 2-2
现在,我们看看如何检索和更新与键相关联的值:
1. 要检索一个与键相关联的值,可以使用get
函数,也可以使用键作为索引:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/042-2.jpg?sign=1739040769-NUjDn7Pjei48agbqclxmtWZSN7XbH4Gk-0-f941863cd6b38132d3466e7605986875)
2. 要更新与键相关联的值,可以使用以下代码:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/042-3.jpg?sign=1739040769-46iQwM0vCxLuf00DxLB6zn6zf6h8XYBt-0-05838864073bb28366c7ede135645849)
请注意,前面的代码演示了如何更新一个与字典中的某个特定键相关的值。
字典的时间复杂度
下表给出了使用大O记号表示的字典的时间复杂度:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/042-4.jpg?sign=1739040769-d3ciImefUDdPc7ortVna2ul6afLwjFTn-0-8aa99e1124abded654c582963f86afab)
从字典的复杂度分析中可以发现一个需要注意的重要现象,那就是获取或设置键值所需的时间与字典的大小完全无关。这意味着,将一个键值对添加到一个大小为3的字典中所花费的时间与将一个键值对添加到一个大小为100万的字典中所花费的时间是一样的。
2.1.4 集合
集合被定义为元素集,可以是不同类型的元素,这些元素都被{ }括起来。例如,请看下面的代码块:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/043-1.jpg?sign=1739040769-dFCt0RPrWhGyT5BGVAw4Py0obUgdHSzz-0-3d052ac141c6a2edb16062dd2e7eda56)
集合定义的特性是,它只存储每个元素的不同值。如果我们试图添加另一个具有重复值的元素,它会忽略重复值,如下面的代码所示:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/043-2.jpg?sign=1739040769-7fKEZEWhkDsqPDmsXzghEXTFvQ47k2mu-0-445aaecdef441eb0ac6a231706b423e7)
为了演示在集合上可以进行什么样的操作,我们来定义两个集合:
- 一个名为yellow的集合,里面包含了黄色的东西。
- 另一个名为red的集合,里面包含了红色的东西。
请注意,这两个集合之间有公共部分。这两个集合及其关系可以借助图2-3进行展示。
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/043-3.jpg?sign=1739040769-qyBOUjP2GxUzgRvA3H4xYSz1OBaAp3bH-0-7f6b59dc9b05ac75ef7827e41964442e)
图 2-3
如果想在Python中实现这两个集合,代码将是这样的:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/043-4.jpg?sign=1739040769-dpDjYb9kpPce0E15RLj1iUnn55VeR2R6-0-5b1ec611ac32194fe10a9b044ce6d41b)
现在,考虑以下代码,它演示了如何使用Python进行集合操作:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/043-5.jpg?sign=1739040769-8RDHUTEu5xMsMqOjloszOdSdvYYzrkuP-0-d9b204ec4160aaa28198f1d41ea593e6)
如前面的代码片段所示,Python中的集合可以有并和交等运算。我们知道,并运算将两个集合的所有元素合并到一起,而交运算则给出两个集合之间的公共元素集合。需要注意以下两点:
yellow|red
用于获得前面定义的两个集合(yellow
和red
)的并。yellow&red
用于获得前面定义的两个集合(yellow
和red
)的交。
集合的时间复杂度
以下是集合的时间复杂度分析:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/044-1.jpg?sign=1739040769-kQ9xFCbDsbR3Gy3aAHEUVRAAlQYkfOas-0-3983c39a7b35007f541444cd569ae478)
从集合的复杂度分析中可以发现一个需要注意的重要现象,那就是添加一个元素所需的时间与该集合的大小完全无关。
2.1.5 数据帧
数据帧是Python的pandas
包中的一种数据结构,用来存储可用的表格数据。它是算法中重要的数据结构之一,用于处理传统的结构化数据。我们看看以下表格:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/044-2.jpg?sign=1739040769-6roHkBmEhzZSPeS3hKZ3ZFGhFz6YN62g-0-8badb0f5beef1120da9b414b48c0399f)
现在,我们使用数据帧来表示它。
可以使用以下代码创建一个简单的数据帧:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/044-3.jpg?sign=1739040769-H7vFy8pk2DFneTvVD5lmxswNDEvhWDWJ-0-04e617de0403b71a7452782453f5e288)
请注意,在上面的代码中,df.column
是一个用来指定各列名称的列表。
数据帧也用于在其他流行的语言和框架中实现表格数据结构,例如R语言和Apache Spark框架。
数据帧术语
我们来看看在数据帧中使用的一些术语:
- 轴(axis):在pandas的文档中,一个数据帧的单列或单行称为轴。
- 轴族(axes):如果轴的数量大于1,则它们作为一组,称为轴族。
- 标签(label):数据帧允许用标签来命名列和行。
创建数据帧的子集
从根本上说,创建数据帧子集的方法主要有两种(比如说子集的名字是myDF
):
- 选择列
- 选择行
选择列
在机器学习算法中,选择合适的特征集是一项重要任务。算法在特定阶段时可能不需要全部特征。在Python中,特征选择是通过选择列来实现的,下面对选择列进行说明。
可以按列的名称来检索各列,如下所示:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/045-1.jpg?sign=1739040769-hpXe3hZs9IGjRivf9IeU0cf7tyF9tFb9-0-6ac56088cd818da63efa6ed61ff01795)
在数据帧中,列的位置是确定的,可以通过指定列的位置取出各列,具体如下:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/045-2.jpg?sign=1739040769-T99rwHEWixNn5Lwoe5abs3z9OMlzlYYr-0-0cb6e99db032394540a63e3859a9c140)
请注意,在这段代码中,我们正在检索DataFrame的前三行(一共有三行数据)。
选择行
数据帧中的每一行都对应着问题空间中的一个数据点。如果我们想要创建问题空间中数据元素的子集,则需要执行选择行操作。这个子集可以通过使用以下两种方法之一来创建:
- 指定各行的位置
- 指定一个过滤器
通过位置来检索各行的子集,具体操作如下:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/046-1.jpg?sign=1739040769-ITYaj2Ee01tXMPWaMcbjbkgvfxodmYKl-0-a0cf0643eae3094d1a7ea7ce7d75bd93)
注意,上面的代码将返回前两行以及所有列。
如果要通过指定过滤器来创建子集,需要使用一个或多个列来定义选择标准。例如,可以通过如下的方法选择数据元素的子集:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/046-2.jpg?sign=1739040769-tUF9aoGMniR6PXOrWYvY7YzlMZ7kkNXo-0-3f743772b75060e94b3a1da86371a786)
请注意,这段代码创建了由所有满足过滤器中规定条件的行所组成的子集。
2.1.6 矩阵
矩阵是一种具有固定列数和行数的二维数据结构,矩阵中的每个元素都可以通过指定它的列和行来引用。
在Python中,可以通过使用numpy
中的array
函数来创建矩阵,如下面的代码所示:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/046-3.jpg?sign=1739040769-fpss3rgJYGm4ppOOO49lNn03SmuMcJRM-0-23970c6a998aa592ddf855bcceaedcc0)
上面的代码创建了一个三行三列的矩阵。
矩阵运算
有很多运算可以用于矩阵数据。例如,我们尝试对前面创建的矩阵进行转置,可以使用transpose()
函数,将列转换成行、行转换成列:
![](https://epubservercos.yuewen.com/C1D54C/21191463108128406/epubprivate/OEBPS/Images/047-1.jpg?sign=1739040769-wbeqn99QteRDVP0thXnHOA4d1eAnwGJh-0-7ecd10bf72715d6eeffa1664d1954d31)
需要注意的是,在多媒体数据处理中经常使用矩阵运算。
前面已经学习了Python中的数据结构,我们下面学习抽象数据类型。