Чому повторення над списком було б швидшим, ніж індексація через нього?


125

Читаючи документацію Java для списку ADT, вона говорить:

Інтерфейс списку надає чотири способи позиційного (індексованого) доступу до елементів списку. Списки (як масиви Java) базуються на нулі. Зауважте, що ці операції можуть виконуватись у часі пропорційно значенню індексу для деяких реалізацій (наприклад, клас LinkedList). Таким чином, ітерація над елементами списку, як правило, є кращою для індексації через нього, якщо абонент не знає реалізації.

Що саме це означає? Я не розумію зробленого висновку.


12
Інший приклад, який може допомогти вам зрозуміти загальний випадок цього, - стаття Джоела Спольського "Назад до основ" - пошук "Алгоритму художника Шлеміеля".
CVn

Відповіді:


211

У пов'язаному списку кожен елемент має вказівник на наступний елемент:

head -> item1 -> item2 -> item3 -> etc.

Для доступу item3ви чітко бачите, що вам потрібно ходити з голови через кожен вузол, поки не досягнете пункт 3, оскільки ви не можете перестрибувати безпосередньо.

Таким чином, якщо я хотів надрукувати значення кожного елемента, якщо я записую це:

for(int i = 0; i < 4; i++) {
    System.out.println(list.get(i));
}

що відбувається це:

head -> print head
head -> item1 -> print item1
head -> item1 -> item2 -> print item2
head -> item1 -> item2 -> item3 print item3

Це жахливо неефективно, тому що кожного разу, коли ви його індексуєте, він перезапускається з початку списку та проходить кожен пункт. Це означає, що ваша складність ефективно O(N^2)лише для просування списку!

Якщо замість цього я зробив це:

for(String s: list) {
    System.out.println(s);
}

то, що відбувається, це таке:

head -> print head -> item1 -> print item1 -> item2 -> print item2 etc.

все в єдиному обході, який є O(N).

Тепер переходимо до іншої реалізації, Listяка полягає в тому ArrayList, що це підтримується простим масивом. У цьому випадку обидва вищеперелічені траверси рівнозначні, оскільки масив є суміжним, тому він дозволяє випадковим стрибкам у довільні положення.


29
Незначне повідомлення: LinkedList здійснить пошук з кінця списку, якщо індекс знаходиться в пізнішій половині списку, але це насправді не змінює основної неефективності. Це робить це лише трохи менш проблематично.
Йоахім Зауер

8
Це жахливо неефективно . Для більших LinkedList - так, для менших він може працювати швидше REVERSE_THRESHOLD, встановлений 18 java.util.Collectionsдюймів, дивно бачити настільки виправдану відповідь без зауваження.
bestsss

1
@DanDiplo, якщо структура пов'язана, так, вона відповідає дійсності. Використання структур LinkedS, однак, є невеликою таємницею. Вони майже завжди працюють набагато гірше, ніж ті, що підтримуються масивом (додатковий слід пам’яті, непривабливість gc та жахлива локальність). Стандартний список у C # є резервним масивом.
bestsss

3
Незначне повідомлення: Якщо ви хочете перевірити, який тип ітерації слід використовувати (індексовано проти Iterator / foreach), ви завжди можете перевірити, чи список реалізує RandomAccess (інтерфейс маркера):List l = unknownList(); if (l instanceof RandomAccess) /* do indexed loop */ else /* use iterator/foreach */
afk5min

1
@ KK_07k11A0585: Насправді розширений цикл у вашому першому прикладі складається в ітератор, як у другому прикладі, тому вони є еквівалентними.
Тудор

35

Відповідь мається на увазі тут:

Зауважте, що ці операції можуть виконуватись у часі пропорційно значенню індексу для деяких реалізацій (наприклад, клас LinkedList)

Зв'язаний список не має властивого індексу; виклику .get(x)потребують здійснення списку , щоб знайти перший запис і виклик .next()х-1 рази (для O (N) або лінійний час доступу), де масив спинками список може тільки індекс в backingarray[x]в O (1) або постійне час.

Якщо ви подивитеся на JavaDoc дляLinkedList , ви побачите коментар

