Bir Yığın, İki Sıra


59

arka fon

Birkaç yıl önce, ben lisanstayken, itfa edilmiş analiz üzerine bir ev ödevi verildi. Sorunlardan birini çözemedim. Ben bunu sormuştu comp.theory , ama tatmin edici bir sonuç ortaya çıktı. TA'nın kanıtlayamadığı bir konuda ısrar ettiğini ve ispatını unuttuğunu söylediğini hatırlıyorum, ve ... [ne biliyorsunuz].

Bugün, sorunu hatırladım. Hala bilmek istekliydim, işte burada ...

Soru

Bu uygulamaya mümkün bir yığın kullanılarak iki sıraları hem böylece PUSH ve POP işlemleri çalıştırmak itfa edilmiş süresi O (1) ? Eğer evet ise, bana nasıl söyleyebilirsin?

Not: uygulamak istiyorsanız durum oldukça kolaydır bir kuyruk ile iki yığınlar (karşılık gelen işlemleri ile Enqueue & Dequeue ). Lütfen farkı gözlemleyin.

Not: Yukarıdaki problem ev ödevi değildir. Ödev herhangi bir alt sınır gerektirmedi; sadece bir uygulama ve çalışma süresi analizi.


2
Sanırım sadece iki kuyruktan başka bir sınırlı alan kullanabilirsiniz (O (1) veya O (log n)). Bana imkansız geliyor, çünkü uzun bir girdi akışının sırasını tersine çevirecek hiçbir yolumuz yok. Ama elbette bu, katı bir iddia haline getirilmediği sürece kanıt değildir….
Tsuyoshi Ito,

@ Tsuyoshi: Sınırlı alan varsayımı konusunda haklısın. Ve evet, söylediğim şuydu (inatçı) TA, ama reddetti :(
MS Dousti

2
@Tsuyoshi: Genel olarak uzayda bir sınır üstlenmen gerektiğini düşünmüyorum, yalnızca yığından itilen ve açılan nesnelerden iki kuyruktan başka hiçbir yerde (ve muhtemelen muhtemelen) saklanmasına izin vermediğini varsayman gerekir. sabit sayıda değişken).
Kaveh

@SadeqDousti Kanımca, bunun mümkün olmasının tek yolu, bir kuyruğun bağlantılı liste uygulamasını kullanıp, bazı işaretçiler her zaman "yığının" üstüne gelmek için kullandıysanız
Charles Addis

2
Aslına bakarsanız, TA gerçekten de "O (1) itfa edilmiş sürede kesin olarak mümkün olan" iki yığın kullanarak bir sıra uygulayın "demek istemiş olabilir.
Thomas Ahle,

Yanıtlar:


45

Gerçek bir cevabım yok, ama işte sorunun açık olduğuna dair bazı kanıtlar:

  • Ming Li, Luc Longpré ve Paul MB Vitányi'de, "Kuyruğun gücü", 1986'da, yakından ilişkili birkaç simülasyonu ele alan Yapılar'da bahsedilmemiştir.

  • Martin Hühne'de, "Birkaç sıranın gücüyle" diye bahsedilmedi, Theor. Zorunlu. Sci. 1993, bir bildiri kağıdı.

  • COCOON 2001'de Holger Petersen, "Deques'e Karşı Yığınlar" da belirtilmemiştir.

  • Burton Rosenberg, "İki kuyruk kullanarak bağlamsız dillerin hızlı sayısal olmayan bir şekilde tanınması". Proc. Lett. 1998, iki sıra kullanarak herhangi bir CFL'yi tanımak için bir O (n log n) iki sıra algoritması verir. Ancak, klasik olmayan bir aşağı itme otomatı, CFL'leri lineer zamanda tanıyabilir. Eğer operasyon başına O (log n) 'den daha hızlı iki kuyruklu bir yığın simülasyonu olsaydı, Rosenberg ve hakemleri bunu bilmeliydiler.


