Bağlantılı bir düğüm yapısının değişmez olmasının pratik bir yolu var mı?


26

Tek bağlantılı bir liste yazmaya karar verdim ve iç bağlantılı düğüm yapısını değiştirilemez hale getirme planım vardı.

Yine de bir engelle karşılaştım. Aşağıdaki bağlantılı düğümlere sahip olduğumu söyleyin (önceki addişlemlerden):

1 -> 2 -> 3 -> 4

ve eklemek istediğimi söyle 5.

Bunu yapmak için, düğüm 4değişmez olduğu için, yeni bir kopyasını oluşturmam gerekiyor 4, ancak nextalanını a içeren yeni bir düğümle değiştirmem gerekiyor 5. Şimdi sorun, 3eskisine gönderme yapmak 4; ekli olmayan 5. Şimdi kopyaya başvurmalı ve kopyayı referans almak için 3kendi nextalanını değiştirmem gerekiyor 4, ama şimdi 2eskisine gönderme yapıyor 3...

Başka bir deyişle, bir ekleme yapmak için, tüm listenin kopyalanması gerekiyor gibi görünüyor.

Sorularım:

  • Düşüncem doğru mu? Tüm yapıyı kopyalamaksızın ekleme yapmanın bir yolu var mı?

  • Görünüşe göre "Etkili Java" tavsiyeyi içeriyor:

    Sınıflar değişken olmalarını sağlamak için çok iyi bir neden olmadıkça değişmez olmalıdır ...

    Bu değişkenlik için iyi bir durum mu?

Bunun önerilen yanıtın bir kopyası olduğunu sanmıyorum çünkü listenin kendisinden bahsetmiyorum; arayüze uymak için değişken olması gerektiği açıktır (yeni listeyi dahili olarak tutmak ve bir alıcı aracılığıyla almak gibi bir şey yapmadan yapın. İkinci düşünceye rağmen, biraz mutasyon gerektirecek olsa bile, sadece minimumda tutulacaktır). Listenin iç kısımlarının değişmez olması gerekip gerekmediğinden bahsediyorum.


Evet diyebilirim - Java'daki nadir görülen değişmez koleksiyonlar ya atmak için geçersiz kılınan mutasyon yöntemleriyle ya da CopyOnWritexxxçoklu iş parçacığı için kullanılan inanılmaz pahalı sınıflardır. Kimse koleksiyonların gerçekten değişmez olmasını beklemiyor (bazı tuhaflıklar
yaratsa da

9
(Alıntıya yorum yapın.) Bu, büyük bir alıntı, çoğu zaman büyük ölçüde yanlış anlaşılır. Ölçülebilirlik , faydalarından dolayı ValueObject'e uygulanmalıdır , ancak Java'da geliştiriyorsanız, değişkenliğin beklendiği yerlerde değişmezliği kullanmak pratik değildir. Bir sınıfın çoğu zaman değişmez olması gereken belirli yönleri ve gereksinimlere dayalı değişebilir olması gereken bazı yönleri vardır.
rwong,

2
ilgili (muhtemelen bir yinelenen): Koleksiyon nesnesi değişmez mi daha iyi? "Performans ek yükleri olmadan, bu koleksiyon (sınıf LinkedList) değişmez koleksiyon olarak tanıtılabilir mi?"
tatarcık

Neden değişmez bir bağlantılı liste oluşturdunuz? Bağlantılı listenin en büyük yararı değişkenliktir, bir diziden daha iyi olmaz mıydınız?
Pieter B

3
Lütfen gereksinimlerinizi anlamama yardım eder misiniz? Mutasyona uğratmak istediğiniz değişmez bir listeye mi sahip olmak istiyorsunuz? Bu bir çelişki değil mi? Listedeki "içliler" ile ne demek istiyorsun? Önceden eklenmiş düğümlerin daha sonra değiştirilemez olması değil, yalnızca listedeki son düğüme eklenmesine izin vermesi gerektiğini mi söylüyorsunuz?
boş

Yanıtlar:


46

