Mengapa std :: list :: reverse memiliki kompleksitas O (n)?

192

Mengapa fungsi terbalik untuk std::listkelas di pustaka standar C ++ memiliki runtime linier? Saya akan berpikir bahwa untuk daftar yang ditautkan dua kali lipat fungsi terbalik seharusnya O (1).

Membalik daftar yang tertaut ganda harus melibatkan penggantian kepala dan ujung ekor.

Ingin tahu
sumber
18
Saya tidak mengerti mengapa orang merendahkan pertanyaan ini. Ini adalah pertanyaan yang masuk akal untuk ditanyakan. Membalik daftar yang tertaut dua kali akan membutuhkan waktu O (1).
Penasaran
46
sayangnya, beberapa orang bingung konsep "pertanyaan itu baik" dengan "pertanyaan itu punya ide bagus". Saya suka pertanyaan seperti ini, di mana pada dasarnya "pemahaman saya tampaknya berbeda dari praktik yang diterima secara umum, tolong bantu selesaikan konflik ini", karena memperluas cara Anda berpikir membantu Anda memecahkan lebih banyak masalah di masa depan! Tampaknya orang lain mengambil pendekatan "itu membuang-buang pemrosesan dalam 99,9999% kasus, bahkan tidak memikirkannya". Jika itu adalah penghiburan, saya telah diturunkan untuk banyak, apalagi!
corsiKa
4
Ya pertanyaan ini mendapat jumlah downvotes banyak untuk kualitasnya. Mungkin itu sama dengan siapa yang mengangkat jawaban Blindy. Dalam keadilan, "membalikkan daftar yang ditautkan dua kali harus melibatkan pengalihan head dan tail point" pada umumnya tidak benar untuk peralatan daftar tertaut standar yang setiap orang pelajari di sekolah menengah, atau untuk banyak implementasi yang digunakan orang. Banyak kali dalam reaksi langsung orang SO terhadap pertanyaan atau jawaban mendorong keputusan yang naik turun / turun. Jika Anda lebih jelas dalam kalimat itu atau menghilangkannya, saya pikir Anda akan mendapatkan lebih sedikit downvotes.
Chris Beck
3
Atau izinkan saya menaruh beban pembuktian kepada Anda, @Curious: Saya telah membuat implementasi daftar yang tertaut ganda di sini: ideone.com/c1HebO . Bisakah Anda menunjukkan bagaimana Anda mengharapkan Reversefungsi diimplementasikan dalam O (1)?
CompuChip
2
@ CompuChip: Sebenarnya, tergantung pada implementasinya, mungkin tidak. Anda tidak perlu boolean tambahan untuk mengetahui pointer mana yang harus digunakan: cukup gunakan yang tidak menunjuk kembali ke Anda ... yang bisa jadi otomatis dengan daftar tertaut XOR's by the way. Jadi ya, itu tergantung pada bagaimana daftar diimplementasikan, dan pernyataan OP dapat diklarifikasi.
Matthieu M.

Jawaban:

187

Secara hipotesis, reversebisa jadi O (1) . Di sana (lagi-lagi secara hipotetis) mungkin ada anggota daftar boolean yang menunjukkan apakah arah daftar yang ditautkan saat ini sama atau berlawanan dengan yang asli tempat daftar itu dibuat.

Sayangnya, itu pada dasarnya akan mengurangi kinerja operasi lainnya (walaupun tanpa mengubah runtime asimptotik). Dalam setiap operasi, boolean perlu dikonsultasikan untuk mempertimbangkan apakah mengikuti pointer "selanjutnya" atau "sebelumnya" dari suatu tautan.

Karena ini mungkin dianggap sebagai operasi yang relatif jarang, standar (yang tidak menentukan implementasi, hanya kompleksitas), menetapkan bahwa kompleksitas dapat linier. Hal ini memungkinkan pointer "berikutnya" selalu berarti arah yang sama secara jelas, mempercepat operasi kasus biasa.

