数据结构与算法 (03)

ipqtjmqj 2020-05-12

继续来进行 pythonCookbook. 学习大佬代码使我快乐, 与其自己写, 还不如直接抄, 然后加上自己的注解, 这样才是最适合我的学习之道哇.

仰之弥高,钻之弥坚

老祖宗还是厉害呀, 要是我能认识到这种境界, 夫复何求呦.

序列元素去重并保持顺序

需求

在一个序列上, 保持元素顺序的前提下, 对相同元素值进行去重

方案

如果序列中的值都是 hashable 类型的, 那直接用 集合 set 或者 生成器 就轻松解决了.

def filter_dumplicate(items):
    """序列元素去重"""
    seen = set()
    for item in items:
        if item not in seen:
            # 所有的 yield item 组成 生成器对象
            yield item 
            seen.add(item)

# test            
a = [1, 5, 2, 1, 9, 1, 5, 10]
print(list(filter_dumplicate(a)))
[1, 5, 2, 9, 10]

我感觉, 这个大佬, 是不是搞得太复杂, 还是我 too simple ?

# 这样不更简单吗
ret = []
for i in a:
    if i not in ret:
        ret.append(i)
print(ret)
[1, 5, 2, 9, 10]

就去个重, 私以为不用那么麻烦的吧. 其实更多的是, 我们通常不用考虑其顺序, 就去重, 这样一来, 直接用集合, 一波带走.

print(set(a))  # 集合元素的顺序, 呃,,我也不清楚
{1, 2, 5, 9, 10}

这类写法, 针对元素是 hashable 的时候ok的, 但对于不可哈希, 如 dict 类型 的序列, 要去重元素的话, 也可以这样.

def dedupe(items, key=None):
    """字典元素去重"""
    seen = set()
    for item in items:
        # key 有值则为dict,取其值, else 是单序列值
        val = item if key is None else key(item)
        if val not in seen:
            yield item 
            seen.add(val)
    
# test 以后写函数, 优先用 yield 而不用 list 来不断 append 啦
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‘])))
[{‘x‘: 1, ‘y‘: 2}, {‘x‘: 1, ‘y‘: 3}, {‘x‘: 2, ‘y‘: 4}]
[{‘x‘: 1, ‘y‘: 2}, {‘x‘: 2, ‘y‘: 4}]

这个把函数改为生成器, 确实优雅呀, 学到了, 以后, 优先考虑哦. 不要再傻傻地只会 append 啦. 基于此中方法, 再对某单个字段或者属性, 或者更复杂的 数据结构来去重, 也是ok的.

可以发现, 针对于序列的遍历, 生成器 yield 则会更加通用, 而非以前傻傻滴, 先定义个空 lst , 过程中不断 append 这样就造成浪费内存了, yield 返回就行啦, 最后再统一 处理即可. 生成器, 对于文件中, 消除重复行, 也是可以的, 套路都固定的.

with open(some_file, ‘r‘) as f:
    for line in dedupe(f):
        ....

命名切片

需求

清理一大堆, 已经无法直视的硬编码切片下标.

方案

假设我们有一个切片, 是一个字符串中固定的位置, 如字符串或文件等.

###### 0123456789012345678901234567890123456789012345678901234567890‘ record = ‘....................100 .......513.25 ..........‘ cost = int(record[20:23]) * float(record[31:37])
record = ‘....................100 .......513.25 ..........‘ cost = int(record[20:23]) * float(record[31:37])
cost = int(record[20:23]) * float(record[31:37])

如果这样写切片, 维护起来简直崩溃, 于是呢,应该而给切片进行命名呀.

shares = slice(20, 33)

即用内置的 slice() 函数 创建一个切片对象, 可以被用在, 任何允许使用的方法.

items = [0, 1, 2, 3, 4, 5, 6]

# 前闭后开
a = slice(2,4)

print(items[2:4])
print(items[a])

items[a] = [88,99]
print(items)

del items[a]
print(items)
[2, 3]
[2, 3]
[0, 1, 88, 99, 4, 5, 6]
[0, 1, 4, 5, 6]

对于一个切片对象a, 还可以调用其 a.start, a.stop 等属性获取更多信息.

print(a)
print(a.start)
print(a.stop)
slice(2, 4, None)
2
4
个人感觉这玩意儿, 也没什么用呀.

序列中出现最多的元素

需求

找出序列中, 出现最多次数的元素.

方案