İşlevsel dillerdeki listelerde, neredeyse her zaman bir baş ve kuyruk, listenin ilk öğesi ve kalan kısmı ile çalışırsınız. Hazırlama çok daha yaygındır, çünkü eklediğiniz gibi eklemek listenin tamamının (veya bağlantılı bir listeye tam olarak benzemeyen diğer tembel veri yapılarının) kopyalanmasını gerektirir.

Zorunlu dillerde, eklemek çok daha yaygındır, çünkü anlamsal olarak daha doğal hissetme eğilimindedir ve listenin önceki sürümlerine yapılan referansları geçersiz kılmakla ilgilenmezsiniz.

Ön hazırlamanın neden tüm listenin kopyalanmasını gerektirmediğinin bir örneği olarak, şunlara sahip olduğunuzu düşünün:

2 -> 3 -> 4

Size bir hazırlık hazırlamak 1:

1 -> 2 -> 3 -> 4

Ancak bir başkasının hala 2listesinin başı olarak referansta bulunup bulunmadığı önemli değildir , çünkü liste değişmezdir ve bağlantılar sadece bir yönden gider. 1Sadece bir referansınız varsa bile orada olduğunu söylemenin bir yolu yok 2. Şimdi, 5herhangi bir listeye eklediyseniz , tüm listenin bir kopyasını çıkarmanız gerekir, çünkü aksi halde diğer listede de görünür.


Ahh, hazırlığın neden bir kopya gerektirmediğine dair iyi bir nokta; Bunu düşünmedim.
Kanserojen

17

Haklısınız, ekleme yapmak istediğiniz tüm düğümleri yerinde düzenlemek istemiyorsanız listenin tamamını kopyalamanızı gerektirir. Zira nextdeğişmez bir ortamda yeni bir düğüm yaratan (şimdi) ikinci-son düğümün nextimlecini belirlememiz gerekir ve sonra üçüncü-son düğümün imlecini ayarlamamız gerekir .

Buradaki temel problemin değişmezlik olmadığını ve appendoperasyonun tavsiye edilmediğini düşünüyorum . Her ikisi de kendi alanlarında mükemmel derecede iyidir. Bunları karıştırmak kötüdür: Değişmez bir liste için doğal (verimli) arabirim, listenin önündeki manipülasyonu vurguladı;

Bu nedenle karar vermenizi öneririm: geçici bir arayüz mü yoksa kalıcı bir arayüz mü istiyorsunuz? Çoğu işlem yeni bir liste oluşturur mu ve değiştirilmemiş bir sürümü erişilebilir (kalıcı) halde bırakır mı ya da böyle bir kod mu yazarsınız?

list.append(x);
list.prepend(y);

Her iki seçenek de iyidir, ancak uygulama arayüzü yansıtmalıdır: Kalıcı bir veri yapısı değişmez düğümlerden faydalanırken, geçici bir yapının örtük olarak verdiği performansı yerine getirmek için içsel değişkenliğe ihtiyacı vardır. java.util.Listve diğer arayüzler geçicidir: Bunları değişmez bir listeye uygulamak uygun değildir ve aslında bir performans tehlikesidir. Değişken veri yapıları üzerindeki iyi algoritmalar, değişmez veri yapıları üzerindeki iyi algoritmalardan genellikle oldukça farklıdır, bu nedenle değişmez bir veri yapısını değişken bir (veya tersi) olarak giydirmek kötü algoritmaları davet eder.

Kalıcı bir liste bazı dezavantajlara sahip olsa da (etkin bir şekilde eklenemez), bunun işlevsel olarak programlanırken ciddi bir sorun olması gerekmez: Birçok algoritma, zihniyeti değiştirerek ve mapveya gibi foldiki sıralı ilkel isimleri kullanmak için yüksek dereceli fonksiyonlar kullanarak verimli bir şekilde formüle edilebilir. ) veya tekrar tekrar hazırlayarak . Ayrıca, hiç kimse sizi yalnızca bu veri yapısını kullanmaya zorlamıyor: Diğerleri (geçici veya kalıcı ancak daha karmaşık) daha uygun olduğunda, onları kullanın. Kalıcı listelerin diğer iş yükleri için bazı avantajları olduğunu da belirtmeliyim: Hafızayı koruyabilecek kuyruklarını paylaşırlar.


4

