web-dev-qa-db-ja.com

順序を維持しながら、2つの重複するリストをマージするPythonの方法

了解しました。2つのリストがあります。

  • [1, 2, 3, 4, 5][4, 5, 6, 7]など、重複するアイテムを持つことができます。
  • オーバーラップにはnotの追加アイテムはありません。たとえば、これはnotになります。 -)発生:[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、- ,4,5、7,8]。わかりやすくするために、重複部分を強調表示しました。

[9、1、1、8、7]、[8、6、7]は[9、1、1、8、7、8、6、7]にマージする必要があります

27
Firnagzen

これは実際にはそれほど難しいことではありません。結局のところ、基本的にあなたがしているのは、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)

最長のオーバーラップをマージすると、すべての場合で最短の結果が生成されるため、どちらが機能するはずです。

5
Adam Smith

次のことを試すことができます。

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

アイデアは、ib[:i])の最初のb要素をia[-i:])の最後のa要素でチェックすることです。 iは、可能な限り一致させたいので、bの長さから1(xrange(len(b), 0, -1))まで降順で取得します。 iを使用して最初のそのようなnextを取得し、それが見つからない場合はゼロ値(next(..., 0))を使用します。 iを見つけた瞬間から、インデックスaからbの要素をiに追加します。

17

可能な最適化がいくつかあります。

  1. 最長のオーバーラップはmaster [-len(addition)]で始まるため、master [1]で開始する必要はありません。

  2. 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
9
thebjorn

些細な最適化の1つは、masterリスト全体を反復することではありません。つまり、while n < len(master)for n in range(min(len(addition), len(master)))に置き換えます(ループ内でnをインクリメントしないでください)。一致するものがない場合、比較対象のスライスの長さが同じでなくても、現在のコードはmasterリスト全体を反復処理します。

もう1つの懸念は、masteradditionを比較するためにスライスを取得していることです。これにより、毎回2つの新しいリストが作成され、実際には必要ありません。このソリューション( 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
_

ここでの考え方は、masteradditionの最後の要素のすべてのインデックスを生成し、それぞれに_1_を追加することです。有効なオーバーラップはmasterの最後の要素で終了する必要があるため、これらの値のみが可能なオーバーラップの長さです。次に、前の要素も並んでいるかどうかを確認できます。

この関数は現在、masteradditionよりも長いことを前提としています(そうでない場合は、_master[-overlap_len + i]_でIndexErrorを取得する可能性があります)。保証できない場合は、_overlap_lens_ジェネレーターに条件を追加します。

また、欲張りではありません。つまり、空でない最小のオーバーラップを探します(merge([1, 2, 2], [2, 2, 3])は_[1, 2, 2, 2, 3]_を返します)。それが「可能な限り最後の有効な位置でマージする」という意味だと思います。貪欲なバージョンが必要な場合は、_overlap_lens_ジェネレーターを逆にします。

6
dddsnn

私は最適化を提供しませんが、問題を見る別の方法を提供します。私には、これは 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]
6
Ale

に基づく https://stackoverflow.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:]
4
TankorSmash

まず、わかりやすくするために、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の最後の要素で終わるようにして、比較の数をさらに制限することもできます。

4

上記のすべてのソリューションは、マージタスクにfor/whileループを使用するという点で類似しています。私は最初に@JuniorCompressorと@TankorSmashによるソリューションを試しましたが、これらのソリューションは2つの大規模なリスト(たとえば、約数百万の要素を持つリスト)をマージするには遅すぎます。

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"])
0
Good Will