1.2.11 数据结构

优质
小牛编辑
133浏览
2023-12-01

数据结构用来将一些数据组织在一起。换句话说,它们是用来存储一系列相关的数据。

在Python中有四种内建数据结构--列表、元组、字典和集合,我们将看到如何使用它们中的每一个,它们是怎样使我们的生活更容易的。

列表

列表是一种数据结构,它保存条目的有序集合。例如,你可以在列表中存储一个序列。这很容易想象,你想像一下购物清单,那里有你要购买物品的一个清单。除非在你的清单上每一行列有一个单独物品,然而,在python中,你在它们中间放上逗号。

条目的列表应包含在方括号内,以便Python明白你在指定一个列表。一旦您创建了一个列表,你可以添加、删除或是搜索列表中的条目。因为我们可以添加和删除条目,我们说一个列表是一个可变的数据类型,即这种类型可以更改。

对象和类的快速介绍

尽管直到现在,我一直推迟讨论对象和类,现在需要一个小小的解释,这样你可以更好的理解列表。我们将在后面章节中详细探讨这一课题。

列表是使用对象和类的一个例子。当我们使用一个变量 i,为它分配一个值,例如把整数5赋值给它,你可以认为它是创建一个类为int(即类型)的对象(即实例)i。事实上,你可以阅读help(int)更好地理解这一点。

类也有方法,也就是为了使用而定义的只关于那个类的函数,只有当你有那个类的对象时,你才可以使用这些函数.例如,Python为list(列表)类提供了一个append方法,它允许你在列表的整改添加一个条目。例如,mylist.append('an item')将给列表mylist添加字符串。注意,我们使用点操作符访问对象的方法。

类也有字段,除了为了使用而定义的只与那个类相关的变量,它什么也没有。只有当你有那个类的对象时,你可以使用那些变量或名字。字段孔明通过点操作符访问的。例如,mylist.field

例子 (保存为using_list.py):

# 这是我的购物清单
shoplist = ['苹果', '芒果', '胡萝卜', '香蕉']

print('我要买', len(shoplist), '个物品。')

print('清单是:', end=' ')
for item in shoplist:
    print(item, end=' ')

print('\n我还要买大米。')
shoplist.append('大米')
print('现在我的清单是', shoplist)

print('现在我将要为我的清单排序')
shoplist.sort()
print('排序后的购物清单是', shoplist)

print('我要买的第一个物品是', shoplist[0])
olditem = shoplist[0]
del shoplist[0]
print('我已经买了', olditem)
print('现在我的清单是', shoplist)

输出:

$ python using_list.py
我要买 4 个物品。
清单是: 苹果 芒果 胡萝卜 香蕉
我还要买大米。
现在我的清单是 ['苹果', '芒果', '胡萝卜', '香蕉', '大米']
现在我将要为我的清单排序
排序后的购物清单是 ['大米', '胡萝卜', '芒果', '苹果', '香蕉']
我要买的第一个物品是 大米
我已经买了大米
现在我的清单是 ['胡萝卜', '芒果', '苹果', '香蕉']

它是如何工作的:

变量shoplist是将要去超市的人的一个购物清单。在shoplist中,我们只存储了要买的物品的名字的字符串,你可以向清单中添加包括数字甚至是其它清单的任何对象

我们也使用了循环for..in遍历清单中的所有条目。到现在为止,你必须认识到一个列表也是一个序列。序列的特性将在后面的章节中讨论。

注意,在print函数中使用end关键字参数,表明输出以一个空格结束而不是通常的换行。

接下来,和前面讨论过的一样,我们使用列表对象的append方法向列表中添加一个项目。然后,我们只把列表简单地传递给print语句,整洁地打印列表的内容,以检查这个条目确实添加到了列表中。

然后,我们使用列表对象的sort方法为列表排序。 这个方法作用到列表本身,并不返回一个修改过的列表,理解这一点很很重要,它不同于对字符串的操作。这也是为什么我们列表是可修改的,而字符串是不可修改的原因。

然后,我们在超市购买了一个物品,我们想把它从购物清单中移除,通过使用del语句来实现。这里,我们提到我们想要移除清单中的哪个物品,del语句为我们将它从清单中移除。我们指定,我们想从清单移除第一项,因此,我们使用del shoplist[0](记住,Python从0开始计数)。

如果你想知道列表对象定义的所有方法,详见help(list)

元组

元组是用来容纳多个对象。认为它们是类似于列表,但是没有列表给你的广泛功能。元组的一个主要特征是他们是不可变,像字符串,即您不能修改元组。

元组是通过在一对可选的圆括号中,项目之间用逗号分隔来定义的。

元组通常用在,一个语句或一个用户定义的函数能够安全地假设为值的集合,即值的元组,不会改变。

