Javaコレクションの時間の複雑さ

1概要

このチュートリアルでは、Java Collection APIのさまざまなコレクションのパフォーマンスについて説明します。コレクションについて話すとき、私たちは通常 List、Map、 および Set データ構造とそれらの一般的な実装について考えます。

まず最初に、一般的な操作に対するBig-Oの複雑な洞察を見ていきます。その後、実行中のいくつかのコレクション操作の実数を示します。

2時間の複雑さ

通常、 時間の複雑さについて話すときは、Big-O表記法 を参照します。簡単に言えば、この表記法は、アルゴリズムを実行する時間が入力のサイズに応じてどのように増加するかを表しています。

Big-O記法https://www.baeldung.com/big-o-notation[theory]または実用的https://www.baeldung.com/java-algorithm-complexity[詳細]の詳細については、便利な記事をご覧ください。 Javaの例]。

3 リスト

簡単なリストから始めましょう。これは順序付けられたコレクションです。

ここでは、 ArrayList、LinkedList、 、および CopyOnWriteArrayList の実装のパフォーマンスの概要について説明します。

3.1. 配列リスト

  • Javaの __ArrayList は配列によって支えられています** 。これはその実装の内部ロジックを理解するのに役立ちます。 ArrayList__のより包括的なガイドはhttps://www.baeldung.com/java-arraylist[この記事の中に]あります。

それでは、まずは共通の操作の時間の複雑さに高いレベルで焦点を当てましょう。

  • add() - O(1) 時間がかかります

  • add(index、element) - 平均して O(n) timeで実行されます。

  • get() - 常に一定の時間です O(1) 操作

  • remove() - 線形の O(n) 時間で実行されます。繰り返します。

削除対象となる要素を見つけるための配列全体 indexOf()** - も線形時間で実行されます。それは繰り返します

内部配列と各要素を一つずつチェックする。だから時間 この操作の複雑さは常に O(n) timeを必要とします contains() ** - 実装は indexOf() に基づいています。それでは

O(n) 時間にも実行

3.2. CopyOnWriteArrayList

List インターフェースのこの実装は、マルチスレッドアプリケーションを扱うときに非常に便利です。これはスレッドセーフであり、https://www.baeldung.com/java-copy-on-write-arraylist[このガイドはこちら]で詳しく説明されています。

これが CopyOnWriteArrayList のパフォーマンスBig-O表記の概要です。

  • add() - 値を追加する位置に依存するので、複雑さ

O(n) です get()** - __O(1) __定数時間演算

  • remove() - __O(n) __timeかかる

  • contains() - 同様に、複雑さは O(n) です

ご覧のとおり、 add() メソッドのパフォーマンス特性のために、このコレクションを使用すると非常にコストがかかります。

3.3. LinkedList

  • LinkedList は、データフィールドを保持するノードと別のノードへの参照** で構成される線形データ構造です。 LinkListの機能の詳細については、https://www.baeldung.com/java-linkedlistを参照してください。

いくつかの基本的な操作を実行するのに必要な時間の平均推定値を提示しましょう。

  • add() - 任意の位置での O(1) 定時間挿入をサポート

  • get() - 要素の検索には __O(n) __timeかかる

  • remove() - 要素を削除すると、 O(1) 操作も必要になります。

要素の位置を指定します contains() ** - 時間の複雑さも

3.4. JVMのウォームアップ

さて、理論を証明するために、実際のデータを試してみましょう。 より正確には、最も一般的なコレクション操作 のJMH(Java Microbenchmark Harness)テスト結果を示します。

JMHツールに慣れていない場合は、https://www.baeldung.com/java-microbenchmark-harness[有用なガイド]をご覧ください。

まず、ベンチマークテストの主なパラメータを紹介します。

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 10)
public class ArrayListBenchmark {
}

次に、ウォームアップの反復回数を 10 に設定します。また、結果の平均実行時間がマイクロ秒単位で表示されるようにします。

3.5. ベンチマークテスト

今度は、パフォーマンステストを実行します。まず、 ArrayList から始めます。

@State(Scope.Thread)
public static class MyState {

    List<Employee> employeeList = new ArrayList<>();

    long iterations = 100000;

    Employee employee = new Employee(100L, "Harry");

    int employeeIndex = -1;

    @Setup(Level.Trial)
    public void setUp() {
        for (long i = 0; i < iterations; i++) {
            employeeList.add(new Employee(i, "John"));
        }

        employeeList.add(employee);
        employeeIndex = employeeList.indexOf(employee);
    }
}

ArrayListBenchmark 内に、初期データを保持するための State クラスを追加します。

