Python 数据结构和算法

来源:互联网 发布:淘宝手机触摸屏 编辑:程序博客网 时间:2024/05/16 14:19

一、写在前面

这篇文章主要介绍了python 内置的数据结构(list、set以及字典),从一些实际的场景中来说明解决方案,主要是阅读《python cookbook》时写下的阅读记录,提高自己在Python开发方面的理解,记录在这里是为了方便可以随时查阅使用。因为时间仓促以及个人理解有限,固有错误的地方请指出,谢谢! 如果转载,请保留作者信息。
邮箱地址:jpzhang.ht@gmail.com
个人博客:https://jianpengzhang.github.io
CSDN博客:http://blog.csdn.net/u011521019

二、将序列分解为单独的变量

任何序列(或可迭代的对象)都可以通过一个简单的赋值操作作为分解为单独的变量。唯一的要求是变量的总数和结构要与序列相吻合。

例如:

>>> p = (1, 2)>>> x, y = p>>> x1>>> y2>>> data = ['jpzhang', 88, 99, (2016, 06, 15)]>>> name, shares, price, date = data>>> name'jpzhang'>>> date(2016, 06, 15)>>> name, shares, price, (year, mon, day) = data>>> day15>>> 

如果元素的数量不匹配,将会得到一个错误的提示:

>>> p = (1, 2)>>> x, y, z = pTraceback (most recent call last):  File "<stdin>", line 1, in <module>ValueError: need more than 2 values to unpack

例如:除了列表元祖,还包括字符串、文件、迭代器只要是可迭代对象

>>> str = "Hello World">>> a, b, c, d, e, f, g, h, i, j, k = str>>> a'H'>>> b'e'>>> g'W'>>> 

例如:去除特定列表中的值,把不需要的值存放到不会用到的变量上,

>>> data = ['jpzhang', 88, 99, (2016, 06, 15)]>>> _, shares, price, _ = data>>> _(2016, 6, 15)>>> shares88

三、从任意长度的可迭代对象中分解元素

从一个可迭代对象中分解出N个元素,可迭代对象的长度可能超过N,实现方式,通过Python的“*表达式”,
例如:需要python3支持,python2.7不支持

#!/usr/bin/python# -*- coding:utf-8 -*- __author__ = 'jpzhang'import numpydef avg(l):    return float(sum(l))/max(len(l),1)def drop_first_last(grades):    # first, *middle, last = grades  --- python3.0    middle = grades[1:-1] # --- python2.7    return numpy.mean(middle) # or def avg()if __name__ == '__main__':    grades = [1, 2, 3, 4, 5]    grades_avg = drop_first_last(grades)    print grades_avg

四、保留最后N个元素

在迭代或者是其他形式的处理过程中对最后几项记录做一个有限的历史纪录统计。
例如:下面的代码对一系列文本行做简单的文本匹配操作,当发现有匹配(python)字符数时就输出当前的匹配行,以及最近前面检查过的N行文本。

#!/usr/bin/python# -*- coding:utf-8 -*- __author__ = 'jpzhang'__sites__ = 'http://www.smallartisan.site/'from collections import dequedef search(lines, pattern, history=5):    previons_line = deque(maxlen=history)    for line in lines:        if pattern in line:            yield line, previons_line        previons_line.append(line)# Example use on a fileif __name__ == '__main__':    with open('./somefile.txt') as f:        for line, prevlines in search(f, 'python', 5):            for pline in prevlines:                print(pline, "end=''")            print(line, "end=''")            print('-'*20)

yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,调用 search() 不会执行 search() 函数,而是返回一个 iterable 对象!在 for 循环执行时,每次循环都会执行 search() 函数内部的代码,执行到 yield line, previons_line 时,fab 函数就返回一个迭代值,下次迭代时,代码从 yield line, previons_line 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield。

如同上面的代码片段所做的一样,当编写搜索某项纪录的代码时,通常会用到含有yield关键字的生成器迭代函数。这将处理搜索过程的代码和使用搜索结果的代码成功解耦开来。