例子 (保存为using_tuple.py):

# 我推荐使用括号表示元组的开始和结束,尽管括号是可选的。
# 毕竟显式声明比隐式声明更加直观
zoo = ('蟒蛇', '大象', '企鹅') # 记住圆括号是可选的
print('动物园中动物有数量有', len(zoo))

new_zoo = '猴子', '骆驼', zoo
print('在新动物园中笼子的数量是', len(new_zoo))
print('在新动物园所有的动物是', new_zoo)
print('从老动物园中带来的动物是', new_zoo[2])
print('从老动物园带来最后的动物是', new_zoo[2][2])
print('在新动物园中动物的数量有', len(new_zoo)-1+len(new_zoo[2]))

输出:

$ python using_tuple.py
动物园中动物有数量有 3
在新动物园中笼子的数量是 3
在新动物园所有的动物是 ('猴子', '骆驼', ('蟒蛇', '大象', '企鹅'))
从老动物园中带来的动物是 ('蟒蛇', '大象', '企鹅')
从老动物园带来最后的动物是 企鹅
在新动物园中动物的数量有 5

它是如何工作的:

变量zoo指的是一个物品的元组。我们看到len函数可以用来获取元组的长度。这也表明,一个元组同样也是一个序列。

因为老动物园zoo将要关闭,我们现在将这些动物迁移到一个新的动物园new_zoo。因此,(新动物园)new_zoo的tuple包含一些已经存在的动物以及从老动物园zoo带来的动物。回到现实,请注意,在一个元组中的元组不失去其特性。

就像列表一样,我们可以通过在一对方括号中指定条目的位置,访问元组中的物品。这被称为索引操作符。我们通过指定new_zoo[2]访问新动物园new_zoo中的第三项,通过指定new_zoo[2][2]访问新动物园new_zoo的第三项。一旦理解这个习语,这是非常简单的。

有0个或1个条目的元组

一个空的元组由一对空的括号如myempty = ()组成。 然而,只有一个对象的元组并非如此简单。你必须通过在第一个对象(唯一的一个)后紧跟一个逗号来指定它,这样Python可以区分是一个元组还是一个表达式中一个对象的括号,例如,如果你想定义一个只含一个对象这为2的元组,你必须使用singleton = (2 , )

Perl程序员应该注意

在一个列表中的列表不会失去其特性,也就是说并不像在Perl中夷为平地。这同样适用于在一个元组中的一个元组,或在一个列表中的元组,或在一个元组中的列表等。就Python而言,他们只是存储在另一个对象中的一个对象。

字典

字典就像一个地址簿,在那里你只通过知道他/她的名字,就可以找到地址或联系人详细信息。也就是说,我们使用(姓名)与(细节)相联系。注意,键必须是独一无二的,就像如果有两个完全相同的名字的人,你无法找到正确的信息。

注意,字典的关键字你只能使用不可变的对象(比如字符串),你可以使用不可变或可变的对象作为字典的值。这基本上意味着,简单地说,对于键你只能使用简单对象。

在字典中的一对键和值是通过使用冒号指定的,如,d = {key1 : value1, key2 : value2 }。注意,键值对用冒号分隔,彼此之间以逗号分隔,所有这些都是包含在一对大括号中。

记住,在字典中键-值对不以任何方式排序。如果你想要一个特定的顺序,那么你将不得不在使用前自己排序。

你将要使用的字典是dict类的对象或实例。

例子 (保存为 using_dict.py):

# 'ab'是英文address book(地址簿)的首个字母

ab = {  'Swaroop'   : 'swaroop@swaroopch.com',
        'Larry'     : 'larry@wall.org',
        'Matsumoto' : 'matz@ruby-lang.org',
        'Spammer'   : 'spammer@hotmail.com'
    }

print("Swaroop的地址是", ab['Swaroop'])

# 删除一个键-值对
del ab['Spammer']

print('\n地址薄中有 {0} 个联系人\n'.format(len(ab)))

for name, address in ab.items():
    print('联系人 {0} 的地址是 {1}'.format(name, address))

# 添加一个键-值对
ab['Guido'] = 'guido@python.org'

if 'Guido' in ab:
    print("\nGuido的地址是", ab['Guido'])

输出:

$ python using_dict.py
Swaroop的地址是 swaroop@swaroopch.com

地址薄中有 3 个联系人

联系人 Larry 的地址是 larry@wall.org
联系人 Matsumoto 的地址是 matz@ruby-lang.org
联系人 Swaroop 的地址是 swaroop@swaroopch.com

Guido的地址是 guido@python.org

它是如何工作的:

我们使用已经讨论过的符号创建字典ab。然后我们通过使用在列表和元组中讨论过的索引操作符--指定关键字来访问键-值对,遵守简单的语法。

