編集:いくつかのベンチマーク結果を追加します。リスト内の項目は約 1000 ~ 5000 で、IList
と をRemoveAt
上回っISet
てRemove
いますが、違いはわずかであるため、心配する必要はありません。コレクションのサイズが 10000 以上になると、本当の楽しみが始まります。それらのデータのみを掲載しています
昨夜ここで質問に答えていて、奇妙な状況に直面しました。
まず、単純なメソッドのセット:
static Random rnd = new Random();
public static int GetRandomIndex<T>(this ICollection<T> source)
{
return rnd.Next(source.Count);
}
public static T GetRandom<T>(this IList<T> source)
{
return source[source.GetRandomIndex()];
}
-------------------------------------------------- -------------------------------------------------- --------------------------------
コレクションから N 個のアイテムをランダムに削除しているとしましょう。私はこの関数を書きます:
public static void RemoveRandomly1<T>(this ISet<T> source, int countToRemove)
{
int countToRemain = source.Count - countToRemove;
var inList = source.ToList();
int i = 0;
while (source.Count > countToRemain)
{
source.Remove(inList.GetRandom());
i++;
}
}
また
public static void RemoveRandomly2<T>(this IList<T> source, int countToRemove)
{
int countToRemain = source.Count - countToRemove;
int j = 0;
while (source.Count > countToRemain)
{
source.RemoveAt(source.GetRandomIndex());
j++;
}
}
ご覧のとおり、最初の関数は an 用ISet
に、2 番目の関数は normal 用に記述されていますIList
。最初の関数では、 からアイテムごとに、 からインデックスごとに削除していますが、どちらも. 特にリストが大きくなると、2 番目の関数のパフォーマンスが最初の関数よりも大幅に低下するのはなぜですか?ISet
IList
O(1)
オッズ (私の見解):
1) 最初の関数では、 (からランダムな項目を取得するために)ISet
が に変換されますが、2 番目の関数ではそのようなことは実行されません。IList
IList
アドバンテージ IList。
2) 最初の関数では への呼び出しGetRandomItem
が行われますが、2 番目の関数では への呼び出しGetRandomIndex
が行われますが、これも 1 ステップ少なくなります。
些細なことですが、IList を活用してください。
3) 最初の関数では、別のリストからランダムなアイテムが取得されるため、取得されたアイテムは既に から削除されている可能性がありますISet
。while
これにより、最初の関数のループでより多くの反復が行われます。2 番目の関数では、反復されるソースからランダム インデックスが取得されるため、反復が繰り返されることはありません。私はこれをテストし、これを確認しました。
i > j 常に有利
IList
。
この動作の理由は、List
アイテムが追加または削除されたときに常にサイズを変更する必要があるためだと思いました。しかし、明らかに他のテストではそうではありません。私は走った:
public static void Remove1(this ISet<int> set)
{
int count = set.Count;
for (int i = 0; i < count; i++)
{
set.Remove(i + 1);
}
}
public static void Remove2(this IList<int> lst)
{
for (int i = lst.Count - 1; i >= 0; i--)
{
lst.RemoveAt(i);
}
}
2 番目の関数の方が高速に実行されることがわかりました。
テスト ベッド:
var f = Enumerable.Range(1, 100000);
var s = new HashSet<int>(f);
var l = new List<int>(f);
Benchmark(() =>
{
//some examples...
s.RemoveRandomly1(2500);
l.RemoveRandomly2(2500);
s.Remove1();
l.Remove2();
}, 1);
public static void Benchmark(Action method, int iterations = 10000)
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < iterations; i++)
method();
sw.Stop();
MsgBox.ShowDialog(sw.Elapsed.TotalMilliseconds.ToString());
}
2つの構造が何であるかを知りたいだけです..ありがとう..
結果:
var f = Enumerable.Range(1, 10000);
s.RemoveRandomly1(7500); => 5ms
l.RemoveRandomly2(7500); => 20ms
var f = Enumerable.Range(1, 100000);
s.RemoveRandomly1(7500); => 7ms
l.RemoveRandomly2(7500); => 275ms
var f = Enumerable.Range(1, 1000000);
s.RemoveRandomly1(75000); => 50ms
l.RemoveRandomly2(75000); => 925000ms
ただし、最も一般的なニーズの場合は、リストで十分です..!