Časová zložitosť zbierok Java

1. Prehľad

V tomto návode povieme si o výkone rôznych kolekcií z Java Collection API. Keď hovoríme o zbierkach, zvyčajne myslíme na Zoznam, Mapa, a Nastaviť dátové štruktúry a ich spoločné implementácie.

Najskôr sa pozrieme na štatistiku zložitosti Big-O pre bežné operácie a potom ukážeme skutočné počty prevádzkových časov niektorých operácií zhromažďovania.

2. Časová zložitosť

Zvyčajne keď hovoríme o časovej zložitosti, máme na mysli zápis Big-O. Jednoducho povedané, notácia popisuje, ako rastie čas na vykonanie algoritmu s veľkosťou vstupu.

K dispozícii sú užitočné zápisy, kde sa dozviete viac o teórii zápisu Big-O alebo praktických príkladoch jazyka Java.

3. Zoznam

Začnime jednoduchým zoznamom - čo je objednaná zbierka.

Tu sa pozrieme na prehľad výkonnosti ArrayList, LinkedList, a CopyOnWriteArrayList implementácie.

3.1. ArrayList

The ArrayList v Jave je kryté poľom. To pomáha pochopiť vnútornú logiku jeho implementácie. Komplexnejší sprievodca pre ArrayList je k dispozícii v tomto článku.

Poďme sa teda najskôr zamerať na časovú zložitosť bežných operácií na vysokej úrovni:

  • pridať () - berie O (1) čas
  • pridať (index, prvok) - v priemere beží v O (n) čas
  • dostať () - je vždy konštantný čas O (1) prevádzka
  • odstrániť () - beží lineárne O (n) čas. Aby sme našli prvok spôsobilý na odstránenie, musíme iterovať celé pole
  • indexOf ()- beží tiež v lineárnom čase. Iteruje cez interné pole a kontroluje každý prvok jeden po druhom. Časová zložitosť tejto operácie teda vždy vyžaduje O (n) čas
  • obsahuje () - implementácia je založená na indexOf (). Tak to tiež zabehne O (n) čas

3.2. CopyOnWriteArrayList

Táto implementácia Zoznam rozhranie je veľmi užitočné pri práci s viacvláknovými aplikáciami. Je to bezpečné pre vlákna a v tejto príručke je to dobre vysvetlené.

Tu je prehľad výkonu Big-O notácie pre CopyOnWriteArrayList:

  • pridať () - záleží na pozícii, ktorej pridáme hodnotu, takže zložitosť je O (n)
  • dostať () - je O (1) prevádzka v konštantnom čase
  • odstrániť () - berie O (n) čas
  • obsahuje () - podobne je to aj so zložitosťou O (n)

Ako vidíme, použitie tejto kolekcie je veľmi drahé kvôli výkonnostným charakteristikám pridať () metóda.

3.3. LinkedList

LinkedList je lineárna dátová štruktúra, ktorá sa skladá z uzlov obsahujúcich dátové pole a odkazu na iný uzol. Pre viac LinkedList funkcií a schopností, pozrite sa na tento článok tu.

Uveďme priemerný odhad času potrebného na vykonanie niekoľkých základných operácií:

  • pridať () - podporuje O (1) vkladanie v konštantnom čase do ľubovoľnej polohy
  • dostať () - hľadanie prvku trvá - O (n) čas
  • odstrániť () - odstránenie prvku tiež trvá O (1) operácia, pretože poskytujeme polohu prvku
  • obsahuje () - taktiež má O (n) časová zložitosť

3.4. Zahrievanie JVM

Teraz, aby sme dokázali teóriu, poďme sa pohrať s aktuálnymi údajmi. Presnejšie uvedieme výsledky testu JMH (Java Microbenchmark Harness) najbežnejších operácií zhromažďovania.

Ak nástroj JMH nepoznáte, prečítajte si tohto užitočného sprievodcu.

Najprv predstavíme hlavné parametre našich testovacích testov:

@BenchmarkMode (Mode.AverageTime) @OutputTimeUnit (TimeUnit.MICROSECONDS) @Warmup (iterácie = 10) verejná trieda ArrayListBenchmark {}

Potom sme nastavili číslo iterácií zahrievania na 10. Prajeme si tiež, aby sa priemerná doba trvania našich výsledkov zobrazila v mikrosekundách.

3.5. Porovnávacie testy

Teraz je čas vykonať testy výkonnosti. Najprv začneme s ArrayList:

@State (Scope.Thread) verejná statická trieda MyState {List employeeList = new ArrayList (); dlhé iterácie = 100000; Zamestnanec zamestnanec = nový zamestnanec (100 L, „Harry“); int zamestnanecIndex = -1; @Setup (Level.Trial) public void setUp () {for (long i = 0; i <iterations; i ++) {employeeList.add (new Employee (i, "John")); } employeeList.add (zamestnanec); employeeIndex = employeeList.indexOf (zamestnanec); }}

Vo vnútri našej ArrayListBenchmark, pridáme Štát triedy na uchovanie počiatočných údajov.

Tu tvoríme ArrayList z Zamestnanec predmety. Po, inicializujeme pomocou 100.000 predmety vo vnútri nastaviť() metóda. The @Štát naznačuje, že @Benchmark testy majú plný prístup k premenným deklarovaným v ňom v rovnakom vlákne.

Na záver je čas pridať testovacie testy pre add (), contains (), indexOf (), remove (), a dostať () metódy:

@Benchmark public void testAdd (ArrayListBenchmark.MyState state) {state.employeeList.add (nový zamestnanec (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) {návrat 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. Výsledky testu

Všetky výsledky sú uvedené v mikrosekundách:

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.test Získať priemet 20 0,007 624 856 ± 51,101

Z výsledkov sa môžeme dozvedieť, že testContains () a testIndexOf () metódy prebiehajú približne v rovnakom čase. Vidíme tiež zreteľne vidieť obrovský rozdiel medzi testAdd (), testGet () skóre metódy zo zvyšných výsledkov. Pridanie prvku trvá 2.296 mikrosekúnd a získanie jednej je 0,007 mikrosekundovej operácie.

Pri vyhľadávaní alebo odstraňovaní prvku sú to zhruba náklady 700 mikrosekundy. Tieto čísla sú dôkazom teoretickej časti, kde sme sa to dozvedeli add (), a dostať ()O (1) časová zložitosť a ostatné metódy sú O (n). n = 10 000 prvkov v našom príklade.

Rovnako môžeme napísať rovnaké testy pre CopyOnWriteArrayList zbierka. Všetko, čo potrebujeme, je vymeniť ArrayList v zozname zamestnancov pomocou CopyOnWriteArrayList inštancia.

Tu sú výsledky referenčného testu:

Porovnávací režim Cnt Skóre Chyba 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

Aj tu čísla potvrdzujú teóriu. Ako vidíme, testGet () v priemere beží za 0,006 ms, čo môžeme považovať za O (1). V porovnaní s ArrayList, všimli sme si tiež výrazný rozdiel medzi testAdd () výsledky metódy. Ako tu máme O (n) zložitosť pre pridať () metóda verzus ArrayList je O (1).

Jasne vidíme lineárny rast času, tak ako sú čísla výkonu 878.166 v porovnaní s 0.051.

Teraz je LinkedList čas:

Benchmark Cnt Score Error test Pridajte 20 2,580 ± 0,003 test Obsahuje 20 1808,102 ± 68,155 test Získajte 20 1561 831 ± 70 876 test Odstráňte 20 0,006 ± 0,001

Z výsledkov vidíme, že pridávanie a odstraňovanie prvkov v LinkedList sú dosť rýchle.

Ďalej existuje značná výkonnostná medzera medzi operáciami pridania / odstránenia a získania / obsahuje.

4. Mapa

S najnovšími verziami JDK sme svedkami výrazného zlepšenia výkonu pre Mapa implementácie, ako napríklad výmena LinkedList s vyváženou štruktúrou uzlov stromu v HashMap, LinkedHashMap interné implementácie. Toto skracuje najhorší scenár pri vyhľadávaní prvkov O (n) do O (log (n)) čas počas HashMap kolízie.

Ak však implementujeme správne .equals () a .hashcode () kolízie metód sú nepravdepodobné.

Ak sa chcete dozvedieť viac o HashMap kolízie, pozrite si tento zápis. Z zápisu sa tiež môžeme dozvedieť, že ukladanie a načítanie prvkov z HashMap trvá neustále O (1) čas.

4.1. Testovanie O (1) Operácie

Ukážme niekoľko skutočných čísel. Po prvé, pre HashMap:

Chyba Benchmark Cnt skóre Chyba HashMapBenchmark.testContainsKey priem. 20 0,009 ± 0,002 HashMapBenchmark.test Získať priem. 20 0,011 ± 0,001 HashMapBenchmark.test Vložte priem. 20 0,019 ± 0,002 HashMapBenchmark.testOdstrániť priem. 20 0,010 ± 0,001

Ako vidíme, čísla dokazujú O (1) konštantný čas na spustenie vyššie uvedených metód. Teraz urobme porovnanie HashMap výsledky testov u druhého Mapa inštančné skóre.

Pre všetky uvedené metódy máme O (1) pre HashMap, LinkedHashMap, IdentityHashMap, WeakHashMap, EnumMap a ConcurrentHashMap.

Uveďme výsledky zostávajúcich výsledkov testov vo forme jednej tabuľky:

Benchmark LinkedHashMap IdentityHashMap WeakHashMap ConcurrentHashMap testContainsKey 0,008 0,009 0,014 0,011 testZískať 0,011 0,109 0,019 0,012 testZadať 0,020 0,013 0,020 0,031 testOdstrániť 0,011 0,115 0,021 0,019

Z výstupných čísel môžeme potvrdiť tvrdenia o O (1) časová zložitosť.

4.2. Testovanie O (log (n)) Operácie

Pre stromovú štruktúru TreeMap a ConcurrentSkipListMap the put (), get (), remove (), containsKey () prevádzkový čas je O (log (n)).

Tu, chceme sa uistiť, že naše testy výkonu budú prebiehať približne v logaritmickom čase. Z tohto dôvodu inicializujeme mapy pomocou n=1000, 10,000, 100,000, 1,000,000 položky nepretržite.

V tomto prípade nás zaujíma celková doba vykonania:

počet položiek (n) 1 000 10 000 100 000 1 000 000 všetky testy celkové skóre 00:03:17 00:03:17 00:03:30 00:05:27 

Kedy n = 1 000 máme spolu 00:03:17 milisekundový čas vykonania. n = 10 000 čas je takmer nezmenený 00:03:18 ms. n = 100 000 má malý nárast 00:03:30. A nakoniec, keď n = 1 000 000 beh sa dokončuje 00:05:27 ms.

Po porovnaní runtime čísel s denník (n) funkcie každého n, môžeme potvrdiť, že korelácia oboch funkcií sa zhoduje.

5. Nastaviť

Spravidla Nastaviť je zbierka jedinečných prvkov. Tu preskúmame HashSet, LinkedHashSet, EnumSet, TreeSet, CopyOnWriteArraySet, a ConcurrentSkipListSet implementácie Nastaviť rozhranie.

Pre lepšie pochopenie vnútorných častí HashSet, táto príručka vám pomôže.

Teraz poďme dopredu a predstavme čísla časovej zložitosti. Pre HashSet, LinkedHashSet, a EnumSet the add (), remove () a obsahuje () prevádzkové náklady konštantné O (1) čas. Vďaka internému HashMap implementácia.

Rovnako tak TreeSetO (log (n)) časová zložitosť pre operácie uvedené pre predchádzajúcu skupinu. Je to kvôli TreeMap implementácia. Časová zložitosť pre ConcurrentSkipListSet je tiež O (log (n)) čas, pretože je založený na dátovej štruktúre zoznamov preskočených.

Pre CopyOnWriteArraySet, the add (), remove () a obsahuje () metódy majú O (n) priemernú časovú zložitosť.

5.1. Skúšobné metódy

Teraz prejdime k našim testom benchmarku:

@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); }

Ďalej ponecháme zostávajúce konfigurácie referenčných hodnôt tak, ako sú.

5.2. Porovnávanie čísel

Pozrime sa na správanie skóre vykonávania za behu pre HashSet a LinkedHashSet majúce n = 1 000; 10 000; 100 000 položky.

Pre HashSet, čísla sú:

Referenčná hodnota 1 000 10 000 100 000. Pridať () 0,026 0,023 0,024. Odstrániť () 0,009 0,009 0,009 .obsahuje () 0,009 0,009 0,010

Podobne výsledky pre LinkedHashSet sú:

Referenčná hodnota 1 000 10 000 100 000. Pridať () 0,022 0,026 0,027. Odstrániť () 0,008 0,012 0,009 .obsahuje () 0,008 0,013 0,009

Ako vidíme, skóre zostáva pri každej operácii takmer rovnaké. O to viac, keď ich porovnáme s HashMap testovacie výstupy, vyzerajú rovnako.

Vo výsledku potvrdzujeme, že všetky testované metódy prebiehajú konštantne O (1) čas.

6. Záver

V tomto článku uvádzame časovú zložitosť najbežnejších implementácií dátových štruktúr Java.

Samostatne ukazujeme skutočný výkon za behu každého typu kolekcie prostredníctvom testov JVM benchmark. Porovnali sme tiež výkonnosť tých istých operácií v rôznych kolekciách. Vďaka tomu sa naučíme vyberať si správnu kolekciu, ktorá vyhovuje našim potrebám.

Úplný kód tohto článku je ako obvykle k dispozícii na serveri GitHub.


$config[zx-auto] not found$config[zx-overlay] not found