我们可以使用我们的老朋友——del语句删除键值对,我们简单地指定字典和要删除的关键字的索引操作符,并将它传递给del语句。对于这个操作,没有必要知道对应于关键字的值。

接下来,我们我们使用字典的items方法,访问字典的每个键-值对的。它返回一个元组的列表,每个元组包含一对值--关键字及紧随其后的值。我们检索这对值并使用for..in循环为每一对分配给相应的变量nameaddress,然后在for..in块中打印这些值。

我们可以通过简单地使用索引操作符来访问一个键并分配值的方式添加新的键值对,像上面的例子中我们所做的添加Guido。

我们可以使用in操作符来检查一个键值对是否存在。

字典dict类的列表方法,请看help(dict)

关键字参数和字典

如果你已经在函数中使用过了关键字参数,那么你已经接触过字典了。想象一下,这个键值对是在函数定义的参数列表中指定的,而当你在函数中访问变量,它只是访问字典的一个键(在编译器设计术语中称为符号表)。

序列

列表、元组和字符串都序列的一个实例,但是什么是序列,它们为什么如此特殊呢 ?

主要特点是成员测试,即in(在)和not in(不在)表达式中和索引操作,这使我们在一个序列中能够直接获取一个特定的对象。

上面提到的——列表、元组和字符串这三种类型的序列,也有允许我们找回一彼序列即序列的一部分的切片操作。

例子 (保存为seq.py):

shoplist = ['苹果', '芒果', '胡萝卜', '香蕉']
name = 'swaroop'

# Indexing or 'Subscription' operation
print('第0项是', shoplist[0])
print('第1项是', shoplist[1])
print('第2项是', shoplist[2])
print('第3项是', shoplist[3])
print('第-1项是', shoplist[-1])
print('第-2项是', shoplist[-2])
print('第0个字符是', name[0])

# 一个列表的切片
print('第1项到第3项是', shoplist[1:3])
print('第2项到末尾是', shoplist[2:])
print('第1到-1项是', shoplist[1:-1])
print('开头到结尾是', shoplist[:])

# 字符串的切片
print('第1到第3个字符是', name[1:3])
print('第2到末尾的字符是', name[2:])
print('第1到-1的字符是', name[1:-1])
print('从头到尾的字符是', name[:])

输出:

$ python seq.py
第0项是 苹果
第1项是 芒果
第2项是 胡萝卜
第3项是 香蕉
第-1项是 香蕉
第-2项是 胡萝卜
第0个字符是 s
第1项到第3项是 ['芒果', '胡萝卜']
第2项到末尾是 ['胡萝卜', '香蕉']
第1到-1项是 ['芒果', '胡萝卜']
开头到结尾是 ['苹果', '芒果', '胡萝卜', '香蕉']
第1到第3个字符是 wa
第2到末尾的字符是 aroop
第1到-1的字符是 waroo
从头到尾的字符是 swaroop

它是如何工作的:

首先,我们看看如何使用索引来获得一个序列的个别项,这也称为订阅操作。当你在方括号中指定一个数字对应一个序列中的某项,如上所示,Python会为你取得序列中相对应位置的项。记住,Python从0开始数数。因此,在序列shoplist中, shoplist[0]取第一项和shoplist[3]获取第四项。

索引也可以是负数,在这种情况下,这个位置从序列的结尾开始计算。因此, shoplist[-1]指的是序列的最后一项, shoplist[-2]取倒数第二个项。

这个切片操作是通过指定序列的名称后面加上一个方括号,方括号中有一对可选的用冒号分隔的数。注意,这非常类似于你到现在一直使用的索引操作,记住这些数字是可选的但冒号不是。

在切片操作中的第一个数字(在冒号前)是切片开始的位置,第二个数字(在冒号后)是切片停止的位置。如果第一个数字没有指定,Python会从序列开头开始,如果第二个数字被冷落,Python会在序列的末尾停止。注意,返回的切片在开始位置开始,在结束位置前结束,也就是说,返回的切片包含开始位置,但不包含结束位置。

因此, shoplist[1:3] 返回序列的切片从位置1开始,包括位置2,但是在位置3停止,因此,返回两个项目的切片。同样,shoplist[:]返回整个序列的一个副本。

你也可以使用负位置做切片。负数用于从序列的结尾开始。例如,shoplist[:-1] 将返回一个不包括序列最后一项,但包含了其它一切的切片。

你也可以为切片提供第三个参数,这是切片的步长(默认情况下,步长为1):

>>> shoplist = ['苹果', '芒果', '胡萝卜', '香蕉']
>>> shoplist[::1]
['苹果', '芒果', '胡萝卜', '香蕉']
>>> shoplist[::2]
['苹果', '胡萝卜']
>>> shoplist[::3]
['苹果', '香蕉']
>>> shoplist[::-1]
['香蕉', '胡萝卜', '芒果', '苹果']

