vlambda博客
学习文章列表

一网打尽Python中4大数据结构


导读

python之禅中有这样一句:simple is better than complex。翻译成中文我想就是“大道至简、大巧不工”。


具体到python中数据结构的选择运用,虽然有很多类型可供选择:除了基本的列表、字典、集合和元组4个基本类型外,collections模块中提供了很多定制化的数据结构,还有专用的堆heapq和枚举enum等。诚然,特定数据结构在某些应用场景下可能有神奇的效果,但把基础数据类型用到极致也可堪称是绝招。



本篇文章主要面向python初学者,介绍列表、字典、集合和元组4个基本数据结构的常用接口和用法,最后通过一道LeetCode原题讲解了数据结构的综合运用。



01 列表

列表可能是在使用python中最为常用的数据结构了,它类似于其他语言中的数组,但又可以存储多种数据类型,同时还可以自适应更改列表长度。可以说,在python中几乎没有一个列表解决不了的数据结构,如果有,那就……


列表简单易用且不失功能强大,除了丰富的魔法方法外,列表支持直接调用的接口并不多(通过dir(list)命令可以查看列表的所有接口),主要包括11个接口方法:


一网打尽Python中4大数据结构

列表类型内置11个方法接口


  • append:在列表尾端增加一个元素

  • insert:在列表指定位置插入一个元素,值得说明的是insert的目标索引位置可以为任意参数,当超过列表长度时会自动截断插入

  • extend:与另一个列表进行拼接扩展

  • pop:删除一个元素,接受一个索引参数,且要求索引为有效索引,不允许超出列表索引范围;缺省为-1,此时删除尾端元素

  • remove:删除一个元素,接受一个列表元素参数,要求该元素在列表中存在,不可缺省

  • clear:清空整个列表,相当于为列表赋值为空列表

  • index:查找目标元素在列表中的索引,要求该元素在列表中存在,否则报错

  • count:计算目标元素在给定列表中的个数,当目标元素不存在时返回0

  • sort:对列表进行inplace排序,可接受一个key参数指定排序规则,接受reverse参数明确是正序还是逆序

  • reverse:对列表进行inplace翻转

  • copy:对列表进行浅拷贝