deque(maxlen=N) 创建了一个固定长度的队列,当有新的记录加入而队列已满时会自动移动除最老的那条记录。例如:

>>> from collections import deque>>> q = deque(maxlen=3)>>> q.append(1)>>> q.append(2)>>> q.append(3)>>> qdeque([1, 2, 3], maxlen=3)>>> q.append(4)>>> qdeque([2, 3, 4], maxlen=3)>>> q.append(5)>>> qdeque([3, 4, 5], maxlen=3)

虽然通过列表操作(append、del)也能够完成deque的功能,但是队列的这种解决方案要更优雅的多,运行速度也将更快。如果deque不指定队列长度,将得到一个无界限的队列,可以在两端执行添加和弹出操作,例如:

>>> from collections import deque>>> q = deque()>>> q.append(1)>>> q.append(2)>>> q.append(3)>>> qdeque([1, 2, 3])>>> q.appendleft(4)>>> qdeque([4, 1, 2, 3])>>> q.pop()3>>> qdeque([4, 1, 2])>>> q.popleft()4

从队列两端添加或者弹出元素的复杂度都是O(1)。这和列表不同,当从列表的头部插入或者移除元素时,列表的复杂度为O(N)

五、找到最大或者最小的N个元素

在集合中查找最大或最小的N个元素。

heapq 模块中有两个函数——nlargest()和nsmallest()——他们正是我们所需要的。例如:

>>> import heapq>>> nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]>>> print(heapq.nlargest(3, nums))[42, 37, 23]>>> print(heapq.nsmallest(3, nums))[-4, 1, 2]

这两个函数都可以接受一个参数key,从而允许他们可以工作在更加复杂的数据结构之上。
例如:

>>> import heapq>>> phone = [{'name': 'sanxing', 'shares': 100, 'price': 100.11},... {'name': 'huawei', 'shares': 89, 'price': 99.31},... {'name': 'xiaomi', 'shares': 22, 'price': 89.11},... {'name': 'iphone', 'shares': 55, 'price': 34.11}]>>> cheap = heapq.nsmallest(3, phone, key=lambda s: s['price'])>>> cheap[{'price': 34.11, 'name': 'iphone', 'shares': 55}, {'price': 89.11, 'name': 'xiaomi', 'shares': 22}, {'price': 99.31, 'name': 'huawei', 'shares': 89}]>>> expensive = heapq.nlargest(3, phone, key=lambda s: s['price'])>>> expensive[{'price': 100.11, 'name': 'sanxing', 'shares': 100}, {'price': 99.31, 'name': 'huawei', 'shares': 89}, {'price': 89.11, 'name': 'xiaomi', 'shares': 22}]

如果正在寻找最大或者最小的N个元素,且同集合中元素的总数目相比,N很小,那么下面的这些函数可以提供更好的性能。首先这些函数会在底层将数据转化成列表,且元素以堆的顺序排列。例如:

>>> nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 37, 2]>>> import heapq>>> heap = list(nums)>>> heapq.heapify(heap)>>> heap[-4, 7, 1, 23, 8, 2, 18, 23, 42, 37, 37, 2]>>> heapq.heappop(heap)-4>>> heap[1, 7, 2, 23, 8, 2, 18, 23, 42, 37, 37]>>> heapq.heappop(heap)1>>> heapq.heappop(heap)2