4
Mükemmel referanslar için +1. Yine de bazı teknikler var: Birincisi gibi bazı yazılar iki kuyruğu kullanarak (soyuttan söyleyebildiğim kadar) bir yığını simüle etme problemini göz önüne almaz. Diğerleri, en kötü durum analizini yapar, itfa edilmiş maliyet değildir.
MS Dousti,

13

Aşağıdaki cevap 'hiledir', çünkü işlemler arasında herhangi bir boşluk kullanmazken, işlemlerin kendisi alanından daha fazlasını kullanabilir . Bu problemi olmayan bir cevap için bu konudaki başka bir yere bakın.O(1)

Ben gerçekten sorunuza bir cevap yok, ben çalışan bir algoritma buldunuz O(n)yerine zaman. Kanıt olmamasına rağmen, bunun sıkı olduğuna inanıyorum. Herhangi bir şey varsa, algoritmaO(n) 'nindaha düşük bir sınırını kanıtlamaya çalışmanınfaydasız olduğunu gösterir, bu nedenle sorunuzu yanıtlamanıza yardımcı olabilir.O(n)O(n)O(n)

İki algoritma veriyorum, birincisi Pop için çalışma zamanı ve ikincisi O ( ) olan basit bir algoritmaO(n)Pop için koşu zamanı. Birincisini esas olarak sadeliği nedeniyle, ikincisini anlaması daha kolay olacak şekilde tanımlarım.O(n)

Daha fazla ayrıntı vermek gerekirse, ilki ek alan yoktur, en kötü durum (ve itfa edilmiş) İtme ve O ( n ) en kötü durum (ve itfa edilmiş) Pop vardır, ancak en kötü durum davranış her zaman tetiklenmez. İki kuyruğun ötesinde herhangi bir boşluk kullanmadığından, Ross Snider tarafından sunulan çözümden biraz 'daha iyi'.O(1)O(n)

