以Pythonic的方式合并具有特定合并顺序的列表?

24

我想从两个列表yz构建列表x。 我希望所有来自y的元素都被放置在ypos指向的位置上。 例如:

y = [11, 13, 15]
z = [12, 14]
ypos = [1, 3, 5]

所以,x 必须是 [11, 12, 13, 14, 15]

另一个例子:

y = [77]
z = [35, 58, 74]
ypos = [3]
所以,x 必须是 [35, 58, 77, 74] 我已经写了一个可以满足我的要求的函数,但它看起来很丑:
def func(y, z, ypos):
    x = [0] * (len(y) + len(z))
    zpos = list(range(len(y) + len(z)))
    for i, j in zip(y, ypos):
        x[j-1] = i
        zpos.remove(j-1)
    for i, j in zip(z, zpos):
        x[j] = i
    return x

如何以Pythonic的方式编写它?


8
我猜你需要将这个问题发布到 https://codereview.stackexchange.com/ 上。 - Tigran Saluev
@TigranSaluev 更改名称并不会使其变得可以,所以我认为这并不是真正的解决办法。 - scharette
1
@martineau,抱歉,已修复。 - danielleontiev
@scharette他已经有可运行的代码,并希望改进其风格以更贴近一种语言。这不在SO的范围内。 - mascoj
6个回答

35
如果列表非常长,反复调用insert可能不太高效。或者,您可以从这些列表中创建两个迭代器,并通过从两个迭代器中获取下一个元素来构造列表,具体取决于当前索引是否在ypos(或其set)中:
>>> ity = iter(y)
>>> itz = iter(z)
>>> syp = set(ypos)
>>> [next(ity if i+1 in syp else itz) for i in range(len(y)+len(z))]
[11, 12, 13, 14, 15]

注意:这将按照y中的顺序插入元素,即y的第一个元素将插入到ypos最低索引处,而不一定是ypos第一个索引处。如果y的元素应该插入到ypos相应元素的索引处,则ypos必须按升序排列(即ypos的第一个索引也是最低的),或者y的迭代器必须按与ypos中的索引相同的顺序排序(之后,ypos本身不必排序,因为我们将其转换为set)。

>>> ypos = [5,3,1]   # y and z being same as above
>>> ity = iter(e for i, e in sorted(zip(ypos, y)))
>>> [next(ity if i+1 in syp else itz) for i in range(len(y)+len(z))]
[15, 12, 13, 14, 11]

2
优秀的方法。它模仿了你从两副牌中分发卡片的方式。 - Eric Duminil
可以等效地将 i+1 改为 i,并将范围更改为 range(1,len(y)+len(z)+1),因为这将消除 n-1i+1 操作,但这只是一个微小的优化问题。 - John B
2
起初可能不太清楚,但基本上你需要再次对ypos进行排序,因为你使用递增的i进行迭代。你可以尝试使用f([15, 13, 11], [12, 14], [5, 3, 1])。它返回[15, 12, 13, 14, 11],就好像ypos[1, 3, 5]一样。 - Eric Duminil
1
@EricDuminil 啊,现在我明白你的意思了。但是 ypos 不需要排序,相反,我的方法忽略了 ypos 的任何顺序,只是按照它们的顺序添加 y 中的元素,而不是在 ypos 中的“对应”位置。有趣的观点。 - tobias_k
1
确实。ypos 可以按任意顺序编写,但 y 应按排序后的 ypos 相同的顺序编写。 :) - Eric Duminil
显示剩余2条评论

12

您应该使用list.insert,这就是它的设计目的!

def func(y, z, ypos):
    x = z[:]
    for pos, val in zip(ypos, y):
        x.insert(pos-1, val)
    return x

和一个测试:

>>> func([11, 13, 15], [12, 14], [1,3,5])
[11, 12, 13, 14, 15]

1
由于问题是关于构建一个新列表,因此您应该复制 z,以便不修改原始列表。 - James
@C.Feenstra 这个问题假设它们是有序的。不可能知道它们应该如何排序(即我们只排序indexes还是将它们zip起来,然后排序),因此我认为这并非必要。但是,如果OP明确表明了他们在这方面的立场,那么我会更新答案 :) - Joe Iddon
是的,在 x 中的顺序必须像您的答案中一样。 - danielleontiev
如果 ypos 可能没有排序,你可以使用 sorted(ypos),即 for pos, val in zip(sorted(ypos), y): - Arthur Tacca
顺便提一下,这种方法对于数百个元素的列表是可以的。但是对于更大的列表,它会变得非常慢。 - Eric Duminil
显示剩余3条评论