当所要找的元素数量相对较小时,函数nlargest()和nsmallest()才是最合适的,如果只是简单的想找出最小或最大的元素(N=1时),那么用min()和max()会更快。如果N和集合本身的大小差不多大,通常更快的方法是先对集合排序,然后最切片操作(例如,使用sorted(items)[:N]或者sorted(items[-N:])。

六、实现优先级队列

实现一个队列,能够以给定的优先级来对元素排序,且每次pop操作时都会返回优先级最高的那个元素。

heapq模块实现了python中的堆排序,并提供了有关的方法。让用python实现排序算法有了简单的快捷的方式。

  • heappush():
    heapq.heappush(heap, item),将item压入到堆书组中heap中。如果不进行此步操作,后面的heappop失效。
  • heappop():
    heapq.heappop(heap),从堆数组heap中取出最小的值,并返回。

例如:

#!/usr/bin/python# -*- coding:utf-8 -*- __author__ = 'jpzhang'__sites__ = 'http://www.smallartisan.site/'import heapqclass 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 %s', self.name)if __name__ == '__main__':    q = PriorityQueue()    q.push(Item('foo'), 1)    q.push(Item('bar'), 5)    q.push(Item('spam'), 4)    q.push(Item('grok'), 1)    print q.pop().name    print('---------------------')    print q.pop().name    print('---------------------')    print q.pop().name    print('---------------------')    print q.pop().name控制台输出:bar---------------------spam---------------------foo---------------------grok

第一次执行pop() 操作时返回的元素具有最高的优先级,拥有相同优先级的两个元素(foo, grok)返回顺序同他们插入到队列的顺序相同。

函数heapq.heappush()以及heapq.heappop()分别实现将元素从列表_queue中插入和移除,且保证列表中第一个元素的优先级最低。heappop()方法重视返回“最小”的元素,因此这就是让队列能够弹出真确元素的关键。此外,由于push和pop操作的复杂度都是O(logN),其中N代表堆中元素的数量,因此就算N的值很大,这些操作的效率也非常高。
代码中,队列以元组(-priority, index, item)的形式组成。把priority取负指是为了让队列能够按照元素的优先级从高到低的顺序排列。这和正常的堆顺序是相反的,一般情况下堆是按从小到大的顺序排序的。
变量index的作用是为了将具有相同优先级的元素以适当的顺序排列。通过维护一个不断递增的索引,元素将以他们入队列时的顺序来排列。但是,index在对具有相同优先级的元素间做比较操作时同样扮演了重要的角色。
更多关于heapq的介绍请参考官方

七、在字典中将多个健映射到多个值上

将健(key)映射到多个值的字典(即所谓的一键多值字典[multidict])
字典是一种关联容器,每个健都映射到一个单独的值上。如果想让健映射到 多个值,需要将这多个值保存到另一个容器列表或集合中。例如:

d = {    'a': [1, 2, 3],    'b': [4, 5]}e = {    'a': {1, 2, 3}    'b': {4, 5}}

如果需要保留元素插入的顺序,就用列表,如果需要消除重复元素(且不在意他们的顺序),就用集合。

快速方便创建这样的字典,可以利用collections模块中的defaultdict类。 defaultdict的一个特点就是它会自动初始化第一个值,这样只需要关注添加的元素即可。
例如:

#!/usr/bin/python# -*- coding:utf-8 -*- __author__ = 'jpzhang'__sites__ = 'http://www.smallartisan.site/'from collections import defaultdictd = defaultdict(list)d['a'].append(1)d['a'].append(2)d['b'].append(4)print(d)print('================================')b = defaultdict(set)b['a'].add(1)b['a'].add(2)b['b'].add(4)print(b)控制台输出:defaultdict(<type 'list'>, {'a': [1, 2], 'b': [4]})================================defaultdict(<type 'set'>, {'a': set([1, 2]), 'b': set([4])})

关于defaultdict,需要注意的一个地方是,他会自动创建字典表项以待稍后的访问(即使这些表项当前在字典中还没有找到)。如果不想要这个功能,可以在普通的字典上调用setdefault方法来取代。
例如:

c = {}c.setdefault('a', []).append(1)c.setdefault('a', []).append(2)c.setdefault('b', []).append(3)print(c['a'])输出:[1, 2]

构建一个一键多值字典是很容易的。但是如果试着自己对第一个值做初始化操作,这就会变的很杂论。例如:

#!/usr/bin/python# -*- coding:utf-8 -*- __author__ = 'jpzhang'__sites__ = 'http://www.smallartisan.site/'from collections import defaultdictd = {}for key, value in pairs:    if key not in d:        d[key] = []    d[key].append(value)# 使用defaultdict后代码会清晰的多:d = defaultdict(list)for key, value in pairs:    d[key].append(value)

八、让字典保持有序

创建一个字典,并且在迭代或序列化这个字典的时候能够控制元素的顺序。
控制一个字典中元素的顺序,你可以使用 collections 模块中的 OrderedDict 类。 在迭代操作的时候它会保持元素被插入时的顺序,示例如下:

#!/usr/bin/python# -*- coding:utf-8 -*- __author__ = 'jpzhang'__sites__ = 'http://www.smallartisan.site/'from collections import OrderedDictdef ordered_dict():    d = OrderedDict()    d['foo'] = 1    d['bar'] = 2    d['spam'] = 3    d['grok'] = 4    # Outputs "foo 1", "bar 2", "spam 3", "grok 4"    for key in d:        print(key, d[key])if __name__ == '__main__':    ordered_dict()控制输出:('foo', 1)('bar', 2)('spam', 3)('grok', 4)

当你构建一个映射结构以便稍后对其做序列化编码成另一种格式时,OrderedDict就显示的特别有用。例如,如果在想进行JSON编码时精确控制各字段的顺序,那么只要首先OrderedDict中构建数据就可以了。

>>> import json>>> json.dumps(d)'{"foo": 1, "bar": 2, "spam": 3, "grok": 4}'>>>

有可能在你本地调试的时候会报错:

Traceback (most recent call last):  File "/Users/jpzhang/Documents/开发/Python/demo/python cookbook/demo06.py", line 6, in <module>    from collections import OrderedDictImportError: cannot import name OrderedDict

网上查阅了下:

http://stackoverflow.com/questions/14358162/funnelweb-error-cannot-import-ordereddictOrderedDict is a new class added to Python 2.7, so it is missing in your case.You can install a backport:pip install ordereddictor add that as a dependency in your buildout, and edit Plone/buildout-cache/eggs/transmogrify.webcrawler-1.2.1-py2.6.egg/transmogrify/webcrawler/webcrawler.py line 21 to change the import from:from collections import OrderedDicttotry:    from collections import OrderedDictexcept ImportError:    # python 2.6 or earlier, use backport    from ordereddict import OrderedDictI have filed an issue in the funnelweb issue tracker to request that this is added to a future release of the package, see issue 22.

OrderedDict 内部维护着一个根据键插入顺序排序的双向链表。每次当一个新的元素插入进来的时候, 它会被放到链表的尾部。对于一个已经存在的键的重复赋值不会改变键的顺序。

需要注意的是,一个 OrderedDict 的大小是一个普通字典的两倍,因为它内部维护着另外一个链表。 所以如果你要构建一个需要大量 OrderedDict 实例的数据结构的时候(比如读取100,000行CSV数据到一个 OrderedDict 列表中去), 那么你就得仔细权衡一下是否使用 OrderedDict 带来的好处要大过额外内存消耗的影响。

九、字典的计算

在字典上对数据执行各式各样的计算(比如求最小值、最大值、排序等等)
例如:假设有一个字典在股票名称和价格间做了映射:
为了对字典值执行有用的计算操作,通常需要使用 zip() 函数先将键和值反转过来。 比如,下面是查找最小和最大股票价格和股票值的代码:

min_price = min(zip(prices.values(), prices.keys()))# min_price is (10.75, 'FB')max_price = max(zip(prices.values(), prices.keys()))# max_price is (612.78, 'AAPL')

同样,要对数据排序只要使用zip() 再配合sorted()就可以了,比如:

prices_sorted = sorted(zip(prices.values(), prices.keys()))# prices_sorted is [(10.75, 'FB'), (37.2, 'HPQ'),#                   (45.23, 'ACME'), (205.55, 'IBM'),#                   (612.78, 'AAPL')]

当进行这些计算时,请注意zip() 创建了一个迭代器,它的内容只能被消费一次。例如下面的代码就是错误的:

prices_and_names = zip(prices.values(), prices.keys())print(min(prices_and_names)) # OKprint(max(prices_and_names)) # ValueError: max() arg is an empty sequence

完整示例代码:

#!/usr/bin/python# -*- coding:utf-8 -*- __author__ = 'jpzhang'__sites__ = 'http://www.smallartisan.site/'prices = {    'ACME': 45.23,    'AAPL': 612.78,    'IBM': 205.55,    'HPQ': 37.20,    'FB': 10.75}min_price = min(zip(prices.values(), prices.keys()))# min_price is (10.75, 'FB')max_price = max(zip(prices.values(), prices.keys()))# max_price is (612.78, 'AAPL')print(min_price)print('-----------------------------')print(max_price)print('------------sorted-----------------')prices_sorted = sorted(zip(prices.values(), prices.keys()))# prices_sorted is [(10.75, 'FB'), (37.2, 'HPQ'),#                   (45.23, 'ACME'), (205.55, 'IBM'),#                   (612.78, 'AAPL')]print(prices_sorted)控制台输出:(10.75, 'FB')-----------------------------(612.77999999999997, 'AAPL')------------sorted-----------------[(10.75, 'FB'), (37.200000000000003, 'HPQ'), (45.229999999999997, 'ACME'), (205.55000000000001, 'IBM'), (612.77999999999997, 'AAPL')]

如果你在一个字典上执行普通的数学运算,你会发现它们仅仅作用于键,而不是值。比如:

min(prices) # Returns 'AAPL'max(prices) # Returns 'IBM'

这个结果并不是你想要的,因为你想要在字典的值集合上执行这些计算。 或许你会尝试着使用字典的 values() 方法来解决这个问题:

min(prices.values()) # Returns 10.75max(prices.values()) # Returns 612.78

不幸的是,通常这个结果同样也不是你想要的。 你可能还想要知道对应的键的信息(比如那种股票价格是最低的?)。

你可以在 min() 和 max() 函数中提供 key 函数参数来获取最小值或最大值对应的键的信息。比如:

min(prices, key=lambda k: prices[k]) # Returns 'FB'max(prices, key=lambda k: prices[k]) # Returns 'AAPL'

但是,如果还想要得到最小值,你又得执行一次查找操作。比如:

min_value = prices[min(prices, key=lambda k: prices[k])]

前面的 zip() 函数方案通过将字典”反转”为(值,键)元组序列来解决了上述问题。 当比较两个元组的时候,值会先进行比较,然后才是键。 这样的话你就能通过一条简单的语句就能很轻松的实现在字典上的求最值和排序操作了。

需要注意的是在计算操作中使用到了(值,键)对。当多个实体拥有相同的值的时候,键会决定返回结果。 比如,在执行 min() 和 max() 操作的时候,如果恰巧最小或最大值有重复的,那么拥有最小或最大键的实体会返回:
例如:

>>> prices = { 'AAA' : 45.23, 'ZZZ': 45.23 }>>> min(zip(prices.values(), prices.keys()))(45.23, 'AAA')>>> max(zip(prices.values(), prices.keys()))(45.23, 'ZZZ')>>>

十、查找两字典的相同点

有两个字典,我们想找出它们中间可能的相同的地方(相同的健、相同的值等)。

考虑两个字典:

a = {    'x' : 1,    'y' : 2,    'z' : 3}b = {    'w' : 10,    'x' : 11,    'y' : 2}

要找出这两个字典中的相同之处,只需要通过keys()或者items()方法执行常见的集合操作即可。例如:

# Find keys in commona.keys() & b.keys() # { 'x', 'y' }# Find keys in a that are not in ba.keys() - b.keys() # { 'z' }# Find (key,value) pairs in commona.items() & b.items() # { ('y', 2) }

这些类型的操作也可以用来修改或过滤掉字典中的内容,例如,假设想创建一个新的字典,其中会去掉某些健。下面是使用了字典推导式的代码示例:

# Make a new dictionary with certain keys removedc = {key:a[key] for key in a.keys() - {'z', 'w'}}# c is {'x': 1, 'y': 2}

一个字典就是一个键集合与值集合的映射关系。 字典的 keys() 方法返回一个展现键集合的键视图对象。 键视图的一个很少被了解的特性就是它们也支持集合操作,比如集合并、交、差运算。 所以,如果你想对集合的键执行一些普通的集合操作,可以直接使用键视图对象而不用先将它们转换成一个set。

字典的 items() 方法返回一个包含(键,值)对的元素视图对象。 这个对象同样也支持集合操作,并且可以被用来查找两个字典有哪些相同的键值对。

尽管字典的 values() 方法也是类似,但是它并不支持这里介绍的集合操作。 某种程度上是因为值视图不能保证所有的值互不相同,这样会导致某些集合操作会出现问题。 不过,如果你硬要在值上面执行这些集合操作的话,你可以先将值集合转换成set,然后再执行集合运算就行了。

十一、从序列中移除重复项且保持元素间顺序不变

我们想去除序列中出现的重复元素,但依旧保持剩下的元素顺序不变

如果序列上的值都是可哈希(hashable)的,那么可以很简单的利用集合或者生成器来解决这个问题。比如:

#!/usr/bin/python# -*- coding:utf-8 -*- __author__ = 'jpzhang'__sites__ = 'http://www.smallartisan.site/'def dedupe(items):    seen = set()    for item in items:        if item not in seen:            yield item            seen.add(item)if __name__ == '__main__':    a = [1, 5, 2, 1, 9, 1, 5, 10]    b = list(dedupe(a))    print(b)

这个方法仅仅在序列中元素为 hashable 的时候才管用。 如果你想消除元素不可哈希(比如 dict 类型)的序列中重复元素的话,你需要将上述代码稍微改变一下,就像这样:

#!/usr/bin/python# -*- coding:utf-8 -*- __author__ = 'jpzhang'__sites__ = 'http://www.smallartisan.site/'def dedupe(items, key=None):    seen = set()    for item in items:        val = item if key is None else key(item)        if val not in seen:            yield item            seen.add(val)if __name__ == '__main__':    a = [ {'x':1, 'y':2}, {'x':1, 'y':3}, {'x':1, 'y':2}, {'x':2, 'y':4}]    print(list(dedupe(a, key=lambda d: (d['x'],d['y']))))    print(list(dedupe(a, key=lambda d: d['x'])))

这里的key参数指定了一个函数,将序列元素转换成 hashable 类型。
如果你想基于单个字段、属性或者某个更大的数据结构来消除重复元素,第二种方案同样可以胜任。

如果你仅仅就是想消除重复元素,通常可以简单的构造一个集合。比如:

>>> a = [1, 5, 2, 1, 9, 1, 5, 10]>>> a[1, 5, 2, 1, 9, 1, 5, 10]>>> set(a)set([1, 2, 10, 5, 9])>>> 

但是这种方式不能保证元素间的顺序不变,因此得到的结果会被打乱。前面展示的解决方案可避免出现这个问题。

对生成器的使用反映一个事实,那就是我们可能会希望这个函数尽可能的通用——不必绑定在只能对列表进行处理。比如,如果想读一个文件,去除其中重复的文本行,可以只需要这样处理:

with open(somefile,'r') as f:for line in dedupe(f):    ...

dedupe()函数模仿了内置函数 sorted() , min() 和 max()对key函数的使用方式

总结

《python cookbook》 这本写的内容比较有实战意义,很多处理方式是我们在日常开发中会比较多的涉及,建议大家没事的时候多翻翻。
在这篇文章发布之初的目的是为了记录下来方便以后自己查询,后来查到,网上已经有牛人将它翻译整理好了,后续不会更新相关的文章,更多的信息可以在线查阅

0 0