質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.49%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Visual Studio

Microsoft Visual StudioはMicrosoftによる統合開発環境(IDE)です。多種多様なプログラミング言語に対応しています。

Q&A

解決済

9回答

33463閲覧

膨大な量のリストの中から不要な項目を削除する処理を高速化する方法があればご教示お願いします。

oryou

総合スコア15

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Visual Studio

Microsoft Visual StudioはMicrosoftによる統合開発環境(IDE)です。多種多様なプログラミング言語に対応しています。

0グッド

5クリップ

投稿2017/06/10 16:59

*現状
現在C#にてプログラミングをしているのですが、どうしても処理の遅さがネックとなって先に進めない状態が続いているため質問させていただきました。

*質問内容
膨大な量のリストの中から不要な項目を削除する処理を高速化する方法があればご教示お願いします。

*要件

  • 約600万件近いデータのリストがあり、その中から不要な項目を削除したいと考えております。
  • 削除したい件数も50万件近くあります。
  • 削除したいデータが必ずしも600万件のリストの中に存在するわけではないので、1つづつ存在するかを確認しなければなりません。
  • 1件1件のデータ自体は文字列で、例えば「花太郎」、「小太郎」みたいな文字列となっています。

*問題点
データ内容自体は異なるのですが、下記のコードでも処理に約1時間ほどかかっています。

*コード内容
データ内容を単純化するためにデータを文字数字にしています。

C#

