合并两个重叠列表的Pythonic方法,保留顺序

Posted

技术标签:

【中文标题】合并两个重叠列表的Pythonic方法,保留顺序【英文标题】:Pythonic way to merge two overlapping lists, preserving order 【发布时间】:2015-07-15 08:32:21 【问题描述】:

好的,所以我有两个列表,如下所示:

他们可以并且将会有重叠的项目,例如,[1, 2, 3, 4, 5][4, 5, 6, 7]。 重叠中不会有其他项目,例如,这不会发生:[1, 2, 3, 4, 5], [3.5, 4, 5, 6, 7] 列表不一定是有序的,也不一定是唯一的。 [9, 1, 1, 8, 7][8, 6, 7]

我想合并列表以保留现有顺序,并在最后一个可能的有效位置进行合并,并且不会丢失任何数据。此外,第一个列表可能很大。我目前的工作代码是这样的:

master = [1,3,9,8,3,4,5]
addition = [3,4,5,7,8]

def merge(master, addition):
    n = 1
    while n < len(master):
        if master[-n:] == addition[:n]:
            return master + addition[n:]
        n += 1
    return master + addition

我想知道的是 - 有没有更有效的方法来做到这一点?它可以工作,但我对此有点怀疑,因为它可能会在我的应用程序中运行到大型运行时 - 我正在合并大量字符串。

编辑:我希望 [1,3,9,8,3,4,5],[3,4,5,7,8] 的合并为:[1,3,9,8 ,3,4,5,7,8]。为清楚起见,我突出显示了重叠部分。

[9, 1, 1, 8, 7], [8, 6, 7] 应该合并到 [9, 1, 1, 8, 7, 8, 6, 7]

【问题讨论】:

您展示的案例的预期输出是什么? master=master+addition 在第三个示例输入中,那些不重叠,那里的输出应该是什么。同样在您的代码的 sn-p 中,这些重叠在哪里? @thefourtheye,我已经编辑了预期的输出。 @Ale 我已经编辑了答案。 【参考方案1】:

您可以尝试以下方法:

>>> a = [1, 3, 9, 8, 3, 4, 5]
>>> b = [3, 4, 5, 7, 8]

>>> matches = (i for i in xrange(len(b), 0, -1) if b[:i] == a[-i:])
>>> i = next(matches, 0)
>>> a + b[i:]
[1, 3, 9, 8, 3, 4, 5, 7, 8]

我们的想法是我们检查b (b[:i]) 的第一个i 元素和a (a[-i:]) 的最后一个i 元素。我们以降序取i,从b 的长度开始直到1 (xrange(len(b), 0, -1)),因为我们希望尽可能匹配。我们使用next 获取第一个这样的i,如果找不到,我们使用零值(next(..., 0))。从我们找到i 的那一刻起,我们将索引i 中的b 的元素添加到a

【讨论】:

@TankorSmash 我不知道 python,但如果你可以添加 cmets 那么你可以让它可读:P. 哎呀,真是一团糟!在没有意识到的情况下,我显然重新实现了这个解决方案,使其在我的回答中更具可读性。伟大的思想都一样,看来 JuniorCompressor :) @TankorSmash,添加一些中间变量(我的编辑)通过降低语法树的最大高度来改进它。恕我直言,这是最干净的,虽然不是最优的算法。【参考方案2】:

有几个简单的优化是可能的。

    您不需要从 master[1] 开始,因为最长的重叠从 master[-len(addition)] 开始

    如果您添加对list.index 的调用,您可以避免为每个索引创建子列表和比较列表:

这种方法使代码也很容易理解(并且使用 cython 或 pypy 更容易优化):

master = [1,3,9,8,3,4,5]
addition = [3,4,5,7,8]

def merge(master, addition):
    first = addition[0]
    n = max(len(master) - len(addition), 1)  # (1)
    while 1:
        try:
            n = master.index(first, n)       # (2)
        except ValueError:
            return master + addition

        if master[-n:] == addition[:n]:
            return master + addition[n:]
        n += 1

【讨论】:

他不使用master[1]。 他的迭代以n = 1 开始,所以除非我弄错了,否则他会开始尝试在master[1] 找到匹配项... n 从另一边开始计数。【参考方案3】:

这实际上并不太难。毕竟,基本上你所做的只是检查 A 末尾的哪个子字符串与 B 的哪个子字符串对齐。

def merge(a, b):
    max_offset = len(b)  # can't overlap with greater size than len(b)
    for i in reversed(range(max_offset+1)):
        # checks for equivalence of decreasing sized slices
        if a[-i:] == b[:i]:
            break
    return a + b[i:]

我们可以通过以下方式使用您的测试数据进行测试:

test_data = ['a': [1,3,9,8,3,4,5], 'b': [3,4,5,7,8], 'result': [1,3,9,8,3,4,5,7,8],
             'a': [9, 1, 1, 8, 7], 'b': [8, 6, 7], 'result': [9, 1, 1, 8, 7, 8, 6, 7]]

all(merge(test['a'], test['b']) == test['result'] for test in test_data)

这会遍历所有可能导致重叠的切片组合,并在找到重叠的结果时记住重叠的结果。如果没有找到,它将使用i 的最后一个结果,它始终是0。无论哪种方式,它都会返回所有 a 以及所有超过 b[i] 的内容(在重叠的情况下,这是不重叠的部分。在不重叠的情况下,它就是一切)

请注意,我们可以在极端情况下进行一些优化。例如,这里最坏的情况是它遍历整个列表而没有找到任何解决方案。您可以在开始时添加一个快速检查,这可能会使最坏的情况短路

def merge(a, b):
    if a[-1] not in b:
        return a + b
    ...

事实上,您可以将该解决方案更进一步,并可能使您的算法更快

def merge(a, b):
    while True:
        try:
            idx = b.index(a[-1]) + 1  # leftmost occurrence of a[-1] in b
        except ValueError:  # a[-1] not in b
            return a + b
        if a[-idx:] == b[:idx]:
            return a + b[:idx]

但是,在以下情况下,这可能找不到最长的重叠:

a = [1,2,3,4,1,2,3,4]
b = [3,4,1,2,3,4,5,6]
# result should be [1,2,3,4,1,2,3,4,5,6], but
# this algo produces [1,2,3,4,1,2,3,4,1,2,3,4,5,6]

您可以使用rindex 而不是index 来匹配最长切片而不是最短切片,但我不确定这对您的速度有何影响。它当然更慢,但它可能无关紧要。您还可以记住结果并返回最短的结果,这可能是一个更好的主意。

def merge(a, b):
    results = []
    while True:
        try:
            idx = b.index(a[-1]) + 1  # leftmost occurrence of a[-1] in b
        except ValueError:  # a[-1] not in b
            results.append(a + b)
            break
        if a[-idx:] == b[:idx]:
            results.append(a + b[:idx])
    return min(results, key=len)

这应该可行,因为合并最长的重叠应该在所有情况下产生最短的结果。

【讨论】:

我选择这个作为答案,因为它提供了额外的清晰度,但是对于一个非常紧凑和聪明(如果更难阅读)的解决方案,请查看here。【参考方案4】:

一个微不足道的优化不是迭代整个master 列表。即,用for n in range(min(len(addition), len(master))) 替换while n &lt; len(master)(并且不要在循环中增加n)。如果没有匹配,您当前的代码将遍历整个 master 列表,即使被比较的切片长度不同。

另一个问题是,您需要对masteraddition 进行切片来比较它们,这每次都会创建两个新列表,而且实际上并没有必要。此解决方案(受Boyer-Moore 启发)不使用切片:

def merge(master, addition):
    overlap_lens = (i + 1 for i, e in enumerate(addition) if e == master[-1])
    for overlap_len in overlap_lens:
        for i in range(overlap_len):
            if master[-overlap_len + i] != addition[i]:
                break
        else:
            return master + addition[overlap_len:]
    return master + addition

这里的想法是在addition中生成master的最后一个元素的所有索引,并在每个索引中添加1。由于有效的重叠必须以master 的最后一个元素结束,因此只有这些值是可能重叠的长度。然后我们可以检查它们中的每一个,如果它之前的元素也排成一行。

该函数当前假定masteraddition 长(如果不是,您可能会在master[-overlap_len + i] 处获得IndexError)。如果不能保证,请在overlap_lens 生成器中添加条件。

它也是非贪婪的,即它寻找最小的非空重叠(merge([1, 2, 2], [2, 2, 3]) 将返回 [1, 2, 2, 2, 3])。我认为这就是您所说的“在最后一个可能的有效位置合并”的意思。如果你想要一个贪婪的版本,反转overlap_lens 生成器。

【讨论】:

【参考方案5】:

我不提供优化,而是以另一种方式看待问题。对我来说,这似乎是http://en.wikipedia.org/wiki/Longest_common_substring_problem 的一个特例,其中子字符串始终位于列表/字符串的末尾。以下算法是动态规划版本。

def longest_common_substring(s1, s2):
    m = [[0] * (1 + len(s2)) for i in xrange(1 + len(s1))]
    longest, x_longest = 0, 0
    for x in xrange(1, 1 + len(s1)):
        for y in xrange(1, 1 + len(s2)):
            if s1[x - 1] == s2[y - 1]:
                m[x][y] = m[x - 1][y - 1] + 1
                if m[x][y] > longest:
                    longest = m[x][y]
                    x_longest = x
            else:
                m[x][y] = 0
    return x_longest - longest, x_longest

