Std :: list :: reverse neden O (n) karmaşıklığına sahiptir?


192

Neden std::listC ++ standart kitaplığında sınıf için ters işlevi doğrusal çalışma zamanı var? Çift bağlantılı listelerde ters fonksiyonun O (1) olması gerektiğini düşünürdüm.

Çift bağlantılı bir listeyi tersine çevirmek sadece baş ve kuyruk işaretleyicilerini değiştirmeyi içermelidir.


18
İnsanların neden bu soruyu küçümsediğini anlamıyorum. Sormak son derece makul bir soru. Çift bağlantılı bir listenin ters çevrilmesi O (1) zamanını alacaktır.
Meraklı

46
ne yazık ki, bazı insanlar "soru iyidir" kavramlarını "sorunun iyi bir fikri vardır" ile karıştırmaktadır. "Anlayışım yaygın olarak kabul edilen bir uygulamadan farklı görünüyor, lütfen bu çatışmayı çözmeye yardımcı olun" gibi, bu tür soruları seviyorum, çünkü düşüncelerinizi genişletmek, yolda çok daha fazla sorunu çözmenize yardımcı oluyor! Diğerleri "bu vakaların% 99.9999'unda işleme kaybıdır, düşünmeyin bile" yaklaşımını benimsiyor gibi görünecektir. Eğer herhangi bir teselli varsa, çok, çok daha az için indirildim!
corsiKa

4
Evet, bu soru kalitesi için aşırı miktarda aşağı oy aldı. Muhtemelen Blindy'nin cevabını kim onayladı. Adil olmak gerekirse, "çift bağlantılı bir listeyi tersine çevirmek sadece baş ve kuyruk işaretleyicilerini değiştirmeyi içermelidir" genel olarak herkesin lisede öğrendiği standart bağlantılı liste veya insanların kullandığı birçok uygulama için geçerli değildir. SO insanların soru ya da cevaba ani tepki veren tepkilerin çoğu kez yukarı / aşağı kararını yönlendirir. Eğer bu cümle içinde daha açık olsaydınız ya da atlamış olsaydınız, sanırım daha az downvotes alırsınız.
Chris Beck

3
Ya da sana ispat yükü getireyim, @Curious: Burada iki kat bağlantılı bir liste uygulaması hazırladım : ideone.com/c1HebO . ReverseFonksiyonun O (1) 'de nasıl uygulanacağını belirtebilir misiniz ?
CompuChip

2
@CompuChip: Aslında, uygulamaya bağlı olarak, olmayabilir. Hangi işaretçiyi kullanacağınızı bilmek için fazladan bir boole ihtiyacınız yoktur: sadece size işaret etmeyen birini kullanın ... bu arada XOR'lu bağlantılı bir liste ile otomatik olarak iyi olabilir. Dolayısıyla evet, listenin nasıl uygulandığına bağlıdır ve OP ifadesi açıklığa kavuşturulabilir.
Matthieu M.

Yanıtlar:


187

Varsayımsal reverseolarak O (1) olabilirdi . Bağlantılı listenin yönünün şu anda listenin oluşturulduğu orijinalle aynı mı yoksa karşıt mı olduğunu gösteren (yine varsayımsal olarak) bir boole liste üyesi olabilirdi.

Ne yazık ki, bu temelde diğer tüm işlemlerin performansını azaltacaktır (asimptotik çalışma zamanını değiştirmeden de olsa). Her işlemde, bir bağlantının "sonraki" veya "önceki" işaretçisini izleyip izlemeyeceğini düşünmek için bir boole danışılması gerekir.

Bu muhtemelen nispeten nadir bir işlem olarak kabul edildiğinden, standart (uygulamaları dikte etmeyen, sadece karmaşıklığı dikte eden), karmaşıklığın doğrusal olabileceğini belirtti. Bu, "sonraki" işaretçilerin her zaman açık bir şekilde aynı yönü ifade etmesini sağlayarak, genel durum işlemlerini hızlandırır.


29
@MooseBoys: Benzetmene katılmıyorum. Fark, bir liste halinde, uygulama sağlayabilir olduğu reverseile O(1)karmaşıklığı başka operasyonun büyük o etkilemeden bu boole bayrak hile kullanarak. Ancak, pratik olarak, teknik olarak O (1) olsa bile, her operasyonda ekstra bir dal maliyetlidir. Buna karşılık, sortO (1) ve diğer tüm işlemlerin aynı maliyette olduğu bir liste yapısı oluşturamazsınız . Sorunun amacı, görünüşe göre, O(1)sadece büyük O'yu önemsiyorsanız, ücretsiz olarak tersine dönebilirsiniz, neden bunu yapmadılar.
Chris Beck

9
XOR bağlantılı bir liste kullandıysanız, geri alma süresi sabit hale gelir. Yine de bir yineleyici daha büyük olurdu ve bunu arttırmak / azaltmak, biraz daha hesaplama açısından pahalı olacaktır. Bu, her türlü bağlantılı liste için kaçınılmaz bellek erişimleri tarafından engellenebilir .
Tekilleştirici

