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

Posted

技术标签:

【中文标题】交换列表中元素的更好方法?【英文标题】:Better way to swap elements in a list? 【发布时间】:2017-01-03 04:06:39 【问题描述】:

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

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 不,我没有 [i for sub in [(l[i-1],l[i]) for i in xrange(1,len(l),2)] for i in sub] 【参考方案1】:

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

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;

已编辑说明

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

要了解列表切片,here 已经对列表切片表示法有一个很好的答案和解释。 简单地说:

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, 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,我们可以使用多次赋值,并原地更新原列表:

first , second  = second , first

即:

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

附带说明,要获得一个新列表但不改变原始l,我们可以从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]

希望我不会让你们中的任何人对这个附加的解释感到困惑。如果是这样,请帮助更新我的并使其变得更好:-)

【讨论】:

@MoinuddinQuadri,谢谢,这种多重分配方法将就地更新列表。如果您想要一个新列表,其他答案如 alexce 是要走的路,或者您可以先分配一个新列表,即。 a = l[:]; a[::2], a[1::2] = a[1::2], a[::2] 我仍然会在这段代码中添加注释。很难看。 @jpmc26,添加了一个解释——希望这不会让事情变得更糟 :-) 或者请帮助编辑解释,让其他人更清楚。 这也比大多数其他解决方案快得多(4 倍),对于我的 Core2Duo 上的 100k 个元素和 Python2.7(Ubuntu15.10 x86-64)。它在运行时不会进行任何 mmap/munmap 系统调用,因此它实际上是就地交换,而不是让解释器分配和释放暂存内存。 (Kasramvd 现在已删除的基准答案有一个错误:它对同一件事进行了 4 次计时。在我修复了该错误后,该代码很有用。希望他/她能将其取消删除,作为一个有用的速度测试比较答案。 ) 很棒的想法和简单,但很棒的技术。喜欢解决问题的方法。【参考方案2】:

这里有一个可以解决问题的列表理解:

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]

^ 是 exclusive or 运算符。 i^1 所做的只是翻转 i 的最低有效位,有效地将 0 与 1 交换,将 2 与 3 交换等等。

【讨论】:

这是打高尔夫球的代码,我同意 ☺。 “would”的错字,你必须是“wouldnt”的意思。唯一的问题是它不适用于奇数个列表条目 这真是太聪明了。【参考方案3】:

您可以使用pairwise iteration 并链接到flatten the list:

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

【讨论】:

我不太了解那里的*。我尝试了代码,我看看如果我不使用它会发生什么,我只是不明白它的含义。它只是标准语法吗? @GáborErdős 可能会使元组变平 @Ev.Kounis 如果* 将其变平,chain 会做什么? :) @GáborErdős 当然,这取决于chain() function 的工作原理。它需要多个迭代器,它会一个一个地迭代。我还添加了不需要解包的 from_iterable() 选项。 @alecxe,仅仅使用l[::2], l[1::2] = l[1::2], l[::2]不够吗?【参考方案4】:

最佳答案之间的基准:

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) 在 python3.4 中表现得非常好,原因是在 python3.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() 函数在 python3.X 中它返回一个迭代器,该迭代器根据需要生成项目。而且由于chain.from_iterable() 需要再次迭代这些项目,因此在此之前这样做也是完全多余的(zip 在 python 2 中所做的事情)。

代码:

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)))

【讨论】:

