web-dev-qa-db-ja.com

Javaを使用してファイル内の重複する行を削除する

私が取り組んでいるプロジェクトの一環として、重複する行エントリで生成したファイルをクリーンアップしたいと思います。ただし、これらの重複は多くの場合、お互いの近くでは発生しません。 Java(基本的にはファイルのコピーを作成し、次にネストされたwhileステートメントを使用して1つのファイルの各行を他のファイルの残りの行と比較しました)でそうする方法を思いつきました)問題は、生成されたファイルがかなり大きく、テキストが重い(約225k行のテキスト、約40 MB)ことです。現在のプロセスには63時間かかると推定しています!これは間違いなく受け入れられません。

ただし、これには統合ソリューションが必要です。できればJavaで。何か案は?ありがとう!

25
Monster

うーん... 40メガは、行のSetを作成して、それらをすべて出力して戻すのに十分なほど小さいようです。これは、O(n2)I/O作業。

これは次のようなものです(例外を無視):

public void stripDuplicatesFromFile(String filename) {
    BufferedReader reader = new BufferedReader(new FileReader(filename));
    Set<String> lines = new HashSet<String>(10000); // maybe should be bigger
    String line;
    while ((line = reader.readLine()) != null) {
        lines.add(line);
    }
    reader.close();
    BufferedWriter writer = new BufferedWriter(new FileWriter(filename));
    for (String unique : lines) {
        writer.write(unique);
        writer.newLine();
    }
    writer.close();
}

順序が重要な場合は、LinkedHashSetの代わりにHashSetを使用できます。要素は参照によって格納されるため、追加のリンクリストのオーバーヘッドは、実際のデータ量と比較して重要ではありません。

編集:ワークショップアレックスが指摘したように、一時ファイルを作成してもかまわない場合は、その行を読みながら単に印刷することができます。これにより、HashSetの代わりに単純なLinkedHashSetを使用できます。しかし、このようなI/Oバウンド操作の違いに気付くとは思いません。

37
Michael Myers

わかりました。ほとんどの答えは、ハッシュセットなどに行を追加してから、そのセットから再び移動するため、少しばかげて遅くなります。疑似コードで最も最適なソリューションを示します。

Create a hashset for just strings.
Open the input file.
Open the output file.
while not EOF(input)
  Read Line.
  If not(Line in hashSet)
    Add Line to hashset.
    Write Line to output.
  End If.
End While.
Free hashset.
Close input.
Close output.

皆さん、どうか、必要以上に難しくしないでください。 :-)並べ替えについても気にしないでください。

15
Wim ten Brink

同様のアプローチ