3
@IlyaPopov: Her düğümün gerçekten buna ihtiyacı var mı? Kullanıcı asla liste düğümünün kendisiyle ilgili herhangi bir soru sormaz, sadece ana liste gövdesi. Bu nedenle, kullanıcının çağırdığı herhangi bir yöntem için boole erişim kolaydır. Liste tersine çevrilirse yineleyicilerin geçersiz kılınması kuralını yapabilir ve / veya örneğin boole'nin bir kopyasını yineleyici ile depolayabilirsiniz. Bu yüzden mutlaka büyük O'yu etkilemeyeceğini düşünüyorum. İtiraf edeceğim, şartname üzerinden satır satır gitmedim. :)
Chris Beck

4
@Kevin: Hm, ne? Zaten iki işaretçiyi doğrudan xor edemezsiniz, önce onları tamsayılara dönüştürmeniz gerekir (açıkça yazın std::uintptr_t. Sonra onları xor yapabilirsiniz.
Deduplicator

3
@Kevin, kesinlikle C ++ 'da XOR bağlantılı bir liste yapabilirsiniz, bu tür şeyler için pratik olarak poster-çocuk dili. Hiçbir şey kullanmanız gerektiğini söylemez std::uintptr_t, bir chardiziye yayınlayabilir ve ardından bileşenleri XOR yapabilirsiniz. Daha yavaş ama% 100 taşınabilir. Muhtemelen bu iki uygulama arasında seçim yapabilir ve ikincisini yalnızca uintptr_teksikse yedek olarak kullanabilirsiniz . Bazıları bu cevapta açıklanırsa: stackoverflow.com/questions/14243971/…
Chris Beck

61

Bu olabilir olmak O liste “anlamını değiştirmeyi sağlayan bir bayrak saklamak olsaydı (1) prev” ve “ nexther düğüm vardır” işaretçiler. Listeyi tersine çevirmek sık sık yapılan bir işlemse, bu tür bir ekleme aslında yararlı olabilir ve bu listenin uygulanmasının mevcut standart tarafından yasaklanmasının herhangi bir nedeni bilmiyorum . Bununla birlikte, böyle bir bayrağa sahip olmak listenin sıradan geçişini daha pahalı hale getirecektir (yalnızca sabit bir faktörle), çünkü

current = current->next;

içinde operator++liste yineleyici, sana ulaşabilir

if (reversed)
  current = current->prev;
else
  current = current->next;

bu da kolayca eklemeye karar vereceğiniz bir şey değil. Listelerin genellikle ters çevrildiklerinden çok daha sık geçtiği göz önüne alındığında, standardın bu tekniği zorunlu kılması çok mantıklı olmaz . Bu nedenle, ters işlemin doğrusal karmaşıklığa sahip olmasına izin verilir. Bununla birlikte, daha önce de belirtildiği gibi, “optimizasyon” unuzu teknik olarak uygulamanıza izin verilecek şekilde tO (1) ⇒ tO ( n ) olduğunu unutmayın.

Bir Java veya benzer bir arka plandan geliyorsanız, yineleyicinin her defasında bayrağı neden kontrol etmesi gerektiğini merak edebilirsiniz. Bunun yerine, her ikisi de ortak bir taban türünden türetilen ve ayrı ayrı uygun yineleyiciye sahip olan std::list::beginve std::list::rbeginpolimorfik olarak geri dönen iki ayrı yineleyici türümüz olamaz mı? Mümkün olsa da, bu her şeyi daha da kötüleştirecektir, çünkü yineleyiciyi ilerletmek şimdi dolaylı (inline etmek zor) bir işlev çağrısı olacaktır. Java'da zaten bu fiyatı rutin olarak ödüyorsunuz, ancak daha sonra, performans kritik olduğunda birçok insanın C ++ için ulaşmasının nedenlerinden biri budur.

Tarafından işaret edildiği gibi , Benjamin Lindley beri, yorumlardaki reverseinvalidate yineleyiciler izin verilmez, sadece bir yaklaşım standardına izin verdiği çift dolaylı bellek erişimine neden Yineleyici içindeki listesine bir işaretçi geri saklamak için gibi görünüyor.


7
@galinette: std::list::reverseyineleyicileri geçersiz kılmaz.
Benjamin Lindley

1
@galinette Maalesef, yazdığınız gibi “ düğüm başına bayrak” ın aksine önceki yorumunuzu “ yineleyici başına işaretle ” olarak yanlış okudum . Tabii ki, düğüm başına bir bayrak , yine, hepsini çevirmek için doğrusal bir geçiş yapmak zorunda kalacağınız gibi karşı üretken olacaktır.
5gon12eder

2
@ 5gon12eder: Dallamayı çok düşük maliyetle ortadan kaldırabilirsiniz: nextve previşaretçileri bir dizide saklayın ve yönü bir 0veya olarak saklayın 1. İleriye doğru yinelemek için, takip eder pointers[direction]ve tersine yinelersiniz pointers[1-direction](veya tersi). Bu yine de biraz ek yük getirecektir, ancak muhtemelen bir daldan daha az olacaktır.
Jerry Coffin

4
Büyük olasılıkla yineleyicilerin içindeki listeye bir işaretçi depolayamazsınız. swap(), sabit bir süre olarak belirtilir ve yineleyicileri geçersiz kılmaz.
Tavian Barnes

1
@TavianBarnes Lanet olsun! Peki, üçlü indirme o zaman… (Yani, aslında üçlü değil. Bayrağı dinamik olarak ayrılmış bir nesneye depolamanız gerekir, ancak yineleyicideki işaretçi elbette listenin üzerine indirmek yerine doğrudan o nesneyi gösterebilir.)
5gon12eder

37

Şüphesiz, çift yönlü yineleyicileri destekleyen tüm kapsayıcılar rbegin () ve rend () kavramına sahip olduğundan, bu soru tartışmalıdır?

Yineleyicileri tersine çeviren bir proxy oluşturmak ve bu yolla kapsayıcıya erişmek çok önemlidir.

Bu işlemsizlik aslında O (1) 'dir.

gibi:

#include <iostream>
#include <list>
#include <string>
#include <iterator>

template<class Container>
struct reverse_proxy
{
    reverse_proxy(Container& c)
    : _c(c)
    {}

    auto begin() { return std::make_reverse_iterator(std::end(_c)); }
    auto end() { return std::make_reverse_iterator(std::begin(_c)); }

    auto begin() const { return std::make_reverse_iterator(std::end(_c)); }
    auto end() const { return std::make_reverse_iterator(std::begin(_c)); }

    Container& _c;
};

template<class Container>
auto reversed(Container& c)
{
    return reverse_proxy<Container>(c);
}

int main()
{
    using namespace std;
    list<string> l { "the", "cat", "sat", "on", "the", "mat" };

    auto r = reversed(l);
    copy(begin(r), end(r), ostream_iterator<string>(cout, "\n"));

    return 0;
}

beklenen çıktı:

mat
the
on
sat
cat
the

Bu göz önüne alındığında, bana göre, standartlar komitesi, O'nun (1) konteynerin ters sıralamasını zorunlu kılmak için zaman almadı ve bu gerekli değildir ve standart kütüphane büyük ölçüde sadece kesinlikle gerekli olanı zorunlu kılma prensibi üzerine inşa edilmiştir. tekrardan kaçınmak.

Sadece benim 2c.


18

Çünkü her düğümü ( ntoplam) geçmeli ve verilerini güncellemelidir (güncelleme adımı gerçekten O(1)). Bu tüm operasyonu yapar O(n*1) = O(n).


27
Çünkü her öğe arasındaki bağlantıları da güncellemeniz gerekiyor. Bir parça kağıt çıkarın ve aşağı oylama yerine dışarı çekin.
Blindy

11
Öyleyse neden bu kadar emin olduğunu soruyorsun? Hem zamanımızı harcıyorsun.
Blindy

28
@Çift Bağlantılı listenin düğümlerinin yön duygusu vardır. Oradan neden.
Ayakkabı

11
@Blindy İyi bir cevap tamamlanmalıdır. “Bir parça kağıt çıkarın ve çıkarın” bu nedenle iyi bir cevabın gerekli bir bileşeni olmamalıdır. İyi cevap olmayan cevaplar aşağı oylara tabidir.
RM

7
@ Shoe: Zorunlu mu? Lütfen XOR bağlantılı listeyi ve benzerlerini araştırın.
Tekilleştirici

2

Ayrıca her düğüm için önceki ve sonraki işaretçiyi değiştirir. Bu yüzden Lineer alır. O (1) 'de, bu LL'yi kullanan fonksiyonun LL hakkında normal veya ters erişiyormuş gibi girdi olarak alması halinde de yapılabilir.


1

Sadece bir algoritma açıklaması. Öğeleri içeren bir diziniz olduğunu düşünün, sonra tersine çevirmeniz gerekir. Temel fikir, ilk pozisyondaki elemanı son pozisyona, ikinci pozisyondaki elemanı sondan bir önceki pozisyona değiştiren her elemanın tekrarlanmasıdır. Dizinin ortasına geldiğinizde tüm elemanlar değişmiş olacak, böylece O (n) olarak kabul edilen (n / 2) iterasyonlarında olacaksınız.


1

O (n) 'dir çünkü listeyi ters sırada kopyalaması gerekir. Her bir öğe işlemi O (1) şeklindedir, ancak tüm listede n tane vardır.

Tabii ki, yeni liste için yer ayarlama ve daha sonra işaretçileri değiştirme gibi bazı sabit zamanlı işlemler vardı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.