数据结构和算法
Python内置数据结构:
列表list、集合set、字典dict。本文针对各种数据结构的提出搜索,排列、以及筛选等这一类常见的问题的解决方案。
一、将序列分解为单独的变量(分解操作)
任何的元组、序列或可迭代对象都可以通过一个简单的赋值操作来分解为单独的变量。(包括字符串、文件、迭代器以及生成器)
唯一要求就是变量的总数和结构要与序列相吻合。
>>> p = (4,5)
>>> a, b = p
当做分解操作时,有时候可能想丢弃某些特定的值。Python并没有提供特殊的语法来实现这一点,但是可以选一个用不到的变量名,作为要丢弃的值得名称。
>>> data = ['Iphone', 50, 91.1, (2019, 9, 9) ]
>>> _, shares, price, _ = data
二、从任意长度的可迭代对象中分解元素
从某个可迭代对象中分解出N个元素,但是这个可迭代对象的长度可能超过N,导致出现,分解的值过多的异常。
使用 *表达式 解决这个问题。如去掉第一个和最后一个,留下中间计算平均值。
>>> record = ('Tom', '442994909@qq.com', '188xxxxxxxx', '020xxxxxxxxx')
>>> name, email, *phone_number = record
由*表达式 修饰的变量可以位于列表的任意位置。
(1)可以和拆分操作 split,相结合使用。
>>> line = 'firefly:*:2:2:root:/usr/bin:/usr/bin/empty'
>>> uname, *fields, homedir, sh = line.splt(':')
(2)分解出值然后丢弃。
>>> data = ['Iphone', 50, 91.1, (2019, 9, 9) ]
>>> name, *_, (year, *_) = data
(3)具有拆分功能的函数,实现递归算法。
>>> def sum(item):
... head, *tail = items
... return head + sum(tail) if tail else head
▲ return head + sum(tail) if tail else head 表示: return tail 或者 head 或者 两个值同时 return,注意与 return head + ( sum(tail) if tail else head )区别
三、保存最后N个元素
使用deque(maxlen=N),创建一个固定长度的队列。当有新记录加入而队列已满时会自动移除最老的那条记录。
from collections import deque def search_match(f,pattern,history=5): previous_lines = deque(maxlen=history) for line in f: if pattern in line: yield line,previous_lines previous_lines.append(line) with open('re.txt','w') as f: for line, previouos_lines in search_match(f,'python',5): for pline in previouos_lines: print(pline,end='') print(line,end='') print('-'*20)
如果不指定队列的大小,也就得到一个无界限的队列,可以在两端执行添加和弹出操作。
>>> q = deque()
>>> q.append(1)
>>> q.appendleft(4)
>>> q.pop()
>>> q.popleft()
从队列两端添加或弹出元素的复杂度都是O(1),这和列表不同,当从列表的头部插入或移除元素时,列表的复杂度为O(N)。
四、找到最大最小的N个元素
在集合中找出最大或最小的N个元素。
heapq模块中有两个函数:nlargest()和nsmallest(),这两个函数都可以接受一个key,从而允许它们工作在更加复杂的数据结构之上。
cheap = heapq.nsmallest(3, portfolio, key=lambda s:s['price'])
expensive = heapq.nlargest(3, portfolio, key=lambda s:s['price'])
(1)当寻找的最大最小N个元素,在总数目中相比,N很小时,那么鞋棉函数可以提供更好的性能。
>>> heap = list(nums)
>>> heapq.heapify(heap)
这个函数会在底层将数据转化成列表,且元素会以堆的顺序排序。
堆最重要的特性就是heap[0]总是最小那个的元素。
接下来的元素可以通过heapq.heappop()方法获取。复杂度为O(logN),N代表堆的大小。
(2)当要找的元素数量相对较小时,函数nlargest()和nsmallest()才是最适用的。
(3)如果只是简单找最大最小元素(N=1)时,那么用min()和max()会更加快。
(4)如果N和集合本身的大小差不多大,更快的方法是对集合排序,然后做切片操作。sorted(items)[:N]或sorted(items)[-N:]
(5)函数nlargest()和nsmallest()会根据使用方式的不同,自动做出一些优化措施。
五、实现优先级队列
实现一个队列,能以给定的优先级来对元素排序,其每次pop操作时都会返回优先级最高的那个元素。
利用heapq模块实现一个简单的优先级队列:
import heapq class PriorityQueue: def __init__(self): self._queue = [] self._inedx = 0 def push(self,item,priority): heapq.heappush(self._queue,(-priority,self._inedx,item)) self._inedx += 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) p = PriorityQueue() p.push(Item('Maria'),3) p.push(Item('Lilly'),5) p.push(Item('David'),7) print(p.pop()) #Item('David')
(1)函数heapq.heappush()以及heapq.heappop()分别实现将元素从列表_queue中插入和移除,且保证列表中第一个元素的优先级最低。
(2)heappop()方法总是返回“最小”的元素。
(3)队列以元组(-priority, index, item)的形式组成。把priority取负值是为了让队列能够按元素的优先级从高到低的顺序排列。
(4)一般情况下,堆是按照从小到大的顺序排序的。
(5)变量index作用是为了将具有相同优先级的元素以适当的顺序排列。元素将按照入队列时的顺序来排序。
六、在字典中将键映射到多个值上
将一个key映射到多个值得字典上,(一键多值字典)。
a = { 'a':[1,2,3], 'b':[4,5] } b = { 'c':{5,7,9}, 'd':{2,3,4} }
需要将多个值保存到另一个容器如列表或集合中,为了方便创建这样的字典,可以使用collections.defaultdict类,省去初始化时创建空列表,空集合的麻烦。
d = collections.defaultdict(list)
d = collections.defaultdict(set)
d['a'].append(1)
或者使用普通字典上调用setdefault()方法来取代。
d = {}
d.setdefault('a', []).append(1)
七、让字典保持有序
使用collections.OrderedDict类。
d = collections.OrderedDict()
d['a'] = 1
OrderedDict类内部维护了一个双向链表,它会根据元素加入的顺序来排列键的位置。
第一个新加入的元素被放置在链表的末尾。接下来对已存在的键做重新赋值不会改变键的顺序。
OrderedDict的大小是普通字典的2倍多,这是由于它额外创建的链表所致的。
八、与字典有关的计算(最大、最小、排序)
为了对字典内容做些有用的计算,通常会利用 zip()将字典的 key和 value反转过来。
min_price = min(zip(prices_dict.values(), prices.keys()))
max_price = max(zip(prices_dict.values(), prices.keys()))
prices_sorted = sorted(zip(prices_dict.values(), prices.keys()))
▲ zip()创建了一个迭代器,它的内容只能被消费一次。
九、在两个字典中寻找相同点
如下两个字典
a = { 'x':1, 'y':2, 'z':3, } b= { 'w':10, 'x':11, 'y':2, }
只需通过keys()和items()方法执行常见的集合操作即可。
a.keys() & b.keys() # { 'x', 'y' }
a.keys() - b.keys() # { 'z' }
a.items() & b.items() # { ('y', 2) }
修改或者过滤字典中的内容。
c = {key:a[key] for key in a.keys() - {'z', 'w'} }
十、从序列中移除重复项且保持元素间顺序不变
如果序列中的值是可哈希的,那么这个问题可以通过使用集合和生成器轻松解决。
def dedupe(items): seen = set() for item in items: if item not in seen: yield item seen.add(item)
如果想在不可哈希的对象序列中去除重复项。
def dedupe(items,key=None): seen = set() for item in items: val = key if key is None else key(item) if val not in seen: yield item seen.add(val) a = [ {'x':1,'y':2},{'x':1,'y':3},{'x':1,'y':2},{'x':2,'y':4},] print(list(dedupe(a,key=lambda x:(x['x'],x['y'])))) # [{'y': 2, 'x': 1}, {'y': 3, 'x': 1}, {'y': 4, 'x': 2}]
如果想读一个文件,去除其中重复的文本行,可以只需这样处理:
with open(somefile, 'r') as f: for line in dedupe(f): ...
十一、对切片命名
内置的slice()函数会创建一个切片对象,可以用在任何允许进行切片操作的地方。
>>> items = [0,1,2,3,4,5,6] >>> a = slice(2,4) >>> items[2:4] >>> items[a] [2, 3]
通过使用indices(size)方法将切片映射到特定大小的序列上。返回一个(start, stop, step)元组。
>>> s = 'HelloWorld' >>> a.indices(len(s)) (2, 4, 1) >>> for i in range(*a.indices(len(s))): ... print(s[i])
l
l
十二、找出序列中出现次数最多的元素
使用collections.Counter类,其中most_common()方法能直接得到答案。
>>> word_counts = collections.Counter(words)
>>> top_three = word_counts.most_common(3)
手动增加计,通过自增或者update()方法:
word_counts[word] += 1
word_counts.update(morewords)
Counter对象还支持同各种数学运算操作结合使用。
>>> a = Counter(words)
>>> b = Counter(morewords)
>>> c = a + b
十三、通过公共键对字典列表排序
operator.itemgetter函数对这类结构进行排序是非常简单的。
>>> from operator import itemgetter
>>> rows_by_flname = sorted(rows, key=itemgetter('frame', 'lname'))
>>> rows_by_uid = sorted(rows, key=itemgetter('uid'))
>>> min(rows, key=itemgetter('uid'))
>>> max(rows, key=itemgetter('uid'))
同样,使用lambda表达式也可以。
>>> rows_by_flname = sorted(rows, key=lambda x: (x['fname'], x['lname']))
>>> rows_by_uid = sorted(rows, key=lambda x:x['uid'])
十四、对不原生支持比较操作的对象排序
如果应用中有一系列的User对象实例,而我们想通过user_id属性来对他们排序,则可以提供一个可调用对象将User实例作为输入然后返回user_id。
class User: def __init__(self, user_id): self.user_id = user_id def __repr__(self): return 'User({})'.format(self.user_id) users = [User(23),User(12),User(33)] a = sorted(users, key=lambda x:x.user_id)
或者使用operator.itemgetter()
十五、根据字段将记录分组
有一系列的字典或对象实例,我们想根据某个特定的字段(如日期)来分组迭代数据。
itertools.groupby()函数对数据进行分组。groupby()创建一个迭代器,每次迭代返回一个value和子迭代器sub_itertor,子迭代器产生所有该分组内的项。
(1)先进行排序:
>>> rows.sort(key=itemgetter('date'))
(2)然后分组:
for date, item in itertools.groupby(rows, key=itemgetter('data')): print(data) for i in item: print(i)
如果只分组不需要排序,建立一个一键多值字典即可。
rows_by_date = collections.defaultdict(list) for row in rows: rows_by_date[row['date']].append(row)
十六、筛选序列中元素
(1)列表推导式:
n = [n for n in my_list if n > 0]
(2)生成器表达式:
n = (n for n in my_list if n > 0)
(3)filter() 函数处理:
def is_int(val): try: x = int(val) return True except ValueError: return False ivals = list(filter(is_int,my_list))
(4)新值替换旧值
n = [n if n > 0 else 0 for n in my_list]
(5)itertools.compress(),接受一个可迭代对象和一个布尔选择器序列作为输入。输出,在布尔序列中为True的可迭代对象元素。
>>> more5 = [n > 5 for n in counts] >>> more5 [False, False, True, True, False] list( itertools.compress(addresses, more5) )
十七、从字典中提取子集
利用字典推导式:
p1 = { key:value for key, value in prices.items() if value > 100 }
p2 = { key:value for key, value in prices.items() if key in tech_name }
十八、将名称映射到序列的元素中
通过名称来访问元素,减少结构中对位置的依赖性。
相比普通的元组,collections.namedtuple()。命名元组只增加了极小的开销就提供了这些便利。
实际上collections.namedtuple()是一个工厂方法,他返回的是Python中标准元组类型的子类。
>>> Subscriber = namedtuple('Subscriber', ['addr', 'joined'])
>>> sub = Subscriber('442994909@qq.com', '2020-2-2')
>>> addr, joined = sub
命名元组的主要作用在于将代码同它所控制的元素位置间解耦。
同时还作为字典的替代,但是namedtuple是不可变的。
可以通过_replace()方法来实现修改,会创建一个全新的命名元组,并对相应的值做替换。
>>> s = s._replace(addr='xxx@qq.com')
_replace(),还可以作为一种简便的方法填充具有可选或缺失字段的命名元组。
stock_prototype = Stock('', 0, 0.0, None, None) def dict_to_stock(s): return stock_prototype._replace(**s)
二十、将多个映射合并为单个映射
想执行查找操作,必须得检查这两个字典,一种简单的方法时利用collections.ChainMap类来解决这个问题。
>>> a = {'x':1, 'z':3} >>> b = {'y':2, 'z':4} >>> from collections import ChainMap >>> c = ChainMap(a,b) >>> print(c['x']) 1 >>> print(c['y']) 2 >>> print(c['z']) 3
ChainMap可接受多个映射然后在逻辑上使它们表现为一个单独的映射结构。
这些映射在字面上并不会合并在一起。相反,ChainMap只是简单得维护一个记录底层映射关系的列表,然后重新定义常见的字典操作来扫描这个列表。
如果有重复的键,那么这里会采用第一个映射中所对应的值。
ChainMap与带有作用域的值,全局变量,局部变量一起使用时特别有用。
>>> values = ChainMap() >>> values['x'] = 1 >>> values = values.new_child() >>> values['x'] = 2 >>> values = values.new_child() >>> values['x'] = 3 >>> values ChainMap({'x': 3}, {'x': 2}, {'x': 1}) >>> values['x'] 3 >>> values = values.parents >>> values['x'] 2 >>> values = values.parents >>> values['x'] 1 >>> values ChainMap({'x': 1})
(2)利用字典的update()方法将多个字典合并在一起。
>>> a = {'x':1, 'z':3} >>> b = {'y':2, 'z':4} >>> merged = dict(b) >>> merged.update(a) >>> merged {'x': 1, 'y': 2, 'z': 3}
需要单独构建一个字典,如果其中任何一个原始字典作了修改,这个改变不会反应到合并后的字典中。!
而ChainMap使用的就是原始的字典。