ここでは、 Employee オブジェクトの ArrayList を作成します。その後、** setUp() メソッド内の 100.000 個のアイテムで初期化します。 @ State は、 @ Benchmark テストが同じスレッド内で宣言されている変数にフルアクセスできることを示します。

最後に、 add()、contains()、indexOf()、remove()、 、および get() メソッドのベンチマークテストを追加します。

@Benchmark
public void testAdd(ArrayListBenchmark.MyState state) {
    state.employeeList.add(new Employee(state.iterations + 1, "John"));
}

@Benchmark
public void testAddAt(ArrayListBenchmark.MyState state) {
    state.employeeList.add((int) (state.iterations), new Employee(state.iterations, "John"));
}

@Benchmark
public boolean testContains(ArrayListBenchmark.MyState state) {
    return state.employeeList.contains(state.employee);
}

@Benchmark
public int testIndexOf(ArrayListBenchmark.MyState state) {
    return state.employeeList.indexOf(state.employee);
}

@Benchmark
public Employee testGet(ArrayListBenchmark.MyState state) {
    return state.employeeList.get(state.employeeIndex);
}

@Benchmark
public boolean testRemove(ArrayListBenchmark.MyState state) {
    return state.employeeList.remove(state.employee);
}

3.6. 試験結果

すべての結果はマイクロ秒単位で表示されます。

Benchmark                        Mode  Cnt     Score     Error
ArrayListBenchmark.testAdd       avgt   20     2.296 ±   0.007
ArrayListBenchmark.testAddAt     avgt   20   101.092 ±  14.145
ArrayListBenchmark.testContains  avgt   20   709.404 ±  64.331
ArrayListBenchmark.testGet       avgt   20     0.007 ±   0.001
ArrayListBenchmark.testIndexOf   avgt   20   717.158 ±  58.782
ArrayListBenchmark.testRemove    avgt   20   624.856 ±  51.101
  • 結果から、 __ testContains() testIndexOf() メソッドはほぼ同時に実行されることがわかります。また、 testAdd()、testGet() メソッドのスコアが他の結果と大きく異なることも明らかです。要素を追加するには2 。296 __マイクロ秒かかり、1つ取得するのは0.007マイクロ秒の操作です。

要素を検索または削除している間、およそ700マイクロ秒かかります。

これらの数は理論的部分の証明であり、そこでは add()、 、および get() O(1) 時間複雑さを持ち、他のメソッドは O(n) であることを学びました。この例では n = 10.000 要素です。

同様に、 CopyOnWriteArrayList コレクションについても同じテストを書くことができます。必要なのは、employeeList内の ArrayList CopyOnWriteArrayList インスタンスに置き換えるだけです。

ベンチマークテストの結果は次のとおりです。

Benchmark                          Mode  Cnt    Score     Error
CopyOnWriteBenchmark.testAdd       avgt   20  652.189 ±  36.641
CopyOnWriteBenchmark.testAddAt     avgt   20  897.258 ±  35.363
CopyOnWriteBenchmark.testContains  avgt   20  537.098 ±  54.235
CopyOnWriteBenchmark.testGet       avgt   20    0.006 ±   0.001
CopyOnWriteBenchmark.testIndexOf   avgt   20  547.207 ±  48.904
CopyOnWriteBenchmark.testRemove    avgt   20  648.162 ± 138.379

ここでも数字が理論を裏付けています。ご覧のとおり、 testGet() は平均0.006ミリ秒で実行され、これを O(1) と見なすことができます。 ** ArrayList と比較すると、 testAdd() メソッドの結果には大きな違いがあります。 add() メソッドの複雑さは、__ArrayListのO(1)の複雑さと同じです。

  • パフォーマンスの数値は 0.051 ** と比較して 878.166 であるため、時間の直線的な増加をはっきりと見ることができます。

さて、それは LinkedList 時間です:

Benchmark        Cnt     Score       Error
testAdd          20     2.580        ± 0.003
testContains     20     1808.102     ± 68.155
testGet          20     1561.831     ± 70.876
testRemove       20     0.006        ± 0.001

LinkedList で要素を追加したり削除したりするのは非常に速いことがスコアからわかります。

さらに、追加/削除操作とget/contains操作の間にはパフォーマンス上の大きなギャップがあります。

4 地図

JDKの最新バージョンでは、 Map 実装のパフォーマンスが大幅に向上しています。たとえば、 __HashMap、LinkedHashMap 内部実装では、 LinkedList をバランスの取れたツリーノード構造に置き換えることができます。 ** これは、 HashMap の衝突時に、要素検索の最悪のシナリオを O(n) から O(log(n))__に短縮します。