注意,当步长是2时,我们获得位置0、2、……的项目,当步长是3晨,我们获得位置是0、3、等等的项目。

使用Python解释器的交互式提示,尝试指定切片的不同组合,以便你可以立刻看到结果。序列的一大好处是,你可以以同样的方式访问元组、列表和字符串!

集合

集合是简单对象的无序集合,用于一个集合中对象的存在比它的顺序或发生多少次更重要的时候。

使用集合,你可以测试成员,它是否是集合的子集以及找到两个集合的交集,等等。

>>> bri = set(['巴西', '俄罗斯', '印度'])
>>> '印度' in bri
True
>>> 'usa' in bri
False
>>> bric = bri.copy()
>>> bric.add('中国')
>>> bric.issuperset(bri)
True
>>> bri.remove('俄罗斯')
>>> bri & bric # 或者 bri.intersection(bric)
{'巴西', '印度'}

它是如何工作的:

这个例子是非常一目了然的,因为它涉及到学校教的数学的基本集合理论。

关联

当你创建一个对象,并赋给它一个值,该变量只是指向对象,并不代表对象本身!也就是说,变量名称指向你电脑中内存中的存储对象的那部分。这就是所谓的把名字绑定给对象。

一般来说,你不需要担心这个,但是对引用有一个需要你注意的微妙的影响。

例子 (保存为reference.py):

print('简单的分配')
shoplist = ['苹果', '芒果', '胡萝卜', '香蕉']
# mylist是指向同一对象的另一个名字!
mylist = shoplist 

# 我买到了第一项物品,因此我从清单中移除它
del shoplist[0] 

print('shoplist是', shoplist)
print('mylist是', mylist)
# 注意shoplist和mylist都打印没有‘苹果’的相同的清单
# 证明它们指向相同的对象

print('通过制作完整的切片复制')
# 通过制作完整的切片复制
mylist = shoplist[:] 
# 移除第一项
del mylist[0] 

print('shoplist是', shoplist)
print('mylist是', mylist)
# 注意,现在两个清单不同

输出:

$ python reference.py
简单的分配
shoplist是 ['芒果', '胡萝卜', '香蕉']
mylist是 ['芒果', '胡萝卜', '香蕉']
通过制作完整的切片复制
shoplist是 ['芒果', '胡萝卜', '香蕉']
mylist是 ['胡萝卜', '香蕉']

它是如何工作的:

在注释中有更多有用的解释。

记住,如果你想要复制一个列表或这种类型的序列或复杂的对象(而不是简单的对象如整数),那么您必须使用切片操作复制。如果你只是用另一个变量名指定,两个变量将“关联”到相同的对象,如果你不小心,这可能会引起麻烦。

Per程序员需要注意

记住,列表的一个赋值语句并不创建一个副本。你必须使用切片操作复制序列。

关于字符串的更多

之前,我们已经详细讨论了字符串。在这能了解更多吗?嗯,你知道吗,字符串也是对象和也有做任何事情的方法--从检查的部分字符串到从字符串中分离。

在程序中你使用的字符串都是str类的对象,在下面的例子中将演示这个类的一些有用的方法,这些方法的完整列表,请看help(str)。

例子 (保存为 str_methods.py):

# 这是一个字符串对象
name = 'Swaroop' 

if name.startswith('Swa'):
    print('是的,字符串以"Swa"开始')

if 'a' in name:
    print('是的,它包含字符串"a"')

if name.find('war') != -1:
    print('是的,它包含字符串"war"')

delimiter = '_*_'
mylist = ['巴西', '俄罗斯', '印度', '中国']
print(delimiter.join(mylist))

输出:

$ python str_methods.py
是的,字符串以"Swa"开始
是的,它包含字符串"a"
是的,它包含字符串"war"
巴西_*_俄罗斯_*_印度_*_中国

它是如何工作的:

在这里,我们看到字符串的很多方法在起作用。startswith方法是用来找出字符串是否以给定的字符串开始的。in操作符是用来检查一个给定的字符串是否是一个字符串的一部分。

find方法用于定位给定的子字符串在字符串内的位置,如果不能成功找到子字符串它返回-1。str类也有一个整洁的方法来join(连接)一个序列的字符串,用充当分隔符的字符串连接序列中每个条目,返回一个由它生成的巨大的字符串。

小结

我们详细探索了Python各种内建的数据结构,写合理大小的程序,这些数据结构是至关重要的。

现在,我们有很多Python的基本知识已经到位,下面,我们看看如何设计和写一个真实的Python程序。


继续阅读解决问题