【问题标题】:Sorting a list of lists by item frequency in Python 2.3在 Python 2.3 中按项目频率对列表列表进行排序
【发布时间】:2012-06-18 13:33:22
【问题描述】:

我有一个列表,其中包含此类项目的子列表。

mylist = [
['ITEM A', 'YES', 'NO', 'YES', 'YES', 'NO', 'NO', 'NO', 'NO', 'NO'],
['ITEM B', 'YES', 'NO', 'YES', 'YES', 'NO', 'NO', 'NO', 'NO', 'MAYBE'],
['ITEM C', 'YES', 'YES', 'YES', 'YES', 'NO', 'NO', 'MAYBE', 'NO', 'MAYBE']
]

现在我想在这种情况下对子列表进行排序 - 每行(即子列表)中的项目 'YES''MAYBE' 越多,它就会向上移动。每行的'NO's 越多,它在排序列表中的位置就越靠后。

理想的结果是——

mylist = [
['ITEM C', 'YES', 'YES', 'YES', 'YES', 'NO', 'NO', 'MAYBE', 'NO', 'MAYBE'],
['ITEM B', 'YES', 'NO', 'YES', 'YES', 'NO', 'NO', 'NO', 'NO', 'MAYBE'],
['ITEM A', 'YES', 'NO', 'YES', 'YES', 'NO', 'NO', 'NO', 'NO', 'NO']
]
#Item C has 4 'YES' and 2 'MAYBE'
#Item B has 3 'YES' and 1 'MAYBE'
#Item C has 3 'YES'

很遗憾,我被 Python 2.3 困住了,需要找出最有效的方法。

【问题讨论】:

  • @Nima 有时人们会在工作中坚持使用旧版本(并且决策者对升级缺乏/抵抗)。我在这样的地方工作,所以 OP 可能别无选择。
  • @Levon 说得好。我正在开发一个遗留系统,目前在 2.3 中开发。它吹了,但这是必要的。

标签: python list sorting


【解决方案1】:

要在 Python 2.3 或更低版本中按键排序,可以使用cmp 参数。但有时key 样式排序更容易阅读;无论如何,它的工作量更少,因为 cmp 将被调用 O(n log n) 次,而 key 函数将仅被调用 O(n) 次。

考虑到这一点,这里有一种方法可以在更高版本的 Python 中重现 key 参数的行为。它使用 decorate-sort-undecorate 成语,也就是 Schwartzian Transform。这不会像复制副本那样节省空间,但是对于大型列表,它可能会更节省时间。我将其命名为sorted,因为它大致再现了2.4 中添加的sorted 函数;检查 python 版本并有条件地导入它,这样您就不会在较新版本中破坏内置的 sorted - 或者只是重命名它。

def sorted(seq, key=lambda x: None, reverse=False):
    seq = [(key(x), i, x) for i, x in enumerate(seq)]
    seq.sort()
    if reverse:
        seq.reverse()
    return [x for k, i, x in seq]

请注意,enumerate 仅在您关心对具有相等键的不相等值进行稳定排序时才需要;它会减慢头发的功能。对您的数据进行了测试:

>>> key=lambda x: (x.count('YES'), x.count('MAYBE'), x.count('NO'))
>>> my_sorted(mylist, key=key, reverse=True)
[['ITEM C', 'YES', 'YES', 'YES', 'YES', 'NO', 'NO', 'MAYBE', 'NO', 'MAYBE'], 
 ['ITEM B', 'YES', 'NO', 'YES', 'YES', 'NO', 'NO', 'NO', 'NO', 'MAYBE'], 
 ['ITEM A', 'YES', 'NO', 'YES', 'YES', 'NO', 'NO', 'NO', 'NO', 'NO']]

您也可以考虑使用字典进行计数;这样,只需要一次通过。但是,count 已充分优化,至少在我的机器上,三遍仍然比一个 Python for 循环快。因此,仅当您需要计算大量值时才使用它。我将把它留在这里以供后代使用:

def my_key(inner_list):
    counts = {'YES':0, 'MAYBE':0, 'NO':0}
    for i in inner_list:
        if i in counts:
            counts[i] += 1
    return (counts['YES'], counts['MAYBE'], counts['NO'])

我做了一些测试;为长篇大论道歉。以下内容仅供好奇者参考。

我的测试表明,在较小的列表中,装饰、排序、取消装饰已经比使用内置排序 + cmp 更快。在更大的列表中,差异变得更加显着。定义:

def key_count(x):
    return (x.count('YES'), x.count('MAYBE'), x.count('NO'))

def key_dict(inner_list):
    counts = {'YES':0, 'MAYBE':0, 'NO':0}
    for i in inner_list:
        if i in counts:
            counts[i] += 1
    return (counts['YES'], counts['MAYBE'], counts['NO'])

def decorate_sort(seq, key=lambda x: None, reverse=False):
    seq = [(key(x), i, x) for i, x in enumerate(seq)]
    seq.sort()
    if reverse:
        seq.reverse()
    return [x for k, i, x in seq]

def builtin_sort(seq, key, reverse=False):
    seq.sort(lambda p, q: cmp(key(p), key(q)))
    if reverse:
        seq.reverse()

测试:

>>> mylist = [
... ['ITEM A', 'YES', 'NO', 'YES', 'YES', 'NO', 'NO', 'NO', 'NO', 'NO'],
... ['ITEM B', 'YES', 'NO', 'YES', 'YES', 'NO', 'NO', 'NO', 'NO', 'MAYBE'],
... ['ITEM C', 'YES', 'YES', 'YES', 'YES', 'NO', 'NO', 'MAYBE', 'NO', 'MAYBE']
... ]
>>> %timeit decorate_sort(mylist, key=key_count, reverse=True)
100000 loops, best of 3: 5.03 us per loop
>>> %timeit builtin_sort(mylist, key=key_count, reverse=True)
100000 loops, best of 3: 5.28 us per loop

内置版本已经很慢了!不太通用的版本mylist.sort(lambda p, q: -cmp(key(p), key(q))) 是一个更好的短名单,因为enumerate 添加到decorate_sort;没有它,decorate_sort 更快(在我之前的测试中每个循环 4.28 us):

>>> %timeit mylist.sort(lambda p, q: -cmp(key_count(p), key_count(q)))
100000 loops, best of 3: 4.74 us per loop

虽然在这种情况下使用 key_dict 是错误的:

>>> %timeit decorate_sort(mylist, key=key_dict, reverse=True)
100000 loops, best of 3: 8.97 us per loop
>>> %timeit builtin_sort(mylist, key=key_dict, reverse=True)
100000 loops, best of 3: 11.4 us per loop

在更大的列表上进行测试,结果基本相同:

>>> import random
>>> mylist = [[random.choice(('YES', 'MAYBE', 'NO')) for _ in range(1000)] 
              for _ in range(100)]
>>> %timeit decorate_sort(mylist, key=key_count, reverse=True)
100 loops, best of 3: 6.93 ms per loop
>>> %timeit builtin_sort(mylist, key=key_count, reverse=True)
10 loops, best of 3: 34.5 ms per loop

不太通用的版本现在比decorate_sort慢。

>>> %timeit mylist.sort(lambda p, q: -cmp(key_count(p), key_count(q)))
100 loops, best of 3: 13.5 ms per loop

key_dict 仍然较慢。 (但比builtin_sort 快!)

>>> %timeit decorate_sort(mylist, key=key_dict, reverse=True)
10 loops, best of 3: 20.4 ms per loop
>>> %timeit builtin_sort(mylist, key=key_dict, reverse=True)
10 loops, best of 3: 103 ms per loop

因此结果是,Schwartzian 变换提供了一个更快更通用的解决方案——这是一种罕见而美妙的组合。

【讨论】:

  • 感谢您提供非常详细的分析。我会检查所有这些。非常感谢。
  • 不幸的是,您的 Schwartzian 变换可能会破坏稳定排序保证(如果 key(x)key(y) 比较相等但 xy 比较不相等)。一个简单的解决方法是将索引存储在转换后的元组中:seq = [(key(x), i, x) for i, x in enumerate(seq)]
  • 此外,它不允许提供 both keycmp - 如果您愿意的话!我已经在我的回答中通过这两个修复扩展了您的解决方案。
  • @ecatmur,是的,我有点故意离开cmp。如果你想使用cmp,只需使用内置版本,它比任何用 Python 编写的代码都要快。另外,我之前看到有人同时使用cmpkey,我认为这是个坏主意。在这种情况下,我更喜欢 Python 3 的处理方式。不过,感谢您提醒我有关排序稳定性的信息。
【解决方案2】:

一般解决方案:使用 list.sort 和返回元组的键函数:

mylist.sort(key=lambda sl: (sl.count('YES') + sl.count('MAYBE'), -sl.count('NO')), reverse=True)

keyreverse 是在 Python 2.4 中添加的,因此您必须手动完成:

key = lambda sl: (sl.count('YES') + sl.count('MAYBE'), -sl.count('NO'))
mylist.sort(lambda p, q: -cmp(key(p), key(q)))

如果key 很慢,最好使用一种解决方案,该解决方案仅对每个项目计算一次key 函数(所谓的“Schwartzian transform”)。请注意 >=Python 2.4 已经执行了这个优化(或类似的):

def key_sort(seq, cmp=None, key=None, reverse=False):
    if key is not None:
        transform = [(key(x), i, x) for i, x in enumerate(seq)]
        transform.sort(None if cmp is None else lambda (k, _, _), (l, _, _): cmp(k, l))
        seq[:] = [x for _, _, x in transform]
    else:
        seq.sort(cmp)
    if reverse:
        seq.reverse()

【讨论】:

  • +1,但 OP 可能需要使用 key。主要思想是它应该返回一个元组。如果'YES''MAYBE' 更好,我会建议类似(sl.count('YES'), sl.count('MAYBE'), -sl.count('NO'))
  • 我认为在python2.3中对works a bit differently进行排序
  • 为 YES 和 MAYBE 添加一些权重。比如说,(100, 10, -100) 代表 (YES, MAYBE, NO)
  • @Ribtoks 如果 10 MAYBE 算作 1 YES 则有效;如果要分别考虑成功和失败,元组方法是必要的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-01-20
  • 1970-01-01
  • 1970-01-01
  • 2021-04-16
  • 2014-06-19
  • 2013-08-11
  • 1970-01-01
相关资源
最近更新 更多