İkincisi, tek bir tamsayı alanı kullanır (yani fazladan boşluk), O ( 1 ) en kötü durumda (ve itfa edilmiş) Push ve O ( O(1)O(1)itfa edilmiş Pop. Bu nedenle çalışma süresi, 'basit' yaklaşımdan önemli ölçüde daha iyidir, ancak biraz daha fazla alan kullanır.O(n)

İlk algoritma

İki sıraları vardır: sıra ve sıra s e c O , n d . f i r s t ise, bizim itme sıra "olacaktır s e c O , n d zaten 'yığın sırada' sıra olacaktır.firstsecondfirstsecond

  • Sadece üzerine parametrenin kuyruğa alınmasından yapılır itme .first
  • Haşhaş şu şekilde yapılır. Eğer , basitçe sıradan çıkarma boş s e c O , n d sonucu döndürür. Aksi takdirde, geri f ı r s t , tüm ekleme s e c O , n d için f ı r s t ve takas f ı r s t ve s e c O , n d . Daha sonra sıradan çıkarma s e c Ofirstsecondfirstsecondfirstfirstsecond ve dequeue sonucunu döndürür.second

İlk algoritma için C # kodu

Daha önce hiç C # görmemiş olsanız bile, bu oldukça okunaklı olabilir. Jeneriklerin ne olduğunu bilmiyorsanız, sadece 'T' örneklerini zihninizde 'string' ile değiştirin, bir dizi dizge için.

public class Stack<T> {
    private Queue<T> first = new Queue<T>();
    private Queue<T> second = new Queue<T>();
    public void Push(T value) {
        first.Enqueue(value);
    }
    public T Pop() {
        if (first.Count == 0) {
            if (second.Count > 0)
                return second.Dequeue();
            else
                throw new InvalidOperationException("Empty stack.");
        } else {
            int nrOfItemsInFirst = first.Count;
            T[] reverser = new T[nrOfItemsInFirst];

            // Reverse first
            for (int i = 0; i < nrOfItemsInFirst; i++)
                reverser[i] = first.Dequeue();    
            for (int i = nrOfItemsInFirst - 1; i >= 0; i--)
                first.Enqueue(reverser[i]);

            // Append second to first
            while (second.Count > 0)
                first.Enqueue(second.Dequeue());

            // Swap first and second
            Queue<T> temp = first; first = second; second = temp;

            return second.Dequeue();
        }
    }
}

analiz

O(1)firstsecondO(n)n

İkinci algoritma

İki sıraları vardır: sıra f ı r s t ve sıra s e c O , n d . f i r s t ise, bizim itme sıra "olacaktır s e c O , n dfirstsecondfirstsecond

firstsecondfirstsecondsecondfirstsecond

  • first
  • firstsecondfirst|first|<|second|firstsecondfirstfirstsecondsecond

İlk algoritma için C # kodu

Daha önce hiç C # görmemiş olsanız bile, bu oldukça okunaklı olabilir. Jeneriklerin ne olduğunu bilmiyorsanız, sadece 'T' örneklerini zihninizde 'string' ile değiştirin, bir dizi dizge için.

public class Stack<T> {
    private Queue<T> first = new Queue<T>();
    private Queue<T> second = new Queue<T>();
    int unsortedPart = 0;
    public void Push(T value) {
        unsortedPart++;
        first.Enqueue(value);
    }
    public T Pop() {
        if (first.Count == 0) {
            if (second.Count > 0)
                return second.Dequeue();
            else
                throw new InvalidOperationException("Empty stack.");
        } else {
            int nrOfItemsInFirst = first.Count;
            T[] reverser = new T[nrOfItemsInFirst];

            for (int i = nrOfItemsInFirst - unsortedPart - 1; i >= 0; i--)
                reverser[i] = first.Dequeue();

            for (int i = nrOfItemsInFirst - unsortedPart; i < nrOfItemsInFirst; i++)
                reverser[i] = first.Dequeue();

            for (int i = nrOfItemsInFirst - 1; i >= 0; i--)
                first.Enqueue(reverser[i]);

            unsortedPart = 0;
            if (first.Count * first.Count < second.Count)
                return first.Dequeue();
            else {
                while (second.Count > 0)
                    first.Enqueue(second.Dequeue());

                Queue<T> temp = first; first = second; second = temp;

                return second.Dequeue();
            }
        }
    }
}

analiz

O(1)

O(n)|first|<|second|firstO(|first|)=O(n)|first||second|nnO(n)O(nn)=O(n)

Son not

O(n)first


İlk paragrafları düzenledim, böylece cevabım soruya verilen cevap olarak belirlendi.
Alex ten Brink,

6
Tersine çevirmek için bir dizi (ters çevirici) kullanıyorsunuz! Bunu yapmana izin verildiğini sanmıyorum.
Kaveh

Doğru, yöntemleri uygularken fazladan boşluk kullanıyorum, ancak bunun izin verilebileceğini düşündüm: iki yığını kullanarak bir sıra uygulamak için basit bir şekilde, yığınlardan birini bir noktada tersine çevirmek zorundasınız. Bunu yapmak için fazladan alana ihtiyaç duyduğunuzu biliyorum, bu yüzden bu soru benzer olduğundan, yöntem çağrıları arasında ek boşluk kullanmadığınız sürece, bir yöntemin yürütülmesi sırasında fazladan boşluk kullanmanın mümkün olacağını düşündüm.
Alex ten Brink,

6
“İki yığını kullanarak bir kuyruğu basit bir şekilde uygulamak istiyorsanız, yığınlardan birini bir noktada tersine çevirmeniz gerekir ve bildiğim kadarıyla bunu yapmak için fazladan alana ihtiyacınız var” --- yapmazsınız. Bir bellek hücresi ve iki yığınla birlikte Enqueue'nin amorti edilmiş maliyetinin 3 olmasını ve Dequeue'nin amorti edilen maliyetinin 1 (yani hem O (1)) olmasını sağlamanın bir yolu vardır. Zor kısım, algoritmanın tasarımı değil, gerçekten kanıtıdır.
Aaron Sterling

Biraz daha düşündükten sonra, gerçekten hile yaptığımı ve önceki yorumumun gerçekten yanlış olduğunu fark ettim. Bunu düzeltmenin bir yolunu buldum: Yukarıdaki iki ile aynı çalışma süresine sahip iki algoritma düşündüm (Push şu anda işlem uzun sürüyor ve Pop artık sabit zamanda yapılıyor). Hepsini yazdıktan sonra yeni bir cevap göndereceğim.
Alex ten Brink,

12

O(n)

O(1)O(n)O(1)

Moderatörlere not: Bunu ayrı bir cevap verme kararımın doğru olup olmadığından tam olarak emin değilim. Asıl cevabımı silmemem gerektiğini düşündüm, çünkü hala soru ile bir ilgisi olabilir.

Algoritması

firstsecondfirstsecondfirstsecondfirstsecond

  • firstfirstfirst
  • firstsecondsecondfirstfirstsecondfirstsecond
  • firstfirstsecond

İlk algoritma için C # kodu

Daha önce hiç C # görmemiş olsanız bile, bu kodun okunabilir olması gerekir. Jeneriklerin ne olduğunu bilmiyorsanız, sadece 'T' örneklerini zihninizde 'string' ile değiştirin, bir dizi dizge için.

public class Stack<T> {
    private Queue<T> first = new Queue<T>();
    private Queue<T> second = new Queue<T>();
    public void Push(T value) {
        // I'll explain what's happening in these comments. Assume we pushed
        // integers onto the stack in increasing order: ie, we pushed 1 first,
        // then 2, then 3 and so on.

        // Suppose our queues look like this:
        // first: in 5 6 out
        // second: in 1 2 3 4 out
        // Note they are both in stack order and first contains the top of
        // the stack.

        // Suppose value == 7:
        first.Enqueue(value);
        // first: in 7 5 6 out
        // second: in 1 2 3 4 out

        // We restore the stack order in first:
        for (int i = 0; i < first.Count - 1; i++)
            first.Enqueue(first.Dequeue());
        // first.Enqueue(first.Dequeue()); is executed twice for this example, the 
        // following happens:
        // first: in 6 7 5 out
        // second: in 1 2 3 4 out
        // first: in 5 6 7 out
        // second: in 1 2 3 4 out

        // first exeeded its capacity, so we merge first and second.
        if (first.Count * first.Count > second.Count) {
            while (second.Count > 0)
                first.Enqueue(second.Dequeue());
            // first: in 4 5 6 7 out
            // second: in 1 2 3 out
            // first: in 3 4 5 6 7 out
            // second: in 1 2 out
            // first: in 2 3 4 5 6 7 out
            // second: in 1 out
            // first: in 1 2 3 4 5 6 7 out
            // second: in out

            Queue<T> temp = first; first = second; second = temp;
            // first: in out
            // second: in 1 2 3 4 5 6 7 out
        }
    }
    public T Pop() {
        if (first.Count == 0) {
            if (second.Count > 0)
                return second.Dequeue();
            else
                throw new InvalidOperationException("Empty stack.");
        } else
            return first.Dequeue();
    }
}

analiz

O(1)

O(n)|first|<|second|O(n)|first||second|O(n)firstO(n)O(nn)=O(n)


Bir cevabı silme hakkında, lütfen meta.cstheory.stackexchange.com/q/386/873 adresini ziyaret edin .
MS Dousti

Çizgiyi anlayamıyorum first.Enqueue(first.Dequeue()). Bir şeyi yanlış yazdın mı?
MS Dousti

Bağlantı için teşekkürler, orijinal cevabımı buna göre güncelledim. İkincisi, algoritmamın yürütülmesi sırasında neler olup bittiğini anlatan koduma birçok yorum ekledim, umarım herhangi bir karışıklığı giderir.
Alex ten Brink

Benim için algoritma düzenlemeden önce daha okunaklı ve anlaşılması kolaydı.
Kaveh

9

Θ(N)

NNNNN

PUSHN(PUSHNPOPN)N

NN/2

Şimdi (ilk set) ile başa çıkma görevini düşünün.N

N/2N/2

N/22N

N/22NNNN/23NΩ(N)


NN

nQ1N/2Q22nn4:1+2++n+n2n

Görünüşe göre Peter'ın cevabı bu alt sınırla çelişiyor?
Joe

PUSHNPOPNO(N)

O(N)

6

O(lgn)pushpoppopO(lgn)pop istenirse ve çıkış kuyruğu boştur, giriş sırasını tersine çevirmek ve çıkış kuyruğunda saklamak için bir dizi mükemmel karıştırma gerçekleştirirsiniz.

O(1)

Bildiğim kadarıyla, bu yeni bir fikir ...



Ahh! Güncellenmiş veya ilgili bir soru aramalıydım. Daha önce cevabınızla ilişkilendirdiğiniz yazılar, k yığınları ve k + 1 yığınları arasında bir ilişki oluşturuyordu. Bu numara k ve k + 1 yığınlarının arasına k kuyruğunun gücünü koyuyor mu? Eğer öyleyse, bu biraz zarif bir yazı tahtası. Her iki durumda da, beni cevabınıza bağladığınız için teşekkür ederim, bu yüzden bunu başka bir mekân için yazmakta fazla zaman kaybetmedim.
Peter Boothe,

1

Fazladan boşluk kullanmadan, belki öncelikli bir sıra kullanmak ve her yeni baskıyı bir öncekinden daha büyük bir öncelik vermek için zorlamak? Yine de O (1) olmazdı.


0

Bir yığını itfa edilmiş sabit sürede uygulamak için kuyrukları alamıyorum. Ancak, en kötü durumda doğrusal zamanda bir yığın uygulamak için iki kuyruk almanın bir yolunu düşünebilirim.

  • AB
  • Bir itme işlemi her defasında, ucu çevirin ve elemanı şimdi bit gösteren kuyruğa yerleştirin. Her şeyi diğer sıradan aç ve geçerli sıraya doğru it.
  • Bir pop işlemi geçerli sıranın önünden çıkar ve dış durum bitine dokunmaz.

Elbette, son operasyonun bir itme ya da pop olup olmadığını bize söyleyen başka bir dışsal durum ekleyebiliriz. Art arda iki itme işlemi gerçekleştirene kadar her şeyi bir sıradan diğerine taşımayı geciktirebiliriz. Bu aynı zamanda pop operasyonu biraz daha karmaşık hale getirir. Bu bize pop operasyonu için O (1) itfa karmaşıklığı verir. Ne yazık ki, itme doğrusal kalır.

Tüm bu işler, çünkü bir basma işlemi her yapıldığında, yeni eleman boş bir sıranın başına yerleştirilir ve tam kuyruk kuyruk ucuna eklenir, böylece elemanlar etkili bir şekilde tersine çevrilir.

Eğer amortismana tabi tutulan sabit zamanlı operasyonlar almak istiyorsanız, muhtemelen daha akıllıca bir şey yapmanız gerekecektir.


4
Şüphesiz, aynı daha kötü durum zaman karmaşıklığına ve karmaşıklığa sahip olmayan tek bir sırayı kullanabilirim, esas olarak sırayı yığının tepesini temsil eden ek bir sıra elemanı ile dairesel bir liste olarak ele alabilirim.
Dave Clarke

Yapabiliyor gibisin! Bununla birlikte, bir yığını bu şekilde taklit etmek için birden fazla klasik kuyruk gerekli görünüyor.
Ross Snider

0

Sıranız önden yüklemeye izin veriyorsa, yalnızca bir sıra (veya daha özel olarak deque) gerektiren önemsiz bir çözüm var. Belki de bu, orijinal sorudaki TA dersinin sırasıdır?

Ön yüklemeye izin vermeden, işte başka bir çözüm:

Bu algoritma iki kuyruk ve iki işaretçi gerektirir, sırasıyla Q1, Q2, birincil ve ikincil olarak adlandırırız. İnitilizasyonun ardından Q1 ve Q2 boştur, primer Q1'e ve sekonder Q2'ye işaret eder.

PUSH operasyonu önemsizdir, sadece şunlardan oluşur:

*primary.enqueue(value);

POP operasyonu biraz daha fazla etkiliyor; birincil kuyruğun son öğesinin tümü hariç ikincil kuyruğa atılmasını, işaretçileri değiştirmeyi ve kalan son öğeyi orijinal kuyruğundan döndürmesini gerektirir:

while(*primary.size() > 1)
{
    *secondary.enqueue(*primary.dequeue());
}

swap(primary, secondary);
return(*secondary.dequeue());

Sınır kontrolü yapılmamıştır ve O (1) değildir.

Bunu yazarken, Alex'in yaptığı gibi, bir süre döngü yerine bir for döngüsü kullanarak tek bir kuyrukla yapılabileceğini görüyorum. Her iki durumda da, PUSH işlemi O (1) ve POP işlemi O (n) olur.


Sırasıyla Q1, Q2 ve queue_p adlı iki kuyruk ve bir işaretçi kullanan başka bir çözüm:

Başlatmanın ardından, Q1 ve Q2 boştur ve queue_p Q1'i gösterir.

Yine, PUSH işlemi önemsiz olmakla birlikte, diğer kuyrukta queue_p işaretleme için bir ek adım daha gerektirir:

*queue_p.enqueue(value);
queue_p = (queue_p == &Q1) ? &Q2 : &Q1;

POP işlemi işlemi öncekine benzer, ancak şimdi sıra boyunca döndürülmesi gereken n / 2 öğe var:

queue_p = (queue_p == &Q1) ? &Q2 : &Q1;
for(i=0, i<(*queue_p.size()-1, i++)
{
    *queue_p.enqueue(*queue_p.dequeue());
}
return(*queue_p.dequeue());

PUSH işlemi hala O (1), ancak şimdi POP işlemi O (n / 2).

Şahsen, bu sorun için, tek, çift uçlu bir sıra (deque) uygulama ve istediğimiz zaman istif demeyi tercih ediyorum.


İkinci algoritmanız Alex'in daha ilgili olanını anlamak için yararlıdır.
hengxin

0

kΘ(n1/k)

k
n
O(1)

iΘ(ni/k)Θ(ni/k)O(1)i+1O(n1/k)i1Θ(n1/k)

mmΩ(mn1/k)o(n1/k)Ω(n1/k)o(n2/k)ko(n)

Θ(logn)


-3

İkinci kuyruğu son kullanıcı olarak kullanarak iki sıra kullanılarak bir yığın uygulanabilir. Öğeler yığına itildiğinde, kuyruğun sonuna eklenirler. Bir öğe her açıldığında, ilk kuyruğun n - 1 elemanları, kalan madde iade edilirken ikinciye taşınmalıdır. genel sınıf QueueStack uygulayan ts IStack {private IQueue q1 = new Queue (); özel IQueue q2 = new Queue (); public void push (E e) {q1.enqueue (e) // O (1)} genel E pop (E e) {while (1 <q1.size ()) // O (n) {q2.enqueue ( q1.dequeue ()); } sw apQueues (); q2.dequeue () döndürür; } p rivate void swapQueues () {IQueue Q = q2; q2 = q1; q1 = Q; }}


2
İtfa edilmiş zaman O (1) hakkındaki sorudaki kısmı özlediniz mi?
David Eppstein,
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.