Neden bir Listeyi yinelemek, onu indekslemekten daha hızlı olsun?


125

ADT Listesi için Java belgelerini okurken şunu söylüyor:

Liste arabirimi, liste öğelerine konumsal (indeksli) erişim için dört yöntem sağlar. Listeler (Java dizileri gibi) sıfır tabanlıdır. Bu işlemlerin, bazı uygulamalar için (örneğin LinkedList sınıfı) dizin değeriyle orantılı olarak zaman içinde yürütülebileceğini unutmayın. Bu nedenle, arayan kişi uygulamayı bilmiyorsa, bir listedeki öğeler üzerinde yineleme yapmak, tipik olarak liste aracılığıyla indekslemeye tercih edilir.

Bu tam olarak ne anlama geliyor? Çıkarılan sonucu anlamıyorum.


12
Bunun genel durumunu anlamanıza yardımcı olabilecek başka bir örnek de Joel Spolsky'nin " Temel Bilgiye Dönüş " adlı makalesi - "Shlemiel the ressamın algoritması" için arama yapın.
bir CVn

Yanıtlar:


211

Bağlantılı bir listede, her öğenin sonraki öğeye bir göstericisi vardır:

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

Erişmek için item3, size ITEM3 ulaşana kadar doğrudan atlayamam beri her düğümden geçen kafasından yürümek gerektiğini açıkça görebiliriz.

Böylece, her bir öğenin değerini yazdırmak istersem, şunu yazarsam:

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

ne olur:

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

Bu korkunç derecede verimsizdir çünkü her indekslediğinizde listenin başından yeniden başlar ve her öğenin üzerinden geçer. Bu, karmaşıklığınızın etkili bir şekilde O(N^2)listeyi aşmak olduğu anlamına gelir !

Bunun yerine şunu yaptım:

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

sonra ne olur:

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

hepsi tek bir geçişte, yani O(N).

Şimdi, diğer uygulanmasına gidiyor Listhangi ArrayList, bu bir basit dizide tarafından desteklenmektedir. Bu durumda, yukarıdaki çapraz geçişlerin her ikisi de eşdeğerdir, çünkü bir dizi bitişiktir, bu nedenle rastgele konumlara rastgele atlamalara izin verir.


29
Küçük uyarı: LinkedList, indeks listenin sonraki yarısındaysa listenin sonundan arayacaktır, ancak bu gerçekten temel verimsizliği değiştirmez. Sadece biraz daha az sorunlu hale getiriyor.
Joachim Sauer

8
Bu korkunç derecede verimsiz . Daha büyük LinkedList için -Evet, daha küçük için daha hızlı çalışabilir REVERSE_THRESHOLD18 inç ayarlanmıştır java.util.Collections, açıklama olmadan bu kadar yükseltilmiş cevabı görmek garip.
bestsss

1
@DanDiplo, yapı Bağlantılıysa, evet doğrudur. LinkedS yapılarını kullanmak ise küçük bir muamma. Neredeyse her zaman dizi destekli olanlardan çok daha kötü performans gösterirler (ekstra bellek ayak izi, gc-düşmanca ve korkunç yerellik). C # 'daki standart liste dizi desteklidir.
bestsss

3
Minör uyarı: kullanılan (Iterator / foreach vs endeksli) hangi yineleme tipi kontrol etmek isterseniz, istediğiniz zaman testi eğer Liste uygular RandomAccess (bir işaretleyici arayüzü):List l = unknownList(); if (l instanceof RandomAccess) /* do indexed loop */ else /* use iterator/foreach */
afk5min

1
@ KK_07k11A0585: Aslında ilk örneğinizdeki geliştirilmiş for döngüsü, ikinci örnekteki gibi bir yineleyicide derlenmiştir, dolayısıyla eşdeğerdirler.
Tudor

35

Cevap burada ima ediliyor:

Bu işlemlerin, bazı uygulamalar için (örneğin LinkedList sınıfı) dizin değeriyle orantılı olarak zaman içinde yürütülebileceğini unutmayın.

Bağlı bir listenin içsel bir indeksi yoktur; arama .get(x), liste uygulamasının ilk girişi bulmasını ve .next()x-1 kez (bir O (n) veya doğrusal zaman erişimi için) çağırmasını gerektirir , burada bir dizi destekli listenin sadece backingarray[x]O (1) veya sabit zamanda indeksleyebileceği .

JavaDoc'aLinkedList bakarsanız , yorumu göreceksiniz

Tüm işlemler, çift bağlantılı bir liste için beklenebileceği gibi çalışır. Listeye endeksleyen işlemler, hangisi belirtilen dizine daha yakınsa, listeyi başından veya sonundan geçirir.

oysa JavaDoc'u içinArrayList karşılık gelen yer alır