public void stripDuplicatesFromFile(String filename) {
    IOUtils.writeLines(
        new LinkedHashSet<String>(IOUtils.readLines(new FileInputStream(filename)),
        "\n", new FileOutputStream(filename + ".uniq"));
}
10
Peter Lawrey

このような何か、おそらく:

BufferedReader in = ...;
Set<String> lines = new LinkedHashSet();
for (String line; (line = in.readLine()) != null;)
    lines.add(line); // does nothing if duplicate is already added
PrintWriter out = ...;
for (String line : lines)
    out.println(line);

LinkedHashSetHashSetとは対照的に挿入順序を保持します(これは、ルックアップ/挿入の方が少し高速ですが)すべての行を並べ替えます。

4
gustafc

順序が重要でない場合、 最も簡単な方法はシェルスクリプト です。

<infile sort | uniq > outfile
3
phihag

コレクションライブラリのSetを使用すると、ファイルを読み取るときに、見られる一意の値を保存できます。

Set<String> uniqueStrings = new HashSet<String>();

// read your file, looping on newline, putting each line into variable 'thisLine'

    uniqueStrings.add(thisLine);

// finish read

for (String uniqueString:uniqueStrings) {
  // do your processing for each unique String
  // i.e. System.out.println(uniqueString);
}
3
brabster
  • ファイルを読み込み、行番号と行を保存します:O(n)
  • アルファベット順にソートします:O(n log n)
  • 重複を削除:O(n)
  • 元の行番号順に並べ替えます:O(n log n)
2
Simon Nickerson

すでに読んだ行を保存する単純なHashSetを試してください。次に、ファイルを反復処理します。重複に遭遇した場合、それらは単に無視されます(セットにはすべての要素を一度しか含めることができないため)。

2
Kevin Dungs

ハッシュセットアプローチは問題ありませんが、すべての文字列をメモリに保存する必要はなく、ファイル内の場所への論理ポインターを微調整することができるため、必要な場合にのみ実際の値を読み取るために戻ることができます。

別の創造的なアプローチは、各行に行の番号を追加し、すべての行を並べ替え、重複を削除し(最後の番号であるはずのトークンを無視して)、最後のトークンでファイルを再度並べ替えて、それを取り除くことです出力で。

1
fortran

2つのスケーラブルなソリューションがあります。スケーラブルとは、手順が安定しているかどうかに応じて、ディスクではなくメモリベースを意味します。安定とは、重複を削除した後の順序が同じであることを意味します。スケーラビリティが問題でない場合は、同じ種類のメソッドにメモリを使用してください。

安定しないソリューションの場合は、まずディスク上のファイルを並べ替えます。これは、ファイルを小さいファイルに分割し、メモリ内の小さいチャンクをソートしてから、ファイルをソートされた順序でマージします。マージでは重複が無視されます。

マージ自体は、次の行の方が大きいことが保証されているため、各ファイルの現在の行のみを比較することにより、ほとんどメモリを使用せずに実行できます。

安定したソリューションは少しトリッキーです。まず、以前と同じようにファイルをチャンクでソートしますが、各行に元の行番号を示します。次に、「マージ」中に、結果を保存せずに、削除する行番号だけを保存します。

次に、上で保存した行番号を無視して、元のファイルを1行ずつコピーします。

0
user44242

行が来る順序と、重複を何回見て見ているかは重要ですか?

そうでない場合、そして多くのデュープ(つまり、書くよりも読むほうが多い)を当てにしている場合、私は並列化ハッシュセットソリューションについても考えます。ハッシュセットを共有リソースとして使用します。

0
mikek
void deleteDuplicates(File filename) throws IOException{
    @SuppressWarnings("resource")
    BufferedReader reader = new BufferedReader(new FileReader(filename));
    Set<String> lines = new LinkedHashSet<String>();
    String line;
    String delims = " ";
    System.out.println("Read the duplicate contents now and writing to file");
    while((line=reader.readLine())!=null){
        line = line.trim(); 
        StringTokenizer str = new StringTokenizer(line, delims);
        while (str.hasMoreElements()) {
            line = (String) str.nextElement();
            lines.add(line);
            BufferedWriter writer = new BufferedWriter(new FileWriter(filename));
            for(String unique: lines){
                writer.write(unique+" ");               
            }
            writer.close();
        }
    }
    System.out.println(lines);
    System.out.println("Duplicate removal successful");
}
0
Anit Chaudhary

この効率的なソリューションについて、2つの仮定を行いました。

  1. 行に相当するBlobがあるか、バイナリとして処理できます
  2. 各行の先頭へのオフセットまたはポインタを保存できます。

これらの仮定に基づくソリューションは次のとおりです。1.行を読み取り、ハッシュマップにキーとして長さを保存します。これにより、ハッシュマップが軽量になります。キーに記載されているその長さを持つすべての行のハッシュマップのエントリとしてリストを保存します。このハッシュマップの作成はO(n)です。ハッシュマップ内の各行のオフセットをマッピングする際に、エントリ-1を除くこのキーの長さの行(オフセット)のリストにあるすべての既存のエントリとオフセットとしてラインBLOBを比較します。重複が見つかった場合は、両方の行を削除してオフセットを保存します-リストのそれらの場所で1。

したがって、複雑さとメモリ使用量を考慮してください。

ハッシュマップメモリ​​、スペースの複雑さ= O(n)ここで、nは行数です。

時間の複雑さ-重複はないが、各行の長さ= mを考慮してすべて等しい長さの行がある場合、行数= nを考慮すると、O(n)になります。 blobを比較できると想定しているため、mは関係ありません。それは最悪のケースでした。

他の場合では、比較を節約しますが、ハッシュマップに必要な追加のスペースはほとんどありません。

さらに、サーバー側でmapreduceを使用して、セットを分割し、後で結果をマージできます。そして、長さまたは行の始まりをマッパーキーとして使用します。

0
AAW

UNIXシェルコマンドを使用できれば、次のようなことができます。

for(i = line 0 to end)
{
    sed 's/\$i//2g' ; deletes all repeats
}

これはファイル全体を反復処理し、sed呼び出しごとに1回だけ一意の各オカレンスを渡します。このようにして、以前に行った一連の検索を実行しません。

0
samoz