Python实战之数据结构和算法

Posted 山河已无恙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python实战之数据结构和算法相关的知识,希望对你有一定的参考价值。

写在前面


  • 博文为《Python Cookbook》读书笔记整理
  • 读了这本书,才发现Python数据处理远比我想的要强大
  • 理解不足小伙伴帮忙指正

傍晚时分,你坐在屋檐下,看着天慢慢地黑下去,心里寂寞而凄凉,感到自己的生命被剥夺了。当时我是个年轻人,但我害怕这样生活下去,衰老下去。在我看来,这是比死亡更可怕的事。--------王小波


数据结构和算法

Python内置了许多非常有用的数据结构,比如列表(list)、集合(set)以及字典(dictionary)、元组(tuple)。在collections模块中也包含了针对各种数据结构的解决方案。

将序列分解为单独的变量

我们有一个包含N个元素的元组或序列,现在想将它分解为N个单独的变量。

┌──(liruilong㉿Liruilong)-[/mnt/e/docker]
└─$ python3
Python 3.9.10 (main, Jan 16 2022, 17:12:18)
[GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> p = (4,5)
>>> x,y = p
>>> x
4
>>> y
5
>>>

居然可以这样,长见识了,类似于javascript ES6中的解构赋值,如果当成函数对象来看,可以看做是拆包

>>> data = [ 'ACME',50,91.1,(2012,12,21)]
>>> n,s,p,d = data
>>> n,s
('ACME', 50)
>>> d
(2012, 12, 21)
>>>

当然,也支持深层次解构

>>> data = [ 'ACME',50,91.1,(2012,12,21)]
>>> name,shares,price,(year,mon,day)=data
>>> year
2012
>>> mon,day
(12, 21)
>>>

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

>>> p = (4,5)
>>> x,y,z = p
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 3, got 2)
>>>

实际上不仅仅只是元组或列表,只要对象恰好是可迭代的,那么就可以执行分解操作。这包括字符串、文件、迭代器以及生成器

>>> s = 'love'
>>> a,b,c,d = s
>>> a,b
('l', 'o')
>>> d
'e'
>>>

当做分解操作时,想丢弃某些特定的值。可以用 '_'充当占位符,,这个在JavaScript ES6Golang也都支持。

>>> data = [ 'ACME',50,91.1,(2012,12,21)]
>>> _,_,a,_=data
>>> a
91.1
>>>

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

需要从某个可选代对象分解出N个元素,但是这个可迭代对象的长度可能超过N,这会导致出现分解的值过多(too many values to unpack)的异常。

>>> record=('Dave','davedexample.com','773-555-1212','1847-555-1212')
>>> name,email,*phone_numbers = record
>>> name
'Dave'
>>> phone_numbers
['773-555-1212', '1847-555-1212']
>>>

类似于Java方法传值的可变参数一样,但是要比java高级的多,java的可变参数只能最后一个,python 则可以在任意位置

>>> record=('Dave','davedexample.com','1224965096@qq.com','773-555-1212')
>>> name,*email,phone = record
>>> name
'Dave'
>>> email
['davedexample.com', '1224965096@qq.com']
>>>

* 式的语法在选代一个变长的元组序列时尤其有用

records = [
    ('foo', 1, 2),
    ('bar', 'hello'),
    ('foo', 3, 4),
]

def do_foo(x, y):
    print('foo', x, y)

def do_bar(s):
    print('bar', s)

for tag, *args in records:
    if tag == 'foo':
        do_foo(*args)
    elif tag == 'bar':
        do_bar(*args)

字符串拆分是的使用 : ''.split('')

>>> line='nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false'
>>> uname,*fields,homedir,sh=line.split(':')
>>> uname
'nobody'
>>> sh
'/usr/bin/false'
>>> fields
['*', '-2', '-2', 'Unprivileged User']
>>>

也可 '*' 和 '_' 两个连用,丢弃多个变量

>>> uname,*_,homedir,sh=line.split(':')
>>> sh
'/usr/bin/false'
>>>

求和递归

items=[1,10,7,4,5,9]
def sum(items): 
    head,*tail=items 
    return head+sum(tail) if tail else head

保存最后N个元素(队列)

我们希望在迭代或是其他形式的处理过程中对最后几项记录做一个有限的历史记录统计。

保存有限的历史记录可算是collections.deque的完美应用场景了

打印满足条件的最后5条记录

#deque(maxlen=N)创建了一个固定长度的双端队列
from collections import deque

def search(lines, pattern, history=5):
    previous_lines=deque(maxlen=history)
    for line in lines:
        if pattern in line:
            # 生成器
            yield line, previous_lines
        previous_lines.append(line)
# Example use on a file
if __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('-1'*20)

deque(maxlen=N)创建了一个固定长度的双端队列

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

尽管可以在列表上手动完成这样的操作(append、del),但队列这种解决方案要优雅得多,运行速度也快得多。从队列两端添加或弹出元素的复杂度都是O(1)。这和列表不同,当从列表的头部插入或移除元素时,列表的复杂度为O(N)

找到最大或最小的N个元素

我们想在某个集合中找出最大或最小的N个元素。

heapq模块中有两个函数nlargest()和nsmallest(),当所要找的元素数量相对较小时,函数nlargest()和nsmallest()才是最适用的,nlargest()和nsmallest()的实际实现会根据使用它们的方式而有所不同,可能会相应作出一些优化措施(比如,当N的大小同输入大小很接近时,就会采用排序的方法)。

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

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

