如何在 Python 中进行循环(子)排序?

How to do a circular (sub)sort of sets in Python?

考虑以下最小化示例:

代码:

a = [(1,'A'), (2,'A'), (3,'A'), (4,'A'), (5,'A')]
b = [(1,'B'), (2,'B'), (3,'B')]
c = []
d = [(1,'D'), (2,'D'), (3,'D'), (4,'D')]

print(sorted(a+b+c+d))

结果:

[(1, 'A'), (1, 'B'), (1, 'D'), (2, 'A'), (2, 'B'), (2, 'D'), (3, 'A'), (3, 'B'), (3, 'D'), (4, 'A'), (4, 'D'), (5, 'A')]

Python 按每个集合的第一项然后是第二项对集合列表进行排序。没关系。 现在,我需要第二个排序顺序在字符串中是“循环的”(不确定这是否是正确的术语)。 此外,我想指定新排序列表中的最后一个字符串。例如,如果我指定 'B',则有序列表应从 'C' 开始。如果 'C' 不存在,它应该从 'D' 开始,等等。但是,指定的字符也可能不在列表中,例如如果 'C' 不存在,新的排序列表仍然应该从 'D'.

开始

编辑:

抱歉,我没有添加所需的集合列表输出顺序以使其清楚。 假设我会指定 mySpecialSort(myList,'B')。 然后应该首先是包含 1 作为最高优先级排序顺序的所有集合,然后是“循环”字符串(这里从 'D' 开始,因为列表中没有 C ).

所需的排序顺序:

[(1, 'D'), (1, 'A'), (1, 'B'), (2, 'D'), (2, 'A'), (2, 'B'), (3, 'D'), (3, 'A'), (3, 'B'), (4, 'D'), (4, 'A'), (5, 'A')]

或缩短可读形式: 1D, 1A, 1B, 2D, 2A, 2B, 3D, 3A, 3B, 4D, 4A, 5A

我想出了一个(麻烦的)解决方案(但是到目前为止 只有)用于单个字符列表上的“循环”排序(这里有重复项)如下:

代码:

myList = ['A', 'D', 'E', 'G', 'Z', 'A', 'J', 'K', 'T']

def myCircularSort(myList,myLast):
    myListTmp = sorted(list(set(myList + [myLast])))                     # add myLast, remove duplicates and sort
    idx = myListTmp.index(myLast)                                        # get index of myLast
    myStart = myListTmp[(idx+1)%len(myListTmp)]                          # get the start list item
    
    myListSorted = sorted(list(set(myList)))                             # sorted original list
    print("Normal sort:                  {}".format(myListSorted))
    idx_start = myListSorted.index(myStart)                              # find start item and get its index
    myNewSort = myListSorted[idx_start:] + myListSorted[0:idx_start]     # split list and put in new order
    print("Circular sort with {} as last: {}\n".format(myLast,myNewSort))

myCircularSort(myList,'D')
myCircularSort(myList,'X')

结果:

Normal sort:                  ['A', 'D', 'E', 'G', 'J', 'K', 'T', 'Z']
Circular sort with D as last: ['E', 'G', 'J', 'K', 'T', 'Z', 'A', 'D']

Normal sort:                  ['A', 'D', 'E', 'G', 'J', 'K', 'T', 'Z']
Circular sort with X as last: ['Z', 'A', 'D', 'E', 'G', 'J', 'K', 'T']    # X actually not in the list

但是,现在我被困在如何获得这种“循环”排序(在集合列表的第二项上)和“正常”排序(在集合列表的第一项上)。

或者,我可能会想到一种“强力”方法来找到最高索引(此处:4)和所有现有字符串(此处:A-Z ) 并检查两个嵌套 for 循环中每个组合的存在。 我是在正确的轨道上,还是会做一些非常复杂和低效的事情,或者我是否缺少一些聪明的 Python 功能?

编辑2:

进一步搜索后,我猜 lambdacmp(x,y) 可以完成这项工作(参见 example),但它似乎不存在于 [=85] =]3 了。所以,可能是 operator.itemgetter()operator.methodcaller() 的东西,我仍然不知道如何使用,因为我缺少很好的例子...

您可以使用字典将字母映射到正确的位置:

from string import ascii_uppercase as ABC

start = ABC.index('D') + 1

sorter = {
    ABC[(n + start) % len(ABC)]: n
    for n in range(len(ABC))
}

myList = ['A', 'D', 'E', 'G', 'Z', 'A', 'J', 'K', 'T']

print(sorted(myList, key=sorter.get))

# ['E', 'G', 'J', 'K', 'T', 'Z', 'A', 'A', 'D']

要使用任意关键字,将它们提取到 keys 列表中,根据需要重新排列并使用 keys.index(word) 作为排序关键字:

myList = [
    (1, 'ARTHUR'),
    (2, 'CHARLIE'),
    (3, 'GEORGE'),
    (4, 'HARRY'),
    (5, 'JACK'),
    (6, 'LEO'),
    (7, 'MUHAMMAD'),
    (8, 'NOAH'),
    (9, 'OLIVER'),
]


def circ_sorted(lst, start):
    keys = sorted(e[1] for e in lst)
    less = sum(1 for k in keys if k <= start)
    keys = keys[less:] + keys[:less]
    return sorted(lst, key=lambda e: (keys.index(e[1]), e[0]))

print(circ_sorted(myList, 'LEO')) ## [MUHAMMAD, NOAH...]
print(circ_sorted(myList, 'IAN')) ## [JACK, LEO...]

使用自定义排序键功能:

from string import ascii_uppercase

order = {c: i for i, c in enumerate(ascii_uppercase)}

def circular_sort(lst, last):
    return sorted(lst, key=lambda x: (x[0], order[x[1]] + 26*(x[1]<=last)))

>>> circular_sort(a+b+c+d, 'B')
[(1, 'D'), (2, 'D'), (3, 'D'), (4, 'D'), (1, 'A'), (2, 'A'), (3, 'A'), (4, 'A'), (5, 'A'), (1, 'B'), (2, 'B'), (3, 'B')]

这只是将 26 添加到小于或等于指定的最后一个字母的任何字母的索引。

哎呀,这非常耗时,但我想我现在有解决方案了。至少结果似乎具有所需的顺序。 模块 functools 提供 cmp_to_key 来替换显然在 Python3 中删除的 cmp()。至少那是我发现的 here.

如果有“本地”Python3 解决方案,我很乐意了解它。欢迎提出意见、改进和简化。

因此,以下代码首先按数字(此处为 1 到 5)对列表的集合进行排序,然后按循环方式(此处为:Ag、Au、Ca、Fe、Ti)按字符串排序,这样最后一个字符串将由 myRef.

决定

代码:

### special numerical and circular alphanumerical sort on a list of sets
from functools import cmp_to_key

# different lists of sets
ag = [(1,'Ag'), (2,'Ag'), (3,'Ag'), (4,'Ag'), (5,'Ag')]
au = [(1,'Au'), (2,'Au')]
ba = []
ca = [(1,'Ca'), (2,'Ca'), (3,'Ca')]
fe = [(1,'Fe'), (2,'Fe')]
ti = [(1,'Ti'), (2,'Ti'), (3,'Ti')]

myList = fe + ti + ag + au + ca + ba     # merge all lists

def mySpecialCircularSort(myList,myRef):
    myList = list(set(myList))                 # remove duplicates
    myListNew = sorted(myList, key=cmp_to_key(lambda a, b: 
        -1 if a[0]<b[0]   else 1 if a[0]>b[0] else 
        -1 if b[1]==myRef else
         1 if a[1]==myRef else
        -1 if a[1]>myRef  and b[1]<myRef else
         1 if a[1]<myRef  and b[1]>myRef else
        -1 if a[1]<b[1]   else
         1 if a[1]>b[1]   else 0))
    print("Circular sort with {} as last: {}".format(myRef,myListNew))

print("Unsorted as is:                {}\n".format(myList))
mySpecialCircularSort(myList,'Ag')
mySpecialCircularSort(myList,'Au')
mySpecialCircularSort(myList,'Ba')   # since Ba-List was empty, the result will be same as 'Au'
mySpecialCircularSort(myList,'Ca')
mySpecialCircularSort(myList,'Fe')
mySpecialCircularSort(myList,'Ti')

结果:

Unsorted as is:                [(1, 'Fe'), (2, 'Fe'), (1, 'Ti'), (2, 'Ti'), (3, 'Ti'), (1, 'Ag'), (2, 'Ag'), (3, 'Ag'), (4, 'Ag'), (5, 'Ag'), (1, 'Au'), (2, 'Au'), (1, 'Ca'), (2, 'Ca'), (3, 'Ca')]

Circular sort with Ag as last: [(1, 'Au'), (1, 'Ca'), (1, 'Fe'), (1, 'Ti'), (1, 'Ag'), (2, 'Au'), (2, 'Ca'), (2, 'Fe'), (2, 'Ti'), (2, 'Ag'), (3, 'Ca'), (3, 'Ti'), (3, 'Ag'), (4, 'Ag'), (5, 'Ag')]
Circular sort with Au as last: [(1, 'Ca'), (1, 'Fe'), (1, 'Ti'), (1, 'Ag'), (1, 'Au'), (2, 'Ca'), (2, 'Fe'), (2, 'Ti'), (2, 'Ag'), (2, 'Au'), (3, 'Ca'), (3, 'Ti'), (3, 'Ag'), (4, 'Ag'), (5, 'Ag')]
Circular sort with Ba as last: [(1, 'Ca'), (1, 'Fe'), (1, 'Ti'), (1, 'Ag'), (1, 'Au'), (2, 'Ca'), (2, 'Fe'), (2, 'Ti'), (2, 'Ag'), (2, 'Au'), (3, 'Ca'), (3, 'Ti'), (3, 'Ag'), (4, 'Ag'), (5, 'Ag')]
Circular sort with Ca as last: [(1, 'Fe'), (1, 'Ti'), (1, 'Ag'), (1, 'Au'), (1, 'Ca'), (2, 'Fe'), (2, 'Ti'), (2, 'Ag'), (2, 'Au'), (2, 'Ca'), (3, 'Ti'), (3, 'Ag'), (3, 'Ca'), (4, 'Ag'), (5, 'Ag')]
Circular sort with Fe as last: [(1, 'Ti'), (1, 'Ag'), (1, 'Au'), (1, 'Ca'), (1, 'Fe'), (2, 'Ti'), (2, 'Ag'), (2, 'Au'), (2, 'Ca'), (2, 'Fe'), (3, 'Ti'), (3, 'Ag'), (3, 'Ca'), (4, 'Ag'), (5, 'Ag')]
Circular sort with Ti as last: [(1, 'Ag'), (1, 'Au'), (1, 'Ca'), (1, 'Fe'), (1, 'Ti'), (2, 'Ag'), (2, 'Au'), (2, 'Ca'), (2, 'Fe'), (2, 'Ti'), (3, 'Ag'), (3, 'Ca'), (3, 'Ti'), (4, 'Ag'), (5, 'Ag')]

我在示例数据中看到一个模式:

a = [(1,'A'), (2,'A'), (3,'A'), (4,'A'), (5,'A')]
b = [(1,'B'), (2,'B'), (3,'B')]
c = []
d = [(1,'D'), (2,'D'), (3,'D'), (4,'D')]

可能这个模式误导了我,而真实数据没有相同的模式。
这种情况,请忽略我的回答。

否则,考虑到 OP 对我评论的回答:

starting point is several separate lists

我提出这个解决方案:

  • 使用源列表构建嵌套列表;
  • 根据起点旋转列表n次;
  • 转置;
  • 展平;

这里是一个实现的例子,定义了一些助手:

from itertools import zip_longest
def rotate(l, n):
    return l[n:] + l[:n]

def transpose(l):
    return [list(filter(None,i)) for i in zip_longest(*tmp)]

def flatten(l):
    return [item for sublist in l for item in sublist]

然后,比如旋转3次开始D:

tmp = [a, b, c, d]
tmp = rotate(tmp, 3)
tmp = transpose(tmp)
tmp = flatten(tmp)
tmp
#=> [(1, 'D'), (1, 'A'), (1, 'B'), (2, 'D'), (2, 'A'), (2, 'B'), (3, 'D'), (3, 'A'), (3, 'B'), (4, 'D'), (4, 'A'), (5, 'A')]