Eğer tek bir bağlantılı listeniz varsa, arka kısımdan daha çok çalıştıysanız, cepheyle birlikte çalışacaksınız.

Prolog ve haskel gibi fonksiyonel diller, ön öğeyi ve dizinin geri kalanını elde etmek için kolay yollar sağlar. Arkaya eklemek, her düğümü kopyalayan bir O (n) işlemidir.


1
Haskell kullandım. Afaik, aynı zamanda tembellik kullanarak sorunun bir kısmını da yok ediyor. Ekler yapıyorum, çünkü Listarayüzden beklenenin bu olduğunu düşündüm (orada olsa yanlış olabilirim). Bu bitin soruyu gerçekten cevapladığını sanmıyorum. Tüm listenin hala kopyalanması gerekiyor; tam bir geçiş gerekli olmayacağından, sadece son eklenen öğeye daha hızlı erişim sağlayacaktır.
Kanserojen

Altta yatan veri yapısı / algoritmasıyla uyuşmayan bir arayüze göre bir sınıf oluşturmak acı ve verimsizliklere karşı bir davettir.
cırcır ucube

JavaDocs açıkça "append" kelimesini kullanır. Uygulama uğruna bunu görmezden gelmenin daha iyi olduğunu mu söylüyorsun?
Kanserojen

2
@Carcigenicate hayır ben denemek ve kalıba değişmez düğümlerle bir tek başına bağlantılı listesi uygun bir hatadır söylüyorumjava.util.List
mandal ucube

1
Tembellik ile artık bağlantılı bir liste olmazdı; liste yoktur, dolayısıyla mutasyon yoktur (ekler). Bunun yerine istek üzerine bir sonraki öğeyi oluşturan bir kod var. Ancak bir listenin kullanıldığı her yerde kullanılamaz. Yani, Java’da argüman olarak geçen bir listeyi değiştirmeyi bekleyen bir yöntem yazarsanız , o listenin ilk önce değişebilir olması gerekir. Jeneratör yaklaşımı, çalışmasını sağlamak ve listenin fiziksel olarak kaldırılmasını mümkün kılmak için eksiksiz bir yeniden yapılandırma (yeniden düzenleme) gerektirir.
rwong

4

Diğerlerinin de belirttiği gibi, değişmez bir tekli bağlantılı listenin, bir ekleme işlemi gerçekleştirilirken tüm listenin kopyalanmasını gerektirdiği konusunda haklısınız.

Genellikle, algoritmanızı uygulama (geçici cons) işlemleri açısından uygulamak için geçici çözümü kullanabilir ve ardından son listeyi bir kez tersine çevirebilirsiniz. Bunun yine de listeyi bir kez kopyalaması gerekir, ancak karmaşıklık ek yükü listenin uzunluğu boyunca doğrusaldır, oysa eki kullanarak art arda kullanarak kolayca ikinci dereceden bir karmaşıklık elde edebilirsiniz.

Fark listeleri (örneğin bkz burada ) ilginç bir alternatiftir. Bir fark listesi bir listeyi sarar ve sabit zamanda bir ekleme işlemi sağlar. Temel olarak, eklemek istediğiniz sürece sarıcıyla çalışırsınız ve tamamladığınızda bir listeye geri dönersiniz. Bu bir şekilde StringBuilderbir dize oluşturmak için a kullandığınızda yaptığınız gibi ve sonuçta Stringarayarak sonucu (değişmez!) Alırsınız toString. Bir fark, StringBuildera'nın değişebilir olmasıdır, ancak bir fark listesi değişmezdir. Ayrıca, bir fark listesini tekrar listeye dönüştürdüğünüzde, yine de tüm yeni listeyi oluşturmanız gerekir, ancak yine de bunu yalnızca bir kez yapmanız gerekir.

DListHaskell'inkine benzer bir arayüz sağlayan değişmez bir sınıf uygulamak oldukça kolay olmalı Data.DList.


4

Bu harika videoyu 2015 React conf by Immutable.js 'nin yaratıcısı Lee Byron tarafından izlemelisiniz . Size verimli bir değişmez liste nasıl uygulanacağını anlamak için işaretçiler ve yapıları verecek yok yinelenen içerik. Temel fikirler şunlardır: - iki liste aynı düğümleri kullandığı sürece (aynı değer, aynı sonraki düğüm), aynı düğüm kullanılır - listeler farklılaşmaya başladığında, işaretçilerin işaretini tutan diverjans düğümünde bir yapı oluşturulur. her listenin bir sonraki özel düğümü