Ami Tavory
sumber
29
@ MooseBoys: Saya tidak setuju dengan analogi Anda. Perbedaannya adalah bahwa, dalam kasus daftar, pelaksanaannya bisa memberikan reversedengan O(1)kompleksitas tanpa mempengaruhi besar-o dari operasi lainnya , dengan menggunakan ini boolean bendera trik. Tetapi, dalam praktiknya cabang tambahan dalam setiap operasi itu mahal, bahkan jika secara teknis O (1). Sebaliknya, Anda tidak dapat membuat struktur daftar yang sortO (1) dan semua operasi lainnya adalah biaya yang sama. Inti dari pertanyaan adalah bahwa, tampaknya, Anda dapat O(1)membalikkan secara gratis jika Anda hanya peduli pada O besar, jadi mengapa mereka tidak melakukan itu.
Chris Beck
9
Jika Anda menggunakan XOR-linked-list, membalikkan akan menjadi waktu yang konstan. Iterator akan lebih besar, dan menambah / mengurangi itu akan sedikit lebih mahal secara komputasi. Itu mungkin dikerdilkan oleh akses memori yang tidak dapat dihindari meskipun untuk semua jenis daftar tertaut.
Deduplicator
3
@IlyaPopov: Apakah setiap node benar-benar membutuhkan ini? Pengguna tidak pernah mengajukan pertanyaan tentang simpul daftar itu sendiri, hanya badan daftar utama. Jadi mengakses boolean mudah untuk metode apa pun yang dipanggil pengguna. Anda dapat membuat aturan bahwa iterator tidak valid jika daftar dibalik, dan / atau menyimpan salinan boolean dengan iterator, misalnya. Jadi saya pikir itu tidak akan mempengaruhi O besar, tentu saja. Saya akui, saya tidak pergi baris demi baris melalui spec. :)
Chris Beck
4
@ Kevin: Hm, apa? Anda tidak dapat xor dua pointer secara langsung, Anda harus mengubahnya menjadi integer terlebih dahulu (jelas bertipe std::uintptr_t. Kemudian Anda dapat xor mereka.
Deduplicator
3
@ Kevin, Anda pasti bisa membuat daftar tertaut XOR di C ++, itu praktis bahasa poster-child untuk hal semacam ini. Tidak ada yang mengatakan Anda harus menggunakan std::uintptr_t, Anda bisa menggunakan chararray dan kemudian XOR komponen. Akan lebih lambat tapi portabel 100%. Kemungkinan Anda bisa membuatnya memilih antara kedua implementasi ini dan hanya menggunakan yang kedua sebagai fallback jika uintptr_thilang. Beberapa jika dijelaskan dalam jawaban ini: stackoverflow.com/questions/14243971/…
Chris Beck
61

Ini bisa menjadi O (1) jika daftar akan menyimpan flag yang memungkinkan bertukar makna pointer " prev" dan " next" yang dimiliki masing-masing node. Jika membalik daftar akan menjadi operasi yang sering, penambahan semacam itu mungkin sebenarnya berguna dan saya tidak tahu alasan mengapa menerapkannya akan dilarang oleh standar saat ini. Namun, memiliki bendera seperti itu akan membuat traversal biasa dari daftar lebih mahal (jika hanya dengan faktor konstan) karena alih-alih

current = current->next;

di operator++daftar iterator, Anda akan dapatkan

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

yang bukan sesuatu yang akan Anda putuskan untuk ditambahkan dengan mudah. Mengingat bahwa daftar biasanya dilalui jauh lebih sering daripada terbalik, akan sangat tidak bijaksana jika standar mengamanatkan teknik ini. Oleh karena itu, operasi sebaliknya diizinkan untuk memiliki kompleksitas linier. Namun, perhatikan bahwa tO (1) ⇒ tO ( n ) jadi, seperti yang disebutkan sebelumnya, menerapkan "optimisasi" Anda secara teknis akan diizinkan.

Jika Anda berasal dari Jawa atau latar belakang serupa, Anda mungkin bertanya-tanya mengapa iterator harus memeriksa bendera setiap kali. Tidak bisakah sebaliknya kita memiliki dua jenis iterator yang berbeda, keduanya berasal dari tipe basis yang sama, dan memiliki std::list::begindan secara std::list::rbeginpolimorfik mengembalikan iterator yang sesuai? Meskipun mungkin, ini akan membuat semuanya menjadi lebih buruk karena memajukan iterator akan menjadi panggilan fungsi tidak langsung (sulit untuk inline) sekarang. Di Jawa, Anda membayar harga ini secara rutin, tetapi sekali lagi, ini adalah salah satu alasan banyak orang meraih C ++ ketika kinerja sangat penting.

Seperti yang ditunjukkan oleh Benjamin Lindley dalam komentar, karena reversetidak diperbolehkan untuk membatalkan iterator, satu-satunya pendekatan yang diizinkan oleh standar tampaknya menyimpan pointer kembali ke daftar di dalam iterator yang menyebabkan akses memori tidak langsung ganda.

5gon12eder
sumber
7
@galinette: std::list::reversetidak membatalkan iterator.
Benjamin Lindley
1
@galinette Maaf, saya salah membaca komentar Anda sebelumnya sebagai "flag per iterator " sebagai kebalikan dari "flag per node " saat Anda menulisnya. Tentu saja, flag per node akan menjadi kontra-produktif seperti yang Anda lakukan, sekali lagi, harus melakukan linear traversal untuk membalik semuanya.
5gon12eder
2
@ 5gon12eder: Anda dapat menghilangkan percabangan dengan biaya yang sangat rendah: simpan nextdan prevpointer dalam array, dan simpan arah sebagai a 0atau 1. Untuk beralih ke depan, Anda akan mengikuti pointers[direction]dan mengulanginya secara terbalik pointers[1-direction](atau sebaliknya). Ini masih akan menambah sedikit overhead, tetapi mungkin kurang dari satu cabang.
Jerry Coffin
4
Anda mungkin tidak dapat menyimpan pointer ke daftar di dalam iterator. swap()ditetapkan sebagai waktu konstan dan tidak membatalkan iterator apa pun.
Tavian Barnes
1
@TavianBarnes Sialan! Nah, triple indirection then… (maksud saya, sebenarnya bukan triple. Anda harus menyimpan bendera dalam objek yang dialokasikan secara dinamis tetapi pointer di iterator tentu saja dapat langsung menunjuk ke objek itu alih-alih secara tidak langsung pada daftar.)
5gon12eder
37

Tentunya karena semua wadah yang mendukung iterator dua arah memiliki konsep rbegin () dan rend (), pertanyaan ini apakah bisa diperdebatkan?

Itu sepele untuk membangun proxy yang membalik iterator dan mengakses wadah melalui itu.

Non-operasi ini memang O (1).

seperti:

#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;
}