1static void Main(string[] args) 2{ 3 // 600万件分のリストを作成 4 List<string> mainList = new List<string>(); 5 for(int i=0; i<6000000; i++) 6 { 7 mainList.Add(i.ToString()); 8 } 9 10 // 50万件分のリストを作成 11 List<string> deleteList = new List<string>(); 12 for(int i=0; i<500000; i++) 13 { 14 deleteList.Add(i.ToString()); 15 } 16 17 // mainListの中にdeleteListの項目があれば削除する 18 // ここの処理を高速化できないかを悩み中です。 19 foreach (string deleteStr in deleteList) 20 { 21 mainList.Remove(deleteStr); 22 } 23}

*補足
データベースが利用できない状況なので、リストで処理を行っています。
処理が早くできればList<string>である必要はないです。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答9

0

ベストアンサー

Listからの削除は大変重い処理です。これはListが「配列(array)」と言われるデータ構造であるため、インデックスアクセスがO(1)になる代わりに、削除がO(n)になってしまうからです。ましてや、Removeで添字ではなくオブジェクトを指定する場合は、検索もO(n)になるため、さらに遅い原因になります。

つまり、オリジナルコードは削除のリスト分繰り返しているためO(n × m)です。(nはmainのサイズ、mはdeleteのサイズ)

解決策は二つ(順序重複維持あり)+二つ(順序重複維持なし)ほどです。

###順序と重複を維持する場合

順序を維持し、また、重複している場合は重複したままにする場合です。

####連結リストを使う

「連結リスト(linked list)」といわれるデータ構造の場合は、削除はO(1)になります(代わりに、インデックスアクセスがO(n)です)。C#では連結リストとしてLinkedListが用意されていますので、こちらを使用すれば、速度アップになります。

と言いたいところですが、この方法ではそれほど速くなりません。

なぜなら、Removeで該当のオブジェクトを探す検索がO(n)の計算量が必要になるからです。削除が速いだけで見つけるのが速くなるわけではありません。結局O(n × m)になってしまいます。

####新たに配列を作る

削除するのではなく、削除対象ではない物だけ抜き出して、新しい配列にします。処理はLINQを使うとコードがすっきりするでしょう。また、削除のリストにHashSetを使うと削除対象かどうかのチェックも高速に行うことができます。

HashSetの作成はO(n)ですが、検索はO(1)です。配列作成も内部がO(1)ならO(n)になりますので、計算量はO(n + m)とかなり改善されます。

###順序と重複を維持しない場合

順序を維持し、また、重複している場合は重複したままにする場合です。

####並び替え済みリストを使う

並び替え済みリストの場合、検索がO(1)と高速になります。C#には並び替え済みのSortedListがありますのでこれが使用できます。ただし、削除はO(n)であり、オリジナルより速い程度です。結局O(n × m)になってしまいます。

重複は維持されますが、順序はソートされるために維持できません。

####ハッシュ化された集合を使う

順序や重複を維持しなければいいのであれば、ハッシュ化された集合が使用できます。検索も削除もほぼO(1)になるため、極めて高速です。

ハッシュ化された集合としてHashSetが使用できます。なお、HashSetでは順序もそのまま維持されるようです(正式ドキュメントが見つけられなかったので、知っている人は教えて欲しい)。ただし、重複は維持されません。

ただし、ハッシュ化された集合自体の作成にO(n)必要になりますので、そこは注意してください。ということで、計算量はO(n + m)とかなり改善されます。。

###検証コード

C#

1using System; 2using System.Collections.Generic; 3using System.Diagnostics; 4using System.Linq; 5 6class MainClass 7{ 8 static void Main(string[] args) 9 { 10 //int mainListSize = 6000000; 11 //int deleteListSize = 500000; 12 int mainListSize = 60000; 13 int deleteListSize = 5000; 14 15 Console.WriteLine("{0}個中{1}個を削除", 16 mainListSize, deleteListSize); 17 18 // 600万件分のリストを作成 19 var mainStopwatch = new Stopwatch(); 20 mainStopwatch.Start(); 21 var mainList = Enumerable.Range(0, mainListSize) 22 .Select(i => i.ToString()) 23 .OrderBy(a => Guid.NewGuid()) 24 .ToList(); 25 mainStopwatch.Stop(); 26 Console.WriteLine("main 作成完了: {0} ms", 27 mainStopwatch.ElapsedMilliseconds); 28 29 // 50万件分のリストを作成 30 var deleteStopwatch = new Stopwatch(); 31 deleteStopwatch.Start(); 32 var deleteList = Enumerable.Range(0, deleteListSize) 33 .Select(i => i.ToString()) 34 .OrderBy(a => Guid.NewGuid()) 35 .ToList(); 36 deleteStopwatch.Stop(); 37 Console.WriteLine("delete 作成完了: {0} ms", 38 deleteStopwatch.ElapsedMilliseconds); 39 40 Console.WriteLine(); 41 42 List<string> list; 43 44 list = Original(new List<String>(mainList), 45 new List<String>(deleteList)); 46 Console.WriteLine("サイズ: {0}", list.Count); 47 Console.WriteLine("最初: {0}", list[0]); 48 49 Console.WriteLine(); 50 51 list = UseLinkedList(new List<String>(mainList), 52 new List<String>(deleteList)); 53 Console.WriteLine("サイズ: {0}", list.Count); 54 Console.WriteLine("最初: {0}", list[0]); 55 56 Console.WriteLine(); 57 58 list = CreateListWithHashset(new List<String>(mainList), 59 new List<String>(deleteList)); 60 Console.WriteLine("サイズ: {0}", list.Count); 61 Console.WriteLine("最初: {0}", list[0]); 62 63 Console.WriteLine(); 64 65 list = UseSortedList(new List<String>(mainList), 66 new List<String>(deleteList)); 67 Console.WriteLine("サイズ: {0}", list.Count); 68 Console.WriteLine("最初: {0}", list[0]); 69 70 Console.WriteLine(); 71 72 list = UseHashSet(new List<String>(mainList), 73 new List<String>(deleteList)); 74 Console.WriteLine("サイズ: {0}", list.Count); 75 Console.WriteLine("最初: {0}", list[0]); 76 } 77 78 static List<String> Original(List<String> mainList, 79 List<String> deleteList) 80 { 81 Console.WriteLine("start Original"); 82 var runStopwatch = new Stopwatch(); 83 runStopwatch.Start(); 84 85 // mainListの中にdeleteListの項目があれば削除する 86 // ここの処理を高速化できないかを悩み中です。 87 foreach (string deleteStr in deleteList) 88 { 89 mainList.Remove(deleteStr); 90 } 91 92 runStopwatch.Stop(); 93 Console.WriteLine("run 完了: {0} ms", 94 runStopwatch.ElapsedMilliseconds); 95 return mainList; 96 } 97 98 static List<String> UseLinkedList(List<String> mainList, 99 List<String> deleteList) 100 { 101 Console.WriteLine("start UseLinkedList"); 102 var runStopwatch = new Stopwatch(); 103 runStopwatch.Start(); 104 105 // LinkedListに変換して処理をする。 106 var mainLinkedList = new LinkedList<String>(mainList); 107 foreach (string deleteStr in deleteList) 108 { 109 mainLinkedList.Remove(deleteStr); 110 } 111 var newList = mainLinkedList.ToList(); 112 113 runStopwatch.Stop(); 114 Console.WriteLine("run 完了: {0} ms", 115 runStopwatch.ElapsedMilliseconds); 116 return newList; 117 } 118 119 static List<String> CreateListWithHashset(List<String> mainList, 120 List<String> deleteList) 121 { 122 Console.WriteLine("start CreateListWithHashset"); 123 var runStopwatch = new Stopwatch(); 124 runStopwatch.Start(); 125 126 // 削除用のHashSetを作成して、処理をする。 127 var deleteSet = new HashSet<String>(deleteList); 128 var newList = mainList 129 .Where((s) => !deleteSet.Contains(s)).ToList(); 130 131 runStopwatch.Stop(); 132 Console.WriteLine("run 完了: {0} ms", 133 runStopwatch.ElapsedMilliseconds); 134 return newList; 135 } 136 137 static List<String> UseSortedList(List<String> mainList, 138 List<String> deleteList) 139 { 140 Console.WriteLine("start UseSortedList"); 141 var runStopwatch = new Stopwatch(); 142 runStopwatch.Start(); 143 144 // SortedListに変換して処理をする。 145 var mainSortedList = new SortedList<String, String>( 146 mainList.ToDictionary(s => s)); 147 foreach (string deleteStr in deleteList) 148 { 149 mainSortedList.Remove(deleteStr); 150 } 151 var newList = mainSortedList.Select(pair => pair.Value) 152 .ToList(); 153 154 runStopwatch.Stop(); 155 Console.WriteLine("run 完了: {0} ms", 156 runStopwatch.ElapsedMilliseconds); 157 return newList; 158 } 159 160 static List<String> UseHashSet(List<String> mainList, 161 List<String> deleteList) 162 { 163 Console.WriteLine("start UseHashSet"); 164 var runStopwatch = new Stopwatch(); 165 runStopwatch.Start(); 166 167 // HashSetに変換して処理をする。 168 var mainHashSet = new HashSet<String>(mainList); 169 foreach (string deleteStr in deleteList) 170 { 171 mainHashSet.Remove(deleteStr); 172 } 173 var newList = mainHashSet.ToList(); 174 175 runStopwatch.Stop(); 176 Console.WriteLine("run 完了: {0} ms", 177 runStopwatch.ElapsedMilliseconds); 178 return newList; 179 } 180 181}

データの並びはランダム化しています。連結リストの場合は削除するデータがどこにあるかによって処理時間が大きく変わるからです。

600万件中50万件だとオリジナルや連結リストが遅すぎるので、6万件中5千件での実行結果です。

60000個中5000個を削除 main 作成完了: 368 ms delete 作成完了: 26 ms start Original run 完了: 4572 ms サイズ: 55000 最初: 57539 start UseLinkedList run 完了: 3258 ms サイズ: 55000 最初: 57539 start CreateListWithHashset run 完了: 13 ms サイズ: 55000 最初: 57539 start UseSortedList run 完了: 2628 ms サイズ: 55000 最初: 10000 start UseHashSet run 完了: 16 ms サイズ: 55000 最初: 57539

UseSortedListの最初の要素は異なります。UseHashSetの場合は重複が維持されないことに注意です。なお、順序づけしておきたい場合はSortedSetを使うという手段もあります。

投稿2017/06/11 01:11

編集2017/06/11 02:34
raccy

総合スコア21735

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

oryou

2017/06/11 12:24

本当にありがとうございます。 実際に自分の環境で検証コードを実行させていただきましたが、1時間もかかっていた処理が僅か1秒程しかかからないのですごく感動しました。 HashSetを利用するだけで、こんなにも処理時間が変わってくるとは思ってもいませんでした。 本当に感謝しています。 ありがとうございました。
guest

0

こんにちは。

コレクションの内部実装を見るとList<T>ってソートしてないので頭から1つ1つ比較するので遅いです。更に連続したメモリを確保するようなのでRemoveも遅そうです。(多量のメモリコピーが発生するため)

mainListの数は最初600万、最後550万ですね。検索で見つかるのに平均して全体の半分と比較したと仮定すると最初平均300万回、最後は275万回の比較を行います。更に平均して287.5万回です。
これを50万回繰り返すので、287.5万回x50万回の比較を行います。

mainListをSortedSet<T>に変更すると、2分探索になりますので、600万個(<2の23乗)から見つける時に必要な比較回数は23回です。それを50万回繰り返します。

287.5万回/23回なので125,000倍の速度アップを期待できます。
SortedSet<T>は一般に言うリスト構造なのでRemoveは高速ですので、更に速くなる筈。


ご提示されたコードは600万個の頭の50万個に削除するものが集中しているから、1削除当たり平均25万回の比較になるため、25万回x50万回の比較になりますね。
ですので、25万回/23回なので1万倍程高速化することが期待できます。1秒かからない筈。

と思ったけど違いますね。常に削除対象が先頭になるので、1削除当たり1回の比較しかしてないはずですね。
となると比較回数は確実に増えます。ご提示のソースの場合、Remove処理がほぼ全てです。リスト構造になることでRemoveは高速になりますから、速度が上がる可能性もあります。やってみないと解らないです。

なお、実データの場合は、こんな極端な偏りがないなら高速化できる筈です。


すいません。実際にはやったことないので、上記は理論値です。
計算間違いもあるかも知れません。違ってたらごめんなさい。

投稿2017/06/10 17:34

編集2017/06/10 17:49
Chironian

総合スコア23272

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

oryou

2017/06/11 13:14

回答ありがとうございます。 今回は他の回答者がHashSetを紹介してくださったので、hashsetを利用して処理をしましたが、SortedSetという方法もあるのですね。 調べたところ、SortedSetはhashsetにソート機能を追加したクラスのようなので、今回のような処理でさらにソートが必要になった場合に利用したいと思います。 プラスアルファで知識が増えたので回答に感謝しています。 ありがとうございました。
guest

0

Removeするより新しいListを再生成した方が早いような気が..

投稿2017/06/10 20:05

dojikko

総合スコア3939

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

oryou

2017/06/11 13:00

回答ありがとうございます。 重複していない要素のみを新規リストに追加していくのも確かにありですね。
guest

0

どうしてもオンメモリで処理しなければならないのでしょうか?

処理対象のデータがデータベースに格納されているなら、データベースの機能をつかってデータの操作を行った方が良いように思います。

投稿2017/06/11 03:15

hidori

総合スコア402

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

oryou

2017/06/11 12:41

回答ありがとうございます。 今回は、データベースを利用できない為オンメモリでの処理となりましたが、やはりデータベースで処理する方が高速化につながるのですね。 データベースを利用できる環境で質問することもあると思いますので、またその時はご教示よろしくお願いします。
guest

0

List<T>は連続領域に要素を順番に配置した配列のような構造となっているため、「1件ずつ削除する」のは非常にコストの高い処理になるのは他の方がおっしゃるとおりと思います。途中の要素を削除した場合、それ以降の要素をひとつずつ前に「詰めなければならない」ので処理時間は詰めなければならない要素の数に比例=(リストのサイズに比例)するのでO(n)ということになります。

よって削除しなければならない要素を一つずつRemoveすると大変遅くなりますが、ListにはRemoveAllというメソッドも用意されています。このメソッドはRemoveを繰り返し呼び出すような実装にはなっていません。以下のリファレンスの備考にあるように計算量はO(n)です。

MSDN: RemoveAll

RemoveAllが内部でRemoveの繰り返しにより実装されているのであれば処理時間はO(n^2)相当になるはずですがO(n)になっているのは、複数の要素を削除する際、削除の度にそれ以降の要素を一つずつずらすようなことをしなくてよい実装になっているからだと思います。(例えば以下のような雰囲気に)

C#

1class List<T> { 2 public void RemoveAll(Predicate<T> predicate) { 3 int curIndex = 0; 4 for (int i = 0; Count; i++) { 5 T element = this[i]; 6 if (!predicate(element)) { 7 if (curIndex < i) { 8 this[curIndex++] = element; 9 } 10 } 11 } 12 RemoveRange(curIndex, Count - curIndex); 13 } 14}

この考えに基づくなら後は「削除すべき要素かどうかを高速に判定する」ということに配慮しHashSetを用いるとよいと思います。(ソートしてバイナリーサーチする方法とHashSetによりサーチする方法を比べれば検索時間はやはりHashSetの方が早いと思います。既にソートされているかどうかや消費メモリー量など条件によっても違うでしょうが...)

以上の考えから、次のように書き換えるとそこそこのスピードになってくれると思います。

C#

1// 質問者さんのコード 2... 3foreach (string deleteStr in deleteList) { 4 mainList.Remove(deleteStr); 5} 6 7// 変更後のコード 8... 9HashSet<string> deleteSet = new HashSet<string>(deleteList); 10mainList.RemoveAll(element => deleteSet.contains(element));

なお、削除すべきリストを作っている部分を最初からHashSetとして作ればスピードとメモリーの消費量は若干よくなると思います。

投稿2017/06/11 04:15

KSwordOfHaste

総合スコア18394

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

KSwordOfHaste

2017/06/11 04:20

あ、書き忘れましたが上記はオリジナルのリストの順番をかえない方法です。つまり質問者さんのオリジナルのアルゴリズムと結果が同一になるという前提です。
oryou

2017/06/11 12:33

回答ありがとうございます。 なぜ、Listでの削除処理が遅いのかがよくわかりました。 リストの要素を削除する度に、後ろの要素を前に持ってくる処理が走るのでそこで時間がかかっていたのですね。 基本的な処理の動きを教えて頂きありがとうございます。 Listを使うメリット、デメリットを考える要素が1つ増えたので感謝しています。 ありがとうございました。
KSwordOfHaste

2017/06/11 12:47

自分の回答意図はRemoveをm回呼び出すよりRemoveAllを1回呼び出す方がぐっと早くなる(RemoveAllはRemoveを単にm回呼び出すだけの便利機能ではなくちゃんと性能上の意味がある)ということでした。大した機能差がないと思えるようなAPIにも自分で論理を組むよりはずっとよいことがあるという一例として参考にしていただければと思います。
guest

0

600万件をデータベースも使わずにソートするとか
ぞっとしない話ですが、もし可能なら…

  1. (元データ + 削除データ + 削除データ) を作成
  2. 上記をソート
  3. 2つ以上同じものがあったら削除

イメージ的にはこんな感じ

sh

1src_file='hoge.txt' # 元データ 2del_file='piyo.txt' # 削除対象 3dst_file='fuga.txt' # 出力ファイル 4 5cat $src_file $del_file $del_file | \ 6sort | \ 7uniq -c | \ 8awk '{if ($1 == 1) print $2}' > $dst_file

投稿2017/06/11 00:49

takasima20

総合スコア7458

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

oryou

2017/06/11 12:50

回答ありがとうございます。 シェルコマンドでの処理方法を提示してくださってありがとうございます。 やはりプログラミング言語よりもシェルコマンドの方が処理をイメージしやすいですね。 ありがとうございました。
guest

0

前提条件として
0. メモリが十分に備わっている
0. mainListの要素の並び順が意味を持っている
0. 削除キーが重複してない。

とします。

さて、提示されたサンプルコードは削除するために、約600万件ものデータを総なめする必要があるため余り効率が良くないです。
また、削除する際のコストも結構かかるので、一度辞書にしてしまって再度リストにすれば良いのではないでしょうか?

csharp

1 2 3using System; 4using System.Collections.Generic; 5using System.Linq; 6 7namespace ConsoleApp3 8{ 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 // 600万件分のリストを作成 14 List<string> mainList = new List<string>(); 15 for (int i = 0; i < 6000000; i++) 16 { 17 mainList.Add(i.ToString()); 18 } 19 20 // 50万件分のリストを作成 21 List<string> deleteList = new List<string>(); 22 for (int i = 0; i < 500000; i++) 23 { 24 deleteList.Add(i.ToString()); 25 } 26 27 //mainListを辞書化する(元の位置を覚えておく為インデックスを持っておく) 28 var mainDictionary = mainList.Select((s, i) => (value:s, index:i)).ToDictionary(t => t.value, t => t.index); 29 30 //辞書に対して削除を行う。 31 foreach (var deleteStr in deleteList) 32 { 33 mainDictionary.Remove(deleteStr); 34 } 35 36 //mainDictionaryからListを再作成(インデックスを元に元の並びを復元する) 37 mainList = mainDictionary.OrderBy(p => p.Value).Select(p => p.Key).ToList(); 38 39 40 41 } 42 } 43} 44 45

投稿2017/06/10 23:33

Tokeiya3

総合スコア260

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

oryou

2017/06/11 13:37

回答ありがとうございます。 今回はHashsetを利用することで高速化を図ることができました。 Dictionaryを使う方法もあるのですね。 調べると「hashsetはキーだけを扱う一種のDictionaryのようなコレクションと見れる」と書かれていたので、そういう処理の方法をDictionaryで実現しているのかなと思いました。
guest

0

main,deleteデータをソートしておく。(順に並んでいれば何もしない)

1.mainを読む
2.deleteを読む
3.main<deleteならmain出力。main読み込み。main終わりならプログラム終了。
main>deleteならdelete読み込み。delete終わりなら残りのmainをすべて出力してプログラム終了。
main=deleteならmain読み込み。main終わりならプログラム終了。
4.3.の繰り返し

これでどうですか?
マッチングというやつです。
メモリーに貯めることもせず、データを比較して新しいマスターを作っていくものです。

投稿2017/06/10 17:24

編集2017/06/10 18:08
maiko0318

総合スコア876

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

oryou

2017/06/11 13:28

回答ありがとうございます。 今回はHashsetを利用することで、高速化をはかることができました。 マッチングという方法も試してみたいと思います。 ありがとうございました。
guest

0

50万件のリストを作成せず、消していけばいけないのですか?

投稿2017/06/10 17:10

maiko0318

総合スコア876

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.49%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問