python cookbook3笔记第一章
实现一个优先级队列
优先级队列(priority queue
) 是0个或多个元素的集合,每个元素都有一个优先权,对优先级队列执行的操作有(1)查找(2)插入一个新元素 (3)删除 一般情况下,查找操作用来搜索优先权最大的元素,删除操作用来删除该元素 。对于优先权相同的元素,可按先进先出次序处理或按任意优先权进行。
下面的类利用heapq
模块实现了一个简单的优先级队列:
import heapq
class PriorityQueue:
def __init__(self):
self._queue = []
self._index = 0
def push(self, item, priority):
heapq.heappush(self._queue, (-priority, self._index, item))
self._index += 1
def pop(self):
return heapq.heappop(self._queue)[-1]
下面是它的使用方式:
>>> class Item:
... def __init__(self, name):
... self.name = name
... def __repr__(self):
... return 'Item({!r})'.format(self.name)
...
>>> q = PriorityQueue()
>>> q.push(Item('foo'), 1)
>>> q.push(Item('bar'), 5)
>>> q.push(Item('spam'), 4)
>>> q.push(Item('grok'), 1)
>>> q.pop()
Item('bar')
>>> q.pop()
Item('spam')
>>> q.pop()
Item('foo')
>>> q.pop()
Item('grok')
在昨天我们学习了heapq
模块,这里我们向队列_index
插入的是元祖,函数heapq.heappush()
会把最小的元素放到第一个,函数heapq.heappop()
会返回最小的元素。于是这里涉及到了元祖之间的比较,元祖之间的比较和字符串类似,先对第一组对应的对象-priority
进行比较,优先级大的-priority
必然小,因此优先级最大的会排在第一个,但优先级相等时则会比较第二组对应的对象_index
,按先进先出策略,由于_index
在上述类里不可能重复,所以不会比较到第三组对象Item
(所以也没必要定义类方法__lt__()
)。
字典中的键映射多个值
d = {} # 一个普通的字典
d.setdefault('a', []).append(1)
d.setdefault('a', []).append(2)
d.setdefault('b', []).append(4)
# defaultdict模块
from collections import defaultdict
d = defaultdict(list)
d['a'].append(1)
d['a'].append(2)
d['b'].append(4)
d = defaultdict(set)
d['a'].add(1)
d['a'].add(2)
d['b'].add(4)
排序字典OrderedDict的使用及源码分析
如果你想控制一个字典中元素的顺序,可以使用collections
模块里的OrderedDict
类:
from collections import OrderedDict
od = OrderedDict({'first':'ame','second':'paparaize','third':'sccc'})
for key in od:
print(key, od[key])
# print
# first ame
# second paparaize
# third sccc
>>> import json
>>> json.dumps(od)
'{"first": "ame", "second": "paparaize", "third": "sccc"}'
由于需要维护一个动态的有序序列,OrderedDict
类里面使用了双向列表这种数据,实现比较有趣,下面简单分析一下源码:
class OrderedDict(dict):
'Dictionary that remembers insertion order'
# An inherited dict maps keys to values.
# The inherited dict provides __getitem__, __len__, __contains__, and get.
# The remaining methods are order-aware.
# Big-O running times for all methods are the same as regular dictionaries.
# The internal self.__map dict maps keys to links in a doubly linked list.
# The circular doubly linked list starts and ends with a sentinel element.
# The sentinel element never gets deleted (this simplifies the algorithm).
# Each link is stored as a list of length three: [PREV, NEXT, KEY].
def __init__(*args, **kwds):
'''Initialize an ordered dictionary. The signature is the same as
regular dictionaries, but keyword arguments are not recommended because
their insertion order is arbitrary.
注意这里说关键字参数不推荐使用,插入顺序是随机的,所以我上面的列子是个坑?我晕
'''
if not args:
raise TypeError("descriptor '__init__' of 'OrderedDict' object "
"needs an argument")
self = args[0]
args = args[1:]
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args))
try:
self.__root
except AttributeError:
self.__root = root = [] # sentinel node
root[:] = [root, root, None]
self.__map = {}
self.__update(*args, **kwds)
什么是哨兵节点,链表里的操作往往是需要依赖’邻居’节点的,例如删除操作就需要访问前一个和后一个节点,但当你访问’邻居’节点的时候可能会遇到链表的边界,这种情况需要去判断后作相应的操作(while p.next!=null
),代码会显得很冗余,而哨兵节点往往能够简化边界条件,防止对边界条件的判断。
这里在__init__
里创建了一个链接头结点和尾结点的哨兵节点root
,root[0]
代表哨兵节点的前驱节点也就是尾结点,root[1]
代表哨兵节点的后驱节点也就是头结点,这样哨兵节点就能控制边界。
下面是重点代码:
def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
'od.__setitem__(i, y) <==> od[i]=y'
# Setting a new item creates a new link at the end of the linked list,
# and the inherited dictionary is updated with the new key/value pair.
if key not in self:
root = self.__root
last = root[0]
last[1] = root[0] = self.__map[key] = [last, root, key]
return dict_setitem(self, key, value)
先拿到哨兵节点root
,得到尾结点root[0]
,新建一个节点[last, root, key]
,使原尾结点的后驱节点和哨兵节点的前驱节点指向新建节点,这样一来新建节点成了新的尾结点,插入到了链表的最后。最后用self.__map
字典将key和链表里的节点关联起来。
def __delitem__(self, key, dict_delitem=dict.__delitem__):
'od.__delitem__(y) <==> del od[y]'
# Deleting an existing item uses self.__map to find the link which gets
# removed by updating the links in the predecessor and successor nodes.
dict_delitem(self, key)
link_prev, link_next, _ = self.__map.pop(key)
link_prev[1] = link_next # update link_prev[NEXT]
link_next[0] = link_prev # update link_next[PREV]
根据key
在self.__map
拿到需删除的节点,然后是基本操作,用上一个节点的后驱节点指向下一个节点,下一个节点的前驱节点指向上一个节点。
def __iter__(self):
'od.__iter__() <==> iter(od)'
# Traverse the linked list in order.
root = self.__root
curr = root[1] # start at the first node
while curr is not root:
yield curr[2] # yield the curr[KEY]
curr = curr[1] # move to next node
def __reversed__(self):
'od.__reversed__() <==> reversed(od)'
# Traverse the linked list in reverse order.
root = self.__root
curr = root[0] # start at the last node
while curr is not root:
yield curr[2] # yield the curr[KEY]
curr = curr[0] # move to previous node
顺序遍历和反向遍历,没什么好讲的,注释也都有,因为有哨兵节点所以代码简洁很多。
def clear(self):
'od.clear() -> None. Remove all items from od.'
root = self.__root
root[:] = [root, root, None]
self.__map.clear()
dict.clear(self)
清空列表,清空双向链表,清空内部的map。
下面内容引用自博客
https://blog.csdn.net/bell10027/article/details/80940260
ok, 为什么不简单使用list来进行保存, 而是要使用这种结构的双向链表?这就涉及到了链表和数组的主要用途. 两者同样是序列,数组按照 index 取值, 对于固定的静态序列数据的存取都是 O(1), 双向链表 按照 pre, next 遍历, 因为节点是可变对象, 可以被引用(对于 od来说就是 self.__map[key]的用途), 对于 动态 的序列存取也是 O(1)。
od显然要维护一个动态序列, 所以链表就是一个非常好的选择。你可能想到list可以del某个元素, 但是这其实破坏了数组的规则, index已被改变, 无法按照原有的index进行存取。需要移动大量数组元素。
summary:
数组静态分配内存,链表动态分配内存
数组在内存中连续,链表不连续
数组元素在栈区,链表元素在堆区
数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n)
这里使用__map来进行定位,所以复杂度也是O(1)
数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。
下面内容摘自
https://python3-cookbook.readthedocs.io/zh_CN/latest/c01/p07_keep_dict_in_order.html
OrderedDict
内部维护着一个根据键插入顺序排序的双向链表。每次当一个新的元素插入进来的时候, 它会被放到链表的尾部。对于一个已经存在的键的重复赋值不会改变键的顺序。
需要注意的是,一个 OrderedDict
的大小是一个普通字典的两倍,因为它内部维护着另外一个链表。 所以如果你要构建一个需要大量 OrderedDict
实例的数据结构的时候(比如读取 100,000 行 CSV 数据到一个 OrderedDict
列表中去), 那么你就得仔细权衡一下是否使用 OrderedDict
带来的好处要大过额外内存消耗的影响。