介绍一下 dict 和 set 这两个数据结构。
dict
dict 是 Python 内置的字典类型,熟悉 Java 的同学可以把它类比为 Map。dict 使用键值对来存储(key-value),它的查找速度特别快。
dict 一般用在什么场景呢?假设我们需要根据公司名字查找公司地址,按照我们之前的写法,我们需要先建立两个 list ,一个存储公司名字,一个存储公司总部地址,然后查找公司名字,记录好列表位置,再从地址列表查找到具体元素,你还得保证两个表元素位置必须一一对应。不仅如此,如果表很长,那遍历查找效率将会非常低。
>>> Inc = ['腾讯','阿里','百度']
>>> adress = ['深圳','杭州','北京']
>>> BaiduAdress = adress[Inc.index('百度')]
>>> BaiduAdress
'北京'
复制代码
我们现在用 dict 实现,使用一个「公司-地址」这样的键值对来进行存储数据,查找的时候,我们只需要输入公司名字,就可以查找到对应的地址,同时,不论 dict 的数据有多少,查找单项的速度都是一样的,而且非常迅速。
>>> Inc_dict = {'腾讯':'深圳','阿里':'杭州','百度':'北京'}
>>> Inc_dict['百度']
'北京'
复制代码
dict 速度这么快的原理就是使用了空间换取时间的方法,将无限集映射到一个有限集中。通过一个散列函数来计算每一个 key 应该存放在内存中的位置,然后把 value 存储在内存的这个位置上,等到需要取出 key 对应的 value 的时候,只需要通过函数计算出这个位置,然后直接去拿就行了。是不是有点像我们查字典的步骤呢?
通过散列函数求出的最终值就是对应的哈希值(Hash),Java 中的 Map 最常用的实现 HashMap 也是用类似的原理来设计的。Hash 算法也是数据结构中特别重要的一个知识点,所以如果我们计算机的基本功扎实,学哪门语言的时候都是融会贯通的。
当然,散列函数本身比较复杂,还要牵扯到冲突的解决问题,简单来说,不同的 key 通过散列函数求得的内存位置可能是一样的,这样就导致了冲突,解决这种冲突的方法有很多,Python 设计者选择了开放定址法,在冲突的时候用另一个不同的函数再计算。在这里我就不深入讨论了,有兴趣的同学可以查阅下相关资料。
我们有很多种方式进行 dict 的初始化,下面几种初始化方式都会获得{"one": 1, "two": 2, "three": 3}:
>>> a = dict(one=1,two=2,three=3)
>>> b = {'one':1,'two':2,'three':3}
>>> c = dict(zip(['one','two','three'],[1,2,3]))
>>> d = dict([('two',2),('one',1),('three',3)])
>>> e = dict({'three':3,'one':1,'two':2})
>>> a == b == c == d == e
True
复制代码
除了通过初始化以外,还可以通过 key 来放入值,再次传入相同 key ,不同 value,将会覆盖前面传入的 value。如果某个 key 不存在,获取该 key 的 value 将会报 KeyError 错误。
>>> Inc_dict['途牛'] = '南京'
>>> Inc_dict['途牛']
'南京'
>>> Inc_dict['途牛'] = '金陵'
>>> Inc_dict['途牛']
'金陵'
>>> Inc_dict['小米']
KeyError:'小米'
复制代码
为了防止获取 key 不存在的情况。我们可以用 in 来判断 dict 中是否已经存储过以这个 key 来存储的键值对。或者用 get() 方法来获取 value,如果 key 不存在,get() 将返回 None,可以设置一个参数来表示 key 不存在时候的默认返回值。
>>> '小米' in Inc_dict
False
>>> Inc_dict.get('小米')
>>> Inc_dict.get('小米','北京')
北京
复制代码
通过 pop(key) 方法,来返回并删除对应的 value:
>>> Inc_dict.pop('腾讯')
'深圳'
>>> Inc_dict
{'阿里':'杭州','百度':'北京','途牛':'金陵'}
复制代码
最后介绍下 dict 的迭代,我们知道 list 迭代可以简单的通过 for 来遍历,dict 迭代需要多做一些操作。
>>> d = {'a':1,'b':2,'c':3}
>>> for key in d:
... print(key)
...
'a'
'c'
'b'
复制代码
dict 默认的迭代方式是迭代 key ,如果你需要迭代 value 可以通过 d.values() 来获取 value 的列表
>>> for value in d.values()
... print(value)
...
1
3
2
复制代码
当然,你还可以同时迭代 key 和 value
>>> for k, v in d.items():
... print(k, v)
...
a 1
c 3
b 2
复制代码
细心的同学一定发现了迭代的顺序和我们初始化定义的顺序是不同的,之前也提到了,dict 内部存放顺序是根据散列函数决定的,所以最后的存放顺序不一定和插入顺序一致,那我们迭代顺序显然是不确定的了。
dict 的设计是典型的以空间换取时间,大家学习 Python 越深入就会发现 Python 的设计里有很多这样的设计, Python 设计的时候,大概已经不是内存最大就 4,500K 的年代了吧(手动嬉笑)。所以 dict 的特点就是,查找和插入的速度非常快,并且不随元素数量的增长而变慢。
注意:key 必须是不可变对象(字符串,整数等),如果 key 是 list,就会报错 TypeError: unhashable type: 'list',tuple 虽然是不可变对象,但如果传入的 tuple 元素有可变对象,依然会报错。
>>> d = {'a':1}
>>> d = {'a':1,(1,):2}
>>> d = {'a':1,(1,):2,(1,[1]):3}
TypeError: unhashable type: 'list'
复制代码
set
set 和 dict 很像,不过 set 不存储键值对,你可以把它想像成只存储 key 的 dict,也可以理解成数学中的无序无重复集合这个概念。所以在 set 中是没有重复元素的,也只能存放不可变元素。我们可以通过一个 list 来创建 set。同样,也是用大括号表示。
>>> s = set([1,2,3])
>>> s
{1,2,3}
>>> s = set([1,2,3,3,3])
>>> s
{1,2,3}
复制代码
我们可以看到,重复的元素自动被过滤了,同时 set 也是无序的,虽然创建时候显示看起来好像是有序的。我们来看看 set 的一些常用方法。
>>> s.add(4)
>>> s
{1,2,3,4}
>>> s.add(4)
>>> s
{1,2,3,4}
复制代码
add(key)添加元素到 set 中,但添加重复元素将不会生效
>>> s.remove(4)
>>> s
{1,2,3}
>>> s.remove(4)
KeyError: 4
>>> s1 = {1,2,3}
>>> s2 = {2,3,4}
>>> s1 & s2
{2,3}
>>> s1 | s2
{1,2,3,4}
复制代码
remove(key)删除元素,如果 key 不存在会报错。同时,set 之前说过可以看成是集合,所以可以做一些交并集的操作。