stmt=inp1 在所有四个块中。修复后,我看到 53.8s / 19.9s / 19.6s / 5.45s,所以你的方法实际上是最慢的 :(,而 @Anzel 是迄今为止最快的(Intel Core2Duo E6600 2.4GHz,python 2.7 from x86-64 Ubuntu 15.10). 你的时间 (同样的事情 4 次) 可能会反映Intel Turbo clock-speed boost during the first ~20 seconds of the first run, 然后在其余时间稳定到最大稳态时钟速度。你也没有说你测试的是什么硬件, 例如 Intel i5- 4570 @ 3.6GHz (Haswell) 与 DDR3-1600 RAM。 (取决于 Python 存储 100k 元素列表的效率,它可能不完全适合缓存,因此 mem perf 可能很重要。)有趣的是,对数组进行随机复制(或就地随机播放)整数是你可以在汇编语言中快速快速完成的事情,或者使用 SSE/AVX 内在函数的 C。加载一个 32 字节的向量,对带有 pshufd 的元素应用编译时间常数 shuffle,然后存储一个 32 字节的向量。这应该以与 memcpy 基本相同的速度运行。 @PeterCordes 这是一个可怕的错误,我绝对同意你对基准的看法,我认为仍然不需要我的解决方案,因为很明显我的代码将比其他代码花费更长的时间,我不知道为什么我在地球上使用了 2 个 for 循环(这意味着更多的解包、复杂性、调用、堆栈作业等),而我在我的回答中写道,其他人正在使用多次迭代、切片等。现在我认为唯一有趣的事情可能是这三个答案之间的基准。 不错的更新。我在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 倍。当然,它是如此非 Pythonic,以至于大多数人都不想使用它。 @PeterCordes 确实,这些就是我所说的智能优化,而 python 已经充满了这样的优化。【参考方案5】:

使用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)]

【讨论】:

【参考方案6】:

另一种方法是创建嵌套列表,其中对颠倒它们的顺序,然后使用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's benchmark test 应用于我的解决方案,我发现这个解决方案比其他最佳答案慢,所以我不推荐它用于大型列表。如果性能不重要,我仍然觉得这很容易阅读。

【讨论】:

我发现这比 *zip 解决方案更容易阅读(IE 立即明白它的作用)。【参考方案7】:

另一种简单重新分配和切片技术的方法

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]

【讨论】:

【参考方案8】:

为了好玩,如果我们在更一般的范围内将“交换”解释为“反向”,则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]

【讨论】:

【参考方案9】:

另一种选择:

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])

【讨论】:

【参考方案10】:

一种使用 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())

【讨论】:

【参考方案11】:

堆栈溢出的新功能。请随时对此解决方案发表评论或反馈。 交换 = [2, 1, 4, 3, 5]

lst = []
for index in range(len(swap)):
        if index%2 == 0 and index < len(swap)-1:
            swap[index],swap[index+1]  = swap[index+1],swap[index]
        lst.append(swap[index])
print(lst)


out = [1, 2, 3, 4, 5]

【讨论】:

【参考方案12】:

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

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 有一种更好的方法来进行交换,这会使代码像这样

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]

【讨论】:

在 Python 中交换不需要临时变量。此外,这并不比 OP 的解决方案更好。 @MorganThrapp 指出。我同意。 这就是你在 C 中编写它的方式,它使 C 程序员易于理解。不过,“手动”迭代列表元素被认为是丑陋的,我并不感到惊讶。它也比 Anzel 在我的 Core2Duo 上接受的答案慢 3 倍(但比其他一些建议快一点)。 (参见 Kasramvd 的已删除答案:使用包含 100k 个元素的列表测试 100 次)。【参考方案13】:
newList = [(x[2*i+1], x[2*i]) for i in range(0, len(x)/2)]

现在找到解压缩元组的方法。我不会做你所有的功课。

【讨论】:

作为奖励,如果有奇数个元素,最后一个被忽略...【参考方案14】:

这里有一个基于modulo 运算符的解决方案:

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)))

【讨论】:

以上是关于交换列表中元素的更好方法?的主要内容,如果未能解决你的问题,请参考以下文章

使用模式匹配交换列表中的元素对

在Python中交换两个元素[重复]

为啥列表元素不交换?

列表中的最小交换元素使其与另一个列表相同并计算python中的交换

如何替换通用列表中每个实例的交换 2 个属性?

交换数组两个数位置方法