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__里创建了一个链接头结点和尾结点的哨兵节点rootroot[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]

根据keyself.__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 带来的好处要大过额外内存消耗的影响。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值