百家号不支持代码格式,文章里的代码排版都是乱的。
如果需要拷贝代码,可以去同名微信公众号。
在教小朋友Python字典类型时,她问了一个非常好的问题:既然都是作为“容器”,为什么有列表了,还需要字典?
给定一个列表,包含3万个随机整数, 和一个目标数。求列表里有多少组数字,两数之和等于目标数。
nums = random.sample(range(1,100000),30000)
target = 88888
如果不考虑时间复杂度,用一个简单的嵌套循环就可以了。
cn = 0
result = []
print(datetime.datetime.now())
for i in range(len(nums)):
for j in range(i+1,len(nums)):
cn += 1
if nums[j] == target - nums[i] :
result.append([nums[i],nums[j]])
print(datetime.datetime.now())
print(cn)
但是如果考虑时间复杂度。
程序外层 i 循环 3万次,而内层j分别循环 29999,29998,29997……,1次
所以,一共要循环 (n * (n-1)) /2 = 449985000次。
如下图,我们用了一个计数器变量cn,每次循环,对 cn 加1。
我们发现,程序果然执行了 4亿多次,用了2分多钟。
根据前面学习过的时间复杂度的知识。
这个程序的时间复杂度是 O( n ** 2)。
如果列表里有10万个元素,需要循环近50亿次。
如果列表里有100万个元素,则需要循环近5000亿次。
那有没有办法可以降低程序的时间复杂度呢?只要1分钟,甚至1秒钟就能完成100万个元素的计算呢?
答案是肯定的,不过我们先来介绍Python里的另外两种容器类型:元组 和 字典
元组
元组类型和列表类型非常相似。
不同之处在于列表用中括号,元组用小括号(或者没有括号)
最重要的不同是 元组里的元素不能修改。
tup1 = (1,2,3,4,5)
tup2 = (1,'a',3,[1,23,2])
tup3 = 'abc',1,2,'d'
从元组里获取元素的方法也和列表一致,可以使用下标或者下标切片。
但是不要试图去改变元组里的元素。比如
tup1[2] = 8
思考一下,下面的操作会报错吗?
tup1 = tup1 +tup2
字典
元组用小括号,列表用中括号,而字典用大括号。
并且和元组、列表不同,字典里的每个元素都要写成 key:value的形式。
形如
d1 = {key1:value1,key2:value2}
比如说某班同学的语、数、外考试成绩
d1 = {'Cherry':[90,89,100],'Mark':[100,90,100],'Ruby':[100,100,100]}
上面的字典实例 d1里,有3个元素。分别是
'Cherry':[90,89,100]
'Mark':[100,90,100]
'Ruby':[100,100,100]
而要从字典里获取元素,也不能再使用下标了。而是要用 d1[key] 的方式。
所以下面的程序
d1['Ruby']
是正确的,而
d1[2]
就会报错!
在字典里,key必须是唯一的。
比如说如果字典里还没有'Tom' 这个 key。
d1['Tom'] = [80,90,80]
就是在字典 d1里新增了一个元素,这个元素的key是 'Tom',value是 [80,90,80]。
而在上面的程序执行完之后,字典里已经有'Tom'这个 key了,再执行
d1['Tom'] = [50,50,60]
就是在修改这个元素的value。字典里不会出现2个'Tom'。
因为字典不能通过下标遍历,所以,如果我想在字典里查找某个key是否存在,或者遍历整个字典时,Python提供了下列2种办法。
key in dict
if 'Ruby' in d1:
print('Ruby in d1')
因为d1 里有‘Ruby’这个key,所以程序里的 if 判断是成立的。
items()函数
for k,v in d1.items():
print(k , ' ', v)
上面的程序相当于遍历了字典里的每一个元素,而变量 k, v 依次等于每个元素的key和value。
用字典,从2分钟变成0.2毫秒
接下来,我们用上面刚学的字典类型,看看是否真的能更快。
因为程序并不复杂,所以直接先看程序。
d1 = {}
result = []
print(datetime.datetime.now())
for k in range(len(nums)):
d1[ nums[k] ] = k
for i in range(len(nums)):
minus = target - nums[i]
if minus in d1:
result.append([nums[i],minus])
print(datetime.datetime.now())
同样是3万个随机整数。
使用列表,用了2分多种,而使用字典,只用了0.2毫秒。
奥秘在什么地方呢?
1. 下面这段代码是把列表里的3万个元素存储到字典里。
因为要遍历列表,所以会循环3万次。
如果不理解的话,看下面这个简单的示例。
2. 下面这段代码外层还是遍历列表,每循环一个元素,都用目标数减去这个元素得到差 minus,然后看 minus 是否在 字典 d1里。
显然外层还是需要循环 3 万次。
外层每循环一次,都要执行一次 minus in d1,从d1里查找 minus。
d1也是3 万个元素,看上去,还是要循环 3万 * 3万 = 9 亿。
但是,虽然 d1有3万个元素,但是minus in d1,非常非常非常快!
快到可以忽略不计。
所以,我们可以认为这段程序只循环了3万次,而非9亿次。
为什么字典查找这么快?
为什么在字典里查找key非常快呢?
前面在介绍列表时,我们说过。Python里任何一个实例都住在“小房子”里,每个房子都有自己的地址。
内置函数id(),可以返回这个地址。
如下图,列表里的每个元素,和字典里的每个元素都有不同的地址。
假设下图是 100层,每层300个房间的大楼。也即一共有 100*300=3万个房间。
每个房间都有一个地址,不妨就用 001001表示第1层第1间房,001002表示第1层第2间房。
现在来了一个人要入住。
['Jim',100,90,100]
如果我们用列表 l1 来表示这栋大楼。下面是入住的代码。
l1.append(['Jim',100,90,100])
l1里可能已经住了很多人了,所以‘Jim’被安排在了 888666号房间。
有一天,警察怀疑‘Jim’是坏人,来抓他。
因为不知道‘Jim’的房间号,只能通过for循环。
最好的情况是‘Jim’住在001001,只要敲一个房间的门。
最坏的情况是‘Jim’住在100300,要敲3万个房间的门。
for i in range(len(l1)):
if 'Jim' == l1[i][0] :
抓起来
如果我们用字典d1来表示这栋大楼。入住的代码应该是
d1[ 'Jim' ] = [100,90,100]
这时,警察来抓‘Jim’,需要敲多少个房间的门呢?
答案是永远都只要敲一个房间的门!
因为通过 ‘Jim’这个key,就能计算出房间号。
在‘Jim’办理入住的时候,房间并不是随便分配的,而是通过一种神奇的函数计算出来。
这种神奇的函数就称为哈希(Hash)函数。
当然,这只是最基础的介绍,Python字典在具体实现哈希的时候,还有很多细节需要考虑。
比如,每个人的名字长度不一样,但经过哈希之后得到的房间号都是6位的。
再比如,不同名字的人,不能哈希到同一个房间号
等等等等
总而言之,Python字典因为在存储元素时,将key哈希转化到了存储地址,所以如果要在字典里查找某个元素的key,不需要循环字典,只需要进行同样的哈希计算就可以得到了。