Усі операції виконуються так, як можна було очікувати для подвійно пов'язаного списку. Операції, що індексують список, перейдуть до списку від початку або до кінця, залежно від того, що ближче до зазначеного індексу.

тоді як JavaDoc дляArrayList має відповідне

Реалізація змінного масиву інтерфейсу List. Реалізує всі необов'язкові операції зі списком та дозволяє всі елементи, включаючи null. Крім реалізації інтерфейсу List, цей клас надає методи маніпулювання розміром масиву, який використовується внутрішньо для зберігання списку. (Цей клас приблизно еквівалентний Vector, за винятком того, що він не синхронізований.)

size, isEmpty, get, set, iterator, І listIteratorоперації виконуються в постійна час. Операція додавання працює в постійному амортизованому часі, тобто додавання n елементів вимагає часу O (n). Усі інші операції виконуються в лінійний час (грубо кажучи). Постійний коефіцієнт низький порівняно з показником для LinkedListреалізації.

Відповідне запитання під назвою "Підсумок Big-O для Java Collections Framework" містить відповідь, що вказує на цей ресурс, "Java Collections JDK6", який може бути корисним.


7

Хоча прийнята відповідь, безумовно, правильна, можу зазначити незначний недолік. Цитуючи Тюдора:

Тепер переходимо до іншої реалізації списку, який є ArrayList, що підтримується простим масивом. У цьому випадку обидва вищеперелічені траверси рівнозначні , оскільки масив є суміжним, тому він дозволяє випадковим стрибкам у довільні положення.

Це не зовсім вірно. Правда така

За допомогою ArrayList рукописний відлічений цикл приблизно в 3 рази швидший

джерело: Проектування для продуктивності, Android-документ Google для Android

Зауважте, що рукописний цикл відноситься до індексованої ітерації. Я підозрюю його через ітератор, який використовується з розширеними для циклів. Це призводить до незначної ефективності виконання покарань у структурі, підкріпленій суміжним масивом. Я також підозрюю, що це може бути правдою для класу Vector.

Моє правило: використовувати розширений цикл, коли це можливо, і якщо ви дійсно дбаєте про продуктивність, використовуйте індексовану ітерацію лише для ArrayLists або Vectors. У більшості випадків це можна навіть ігнорувати - компілятор може оптимізувати це у фоновому режимі.

Я просто хочу зазначити, що в контексті розвитку в Android обидва обходи ArrayLists не обов'язково є рівнозначними . Просто їжа для роздумів.


Ваше джерело - лише Anndroid. Чи справедливо це для інших спільних машин?
Мацеманн

Не повністю впевнений, tbh, але знову ж таки, використання розширених для циклів повинно бути за замовчуванням у більшості випадків.
Друв Гайрола

Для мене це має сенс, позбавлення від усієї логіки ітератора під час доступу до структури даних, яка використовує масив, працює швидше. Я не знаю, якщо в 3 рази швидше, але, звичайно, швидше.
Setzer22

7

Ітерація над списком із зміщенням для пошуку, наприклад i, аналогічна алгоритму художника Шлеміеля .

Шлемієль влаштовується на роботу вуличним живописцем, малюючи пунктирними лініями посеред дороги. У перший день він виймає консервну банку на дорогу і закінчує 300 ярдів дороги. "Це досить добре!" каже його начальник, "ти швидкий працівник!" і платить йому копію.

Наступного дня Шлемієль лише зробить 150 ярдів. "Ну, це не так добре, як вчора, але ти все ще швидкий працівник. 150 ярдів є поважними", і платить йому копійку.

Наступного дня Шлемієль фарбує 30 ярдів дороги. "Тільки 30!" кричить його начальник. "Це неприпустимо! У перший день ви зробили десять разів більше роботи! Що відбувається?"

"Я не можу в цьому допомогти", - каже Шлемієль. "З кожним днем ​​я стаю все далі і далі від банки фарби!"

Джерело .

Ця маленька історія може полегшити розуміння того, що відбувається всередині і чому це настільки неефективно.


4

Щоб знайти i-й елемент a LinkedList реалізації реалізує всі елементи аж до i-го.

Так

for(int i = 0; i < list.length ; i++ ) {
    Object something = list.get(i); //Slow for LinkedList
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.