【问题标题】:Python 3: Reverse consecutive runs in sorted list?Python 3:在排序列表中反转连续运行?
【发布时间】:2017-06-13 09:17:53
【问题描述】:

这是一个问题是What's the most Pythonic way to identify consecutive duplicates in a list?的扩展。

假设你有一个元组列表:

my_list = [(1,4), (2,3), (3,2), (4,4), (5,2)]

然后按每个元组的最后一个值对其进行排序:

my_list = sorted(my_list, key=lambda tuple: tuple[1])
# [(3,2), (5,2), (2,3), (1,4), (4,4)]

然后我们有两个连续的运行(查看每个元组中的最后一个值),即[(3,2), (5,2)][(1,4), (4,4)]

反转每次运行(不是其中的元组)的pythonic方法是什么,例如

reverse_runs(my_list)
# [(5,2), (3,2), (2,3), (4,4), (1,4)]

这可以在生成器中完成吗?

更新

我注意到示例列表可能不清楚。所以改为考虑:

my_list = [(1,"A"), (2,"B"), (5,"C"), (4,"C"), (3,"C"), (6,"A"),(7,"A"), (8,"D")]

reverse_runs 的理想输出在哪里

[(7,"A"), (6,"A"), (1,"A"), (2,"B"), (3,"C"), (4,"C"), (5,"C"), (8,"D")]

为了明确术语,我采用了“运行”的用法,用于描述 TimSort,这是 Python 的排序函数所基于的 - 为它(排序函数)提供了安全性。

因此,如果您对集合进行排序,如果集合是多面的,那么只有 指定的维度会在 上排序,如果两个元素是 对于指定的维度相同,它们的顺序将不会改变。

因此以下函数:

sorted(my_list,key=lambda t: t[1])

产量:

[(1, 'A'), (6, 'A'), (7, 'A'), (2, 'B'), (5, 'C'), (4, 'C'), (3, 'C'), (8, 'D')]

"C"(即(5, 'C'), (4, 'C'), (3, 'C'))上的运行不会受到干扰。

因此,总而言之,来自尚未定义的函数 reverse_runs 的所需输出:

1.) 按元组的最后一个元素对元组进行排序

2.) 保持第一个元素的顺序,反向运行在最后一个元素上

理想情况下,我希望在生成器函数中使用此功能,但这(目前对我而言)似乎是不可能的。

因此可以采取以下策略:

1.) 通过sorted(my_list, key=lambda tuple: tuple[1])按最后一个元素对元组进行排序

2.) 当后续元组 (i+1) 与 (i) 中的最后一个元素不同时,识别每个元组中最后一个元素的索引。即识别运行

3.) 制作一个空列表

4.) 使用拼接运算符,获取、反转并将每个子列表附加到空列表中

【问题讨论】:

  • 连续运行两次是什么意思?
  • @WillemVanOnsem 在排序键中重复。
  • 我认为他将运行定义为每个元组中的第二个元素相等...所以 [(1,2), (2,2), (3,2)] 是运行三..
  • @not_a_robot 正确
  • 也许是一个相关的答案:How do I use Python's itertools.groupby?

标签: python algorithm list generator timsort


【解决方案1】:

我认为这会奏效。

my_list = [(1,4), (2,3), (3,2), (4,4), (5,2)]
my_list = sorted(my_list, key=lambda tuple: (tuple[1], -tuple[0]))

print(my_list)

输出

[(5, 2), (3, 2), (2, 3), (4, 4), (1, 4)]

被误解的问题。不太漂亮,但这应该适合你真正想要的:

from itertools import groupby
from operator import itemgetter


def reverse_runs(l):
    sorted_list = sorted(l, key=itemgetter(1))
    reversed_groups = (reversed(list(g)) for _, g in groupby(sorted_list, key=itemgetter(1)))
    reversed_runs = [e for sublist in reversed_groups for e in sublist]

    return reversed_runs


if __name__ == '__main__':
    print(reverse_runs([(1, 4), (2, 3), (3, 2), (4, 4), (5, 2)]))
    print(reverse_runs([(1, "A"), (2, "B"), (5, "C"), (4, "C"), (3, "C"), (6, "A"), (7, "A"), (8, "D")]))

输出

[(5, 2), (3, 2), (2, 3), (4, 4), (1, 4)]
[(7, 'A'), (6, 'A'), (1, 'A'), (2, 'B'), (3, 'C'), (4, 'C'), (5, 'C'), (8, 'D')]

生成器版本:

from itertools import groupby
from operator import itemgetter


def reverse_runs(l):
    sorted_list = sorted(l, key=itemgetter(1))
    reversed_groups = (reversed(list(g)) for _, g in groupby(sorted_list, key=itemgetter(1)))

    for group in reversed_groups:
        yield from group


if __name__ == '__main__':
    print(list(reverse_runs([(1, 4), (2, 3), (3, 2), (4, 4), (5, 2)])))
    print(list(reverse_runs([(1, "A"), (2, "B"), (5, "C"), (4, "C"), (3, "C"), (6, "A"), (7, "A"), (8, "D")])))

