在给定并行列表的情况下,如何对其中一个进行排序并以同样的方式重新排列另一个?

238
假设我有:
list1 = [3, 2, 4, 1, 1]
list2 = ['three', 'two', 'four', 'one', 'one2']

调用 list1.sort() 会对其进行排序,结果为 [1, 1, 2, 3, 4]。但是,我能否让 list2 与之同步重新排列,以获得像这样的结果?
list1 = [1, 1, 2, 3, 4]
list2 = ['one', 'one2', 'two', 'three', 'four']

有时候,人们会以不同的方式表述问题:给定两个列表,他们想要使用一个列表来确定另一个列表的排序顺序——即,按照与list1中相应值描述的顺序对list2进行排序。诀窍在于这等价于对“键”值(list1)进行排序,然后以同样的方式重新排列list2。换句话说,就是完全按照此处所描述的方式操作。虽然其他问题的一些答案在此之后丢弃了“已排序的键”。
另请参见:如何按照另一个列表中的元素出现位置对列表进行排序? - 这是人们希望根据另一个列表对一个列表进行排序的另一种常见方式。在试图关闭重复问题之前,请特别注意检查OP想要什么。一个关键线索:这些列表需要具有相同的长度吗?

我应该指出,您在list2中的变量不指向list1中的整数。例如,如果更改像list1 [0] = 9这样的值并查看list2,list2 [0]仍将是3。在Python中使用整数时,它不使用引用/指针,而是复制该值。你最好使用list2 = list1[:]。 - Rusty Rob
16个回答

376

解决这个问题的一个经典方法是使用“装饰、排序、去装饰”的习惯用语,使用Python内置的zip函数尤其简单:

>>> list1 = [3,2,4,1, 1]
>>> list2 = ['three', 'two', 'four', 'one', 'one2']
>>> list1, list2 = zip(*sorted(zip(list1, list2)))
>>> list1
(1, 1, 2, 3, 4)
>>> list2 
('one', 'one2', 'two', 'three', 'four')

当然,这些已经不是列表了,但如果有必要,这很容易解决:

>>> list1, list2 = (list(t) for t in zip(*sorted(zip(list1, list2))))
>>> list1
[1, 1, 2, 3, 4]
>>> list2
['one', 'one2', 'two', 'three', 'four']

值得注意的是,上述方法可能会为简洁牺牲速度;就我个人而言,在我的计算机上,对于小型列表而言,占用3行的原地版本略微更快。
>>> %timeit zip(*sorted(zip(list1, list2)))
100000 loops, best of 3: 3.3 us per loop
>>> %timeit tups = zip(list1, list2); tups.sort(); zip(*tups)
100000 loops, best of 3: 2.84 us per loop

另一方面,对于更大的列表,单行版本可能更快:
>>> %timeit zip(*sorted(zip(list1, list2)))
100 loops, best of 3: 8.09 ms per loop
>>> %timeit tups = zip(list1, list2); tups.sort(); zip(*tups)
100 loops, best of 3: 8.51 ms per loop

作为Quantum7指出的,JSF的建议依然更快一些,但它可能只会比其稍微快一点,因为Python在所有基于键的排序中都使用完全相同的DSU习语。它只是发生在更接近底层的位置。(这表明zip例程是多么优化!)
我认为基于zip的方法更加灵活且更易读,所以我更喜欢它。
请注意,当list1的元素相等时,这种方法最终将比较list2的元素。如果list2的元素不支持比较,或者在进行比较时不产生布尔值(例如,如果list2是NumPy数组的列表),则此方法会失败,如果list2的元素非常昂贵,则最好避免比较。
在这种情况下,您可以按照jfs的答案建议对索引进行排序,或者您可以给排序一个键函数,该函数避免比较list2的元素:
result1, result2 = zip(*sorted(zip(list1, list2), key=lambda x: x[0]))

此外,当输入为空时,使用zip(*...)进行转置操作会失败。如果您的输入可能为空,您需要单独处理该情况。