master = [1,3,9,8,3,4,5]
addition = [3,4,5,7,8]
s, e = longest_common_substring(master, addition)
if e - s > 1:
    print master[:s] + addition

master = [9, 1, 1, 8, 7]
addition = [8, 6, 7]
s, e = longest_common_substring(master, addition)
if e - s > 1:
    print master[:s] + addition
else:
    print master + addition

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

【讨论】:

但他正在寻找最短的,而不是最长的。 @NeilG 似乎它实际上是最长的。见***.com/questions/30055830/…。 @Veedrac 然后他自相矛盾:“我想合并列表以保留现有顺序,并在最后可能的有效位置合并”......【参考方案6】:

首先,为了清楚起见,您可以将 while 循环替换为 for 循环:

def merge(master, addition):
    for n in xrange(1, len(master)):
        if master[-n:] == addition[:n]:
            return master + addition[n:]
    return master + addition

然后,您不必比较所有可能的切片,而只需比较master 的切片以addition 的第一个元素开头的那些:

def merge(master, addition):
    indices = [len(master) - i for i, x in enumerate(master) if x == addition[0]]
    for n in indices:
        if master[-n:] == addition[:n]:
            return master + addition[n:]
    return master + addition

所以不要像这样比较切片:

1234123141234
            3579
           3579
          3579
         3579
        3579
       3579
      3579
     3579
    3579
   3579
  3579
 3579
3579

你只是在做这些比较:

1234123141234
  |   |    |
  |   |    3579
  |   3579
  3579

这将在多大程度上加快您的程序取决于您的数据的性质:您的列表中的重复元素越少越好。

您还可以为addition 生成一个索引列表,这样它自己的切片总是以master 的最后一个元素结尾,进一步限制了比较次数。

【讨论】:

切片比较无论如何都会提前结束,所以这不是优化。 @NeilG 不过,您仍然可以分片,即O(n) 如果您使用 numpy 数组而不是列表,那么您将获得现有数组的视图。【参考方案7】:

基于https://***.com/a/30056066/541208:

def join_two_lists(a, b):
  index = 0
  for i in xrange(len(b), 0, -1):
    #if everything from start to ith of b is the 
    #same from the end of a at ith append the result
    if b[:i] == a[-i:]:
        index = i
        break

  return a + b[index:]

【讨论】:

【参考方案8】:

上述所有解决方案在使用 for / while 循环进行合并任务方面都是相似的。我首先尝试了@JuniorCompressor 和@TankorSmash 的解决方案,但是这些解决方案对于合并两个大型列表(例如具有大约数百万个元素的列表)来说太慢了。

我发现使用 pandas 连接大尺寸列表更省时:

import pandas as pd, numpy as np

trainCompIdMaps = pd.DataFrame(  "compoundId": np.random.permutation( range(800) )[0:80], "partition": np.repeat( "train", 80).tolist() )

testCompIdMaps = pd.DataFrame( "compoundId": np.random.permutation( range(800) )[0:20], "partition": np.repeat( "test", 20).tolist() )

# row-wise concatenation for two pandas
compoundIdMaps = pd.concat([trainCompIdMaps, testCompIdMaps], axis=0)

mergedCompIds = np.array(compoundIdMaps["compoundId"])

【讨论】:

【参考方案9】:

您需要的是像 Needleman-Wunsch 这样的序列比对算法。

Needleman-Wunsch 是一种基于动态规划的全局序列比对算法:

我发现这个很好的实现可以在 python 中合并任意对象序列: https://github.com/ajnisbet/paired

import paired

seq_1 = 'The quick brown fox jumped over the lazy dog'.split(' ')
seq_2 = 'The brown fox leaped over the lazy dog'.split(' ')
alignment = paired.align(seq_1, seq_2)

print(alignment)
# [(0, 0), (1, None), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6), (8, 7)]

for i_1, i_2 in alignment:
    print((seq_1[i_1] if i_1 is not None else '').ljust(15), end='')
    print(seq_2[i_2] if i_2 is not None else '')

# The            The
# quick          
# brown          brown
# fox            fox
# jumped         leaped
# over           over
# the            the
# lazy           lazy
# dog            dog

【讨论】:

以上是关于合并两个重叠列表的Pythonic方法,保留顺序的主要内容,如果未能解决你的问题,请参考以下文章

Stone 3D教程:常用的可建构实体造型功能(合并相交和相减)

Stone 3D教程:常用的可建构实体造型功能(合并相交和相减)

Pythonic找到重叠元素的方法

从嵌套字典结构列表(具有两个级别)创建数据框的 Pythonic 方法是啥?

寻找一种更 Pythonic 的方式将一个充满字典的列表合并为一个?

合并两个具有列表的数据集并在合并后使用 pandas 保留列表