我做数据工作, 就会经常做这类的事情, 但我从来没有用过内置工具, 都是, 以字典的方式来进行词频统计, 感觉也行.

内置的 collections.Counter 类, 就专门来解决此类问题的, most_common() 方法, 很厉害的哦.

词频统计, 最为经典的就是.

words = [ ‘look‘, ‘into‘, ‘my‘, ‘eyes‘, ‘look‘, ‘into‘, 
         ‘my‘, ‘eyes‘, ‘the‘, ‘eyes‘, ‘the‘, ‘eyes‘, ‘the‘, 
         ‘eyes‘, ‘not‘, ‘around‘, ‘the‘, ‘eyes‘, "don‘t", 
         ‘look‘, ‘around‘, ‘the‘, ‘eyes‘, ‘look‘, ‘into‘, 
         ‘my‘, ‘eyes‘, "you‘re", ‘under‘ 
        ] 

from collections import Counter 

# 初始化Counter 对象, 将统计序列加进去
word_counts = Counter(words)
print(word_counts.most_common()) 
# 选出频率最高的3个词
print("--"*8)
top_three = word_counts.most_common(3)
print(top_three)
[(‘eyes‘, 8), (‘the‘, 5), (‘look‘, 4), (‘into‘, 3), (‘my‘, 3), (‘around‘, 2), (‘not‘, 1), ("don‘t", 1), ("you‘re", 1), (‘under‘, 1)]
----------------
[(‘eyes‘, 8), (‘the‘, 5), (‘look‘, 4)]

作为输入, Counter 对象可以接受任意, 可哈希元素构成的序列对象. 在底层的实现上, 一个 Counter 对象就是一个车字典, 将元素映射到它出现的次数上.

# Counter 对象, 其实就是字典, value 是频次
print(word_counts[‘eyes‘])
print(word_counts[‘not‘])
8
1

结论就是, 词频统计之类的, 可以优先考虑 Counter 对象, 当然, 我手动弄字典也可以呀, 我乐意, 又不难.

过滤序列元素

需求

过滤序列元素, 根据某些规则

方案

最为简单和我最喜欢用的是, 列表推导式.

my_list = [1, 4, -5, 10, 2, 3, -1]

# 过滤出 大于 0 的元素
print([i for i in my_list if i > 0])

# 小于0, 且是 "奇数"
print([i for i in my_list if i < 0 and i % 2 == 1])
[1, 4, 10, 2, 3]
[-5, -1]

用列表推导式的一个问题在于, 当输入很大, 则会得到一个大的结果集, 浪费内存, 解决方案就是用 迭代器呀.

#  生成器就节约内存了, 优先考虑
pos = (i for i in my_list if i > 0 and i % 2 == 0)
print(pos)

for i in pos:
    print(i)
<generator object <genexpr> at 0x00000217B924F938>
4
10
2

有时候, 过滤条件复杂, 就需要把过滤的规则给放到一个函数中啦, 然后将其放到内置的 filter() 函数中.

values = [‘1‘, ‘2‘, ‘-3‘, ‘-‘, ‘4‘, ‘N/A‘, ‘5‘]

def is_int(val):
    try:
        x = int(val)
        return True
    except ValueError:
        return False
    
# test
list(filter(is_int, values))
[‘1‘, ‘2‘, ‘-3‘, ‘4‘, ‘5‘]

我感觉这 filter 和 map 函数的用法是差不多的, 一个是过滤出, True 的, 一个是做映射, 映射到每个元素. 同时, filter() 函数创建了一个迭代器.

想想, 还是列表推导式, 更加爽一点哦, 比如, 在推导的时候, 还是可以做数据转换的.

lst = [1, 4, -5, 10, -9, 2, 3, -1]

import math
print([math.sqrt(i) for i in lst if i > 0])
[1.0, 2.0, 3.1622776601683795, 1.4142135623730951, 1.7320508075688772]

不仅仅可以过滤, 结合 if - else 还能实现赋值.

# RELU 函数
print([i if i > 0 else 0 for i in my_list])
[1, 4, 0, 10, 2, 3, 0]

小结

  • 序列去重且有序, 用集合 set 和 生成器 yield, 优先考虑
  • 切片对象, 可用 slice 函数来创建命名对象, 便于管理
  • 序列统计, 如词频统计, 优先用 collectiions.Counter().most_common() 方法, 自己写也不难.
  • 过滤序列, 用列表推导式和生成器(元组推导式), 复杂结合 filter 函数, 结合 [a if aaa else b for aaa in xxx ]

相关推荐