ただし、適切な .equals() メソッドと .hashcode() メソッドを実装した場合、衝突は起こりそうにありません。

HashMap 衝突の詳細については、https://www.baeldung.com/java-hashmapを参照してください。 記事から、 HashMap からの要素の格納と取得には定数 O(1) time がかかることもわかります。

4.1. テスト O(1) 操作

実際の数字をいくつか見てみましょう。まず、 HashMap の場合:

Benchmark                         Mode  Cnt  Score   Error
HashMapBenchmark.testContainsKey  avgt   20  0.009 ± 0.002
HashMapBenchmark.testGet          avgt   20  0.011 ± 0.001
HashMapBenchmark.testPut          avgt   20  0.019 ± 0.002
HashMapBenchmark.testRemove       avgt   20  0.010 ± 0.001
  • ご覧のとおり、この数値は上記のメソッドを実行するための O(1) 一定時間を証明しています。

リストされたすべてのメソッドについて、** HashMap、LinkedHashMap、IdentityHashMap、WeakHashMap、EnumMap 、および ConcurrentHashMapの O(1)__があります。

残りのテストスコアの結果を1つの表の形式で表示しましょう。

Benchmark      LinkedHashMap  IdentityHashMap  WeakHashMap  ConcurrentHashMap
testContainsKey    0.008         0.009          0.014          0.011
testGet            0.011         0.109          0.019          0.012
testPut            0.020         0.013          0.020          0.031
testRemove         0.011         0.115          0.021          0.019

出力数から、我々は O(1) 時間の複雑さの主張を確認することができます。

4.2. テスト O(log(n)) 操作

  • 木構造について**

items count (n)         1000      10,000     100,000   1,000,000
all tests total score   00:03:17  00:03:17   00:03:30  00:05:27

5 セット

一般に、 Set は一意の要素の集まりです。ここでは、 Set インターフェイスの HashSet LinkedHashSet EnumSet、TreeSet、CopyOnWriteArraySet、 、および ConcurrentSkipListSet の実装について説明します。

HashSet の内部をよりよく理解するために、https://www.baeldung.com/java-hashset[このガイド]を参考にしてください。

それでは、時間の複雑さの数値を示すために先にジャンプしましょう。

  • HashSet LinkedHashSet、 、および __EnumSet add()の場合、remove() および contains() 操作のコスト定数 O(1) 時間。内部の HashMap__実装に感謝します。**

  • 同様に、**

CopyOnWriteArraySet、 add()、remove()および contains()__メソッドの平均時間はO(n)です。

5.1. 試験方法

それでは、ベンチマークテストにジャンプしましょう。

@Benchmark
public boolean testAdd(SetBenchMark.MyState state) {
    return state.employeeSet.add(state.employee);
}

@Benchmark
public Boolean testContains(SetBenchMark.MyState state) {
    return state.employeeSet.contains(state.employee);
}

@Benchmark
public boolean testRemove(SetBenchMark.MyState state) {
    return state.employeeSet.remove(state.employee);
}

さらに、残りのベンチマーク構成はそのままにします。

5.2. 数を比較する

HashSet __LinkedHashSetのランタイム実行スコアの動作を見てみましょう having n = 1000; 10,000; 100,000アイテム

__HashSetの場合、番号は次のとおりです。

Benchmark      1000    10,000    100,000
.add()         0.026   0.023     0.024
.remove()      0.009   0.009     0.009
.contains()    0.009   0.009     0.010

同様に、 LinkedHashSet の結果は次のとおりです。

Benchmark      1000    10,000    100,000
.add()         0.022   0.026     0.027
.remove()      0.008   0.012     0.009
.contains()    0.008   0.013     0.009

ご覧のとおり、スコアは各操作でほぼ同じです。さらに、それらを HashMap テストの出力と比較すると、それらは同じように見えます。

その結果、テストしたメソッドはすべて定数 O(1) 時間で実行されることを確認しました。

6. 結論

この記事では、Javaデータ構造の最も一般的な実装の複雑な時間について説明します。

それとは別に、JVMベンチマークテストを通じて、各種類のコレクションの実際のランタイムパフォーマンスを示します。また、異なるコレクションにおける同じ操作のパフォーマンスを比較しました。その結果、私たちは自分のニーズに合った正しいコレクションを選ぶことを学びます。

いつもどおり、この記事の完全なコードはhttps://github.com/eugenp/tutorials/tree/master/core-java-collections[over on GitHub]から入手できます。

  • **