Bir tepki öğreticisinden alınan bu görüntü, kırılmış İngilizcemden daha net olabilir:görüntü tanımını buraya girin


2

Kesinlikle Java değil, ama bu makaleyi Scala'da yazılmış, değişmez, ancak performans endeksli kalıcı bir veri yapısı hakkında okumanızı öneriyorum:

http://www.codecommit.com/blog/scala/implementing-persistent-vectors-in-scala

Scala veri yapısı olduğundan, Java'dan da kullanılabilir (biraz daha ayrıntılı). Clojure'da bulunan bir veri yapısını temel alır ve ben de onu sunan daha "yerli" bir Java kütüphanesi olduğundan eminim.

Aynı zamanda, değişmez veri yapılarının oluşturulmasına ilişkin bir not : genellikle istediğiniz, "yapım aşamasında" veri yapınızı, ona elemanlar ekleyerek (tek bir iş parçacığı içinde) "mutasyona sokmanıza" izin veren bir tür "Oluşturucu"; eklemeyi tamamladıktan sonra, "inşaat" nesnesine .build()veya nesneyi .result()"oluşturan" nesneye çağırır , sonra da güvenle paylaşabileceğiniz değişmez bir veri yapısı verir.


1
PersistentVector'un Java bağlantı noktaları hakkında stackoverflow.com/questions/15997996/…
James_pic,

1

Bazen yardımcı olabilecek bir yaklaşım, iki liste tutma nesnesi sınıfına sahip olmaktır - finalileriye bağlanmış bir listedeki ilk düğüme başvuru yapan bir ileri liste nesnesi ve başlangıçta boş olmayan bir finalreferans -null), aynı öğeleri ters sırada tutan bir ters-liste nesnesini finalve bir ters-bağlantılı listenin son maddesine referansla bir ters-liste nesnesini ve (ne zaman ne -null) aynı öğeleri ters sırada tutan bir ileri-liste nesnesini tanımlar.

Bir ileri listeye bir madde prepending veya sadece düğüm bağlantıları tarafından tanımlanan yeni bir düğüm oluşturma gerektirecektir ters-listeye bir madde ekleme finala, referans ve orijinal olarak aynı türde yeni bir liste nesnesi yaratmak finalreferans Bu yeni düğüme.

Bir öğeyi bir ileri listeye eklemek veya bir geri listeye hazırlamak, tam tersi bir listeye sahip olmayı gerektirir; belirli bir liste ile ilk kez yapıldığında, karşıt tipte yeni bir nesne yaratmalı ve bir referansı saklamalıdır; İşlemi tekrarlamak listeyi tekrar kullanmalıdır.

Bir liste nesnesinin dış durumunun, karşı tür bir listeye başvurusunun null olduğu ya da bir karşı sıra listesinin tanımlanmasıyla aynı olacağına dikkat edin. Her finaliş parçacığı finaliçeriğinin tam bir kopyasına başvuruda bulunacağından, çok iş parçacıklı kod kullanırken bile herhangi bir değişken yapmanıza gerek kalmayacaktır .

Bir iş parçacığı üzerindeki kod, listenin ters bir kopyasını oluşturur ve önbelleğe alırsa ve bu kopyanın önbelleğe alındığı alan geçici değilse, başka bir iş parçasındaki kodun önbelleğe alınmış listeyi görmemesi, ancak yalnızca ters etki görmesi mümkün olabilir Bundan, diğer iş parçacığı listesinin başka bir ters kopyası ekleyerek ekstra iş yapmak zorunda kalacaktı. Bu tür bir eylem en kötü verimi olumsuz etkileyebileceği, ancak doğruluğu etkilemeyeceği ve volatiledeğişkenlerin kendi başlarına verimlilik bozuklukları yarattığı için, değişkenin geçici olmaması ve zaman zaman gereksiz işlemlerin olasılığını kabul etmesi daha iyi olacaktır.

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.