【讨论】:

  • 不错...首先按第二个元素排序,然后按每个元组中的第一个元素排序。
  • 然后由第一个元素的取反值
  • 如果sorted(my_list, key=lambda t: t[0]) != my_list,这可能不起作用。
  • 这实际上并不总是有效;考虑:my_list = [(1,"A"), (2,"B"), (5,"C"), (4,"C"), (3,"C"),(6,"A"),(7,"A"),(8,"D")]。简单排序它保留了"C" 上运行的非升序顺序,即sorted(my_list,key=lambda t: t[1]) #yields [(1, 'A'), (6, 'A'), (7, 'A'), (2, 'B'), (5, 'C'), (4, 'C'), (3, 'C'), (8, 'D')]。请注意,上面的代码会生成[(7, 'A'), (6, 'A'), (1, 'A'), (2, 'B'), (5, 'C'), (4, 'C'), (3, 'C'), (8, 'D')],其中很明显“C”(例如 5、4、3)上的运行不会与其原始顺序相反。
  • @SumNeuron 哦,我想我误解了你的问题。我以为您只是希望第二次运行按第一个元组元素的降序排列(即与默认升序相反)。事实上,无论第一次如何对它们进行排序,您都希望无条件地反转运行。
【解决方案2】:

最一般的情况需要 2 种。第一个排序是 reversed 根据第二个条件排序。第二个排序是基于第一个条件的前向排序:

pass1 = sorted(my_list, key=itemgetter(0), reverse=True)
result = sorted(pass1, key=itemgetter(1))

我们可以像这样分多次排序,因为python的排序算法保证是stable

但是,在现实生活中,通常可以简单地构造一个更聪明的键函数,从而允许一次进行排序。这通常涉及“否定”其中一个值并依赖于元组自行排序lexicographically

result = sorted(my_list, key=lambda t: (t[1], -t[0]))

响应您的更新,看起来如下可能是一个合适的解决方案:

from operator import itemgetter
from itertools import chain, groupby
my_list = [(1,"A"), (2,"B"), (5,"C"), (4,"C"), (3,"C"), (6,"A"),(7,"A"), (8,"D")]

pass1 = sorted(my_list, key=itemgetter(1))
result = list(chain.from_iterable(reversed(list(g)) for k, g in groupby(pass1, key=itemgetter(1))))
print(result)

我们可以把表达式拆开:

chain.from_iterable(reversed(list(g)) for k, g in groupby(pass1, key=itemgetter(1)))

试图弄清楚它在做什么......

首先,让我们看看groupby(pass1, key=itemgetter(1))groupby 将产生 2 元组。元组中的第一项 (k) 是“键”——例如从itemgetter(1) 返回的任何内容。分组发生后,密钥在这里并不重要,因此我们不使用它。第二项(g——代表“组”)是一个迭代,它产生具有相同“键”的连续值。这正是您要求的项目,但是,它们按照排序后的顺序排列。您以相反的顺序请求它们。为了反转任意迭代,我们可以从它构造一个列表,然后反转该列表。例如reversed(list(g))。最后,我们需要将这些块再次粘贴在一起,这就是 chain.from_iterable 的用武之地。

如果我们想变得更聪明,从算法的角度来看我们可能会做得更好(假设垃圾箱的“密钥”是可散列的)。诀窍是在字典中对对象进行分类,然后对这些分类进行排序。这意味着我们可能会排序一个比原始列表短得多的列表:

from collections import defaultdict, deque
from itertools import chain

my_list = [(1,"A"), (2,"B"), (5,"C"), (4,"C"), (3,"C"), (6,"A"),(7,"A"), (8,"D")]

bins = defaultdict(deque)
for t in my_list:
    bins[t[1]].appendleft(t)

print(list(chain.from_iterable(bins[key] for key in sorted(bins))))

请注意,是否这比第一种方法做得更好,很大程度上取决于初始数据。由于TimSort 是一个非常漂亮的算法,如果数据开始已经分组到 bin 中,那么这个算法可能不会打败它(不过,我会把它作为练习留给你尝试......)。但是,如果数据分散得很好(导致TimSort 的行为更像MergeSort),那么首先进行分箱可能会稍有优势。

【讨论】:

  • Yes Python 的排序是建立在TimSort 之上的(因此是这个问题的标签),因此 TimSort 在排序时会保留“运行”;从而使我们能够连续应用多种排序以获得唯一列表。然而,这个问题不能简单地依赖于内置的排序功能,因为我们希望对一个(保留运行)进行排序,然后独立地反转这些运行。
  • @SumNeuron -- 具体来说,CPython 使用 TimSort。欢迎实现者选择他们想要的任何算法,只要它是稳定的。但这是一个小问题。我不确定我是否理解您的陈述“我们对一个(保留运行)进行排序,然后独立地反转这些运行”。听起来您想分类到有序的桶中,然后对桶中的东西进行分类。对于稳定的排序,这可以总是通过按照后一个标准对所有内容进行排序,然后按照分桶标准再次排序来完成。
  • 对不起,错字,“一次”。您所描述的内容是正确的,并且我知道稳定类型。问题是每个“桶”都需要独立地反转,即如果你有两个桶[(1,2,3),(5,4,6)],那么它们应该变成[(3,2,1), (6,4,5)]。请查看更新后的示例,了解所需结果的描述。
  • @SumNeuron -- 啊……我想我现在明白了。我已经更新了。
  • 有没有办法在不改变默认字典的情况下做到这一点?
猜你喜欢
  • 1970-01-01
  • 2011-06-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-27
  • 2017-05-04
  • 1970-01-01
  • 2020-03-31
相关资源
最近更新 更多