关于python:交换列表中元素的更好方法?

Better way to swap elements in a list?

我有一大堆列表,看起来像这样:

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

我想交换如下元素:

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

列表的大小可能会有所不同,但它们始终包含偶数个元素。

我对python比较陌生,目前正在这样做:

1
2
3
4
5
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])

我知道这不是真正的Python,我想用更有效的东西。也许是清单理解?


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

1
2
3
4
5
6
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;

带解释的编辑

我相信大多数观众已经熟悉了列表分割和多重分配。如果你不这样做,我会尽我最大的努力解释发生了什么(希望我不要让事情变得更糟)。

为了理解列表切片,这里已经有了一个很好的列表切片符号的答案和解释。简单地说:

1
2
3
4
5
6
7
8
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

让我们看看OP的要求:

1
2
3
4
5
6
7
8
 [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 = second&second = first,我们可以使用多个分配,并在适当的位置更新原始列表:

1
first , second  = second , first

即:

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

作为补充说明,为了得到一个新的列表而不改变原来的l,我们可以从l分配一个新的列表,并执行上面的操作,即:

1
2
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]

希望我不会把你们中的任何人与这个附加的解释混淆。如果有,请帮助更新我的并使其更好:-)


以下是一个单一的清单理解,它可以实现以下功能:

1
2
3
4
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]

理解这一点的关键在于以下演示如何排列列表索引:

1
2
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
2
3
4
>>> 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()避免额外的解包:

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


最佳答案之间的基准:

Python 2.7:

1
2
3
4
('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:

1
2
3
4
inp1 -> 7.913498195000102
inp2a -> 9.680125927000518
inp2b -> 4.728151862000232
inp3 -> 3.1804273489997286

如果您对python 2和3的不同性能感到好奇,那么原因如下:

正如你所看到的,@npe的答案(inp1在python3.4中表现得很好,原因是python3.x range()中是一个智能对象,并不像列表那样保留内存中该范围内的所有项。

In many ways the object returned by range() behaves as if it is a list, but in fact it isn’t. It is an object which returns the successive items of the desired sequence when you iterate over it, but it doesn’t really make the list, thus saving space.

这就是为什么在python 3中,在分割range对象时它不返回列表的原因。

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

第二个显著变化是第三种方法的性能增强(inp3)。如你所见,它和最后一种溶液之间的差别已经从约7秒减小到约2秒。原因是因为zip()函数在python3.x中返回一个迭代器,该迭代器按需生成项。而且由于chain.from_iterable()需要再次迭代这些项,因此在这之前进行迭代是完全多余的(在python 2中,zip所做的)。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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)))


另一种方法是创建嵌套列表,让成对的列表颠倒顺序,然后用itertools.chain.from_iterable将列表展平。

1
2
3
4
>>> 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的基准测试应用到我的解决方案中,我发现这个解决方案比其他顶级答案慢,所以我不推荐它出现在大列表中。不过,如果性能不是关键的,我仍然觉得这很可读。


使用chainlist comprehension的可能答案之一

1
2
3
>>> 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
2
3
4
5
6
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

这假设原始列表的项目数为偶数。如果没有,可以添加一个尝试例外:

1
2
3
4
5
6
7
8
9
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])

有趣的是,如果我们在更一般的范围内将"swap"解释为"reverse",那么可以将itertools.chain.from_iterable方法用于更长长度的子序列。

1
2
3
4
5
6
7
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
2
3
4
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

输出

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

使用numpy的方法

1
2
3
4
5
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())

我认为您的实现没有任何问题。但是你可以做一个简单的交换。

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

编辑显然,python有一种更好的方法来进行交换,这将使代码像这样

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


这里是一个基于modulo操作符的解决方案:

1
2
3
4
5
6
7
8
9
10
l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even = []
uneven = []
for i,item in enumerate(l):
    if i % 2 == 0:
        even.append(item)
    else:
        uneven.append(item)

list(itertools.chain.from_iterable(zip(uneven, even)))

1
newList = [(x[2*i+1], x[2*i]) for i in range(0, len(x)/2)]

现在找到一个解压元组的方法。我不会做你所有的家庭作业。