7
第三行的星号代表什么? - Jeffrey
10
为了阐述上述内容,*运算符执行参数拆包 - senderle
1
J.F. Sebastian提出的排序索引/映射范例对我来说比使用10000个随机整数列表的任何zip解决方案都快大约10%:%timeit index = range(len(l1)); index.sort(key=l1.getitem); map(l1.getitem, index); map(l2.getitem, index)100次循环,3次中的最佳结果:每个循环8.04毫秒(与senderle的timits相比为9.17毫秒,9.07毫秒) - Quantum7
2
在list1和list2中,第一个和第二个zip = zip(sorted(zip(list1, list2)))做了不同的事情。起到了至关重要的作用。 - piedpiper
2
@ashu,在某种意义上,是的!但在另一方面,它们几乎没有什么不同。zip(*x)有一个有趣的特性,它是自己的逆运算:l = [(1, 2), (3, 4)]; list(zip(*zip(*l))) == l返回True。它实际上是一个转置运算符。zip()本身只是相同的运算符,但假定您已经手动解压了输入序列。 - senderle
显示剩余4条评论

52
您可以使用值作为键来对索引进行排序:
indexes = range(len(list1))
indexes.sort(key=list1.__getitem__)

# Or on Python 3, where range does not return a list
indexes = sorted(range(len(list1)), key=list1.__getitem__)

给定排序索引,获取排序列表。
sorted_list1 = map(list1.__getitem__, indexes)
sorted_list2 = map(list2.__getitem__, indexes)

# Python 3 version, converting map iterator to true list
sorted_list1 = list(map(list1.__getitem__, indexes))
sorted_list2 = list(map(list2.__getitem__, indexes))

在你的情况下,你不应该有list1list2,而是应该有一个由一对一对的元素组成的单个列表。
data = [(3, 'three'), (2, 'two'), (4, 'four'), (1, 'one'), (1, 'one2')]

创建它很容易;在Python中进行排序也很简单:
data.sort() # sort using a pair as a key

只按第一个值排序:
data.sort(key=lambda pair: pair[0])

1
这个很酷的地方在于我可以保留索引并稍后对其他内容进行排序,以防list1是影响其他几个数组的重要坐标。 - EL_DON
9
索引 = 列表(range(len(列表1))) 用于 Python 3。 - DonQuiKong
1
@DonQuiKong 如果你想在Python 3中使用这段代码,你还需要在map()周围加上list() - jfs
2
或者,可以使用sorted_list1 = [list1[i] for i in indexes]代替sorted_list1 = list(map(list1.__getitem__, indexes)) - Nathan
当你看到它时,你会发现它很明显,但我的大脑肯定没有想到这一点。 - Mark Rucker
1
@DonQuiKong:我编辑了一个类似的版本。由于你需要同时转换为listsort,在Python 3中创建indexes的版本可以一行完成,没有额外的开销(而在Python 2中,对range的结果使用sorted会产生一个不必要的临时list)。 - undefined

33

我长期以来一直使用senderle提供的答案,直到我发现np.argsort。下面是它的工作方式。

# idx works on np.array and not lists.
list1 = np.array([3,2,4,1])
list2 = np.array(["three","two","four","one"])
idx   = np.argsort(list1)

list1 = np.array(list1)[idx]
list2 = np.array(list2)[idx]

我认为这个解决方案更加直观,而且它的表现非常好。

def sorting(l1, l2):
    # l1 and l2 has to be numpy arrays
    idx = np.argsort(l1)
    return l1[idx], l2[idx]

# list1 and list2 are np.arrays here...
%timeit sorting(list1, list2)
100000 loops, best of 3: 3.53 us per loop

# This works best when the lists are NOT np.array
%timeit zip(*sorted(zip(list1, list2)))
100000 loops, best of 3: 2.41 us per loop

# 0.01us better for np.array (I think this is negligible)
%timeit tups = zip(list1, list2); tups.sort(); zip(*tups)
100000 loops, best for 3 loops: 1.96 us per loop

即使 np.argsort 不是最快的排序方法,但我觉得它更容易使用。


1
我在运行你的示例时遇到了一个错误:TypeError: only integer arrays with one element can be converted to an index(Python 2.7.6,numpy 1.8.2)。要解决这个问题,必须将list1和list2声明为numpy数组。 - BenB
谢谢。这不是我在函数注释中写的吗?无论如何,我认为np.argsort不尝试在内部转换为np.array很愚蠢。 - Daniel Thaagaard Andreasen
我指的是第一个代码片段,因为它不能按照原样运行 :) - BenB
我通过将列表转换为numpy数组来进行了更正。感谢您的评论 :) - Daniel Thaagaard Andreasen
现在它们被转换为Numpy数组两次 ;) - BenB
我称它为防弹,你可以叫它其他的名字 ;) 是的,只需要将列表转换为NumPy数组一次(最好在开始时),然后您就不必再担心它了。 - Daniel Thaagaard Andreasen

