Latar Belakang
Beberapa tahun yang lalu, ketika saya masih sarjana, kami diberi pekerjaan rumah tentang analisis diamortisasi. Saya tidak dapat menyelesaikan salah satu masalah. Saya telah menanyakannya dalam teori comp.the , tetapi tidak ada hasil yang memuaskan muncul. Saya ingat kursus TA bersikeras pada sesuatu yang tidak bisa dia buktikan, dan mengatakan dia lupa buktinya, dan ... [Anda tahu apa].
Hari ini, saya mengingat masalahnya. Saya masih ingin tahu, jadi ini dia ...
Pertanyaan
Apakah mungkin untuk mengimplementasikan stack menggunakan dua antrian , sehingga operasi PUSH dan POP berjalan dalam waktu diamortisasi O (1) ? Jika ya, bisakah Anda memberi tahu saya caranya?
Catatan: Situasinya cukup mudah jika kita ingin mengimplementasikan antrian dengan dua tumpukan (dengan operasi yang sesuai ENQUEUE & DEQUEUE ). Mohon perhatikan perbedaannya.
PS: Masalah di atas bukan pekerjaan rumah itu sendiri. Pekerjaan rumah tidak membutuhkan batas bawah; hanya implementasi dan analisis waktu berjalan.
Jawaban:
Saya tidak memiliki jawaban yang sebenarnya, tetapi inilah beberapa bukti bahwa masalahnya terbuka:
Ini tidak disebutkan dalam Ming Li, Luc Longpré dan Paul MB Vitányi, "Kekuatan antrian", Structures 1986, yang mempertimbangkan beberapa simulasi terkait erat lainnya
Tidak disebutkan dalam Martin Huhne, "Pada kekuatan beberapa antrian", Theor. Comp. Sci. 1993, makalah lanjutan.
Tidak disebutkan dalam Holger Petersen, "Stacks versus Deques", COCOON 2001.
Burton Rosenberg, "Pengakuan cepat nondeterministik bahasa bebas konteks menggunakan dua antrian", Inform. Proc Lett. 1998, memberikan O (n log n) algoritma dua-antrian untuk mengenali CFL menggunakan dua antrian. Tetapi automaton pushdown nondeterministic dapat mengenali CFL dalam waktu linier. Jadi, jika ada simulasi tumpukan dengan dua antrian lebih cepat dari O (log n) per operasi, Rosenberg dan wasitnya seharusnya tahu tentang hal itu.
sumber
Jawaban di bawah ini adalah 'curang', karena walaupun tidak menggunakan ruang antara operasi, operasi itu sendiri dapat menggunakan lebih dari ruang. Lihat tempat lain di utas ini untuk jawaban yang tidak memiliki masalah ini.O(1)
Meskipun saya tidak punya jawaban untuk pertanyaan Anda, saya menemukan sebuah algoritma yang berfungsi di waktu alih-alihO(n). Saya percaya ini ketat, meskipun saya tidak punya bukti. Jika ada, algoritme menunjukkan bahwa mencoba membuktikan batas bawahO(n)adalah sia-sia, sehingga mungkin membantu dalam menjawab pertanyaan Anda.O(n−−√) O(n) O(n)
Saya menyajikan dua algoritma, yang pertama adalah algoritma sederhana dengan waktu berjalan untuk Pop dan yang kedua dengan O ( √O(n) waktu berjalan untuk Pop. Saya menggambarkan yang pertama terutama karena kesederhanaannya sehingga yang kedua lebih mudah dimengerti.O(n−−√)
Untuk memberikan rincian lebih lanjut: yang pertama tidak menggunakan ruang tambahan, memiliki kasus terburuk (dan diamortisasi) Push dan O ( n ) kasus terburuk (dan diamortisasi) Pop, tetapi perilaku kasus terburuk tidak selalu dipicu. Karena tidak menggunakan ruang tambahan di luar dua antrian, itu sedikit 'lebih baik' daripada solusi yang ditawarkan oleh Ross Snider.O(1) O(n)
Yang kedua menggunakan bidang bilangan bulat tunggal (jadi ruang ekstra), memiliki O ( 1 ) kasus terburuk (dan diamortisasi) Push dan O ( √O(1) O(1) Pop diamortisasi. Karenanya waktu berjalan jauh lebih baik daripada pendekatan 'sederhana', namun ia menggunakan ruang ekstra.O(n−−√)
Algoritma pertama
Kami memiliki dua antrian: antrian dan antrian s e c o n d . f i r s t akan menjadi 'push queue' kami, sementara s e c o n d akan menjadi antrian yang sudah ada di 'susun urutan'.first second first second
Kode C # untuk algoritma pertama
Ini seharusnya cukup mudah dibaca, bahkan jika Anda belum pernah melihat C # sebelumnya. Jika Anda tidak tahu apa itu generik, cukup ganti semua contoh 'T' dengan 'string' di pikiran Anda, untuk setumpuk string.
Analisis
Jelas Push bekerja dalam waktu. Pop dapat menyentuh segala sesuatu di dalam f i r s t dan s e c o n d jumlah konstan kali, jadi kita harus O ( n ) dalam kasus terburuk. Algoritme menunjukkan perilaku ini (misalnya) jika seseorang mendorong n elemen ke stack dan kemudian berulang kali melakukan Push menghanguskan dan operasi Pop tunggal berturut-turut.O(1) first second O(n) n
Algoritma kedua
Kami memiliki dua antrian: antrian dan antrian s e c o n d . f i r s t akan menjadi 'push queue' kami, sementara s e c o n d akan menjadi antrian yang sudah ada di 'susun urutan'.first second first second
Ini adalah versi adaptasi dari algoritma pertama, di mana kita tidak langsung 'mengacak' isi dari menjadi s e c o n dfirst second . Sebaliknya, jika berisi sejumlah cukup kecil elemen dibandingkan dengan s e c o n d (yaitu akar kuadrat dari jumlah elemen di s e c o n d ), kita hanya menata f i r s t ke dalam susunan tumpukan dan jangan bergabung denganfirst second second first .second
Kode C # untuk algoritma pertama
Ini seharusnya cukup mudah dibaca, bahkan jika Anda belum pernah melihat C # sebelumnya. Jika Anda tidak tahu apa itu generik, cukup ganti semua contoh 'T' dengan 'string' di pikiran Anda, untuk setumpuk string.
Analisis
Jelas Push bekerja dalam waktu.O(1)
Pop bekerja di waktu diamortisasi. Ada dua kasus: jika| first| < √O(n−−√) |first|<|second|−−−−−−−√ , Maka kita mengocok ke dalam urutan tumpukan di O ( | f i r s t | ) = O ( √first waktu. Jika| first| ≥ √O(|first|)=O(n−−√) , maka kita harus memiliki setidaknya√|first|≥|second|−−−−−−−√ panggilan untuk Push. Karenanya, kami hanya dapat menemukan kasus ini setiap √n−−√ panggilan ke Push dan Pop. Waktu berjalan aktual untuk kasus ini adalahO(n), jadi waktu diamortisasi adalahO( nn−−√ O(n) .O(nn√)=O(n−−√)
Catatan akhir
Dimungkinkan untuk menghilangkan variabel tambahan dengan biaya membuat Pop an operasi, dengan memiliki Pop mereorganisasifirstdi setiap panggilan daripada harus push melakukan semua pekerjaan.O(n−−√) first
sumber
Mengikuti beberapa komentar pada jawaban saya sebelumnya, menjadi jelas bagi saya bahwa saya kurang lebih curang: Saya menggunakan ruang ekstra ( ruang ekstra dalam algoritma kedua) selama pelaksanaan metode Pop saya.O(n−−√)
Algoritme berikut tidak menggunakan ruang tambahan antara metode dan hanya ruang ekstra selama pelaksanaan Push dan Pop. Push memiliki O ( √O(1) O(n−−√) waktu berjalan diamortisasi dan Pop memiliki waktu berjalan terburuk (dan diamortisasi).O(1)
Catatan untuk moderator: Saya tidak sepenuhnya yakin apakah keputusan saya untuk menjadikan ini jawaban terpisah adalah benar. Saya pikir saya tidak boleh menghapus jawaban asli saya karena mungkin masih ada relevansi dengan pertanyaan.
Algoritma
Kami memiliki dua antrian: antrian dan antrian s e c o n d . f i r s t akan menjadi 'cache' kami, sementara s e c o n d akan menjadi 'penyimpanan' utama kami. Kedua antrian akan selalu berada di 'susun urutan'. f i r s t akan berisi elemen-elemen di bagian atas tumpukan dan s e c o n d akan berisi elemen-elemen di bagian bawah tumpukan. Ukuran f i rfirst second first second first second akan selalu paling banyak menjadi akar kuadrat dari s e cfirst .second
Kode C # untuk algoritma pertama
Kode ini harus cukup mudah dibaca, bahkan jika Anda belum pernah melihat C # sebelumnya. Jika Anda tidak tahu apa itu generik, cukup ganti semua contoh 'T' dengan 'string' di pikiran Anda, untuk setumpuk string.
Analisis
Jelas Pop bekerja di dalam kasus terburuk.O(1)
Push bekerja di waktu diamortisasi. Ada dua kasus: jika| first| <O(n−−√) lalu Push mengambilO(|first|<|second|−−−−−−−√ waktu. Jika| first| ≥ √O(n−−√) kemudian Push membutuhkanO(n)waktu, tetapi setelah operasi inifirstakan kosong. Itu akan membutuhkanO(√|first|≥|second|−−−−−−−√ O(n) first waktu sebelum kita mendapatkan kasus ini lagi, jadi waktu diamortisasi adalahO( nO(n−−√) waktu.O(nn√)=O(n−−√)
sumber
first.Enqueue(first.Dequeue())
. Apakah Anda salah mengetik sesuatu?Saya mengklaim kami memiliki biaya perolehan diamortisasi per operasi. Algoritma Alex memberikan batas atas. Untuk membuktikan batas bawah saya berikan urutan terburuk dari gerakan PUSH dan POP.Θ(N−−√)
Urutan kasus terburuk terdiri dari operasi PUSH, diikuti oleh √N operasi PUSH dan √N−−√ operasi POP, sekali lagi diikuti oleh √N−−√ operasi PUSH dan √N−−√ operasi POP, dll. Itu adalah:N−−√
Pertimbangkan situasi setelah operasi PUSH awal . Tidak masalah bagaimana algoritme bekerja, setidaknya satu dari antrian harus memiliki setidaknya N / 2 entri di dalamnya.N N/2
Sekarang pertimbangkan tugas berurusan dengan (set pertama)N−−√ operasi PUSH dan POP. Taktik algoritmik apa pun harus jatuh ke dalam salah satu dari dua kasus:
Dalam kasus pertama, algoritme akan menggunakan kedua antrian. Semakin besar antrian ini memiliki setidaknya entri di dalamnya, jadi kita harus mengeluarkan biaya setidaknya N / 2N/2 N/2 operasi antrian untuk akhirnya mengambil bahkan satu elemen yang kita ENQUEUE dan kemudian perlu DEQUEUE dari antrian yang lebih besar ini.
Dalam kasus kedua, algoritme tidak menggunakan kedua antrian. Ini mengurangi masalah untuk mensimulasikan tumpukan dengan antrian tunggal. Bahkan jika antrian ini pada awalnya kosong, kita tidak dapat melakukan lebih baik daripada menggunakan antrian sebagai daftar bundar dengan akses berurutan, dan tampaknya langsung bahwa kita harus menggunakan setidaknya operasi antrian rata-rata untuk masing-masing2 √N−−√/2 Operasi N stack.2N−−√
Dalam kedua kasus, kami membutuhkan setidaknya waktu (operasi antrian) untuk menangani 2 √N/2 Operasi N stack. Karena kita dapat mengulangi proses ini √2N−−√ kali, kita membutuhkanN √N−−√ waktu untuk memprosesoperasi stack3Nsecara total, memberikan batas bawahΩ( √NN−−√/2 3N waktu diamortisasi per operasi.Ω(N−−√)
sumber
Sejauh yang saya tahu, ini adalah ide baru ...
sumber
Tanpa menggunakan ruang ekstra, mungkin menggunakan antrian yang diprioritaskan dan memaksa setiap dorongan baru untuk memberikan prioritas yang lebih besar daripada yang sebelumnya? Tetap tidak akan menjadi O (1).
sumber
Saya tidak bisa mendapatkan antrian untuk mengimplementasikan tumpukan dalam waktu konstan diamortisasi. Namun, saya bisa memikirkan cara untuk mendapatkan dua antrian untuk mengimplementasikan tumpukan dalam waktu linear kasus terburuk.
Tentu saja, kita dapat menambahkan sedikit keadaan eksternal yang memberi tahu kita apakah operasi terakhir adalah push atau pop. Kita dapat menunda memindahkan semuanya dari satu antrian ke antrian yang lain sampai kita mendapatkan dua operasi push berturut-turut. Ini juga membuat operasi pop sedikit lebih rumit. Ini memberi kita O (1) kompleksitas diamortisasi untuk operasi pop. Sayangnya push tetap linier.
Semua ini bekerja karena setiap kali operasi push dilakukan, elemen baru diletakkan di kepala antrian kosong dan antrian penuh ditambahkan ke ujung ekornya, secara efektif membalikkan elemen.
Jika Anda ingin mendapatkan diamortisasi operasi waktu konstan, Anda mungkin harus melakukan sesuatu yang lebih pintar.
sumber
Ada solusi sepele, jika antrian Anda memungkinkan pemuatan di depan, yang hanya membutuhkan satu antrian (atau, lebih khusus, deque.) Mungkin ini adalah jenis antrian yang diinginkan TA dalam pertanyaan awal?
Tanpa memungkinkan pemuatan di depan, berikut adalah solusi lain:
Algoritma ini membutuhkan dua antrian dan dua pointer, kami akan memanggil mereka masing-masing Q1, Q2, primer, dan sekunder. Setelah inisilisasi, Q1 dan Q2 kosong, poin primer ke Q1 dan poin sekunder ke Q2.
Operasi PUSH itu sepele, hanya terdiri dari:
Operasi POP sedikit lebih terlibat; itu membutuhkan spooling semua kecuali item terakhir dari antrian utama ke antrian sekunder, menukar pointer, dan mengembalikan item yang tersisa terakhir dari antrian asli:
Tidak ada batas pemeriksaan dilakukan, dan itu bukan O (1).
Saat saya mengetik ini, saya melihat bahwa ini dapat dilakukan dengan antrian tunggal menggunakan for loop sebagai pengganti loop sementara, seperti yang telah dilakukan Alex. Either way, operasi PUSH adalah O (1) dan operasi POP menjadi O (n).
Berikut adalah solusi lain yang menggunakan dua antrian dan satu pointer, yang disebut Q1, Q2, dan queue_p, masing-masing:
Setelah inisialisasi, Q1 dan Q2 kosong dan queue_p menunjuk ke Q1.
Sekali lagi, operasi PUSH adalah sepele, tetapi memang membutuhkan satu langkah tambahan menunjuk queue_p di antrian lainnya:
Operasi operasi POP mirip dengan sebelumnya, tetapi sekarang ada n / 2 item yang perlu diputar melalui antrian:
Operasi PUSH masih O (1), tetapi sekarang operasi POP adalah O (n / 2).
Secara pribadi, untuk masalah ini, saya lebih suka ide menerapkan antrian tunggal, ganda (deque) dan menyebutnya stack ketika kita mau.
sumber
sumber
Tumpukan dapat diimplementasikan menggunakan dua antrian dengan menggunakan antrian kedua sebagai ab uffer. Ketika item didorong ke tumpukan mereka ditambahkan ke ujung antrian. Setiap kali item muncul, elemen n - 1 dari antrian pertama harus dipindahkan ke yang kedua, sedangkan item yang tersisa dikembalikan. public class QueueStack mengimplementasikan IStack {private IQueue q1 = new Queue (); IQueue pribadi q2 = Antrian baru (); public void push (E e) {q1.enqueue (e) // O (1)} pop E publik (E e) {while (1 <q1.size ()) // O (n) {q2.enqueue ( q1.dequeue ()); } sw apQueues (); return q2.dequeue (); } p rivate void swapQueues () {IQueue Q = q2; q2 = q1; q1 = Q; }}
sumber