Liste arayüzünün yeniden boyutlandırılabilir dizi uygulaması. Tüm isteğe bağlı liste işlemlerini uygular ve null dahil tüm öğelere izin verir. Liste arabirimini uygulamaya ek olarak, bu sınıf, listeyi depolamak için dahili olarak kullanılan dizinin boyutunu işlemek için yöntemler sağlar. (Bu sınıf, senkronize edilmemiş olması dışında kabaca Vector'e eşdeğerdir.)

size, isEmpty, get, set, iterator, Ve listIteratoroperasyonlar sabit zamanda çalıştırın. Ekleme işlemi amortize edilmiş sabit zamanda çalışır, yani n eleman eklemek O (n) zaman gerektirir. Diğer tüm işlemler doğrusal zamanda çalışır (kabaca konuşursak). Sabit faktör, uygulamaya göre düşüktür LinkedList.

Bir "Java Koleksiyonları Framework için büyük-O Özet" başlıklı ilgili soru bu kaynağa verilen bir cevaptır işaret vardır "Java Koleksiyonları JDK6" yararlı bulabileceğiniz.


7

Kabul edilen cevap kesinlikle doğru olsa da, küçük bir kusura işaret edebilir miyim? Tudor'dan alıntı yapmak:

Şimdi, List'in diğer bir uygulaması olan ArrayList'e gidelim, o basit bir dizi ile destekleniyor. Bu durumda, yukarıdaki çapraz geçişlerin her ikisi de eşdeğerdir , çünkü bir dizi bitişiktir, bu nedenle rastgele konumlara rastgele atlamalara izin verir.

Bu tamamen doğru değil. Gerçek şu ki

Bir ArrayList ile, elle yazılmış sayılan döngü yaklaşık 3 kat daha hızlıdır

kaynak: Performans için Tasarım, Google'ın Android dokümanı

El yazısı döngünün indekslenmiş yinelemeyi ifade ettiğini unutmayın. Döngüler için geliştirilmiş ile kullanılan yineleyici yüzünden olduğundan şüpheleniyorum. Bitişik bir diziyle desteklenen bir yapıda, cezada küçük bir performans üretir. Bunun Vector sınıfı için de geçerli olabileceğinden şüpheleniyorum.

Benim kuralım, mümkün olduğunda gelişmiş for döngüsünü kullanın ve performansı gerçekten önemsiyorsanız, dizine alınmış yinelemeyi yalnızca Dizi Listeleri veya Vektörler için kullanın. Çoğu durumda, bunu bile göz ardı edebilirsiniz - derleyici bunu arka planda optimize ediyor olabilir.

Android'deki geliştirme bağlamında, ArrayLists'in her iki geçişinin de mutlaka eşdeğer olmadığını belirtmek istiyorum . Düşünce için sadece yemek.


Kaynağınız yalnızca Anndroid. Bu, diğer JVM'ler için de geçerli mi?
Matsemann

Tamamen emin değilim, ancak yine de, gelişmiş for döngüleri kullanmak çoğu durumda varsayılan olmalıdır.
Dhruv Gairola

Bana mantıklı geliyor, bir dizi kullanan bir veri yapısına erişirken tüm yineleyici mantığından kurtulmak daha hızlı çalışıyor. 3 kat daha hızlı mı bilmiyorum ama kesinlikle daha hızlı.
Setzer22

7

Arama için bir ofset içeren bir liste üzerinde yineleme yapmak, ressamın algoritması Shlemiel'ei benzer .

Shlemiel, yolun ortasındaki noktalı çizgileri boyayarak sokak ressamı olarak işe girer. İlk gün yola bir kutu boya götürür ve yolun 300 metresini bitirir. "Bu oldukça iyi!" patronu "sen hızlı bir işçisin!" ve ona bir kopek ödüyor.

Ertesi gün Shlemiel sadece 150 yarda bitti. "Pekala, bu dünkü kadar iyi değil, ama yine de hızlı bir işçisiniz. 150 yarda saygın bir yer," ve ona bir kopek ödüyor.

Ertesi gün Shlemiel yolun 30 metreyi boyar. "Sadece 30!" patronu bağırır. "Bu kabul edilemez! İlk gün on kat bu kadar iş yaptınız! Neler oluyor?"

"Engel olamıyorum" diyor Shlemiel. "Boya kutusundan her gün daha da uzaklaşıyorum!"

Kaynak .

Bu küçük hikaye, içeride neler olup bittiğini ve neden bu kadar verimsiz olduğunu anlamayı kolaylaştırabilir.


4

Bir gerçeklemenin i'inci elemanını bulmak, i-inci'ye LinkedListkadar tüm elemanlardan geçer.

Yani

for(int i = 0; i < list.length ; i++ ) {
    Object something = list.get(i); //Slow for LinkedList
}
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.