import  heapq

portfoli = [
    'name': 'AAPL', 'shares': 50, 'price': 543.22,
    'name': 'FB', 'shares': 200, 'price': 21.09,
    'name': 'HPQ', 'shares': 35, 'price': 31.75,
    'name': 'YHOO', 'shares': 45, 'price': 16.35,
    'name': 'ACME', 'shares': 75, 'price': 115.65
]
print(heapq.nsmallest(2,portfoli,key=lambda s: s['price']))
print(heapq.nlargest(2,portfoli,key=lambda s: s['shares']))
['name': 'YHOO', 'shares': 45, 'price': 16.35, 'name': 'FB', 'shares': 200, 'price': 21.09]
['name': 'FB', 'shares': 200, 'price': 21.09, 'name': 'ACME', 'shares': 75, 'price': 115.65]

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

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

堆最重要的特性就是heap[0]总是最小那个的元素。此外,接下来的元素可依次通过heapq.heappop方法轻松找到。该方法会将第一个元素(最小的)弹出,然后以第二小的元素取而代之(这个操作的复杂度是O(logN),N代表堆的大小)

想找到最小或最大的元素(N=1时),那么用min()和max)会更加快。

>>> min(heap)
2
>>> max(heap)
42
>>> heap
[2, 7, 8, 23, 42, 37, 18, 23]
>>>

如果N和集合本身的大小差不多大,通常更快的方法是先对集合排序,然后做切片操作,使用sorted(items)[:N]或者sorted(items)[-N:])

实现优先级队列

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

heapq模块提供了堆排序算法的实现,heapq有两种方式创建堆,

  • 一种是使用一个空列表,然后使用heapq.heappush()函数把值加入堆中
  • 一种就是使用heap.heapify(list)转换列表成为堆结构
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):
        #!r直接反应对象本体
        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)
print(q.pop())
print(q.pop())
print(q.pop())

队列以元组(-priority,index,item)的形式组成。把priority取负值是为了让队列能够按元素的优先级从高到低的顺序排列。一般情况下是最小堆。

变量index的作用是为了将具有相同优先级的元素以适当的顺序排列。通过维护一个不断递增的索引,元素将以它们入队列时的顺序来排列。没有哪两个元组会有相同的index值(一旦比较操作的结果可以确定,Python就不会再去比较剩下的元组元素了)

如果想将这个队列用于线程间通信,还需要增加适当的锁和信号机制

在字典中将键映射到多个值上

我们想要一个能将键(key)映射到多个值的字典(即所谓的一键多值字典[multidict])

字典是一种关联容器,每个键都映射到一个单独的值上。如果想让键映射到多个值,需要将这多个值保存到另一个容器如列表或集合中。

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

defaultdict(<class 'list'>, 'a': [1, 2], 'b': [4])
>>> from  collections import defaultdict
>>> d = defaultdict(list)
>>> d['a'].append(1)
>>> d['a'].append(2)
>>> d['b'].append(4)
>>> d
defaultdict(<class 'list'>, 'a': [1, 2], 'b': [4])
>>> d= defaultdict(set)
>>> d['a'].add(1)
>>> d['a'].add(3)
>>> d['a'].add(3)
>>> d
defaultdict(<class 'set'>, 'a': 1, 3)
>>>

用于分组,有一种java8里面Stream的味道

d=defaultdict(list)
for key, value in pairs: 
  d[key]. append(value)

让字典保持有序

要控制字典中元素的顺序,可以使用collections模块中的OrderedDict类。当对字典做迭代时,它会严格按照元素初始添加的顺序进行。例如:

>>> from collections import OrderedDict
>>> d = OrderedDict()
>>> d['1']=1
>>> d['2']=2
>>> d['3']=3
>>> d
OrderedDict([('1', 1), ('2', 2), ('3', 3)])
>>>

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

>>> import json
>>> json.dumps(d)
'"1": 1, "2": 2, "3": 3'
>>>

OrderedDict 内部维护了一个双向链表,它会根据元素加入的顺序来排列键的位置。第一个新加入的元素被放置在链表的末尾。接下来对已存在的键做重新赋值不会改变键的顺序

OrderedDict的大小是普通字典的2倍多,这是由于它额外创建的链表所致。因此,如果打算构建一个涉及大量OrderedDict实例的数据结构(例如从CSV文件中读取100000行内容到OrderedDict列表中),那么需要认真对应用做需求分析,是否可以用内存换便利

与字典有关的计算问题

我们想在字典上对数据执行各式各样的计算(比如求最小值、最大值、排序等)。

通常会利用zip()将字典的键和值反转过来

>>> prices=
... 'ACME':45.23,
... 'AAPL':612.78,
... 'IBM1':205.55,
... 'HPQ':37.20,
... 'FB':10.75
... 
>>>
>>> min(zip(prices.values(),prices.keys()))
(10.75, 'FB')
>>> max(zip(prices.values(),prices.keys()))
(612.78, 'AAPL')
>>>

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

>>> sorted(zip(prices.values(),prices.keysPYTHON的数据结构和算法介绍

算法系列之赫夫曼编码实战一数据压缩数据解压

数据挖掘实战之随机森林算法使用

机器学习实战之kNN算法

机器学习实战之 第七章 集成方法(随机森林和 AdaBoost)

7-Python3从入门到实战—基础之数据类型(字典-Dictionary)