更好的在列表中交换元素的方法?

49

我有一堆看起来像这个的列表:

l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

我想按以下方式交换元素:

final_l = [2, 1, 4, 3, 6, 5, 8, 7, 10, 9]
  • 列表的大小可能会有所不同,但它们始终包含偶数个元素。
  • 我对Python还比较新,目前我的写法是这样的:
l =  [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
final_l = []
for i in range(0, len(l)/2):
    final_l.append(l[2*i+1])
    final_l.append(l[2*i])

我知道这不是很符合Pythonic的风格,想使用更高效的方法。也许可以用列表推导式?


你需要保持原始列表不变吗? - DeepSpace
@DeepSpace 不,我不是。 - EliseB
返回翻译文本:[i for sub in [(l[i-1],l[i]) for i in xrange(1,len(l),2)] for i in sub] - Abhijay Ghildyal
14个回答

113

不需要复杂的逻辑,只需使用切片和步长重新排列列表:

In [1]: l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

In [2]: l[::2], l[1::2] = l[1::2], l[::2]

In [3]: l
Out[3]: [2, 1, 4, 3, 6, 5, 8, 7, 10, 9]

TLDR;

编辑并解释

我相信大多数的读者已经熟悉了列表切片和多重赋值。但是如果你还没有,我会尽力解释(希望我不会让它更糟)。

要理解列表切片,这里已经有了一个非常好的回答和列表切片符号的解释。简而言之:

a[start:end] # items start through end-1
a[start:]    # items start through the rest of the array
a[:end]      # items from the beginning through end-1
a[:]         # a copy of the whole array

There is also the step value, which can be used with any of the above:

a[start:end:step] # start through not past end, by step

让我们来看一下原帖发布者的要求:

 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]  # list l
  ^  ^  ^  ^  ^  ^  ^  ^  ^  ^
  0  1  2  3  4  5  6  7  8  9    # respective index of the elements
l[0]  l[2]  l[4]  l[6]  l[8]      # first tier : start=0, step=2
   l[1]  l[3]  l[5]  l[7]  l[9]   # second tier: start=1, step=2
-----------------------------------------------------------------------
l[1]  l[3]  l[5]  l[7]  l[9]
   l[0]  l[2]  l[4]  l[6]  l[8]   # desired output

第一层将是:l[::2] = [1, 3, 5, 7, 9]

第二层将是:l[1::2] = [2, 4, 6, 8, 10]

由于我们想要重新分配first = secondsecond = first,我们可以使用多个赋值,并直接在原始列表中更新:

first , second  = second , first

就是说:

l[::2], l[1::2] = l[1::2], l[::2]

顺便提一下,如果想要得到一个新的列表但不改变原来的l,我们可以从l中赋值一个新的列表,然后执行上述操作,即:

如果要得到新列表,可以用 list(l) 复制一份新列表进行操作。

n = l[:]  # assign n as a copy of l (without [:], n still points to l)
n[::2], n[1::2] = n[1::2], n[::2]

希望我的补充解释没有让你们感到困惑。如果有的话,请帮忙更新并改进它 :-)


2
@MoinuddinQuadri,感谢您的回答。这种多重赋值方法将会就地更新列表。如果您需要一个新的列表,可以选择像alexce提供的其他答案,或者您可以先赋一个新的列表,例如:a = l[:]; a[::2], a[1::2] = a[1::2], a[::2] - Anzel
2
我还会在这段代码中添加一条注释。看起来非常困难。 - jpmc26
1
@jpmc26,已添加解释 - 希望这不会让情况变得更糟 :-) 或者请帮忙编辑解释并使其对其他人更清晰易懂。 - Anzel
2
这也比大多数其他解决方案快得多(4倍),在我的Core2Duo上使用Python2.7(Ubuntu15.10 x86-64)处理100k个元素时如此。它在运行时不会进行任何mmap/munmap系统调用,因此实际上是原地交换而不是使解释器分配和释放临时内存。(Kasramvd的已删除答案在基准测试中有一个错误:它四次计时了同一件事情。修复了该错误后,代码非常有用。希望他/她能将其作为有用的速度测试比较答案重新发布。) - Peter Cordes
1
思路棒极了,技术简单却妙不可言。喜欢解决问题的方式。 - be_good_do_good
1
必须得说,这让我大开眼界。我正在进行像素交换(RGBA -> BGRA),通过从列表中选择四元组,然后执行“newlist.extend(quad)”来完成,但速度非常慢(仅在执行上百万次时会如此)。我切换到了“pixel_data [:: 4],pixel_data [2 :: 4] = pixel_data [2 :: 4],pixel_data [:: 4]”,速度增加了一个数量级。也许更多,因为之后的分析器上甚至没有显示出来。看来我需要更深入地研究切片,没想到它可以这样做。 - John C

36

这里有一个单行列表推导式可以完成这个任务:

In [1]: l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

In [2]: [l[i^1] for i in range(len(l))]
Out[2]: [2, 1, 4, 3, 6, 5, 8, 7, 10, 9]
理解它的关键在于以下展示,它是如何对列表索引进行置换的:
In [3]: [i^1 for i in range(10)]
Out[3]: [1, 0, 3, 2, 5, 4, 7, 6, 9, 8]

^异或运算符。 i^1的作用只是翻转i的最低有效位,从而有效地交换0和1、2和3等。


1
这是代码高尔夫,我批准 ☺。 "would" 有错别字,你应该是指 "wouldn't"。唯一的问题是它不能处理列表条目为奇数的情况。 - Ma0
2
这真是聪明的。 - laike9m
我对Python还很陌生,但是In [1]和Out [2]是什么意思?为什么需要它,这种语法有名称吗? - Jumpman

20

您可以使用成对迭代和链式方法展平列表:

>>> from itertools import chain
>>>
>>> list(chain(*zip(l[1::2], l[0::2])))
[2, 1, 4, 3, 6, 5, 8, 7, 10, 9]

或者,您可以使用itertools.chain.from_iterable()来避免额外的解包:

>>> list(chain.from_iterable(zip(l[1::2], l[0::2])))
[2, 1, 4, 3, 6, 5, 8, 7, 10, 9]

1
我不太理解那里的 *。我尝试了一下代码,如果我不使用它,我知道会发生什么,但我只是不理解它的含义。这只是标准语法吗? - Gábor Erdős
@GáborErdős 可能会将元组展平。 - Ma0
@Ev.Kounis 如果 * 是扁平化操作,那么 chain 又是什么呢? :) - Gábor Erdős
@GáborErdős 当然,这取决于chain()函数的工作方式。它需要多个可迭代对象,逐个迭代它们。我还添加了from_iterable()选项,它不需要解包。 - alecxe
1
@alecxe,仅使用l[::2],l[1::2] = l[1::2],l[::2]不就足够了吗? - Anzel
显示剩余2条评论

11

顶尖答案间的基准测试:

Python 2.7:

('inp1 ->', 15.302665948867798) # NPE's answer 
('inp2a ->', 10.626379013061523) # alecxe's answer with chain
('inp2b ->', 9.739919185638428) # alecxe's answer with chain.from_iterable
('inp3 ->', 2.6654279232025146) # Anzel's answer

Python 3.4:

inp1 -> 7.913498195000102
inp2a -> 9.680125927000518
inp2b -> 4.728151862000232
inp3 -> 3.1804273489997286

如果你对比Python 2和3的性能差异感到好奇,这里是原因:

正如你所看到@NPE的回答(inp1)在Python 3.4中表现得更好,原因是在Python 3.X range() 是一个智能对象,不像列表那样在内存中保留范围内的所有项。

range() 返回的对象在许多方面行为类似于列表,但实际上并不是列表。当您迭代它时,它会返回所需序列的连续项,但它不会真正地创建列表,从而节省空间。

这就是为什么在Python 3中切片范围对象时不返回列表的原因。

# python2.7
>>> range(10)[2:5]
[2, 3, 4]
# python 3.X
>>> range(10)[2:5]
range(2, 5)

第二个重大变化是第三种方法(inp3)的性能提升。正如您所看到的,它与上一个解决方案之间的差异已经降至约2秒(从约7秒)。原因是由于 zip() 函数,在Python 3.X中它返回一个按需生成项目的迭代器。而且既然 chain.from_iterable() 需要再次遍历项目,那么在此之前也完全没有必要这样做(这就是Python 2中的zip所做的)。

代码:

from timeit import timeit


inp1 = """
[l[i^1] for i in range(len(l))]
   """
inp2a = """
list(chain(*zip(l[1::2], l[0::2])))
"""
inp2b = """
list(chain.from_iterable(zip(l[1::2], l[0::2])))
"""
inp3 = """
l[::2], l[1::2] = l[1::2], l[::2]
"""

lst = list(range(100000))
print('inp1 ->', timeit(stmt=inp1,
                        number=1000,
                        setup="l={}".format(lst)))
print('inp2a ->', timeit(stmt=inp2a,
                        number=1000,
                        setup="l={}; from itertools import chain".format(lst)))
print('inp2b ->', timeit(stmt=inp2b,
                        number=1000,
                        setup="l={}; from itertools import chain".format(lst)))
print('inp3 ->', timeit(stmt=inp3,
                        number=1000,
                        setup="l={}".format(lst)))

