Java配列に値が含まれているかどうかを確認します
1. 概要
この記事では、指定された値を配列で検索するさまざまな方法について説明します。
また、JMH(Java Microbenchmark Harness)を使用してこれらのパフォーマンスを比較し、どの方法が最適かを判断します。
2. セットアップ
この例では、テストごとにランダムに生成されたStringsを含む配列を使用します。
String[] seedArray(int length) {
String[] strings = new String[length];
Random value = new Random();
for (int i = 0; i < length; i++) {
strings[i] = String.valueOf(value.nextInt());
}
return strings;
}
各ベンチマークで配列を再利用するために、配列とカウントを保持する内部クラスを宣言して、JMHのスコープを宣言できるようにします。
@State(Scope.Benchmark)
public static class SearchData {
static int count = 1000;
static String[] strings = seedArray(1000);
}
3. 基本検索
一致するものが見つかるまで各メンバーを調べるThree commonly used methods for searching an array are as a List, a Set, or with a loop。
各アルゴリズムを実装する3つの方法から始めましょう。
boolean searchList(String[] strings, String searchString) {
return Arrays.asList(SearchData.strings)
.contains(searchString);
}
boolean searchSet(String[] strings, String searchString) {
Set stringSet = new HashSet<>(Arrays.asList(SearchData.strings));
return stringSet.contains(searchString);
}
boolean searchLoop(String[] strings, String searchString) {
for (String string : SearchData.strings) {
if (string.equals(searchString))
return true;
}
return false;
}
これらのクラスアノテーションを使用して、JMHに平均時間をマイクロ秒単位で出力し、5回のウォームアップ反復を実行して、テストの信頼性を確保するように指示します。
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 5)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
そして、各テストをループで実行します。
@Benchmark
public void searchArrayLoop() {
for (int i = 0; i < SearchData.count; i++) {
searchLoop(SearchData.strings, "T");
}
}
@Benchmark
public void searchArrayAllocNewList() {
for (int i = 0; i < SearchData.count; i++) {
searchList(SearchData.strings, "T");
}
}
@Benchmark
public void searchArrayAllocNewSet() {
for (int i = 0; i < SearchData.count; i++) {
searchSet(SearchData.strings, "S");
}
}
各メソッドに対して1000回の検索を実行すると、結果は次のようになります。
SearchArrayTest.searchArrayAllocNewList avgt 20 937.851 ± 14.226 us/op
SearchArrayTest.searchArrayAllocNewSet avgt 20 14309.122 ± 193.844 us/op
SearchArrayTest.searchArrayLoop avgt 20 758.060 ± 9.433 us/op
The loop search is more efficient than others.しかし、これは少なくとも部分的にはコレクションの使用方法によるものです。
searchList()を呼び出すたびに新しいListインスタンスを作成し、searchSet()を呼び出すたびに新しいListと新しいHashSetを作成します。 これらのオブジェクトを作成すると、アレイをループする場合とは異なり、追加のコストが発生します。
4. より効率的な検索
ListとSetの単一インスタンスを作成し、それらを検索ごとに再利用するとどうなりますか?
やるだけやってみよう:
public void searchArrayReuseList() {
List asList = Arrays.asList(SearchData.strings);
for (int i = 0; i < SearchData.count; i++) {
asList.contains("T");
}
}
public void searchArrayReuseSet() {
Set asSet = new HashSet<>(Arrays.asList(SearchData.strings));
for (int i = 0; i < SearchData.count; i++) {
asSet.contains("T");
}
}
上記と同じJMHアノテーションを使用してこれらのメソッドを実行し、比較のために単純なループの結果を含めます。
非常に異なる結果が表示されます。
SearchArrayTest.searchArrayLoop avgt 20 758.060 ± 9.433 us/op
SearchArrayTest.searchArrayReuseList avgt 20 837.265 ± 11.283 us/op
SearchArrayTest.searchArrayReuseSet avgt 20 14.030 ± 0.197 us/op
Listの検索は以前よりわずかに高速ですが、Setはループに必要な時間の1%未満に低下します。
各検索から新しいコレクションを作成するために必要な時間を削除したので、これらの結果は理にかなっています。
ハッシュテーブルの検索では、HashSetの基礎となる構造の時間計算量は0(1)ですが、ArrayListの基礎となる配列は0(n)です。
5. バイナリ検索
配列を検索する別の方法は、binary searchです。 非常に効率的ですが、バイナリ検索では配列が事前にソートされている必要があります。
配列を並べ替えて、バイナリ検索を試してみましょう。
@Benchmark
public void searchArrayBinarySearch() {
Arrays.sort(SearchData.strings);
for (int i = 0; i < SearchData.count; i++) {
Arrays.binarySearch(SearchData.strings, "T");
}
}
SearchArrayTest.searchArrayBinarySearch avgt 20 26.527 ± 0.376 us/op
バイナリ検索は非常に高速ですが、HashSet:よりも効率は劣りますが、バイナリ検索の最悪の場合のパフォーマンスは0(log n)であり、配列検索とハッシュテーブルのパフォーマンスの間に位置します。
6. 結論
配列を検索するいくつかの方法を見てきました。
結果に基づくと、HashSetは、値のリストを検索するのに最適です。 ただし、事前に作成してSet.に保存する必要があります
いつものように、例の完全なソースコードはover on GitHubで入手できます。