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

问题描述

考虑以下最小化示例:

代码

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

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

结果:

[(1,(1,'A')]

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

编辑:

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

所需的排序顺序:

[(1,'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','T','Z']
Circular sort with D as last: ['E','D']

normal sort:                  ['A','Z']
Circular sort with X as last: ['Z','T']    # X actually not in the list

但是,现在我被困在如何将这种“循环”排序(在集合列表的第二项上)与“正常”排序(在集合列表的第一项上)结合起来。>

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

编辑 2:

经过一些进一步的搜索,我猜 lambdacmp(x,y) 会完成这项工作(参见 example),但它似乎不再存在于 python3 中。所以,那么可能是带有 operator.itemgetter()operator.methodcaller() 的东西,我仍然不知道如何使用,因为我错过了很好的例子......

解决方法

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

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','T','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...]
,

呼,这很费时间,但我想我现在有一个解决方案。至少结果似乎具有所需的顺序。 模块 functools 提供了 cmp_to_key 来替换 cmp(),它显然在 Python3 中被删除了。至少这是我发现的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'),'Ag')]
au = [(1,'Au'),'Au')]
ba = []
ca = [(1,'Ca'),'Ca')]
fe = [(1,'Fe'),'Fe')]
ti = [(1,'Ti'),'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,(1,'Ca')]

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

具有自定义排序键功能:

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'),'A'),'B'),'B')]

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

,

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

a = [(1,'A')]
b = [(1,'B')]
c = []
d = [(1,'D')]

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

否则,鉴于 OP 对我的评论的回答:

起点是几个单独的列表

我提出这个解决方案:

  • 用源列表构建一个嵌套列表;
  • 根据起点将列表旋转 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]

然后,例如旋转三圈以从 D 开始:

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