1
在所有四个块中,stmt=inp1 都需要修复。修复后,我看到的时间是53.8秒/19.9秒/19.6秒/5.45秒,所以你的方法实际上是最慢的 :(,而@Anzel的方法则是迄今为止最快的(Intel Core2Duo E6600 2.4GHz,来自x86-64 Ubuntu 15.10的python 2.7)。你的时间(对于同样的事情4次)可能反映了第一次运行前约20秒的英特尔Turbo时钟速度提升,然后在其余时间内稳定在最大稳态时钟速度。你也没有说你测试的硬件是什么,例如Intel i5-4570 @ 3.6GHz (Haswell) with DDR3-1600 RAM。 - Peter Cordes
根据Python存储100k元素列表的效率,它可能无法完全适应缓存,因此内存性能可能很重要。有趣的是,制作一个整数数组的洗牌副本(或原地洗牌)是您可以在汇编语言或具有SSE / AVX内部函数的C中极快速完成的操作。加载32字节的向量,使用pshufd对元素应用编译时常量洗牌,然后存储32字节的向量。这应该以与memcpy基本相同的速度运行。 - Peter Cordes
@PeterCordes 那是一个可怕的错误,我绝对同意您关于基准测试的观点,但我认为没有必要使用我的解决方案,因为很明显我的代码会比其他解决方案慢,我不知道为什么我要使用2个循环(这意味着更多的拆包、复杂性、调用、堆栈作业等),而我在回答中写道其他人正在使用多次迭代、切片等,现在我认为唯一有趣的事情可能是这三个答案之间的基准测试。 - Mazdak
2
不错的更新。我在strace下运行了这些代码,并注意到Anzel的版本与其他版本不同,在运行时没有产生mmap/munmap调用。因此,它是在原地操作而不是分配和销毁临时副本。这就是为什么它要快得多的部分原因。我还计时了Matthias答案中类似C语言的for i in range(0, len(l), 2): l[i], l[i+1] = l[i+1], l[i],发现它比NPE或alexce的要快,但比Anzel的慢3倍。当然,它非常不符合Python风格,大多数人都不想使用它。 - Peter Cordes
@PeterCordes 确实,这些被我称为智能优化,而 Python 则充满了这样的优化。 - Mazdak
很好的基准测试,如果有人感兴趣,我用下面的解决方案(inp4)重复了一遍基准测试,但我发现我的是最慢的,这里是我得到的时间:inp1 -> 7.8195558990119025 inp2a -> 5.318918139004381 inp2b -> 3.2198629069898743 inp3 -> 1.284413435991155 inp4 -> 9.924097775976406 - Chris_Rands

3

另一种方法是用相反的顺序创建嵌套列表对,然后使用 itertools.chain.from_iterable 将这些列表扁平化。

>>> from itertools import chain
>>> l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> list(chain.from_iterable([[l[i+1],l[i]] for i in range(0,(len(l)-1),2)]))
[2, 1, 4, 3, 6, 5, 8, 7, 10, 9]

编辑:我刚应用了Kasramvd的基准测试到我的解决方案中,发现这个解决方案比其他顶级答案慢,所以我不建议在大列表中使用它。如果性能不是关键问题,我仍然觉得这个解决方案很易读。


1
我觉得这比使用“zip”解决方案更容易阅读(即可以立即理解其作用)。 - GreenAsJade

3

使用chainlist comprehension的可能答案之一:

>>> l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> list(chain([(l[2*i+1], l[2*i]) for i in range(0, len(l)/2)]))
[(2, 1), (4, 3), (6, 5), (8, 7), (10, 9)]

1

另一种方法是通过重新赋值和切片技术来实现

l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
for a in range(0,len(l),2):
    l[a:a+2] = l[a-len(l)+1:a-1-len(l):-1]
print l

输出

[2, 1, 4, 3, 6, 5, 8, 7, 10, 9]

1

如果我们将“swap”在更广泛的范围内解释为“反转”,那么可以使用itertools.chain.from_iterable方法来处理更长长度的子序列。

l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

def chunk(list_, n):
    return (list_[i:i+n] for i in range(0, len(list_), n))

list(chain.from_iterable(reversed(c) for c in chunk(l, 4)))
# [4, 3, 2, 1, 8, 7, 6, 5, 10, 9]

1
另一种选择:
final_l = list() # make an empty list
for i in range(len(l)): # for as many items there are in the original list
    if i % 2 == 0: # if the item is even
        final_l.append(l[i+1]) # make this item in the new list equal to the next in the original list
    else: # else, so when the item is uneven
        final_l.append(l[i-1]) # make this item in the new list equal to the previous in the original list

假设原始列表中有偶数项。如果不是,则可以添加 try-except
final_l = list()
for i in range(len(l)):
    if i % 2 == 0:
        try: # try if we can add the next item
            final_l.append(l[i+1])
        except:  # if we can't (because i+1 doesnt exist), add the current item
            final_l.append(l[i])
    else:
            final_l.append(l[i-1])

0
使用Numpy的一种方法
import numpy as np

l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
l = np.array(l)
final_l = list(np.flip(l.reshape(len(l)//2,2), 1).flatten())

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