8

当处理大型列表时,使用numpy可能是一个不错的选择。

算法

  • 创建一个与y+z一样大的新数组
  • 计算z值的坐标
  • ypos处将y值赋给x
  • zpos处将z值赋给x

复杂度应为O(n),其中n是值的总数。

import numpy as np

def distribute_values(y_list, z_list, y_pos):
    y = np.array(y_list)
    z = np.array(z_list)
    n = y.size + z.size
    x = np.empty(n, np.int)
    y_indices = np.array(y_pos) - 1
    z_indices = np.setdiff1d(np.arange(n), y_indices, assume_unique=True)
    x[y_indices] = y
    x[z_indices] = z
    return x

print(distribute_values([11, 13, 15], [12, 14], [1, 3, 5]))
# [11 12 13 14 15]
print(distribute_values([77], [35, 58, 74], [3]))
# [35 58 77 74]

作为额外的好处,当ypos未排序时,它也能正常工作。
print(distribute_values([15, 13, 11], [12, 14], [5, 3, 1]))
# [11 12 13 14 15]
print(distribute_values([15, 11, 13], [12, 14], [5, 1, 3]))
# [11 12 13 14 15]

性能

n 设置为 100 万时,这种方法比@tobias_k的答案稍快,并且比@Joe_Iddon的答案快 500 倍。

列表是按照以下方式创建的:

from random import random, randint
N = 1000000
ypos = [i+1 for i in range(N) if random()<0.4]
y = [randint(0, 10000) for _ in ypos]
z = [randint(0, 1000) for _ in range(N - len(y))

以下是使用%timeit和IPython得出的结果:

使用%timeit和IPython,结果如下:

%timeit eric(y, z, ypos)
131 ms ± 1.54 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

%timeit tobias(y, z, ypos)
224 ms ± 977 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

%timeit joe(y,z, ypos)
54 s ± 1.48 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

不错的时序分析,但我想说我的只需要多1.5倍的时间(实际上,我的分别是135和179毫秒)。 :-P 不过我很惊讶Joe的没有更慢,我本来以为它应该是二次的。 - tobias_k
2
@tobias_k:是的,这取决于系统和底层库。对于如此大的列表,1.5或2的因子并不多。我对Joe的回答感到惊讶,他的列表较小。例如,当n=100时,它实际上比你的要快一点。 - Eric Duminil

2

如果你希望将ypos中的元素放置在x索引处,其中每个元素在ypos中的索引应对应着相同y索引的元素:

  1. 使用所有空值初始化所需大小的x
  2. 遍历压缩的yypos元素,将每个相应的y元素填入x中。
  3. 遍历x并用z值替换每个剩余的空值,其中每个替换都将从递增的z中选择。

y = [11, 13, 15]
z = [12, 14]
ypos = [1, 5, 3]

x = [None] * (len(y) + len(z))
for x_ypos, y_elem in zip(ypos, y):
    x[x_ypos - 1] = y_elem

z_iter = iter(z)
x = [next(z_iter) if i is None else i for i in x]
# x -> [11, 12, 15, 14, 13]

2
假设 ypos 索引已排序,这里有另一种使用迭代器的解决方案,虽然这个解决方案也支持未知或无限长度的 ypos
import itertools

def func(y, ypos, z):
    y = iter(y)
    ypos = iter(ypos)
    z = iter(z)
    next_ypos = next(ypos, -1)
    for i in itertools.count(start=1):
        if i == next_ypos:
            yield next(y)
            next_ypos = next(ypos, -1)
        else:
            yield next(z)

1

Pythonic way

y = [11, 13, 15]
z = [12, 14]
ypos = [1, 3, 5]

x = z[:]

for c, n in enumerate(ypos):
    x.insert(n - 1, y[c])

print(x)

输出

[11, 12, 13, 14, 15]

在一个函数中

def func(y, ypos, z):
    x = z[:]
    for c,n in enumerate(ypos):
        x.insert(n-1,y[c])
    return x

print(func([11,13,15],[1,2,3],[12,14]))

outoput

[11, 12, 13, 14, 15]

使用zip函数

y, z, ypos = [11, 13, 15], [12, 14], [1, 3, 5]

for i, c in zip(ypos, y):
    z.insert(i - 1, c)

print(z)

[输出:]

> [11, 12, 13, 14, 15]

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