output yang diharapkan:

mat
the
on
sat
cat
the

Mengingat hal ini, bagi saya tampaknya komite standar tidak meluangkan waktu untuk mengamanatkan O (1) memesan kembali wadah karena itu tidak perlu, dan perpustakaan standar sebagian besar dibangun berdasarkan prinsip mandat hanya apa yang benar-benar diperlukan sementara menghindari duplikasi.

Hanya 2c saya.

Richard Hodges
sumber
18

Karena harus melintasi setiap node ( ntotal) dan memperbarui datanya (langkah pembaruan memang O(1)). Ini membuat seluruh operasi O(n*1) = O(n).

Buta
sumber
27
Karena Anda perlu memperbarui tautan antara setiap item juga. Keluarkan selembar kertas dan tarik keluar bukannya downvoting.
Blindy
11
Mengapa bertanya apakah Anda begitu yakin? Anda membuang-buang waktu kita.
Blindy
28
@Curious Node daftar yang tertaut secara ganda memiliki arah. Alasan dari sana.
Sepatu
11
@ Blindy Jawaban yang bagus harus lengkap. "Keluarkan selembar kertas dan tarik keluar" dengan demikian seharusnya tidak menjadi komponen yang diperlukan dari jawaban yang baik. Jawaban yang bukan jawaban yang baik akan dikenakan downvotes.
RM
7
@Shoe: Apakah harus? Silakan teliti XOR-linked-list dan sejenisnya.
Deduplicator
2

Ini juga menukar pointer sebelumnya dan berikutnya untuk setiap node. Karena itulah dibutuhkan Linear. Meskipun dapat dilakukan dalam O (1) jika fungsi menggunakan LL ini juga mengambil informasi tentang LL sebagai input seperti apakah itu mengakses secara normal atau terbalik.

techcomp
sumber
1

Hanya penjelasan algoritma. Bayangkan Anda memiliki array dengan elemen, maka Anda perlu membalikkannya. Ide dasarnya adalah untuk beralih pada setiap elemen mengubah elemen di posisi pertama ke posisi terakhir, elemen di posisi kedua ke posisi kedua dari belakang, dan sebagainya. Ketika Anda mencapai di tengah array Anda akan memiliki semua elemen berubah, sehingga dalam (n / 2) iterasi, yang dianggap O (n).

danilobraga
sumber
1

Ini O (n) hanya karena perlu menyalin daftar dalam urutan terbalik. Setiap operasi item individu adalah O (1) tetapi ada n dari mereka di seluruh daftar.

Tentu saja ada beberapa operasi waktu konstan yang terlibat dalam mengatur ruang untuk daftar baru, dan mengubah pointer sesudahnya, dll. Notasi O tidak mempertimbangkan konstanta individual begitu Anda memasukkan faktor n orde pertama.

SDsolar
sumber