16
这可以使用 Perl 程序员称之为 Schwartzian transform 的方法来完成,也被称为 decorate-sort-undecorate 惯用语。内置的 Python 排序是稳定的,因此两个 1 不会造成问题。
>>> l1 = [3, 2, 4, 1, 1]
>>> l2 = ['three', 'two', 'four', 'one', 'second one']
>>> zip(*sorted(zip(l1, l2)))
[(1, 1, 2, 3, 4), ('one', 'second one', 'two', 'three', 'four')]

3
然而,如果你发现需要这样做,你应该强烈重新考虑是否要使用两个“平行”数据列表,而不是保留一个2元组(一对)列表,或者甚至创建一个类。 - Karl Knechtel

5
您可以使用zip()sort()函数来完成这个任务:
Python 2.6.5 (r265:79063, Jun 12 2010, 17:07:01)
[GCC 4.3.4 20090804 (release) 1] on cygwin
>>> list1 = [3,2,4,1,1]
>>> list2 = ['three', 'two', 'four', 'one', 'one2']
>>> zipped = zip(list1, list2)
>>> zipped.sort()
>>> slist1 = [i for (i, s) in zipped]
>>> slist1
[1, 1, 2, 3, 4]
>>> slist2 = [s for (i, s) in zipped]
>>> slist2
['one', 'one2', 'two', 'three', 'four']

希望这可以帮到您。

1
有其他人遇到过“AttributeError: 'zip' object has no attribute 'sort'”错误吗?我在想这个答案是否适用于Python的早期版本而不是当前版本。 - Non-Contradiction

5

一种方法是通过对身份 [0,1,2,...n] 进行排序来跟踪每个索引的位置。

这适用于任何数量的列表。

然后将每个项移动到其位置。使用拼接是最好的选择。

list1 = [3,2,4,1, 1]
list2 = ['three', 'two', 'four', 'one', 'one2']

index = list(range(len(list1)))
print(index)
'[0, 1, 2, 3, 4]'

index.sort(key = list1.__getitem__)
print(index)
'[3, 4, 1, 0, 2]'

list1[:] = [list1[i] for i in index]
list2[:] = [list2[i] for i in index]

print(list1)
print(list2)
'[1, 1, 2, 3, 4]'
"['one', 'one2', 'two', 'three', 'four']"

请注意,我们甚至可以在不排序列表的情况下迭代它们:
list1_iter = (list1[i] for i in index)

4

那么:

list1 = [3,2,4,1, 1]
list2 = ['three', 'two', 'four', 'one', 'one2']

sortedRes = sorted(zip(list1, list2), key=lambda x: x[0]) # use 0 or 1 depending on what you want to sort
>>> [(1, 'one'), (1, 'one2'), (2, 'two'), (3, 'three'), (4, 'four')]

4

如果你使用 numpy,可以使用 np.argsort 获取排序后的索引,然后将这些索引应用到列表中。这适用于任何你想要排序的列表。

import numpy as np

arr1 = np.array([4,3,1,32,21])
arr2 = arr1 * 10
sorted_idxs = np.argsort(arr1)

print(sorted_idxs)
>>> array([2, 1, 0, 4, 3])

print(arr1[sorted_idxs])
>>> array([ 1,  3,  4, 21, 32])

print(arr2[sorted_idxs])
>>> array([ 10,  30,  40, 210, 320])

2

如果列表2中没有相同的值,您可以在sorted()方法中使用key参数。

以下是代码:

sorted(list2, key = lambda x: list1[list2.index(x)]) 

它根据list1中相应的值对list2进行排序,但请确保在使用时,list2中没有两个值的评估结果相等,因为list.index()函数会给出第一个值。


sorted 在某些情况下虽然能够工作,但是速度较慢。 - user4985526

1
我想提供一个解决方案,如果您需要同步排序超过2个列表:
def SortAndSyncList_Multi(ListToSort, *ListsToSync):
    y = sorted(zip(ListToSort, zip(*ListsToSync)))
    w = [n for n in zip(*y)]
    return list(w[0]), tuple(list(a) for a in zip(*w[1]))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接