列表的这些方法中,除了clear用的较少外,其他都是常用接口,需要注意的是虽然pop、remove、index和insert操作语法比较类似,但存在一个最大的不同是:insert接受的索引参数可以是任意索引,无论是否超出列表合法索引;而pop接受的索引必须是合法索引、index和remove接受的元素必须是存在的元素,否则会报错。例如:
1lyst = [1235]
2#索引9超出合法范围,自动在尾端插入
3lyst.insert(910#[1, 2, 3, 5, 10]
4#索引9超出合法范围,pop操作报错
5lyst.pop(9#IndexError: pop index out of range
6#元素100不在列表中,index报错
7lyst.index(100#ValueError: 100 is not in list
8#元素100不在列表中,remove报错
9lyst.remove(100#ValueError: list.remove(x): x not in list


当然,列表的强大之处不仅在于这11个接口,更加pythonic的操作是列表切片和列表推导式,这两者用得好,基本可以替代很多接口方法,更能免去很多for循环操作,性能也更加高效。



02 字典

列表之外,字典可能是python中用的也比较多的数据结构了,由于字典的底层应用哈希映射,所以要求字典的所有key必须是不可变元素(可哈希对象),增删改查操作一般都能实现O(1)复杂度,是低复杂度的必备数据结构。


一网打尽Python中4大数据结构

字典类型内置11个方法接口


  • fromkeys:从一个序列化对象(如列表等)创建一个字典,同时可接受一个缺省参数作为value,缺省时value为None

  • setdefault:与查找的get方法类似,当查找的key存在时返回其value值;否则在字典中增加该键值对,若value缺省,则value为None

  • pop:接受一个key,删除该元素并返回其value值,实际上相当于列表的remove

  • popitem:不接受任何参数,删除字典最后一个元素并返回其value值(python3.6以后,字典元素按照插入先后默认有序),当字典为空时引发错误,实际上相当于列表的pop()缺省参数操作

  • clear:与列表clear类似,清空字典

  • update:相当于列表的extend操作,但遇到相同的key时会保留后面字典中相应的value值

  • keys:返回字典的所有键

  • values:返回字典的所有值

  • items:返回字典的所有键值对,每个键值对为元组形式

  • get:接受一个key和一个默认value,当字典中存在该元素时返回其value,否则返回默认值

  • copy:字典的浅拷贝


这里对pop和popitem、setdefault和get以及update操作进行举例:
 1# popitem删除最后一个元素
2dic = {'a':1'b':2'c':3}
3dic.popitem()
4dic #{'a': 1, 'b': 2}
5# pop删除指定元素
6dic = {'a':1'b':2'c':3}
7dic.pop('a')
8dic #{'b': 2, 'c': 3}
9# setdefault操作
10dic = {'a':1'b':2'c':3}
11val = dic.setdefault('e'100)
12dic #{'a': 1, 'b': 2, 'c': 3, 'e': 100}
13val #100
14# get操作
15dic = {'a':1'b':2'c':3}
16val = dic.get('e'100)
17dic #{'a': 1, 'b': 2, 'c': 3}
18val #100
19# update操作
20dic = {'a':1'b':2'c':3}
21dic2 = {'a':4'd':10}
22dic.update(dic2)
23dic #{'a': 4, 'b': 2, 'c': 3, 'd': 10}



03 集合

集合操作可能最常见于用于对列表去重,它的最大特性是各元素仅保留1次,底层也是应用了哈希函数,所以在集合中查找元素一般也可实现O(1)复杂度,同时集合的嵌套元素也要求是不可变类型(可哈希对象)。


一网打尽Python中4大数据结构

集合类型内置17个方法接口


  • add:在集合中增加一个元素,如果元素已存在,则无实际操作

  • pop:不接受任何参数,堪称是最神秘的操作,不同于列表的从尾端删除、字典的指定键删除,集合的pop操作看似是"随机"删除。但实际上是按照加入集合的先后顺序,删除"最早"加入的元素

  • remove:类似于列表的remove操作,移除指定元素,当元素不存在时引发错误

  • discard:remove的替代版,当元素存在时移除,元素不存在时误操作且不报错

  • clear:清空集合

  • update:接受一个可迭代对象(可以不是集合类型),类似字典的update操作,逐一插入

  • copy:集合的浅拷贝


举个例子:
 1# pop删除最早的元素
2s = {123}
3# s.pop() #删除1,删除后s = {2, 3}
4# 在原集合中将1改为4,即此时s = {4, 2, 3)
5s = {423}
6s.pop() #删除2,删除后s = {4, 3}
7# remove与discard操作
8s = {123}
9s.remove(4#KeyError: 4
10s.discard(4#无操作


除了与列表和字典中类似的增删改操作外,集合还支持数学概念下的集合操作,如交集、并集、差集等。

  • intersection:接受两个集合作为参数,求两个集合的交集,生成新集合作为返回结果

  • intersection_update:对intersection的变形,在调用方法的集合上进行inplace操作,无返回值

  • isdisjoint:判断两个集合中是否存在公共元素,不存在公共元素时结果为True,否则为False

  • union:接受两个集合作为参数,返回并集的新集合作为返回值。ps:并集操作的inplace操作接口即为update

  • difference:接受两个集合作为参数,求前者与后者的差集,生成新集合作为返回结果

  • difference_update:与交集类似,对调用方法的集合进行inplace操作

  • symmetric_difference:对称差集,类似于补集,返回两个集合除公共元素意外的并集,即A有B无或A无B有的元素

  • symmetric_difference_update:对调用方法的集合进行inplace操作

  • issubset:判断是否是子集,返回bool结果

  • issuperset:判断是否是超集,返回bool结果
 1#inplace求交集
2s1 = {123}
3s2 = {245}
4s1.intersection_update(s2)
5s1 #{2}
6#返回交集结果
7s1 = {123}
8s2 = {245}
9s3 = set.intersection(s1, s2)
10s3 #{2}
11# 判断是否存在交集,若存在则返回False,否则返回True
12s1 = {123}
13s2 = {23}
14set.isdisjoint(s1, s2) #True
15# 判断子集和超集
16s1 = {123}
17s2 = {23}
18s1.issubset(s2) #False
19s1.issuperset(s2) #True

另外,集合的底层哈希实现决定了它有一个神奇特性,即可实现序列的排序:
1import random
2s = list(range(10))
3random.shuffle(s)
4print(s) #[3, 4, 9, 5, 8, 6, 7, 2, 0, 1]
5print(list(set(s))) #[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


当然,要求排序的元素不存在重复元素,否则……



04 元组

如果说列表、字典和集合都有其各自擅长应用场景的话,那么元组可能是最没有存在感的数据结构:它接口有限、功能单一,而且是不可变类型。一般而言,用元组解决的问题都可以用列表实现。但使用用元组时,更多在于暗示该序列为不可变类型。当然,当元组内嵌套子列表时实际上是可以对嵌套的子列表进行更改操作的。


正因为元组的不可变特性,其操作接口十分有限,仅包括查找和计数两个接口:

一网打尽Python中4大数据结构

元组类型内置2个方法接口


  • index:查找给定元素的索引,若元素不存在报错

  • count:对给定元素在元组中的出现次数计数,不存在时返回0


举个例子:
1t = (12)
2t.index(3#ValueError: tuple.index(x): x not in tuple
3t.count(3# 0


需要注意元组初始化时的一个常见错误:当元组元素个数为1个时,要在元素后面加一个",",否则会被转为相应的元素;而当元组元素个数为多个时,小括号可以省略:
1# 单元素元组的初始化要加","
2t = ('s')
3type(t) # str
4t = ('s',)
5type(t) #tuple
6# 多元素元组初始化时可省略小括号
7t = '2'1True
8type(t) #tuple


另外,考虑元组的不可变特性,所以元组也常用于以多个元素作为key的字典存储,而这是列表和集合等可变类型所不具备的:
1dic = dict()
2dic[[12]] = 1 #TypeError: unhashable type: 'list'
3dic[{12}] = 1 #TypeError: unhashable type: 'set'
4dic[(12)] = 1 #可正常存储为字典



05 综合案例

这里列举一个LeetCode每日一题的例子:

LeetCode 355. 设计推特 

设计一个简化版的推特(Twitter),可以让用户实现发送推文,关注/取消关注其他用户,能够看见关注人(包括自己)的最近十条推文。


你的设计需要支持以下的几个功能: 


postTweet(userId, tweetId): 创建一条新的推文 


getNewsFeed(userId): 检索最近的十条推文。每个推文都必须是由此用户关注的人或者是用户自己发出的。推文必须按照时间顺序由最近的开始排序。


follow(followerId, followeeId): 关注一个用户 


unfollow(followerId, followeeId): 取消关注一个用户


题目要求实现推特的几个常用功能,包括创建(增)、检索(查)、关注(改或增)、取消关注(删),可以说综合运用了数据结构的各种常用操作。为了实现较好的时间复杂度,结合python中4个常用数据结构的各自特性:


  • 保存用户列表:这是一个隐藏的功能,创建推文或者关注操作的用户不存在时,首先要进行用户创建。为实现O(1)复杂度,当然是选用字典保存所有用户id

  • 创建推文:为了存储推文,列表、字典、集合都可以,因为不存在特殊要求,所以选用列表即可

  • 检索最近10条推文:这是本题的难点,因为是要检索自己已关注用户的所有推文中的最近10条,所以存在合并后的TOP10问题。当然,实现的方式有很多,堆heapq可能是比较理想的,但实际上一个列表也足以满足需要

  • 关注和取消关注:实际上就是维护每个用户的关注序列,考虑到后续还有取关的操作,加之题目设定了一些无效操作(例如重复关注和自己关注自己),所以列表的复杂度难以满足要求,字典和集合都可以,这里选用集合,因为集合的discard接口可很好的处理元素不存在时的删除操作。

  • 另外:由于题目中要求查找最新的推文时,无法仅按照推文id大小查找先后顺序,所以在创建新的推文时不仅保存期推文id,还保留了一个推文绝对id字段来保留全局先后顺序,当然是运用元组最为合适了


 1class Twitter:
2    def __init__(self):
3        """
4        Initialize your data structure here.
5        """

6        self.user = {}#当前系统用户列表,每个用户对应2个子字典,F和T
7        self.Tid = 0 #记录推文绝对id
8
9    def _new_user(self, userId):
10        """
11        create a new user, follow self 
12        """

13        F = {userId}#关注者集合,并初始时关注自己
14        T = []#推文列表
15        self.user[userId] = {'F':F, 'T':T}
16
17    def postTweet(self, userId: int, tweetId: int) -> None:
18        """
19        Compose a new tweet.
20        """

21        if userId not in self.user:
22            self._new_user(userId)
23        self.user[userId]['T'].append((self.Tid, tweetId))#更新推文列表,记录为元组:(推文绝对id, 推文id)
24        self.Tid += 1
25
26    def getNewsFeed(self, userId: int) -> List[int]:
27        """
28        Retrieve the 10 most recent tweet ids in the user's news feed. Each item in the news feed must be posted by users who the user followed or by the user herself. Tweets must be ordered from most recent to least recent.
29        """

30        if userId not in self.user:#用户不存在
31            return []
32        Tlist = []
33        for uid in self.user[userId]['F']:
34            Tlist.extend(self.user[uid]['T'])#关注的推文
35        Tlist.sort(reverse=True)
36        T10 = Tlist[:10]#列表逆序排序后取前10
37        return [T[1for T in T10]
38
39    def follow(self, followerId: int, followeeId: int) -> None:
40        """
41        Follower follows a followee. If the operation is invalid, it should be a no-op.
42        """

43        if followerId not in self.user:
44            self._new_user(followerId)
45        if followeeId not in self.user:
46            self._new_user(followeeId)
47        self.user[followerId]['F'].add(followeeId)#利用集合的自动去重特性,省去重复关注的判断
48
49    def unfollow(self, followerId: int, followeeId: int) -> None:
50        """
51        Follower unfollows a followee. If the operation is invalid, it should be a no-op.
52        """

53        if followerId in self.user and followeeId in self.user and followeeId != followerId:
54            self.user[followerId]['F'].discard(followeeId)#集合的删除,省去判断元素是否存在


以上,就是综合运用了python中4个基本数据结构各自特性的一个案例,基本上是考虑了各数据类型的优点。



● 

● 

● 
● 
● 
● 


每天分享一些有趣的干货