Mengapa menambahkan ke Daftar di Scala memiliki kompleksitas waktu O (n)?

13

Saya baru saja membaca bahwa waktu pelaksanaan operasi append untuk List(: +) tumbuh secara linear dengan ukuran List.

Menambahkan ke operasi Listsepertinya cukup umum. Mengapa cara idiomatis untuk melakukan ini adalah dengan menambahkan komponen dan kemudian membalik daftar? Itu juga tidak bisa menjadi kegagalan desain karena implementasi dapat diubah pada titik mana pun.

Dari sudut pandang saya, baik prepending dan appending harus O (1).

Apakah ada alasan yang sah untuk ini?

DPM
sumber
2
Tergantung pada definisi Anda tentang "sah". Scala sangat ke dalam struktur data yang tidak berubah, daftar anonim di mana-mana, komposisi fungsional, dll. Implementasi daftar default (tanpa pointer yang bisa berubah-ubah ke daftar ekor) berfungsi dengan baik untuk gaya itu. Jika Anda memerlukan daftar yang lebih kuat, setidaknya sangat mudah untuk menulis wadah Anda sendiri yang hampir tidak dapat dibedakan dari yang standar.
Kilian Foth
1
Terkait situs silang - Menambahkan elemen ke akhir daftar di Scala - ada sedikit di sana tentang sifatnya. Ini muncul bahwa daftar di scala adalah kekal, sehingga Anda perlu menyalinnya, yaitu O (N).
Anda dapat menggunakan salah satu dari banyak struktur data yang dapat berubah, atau struktur data yang tidak berubah dengan O (1) menambah waktu (Vektor) yang disediakan Scala. List[T]mengasumsikan Anda menggunakannya seperti yang Anda lakukan dalam bahasa fungsional murni - umumnya bekerja dari kepala dengan dekonstruksi dan prepends.
KChaloux
3
Prepending akan menempatkan pointer simpul berikutnya dari kepala baru ke daftar abadi yang ada - yang tidak dapat berubah. Itu O (1).
1
Untuk pekerjaan mani dan analisis pada topik umum pengukuran kompleksitas struktur data dalam FP murni, bacalah tesis Okasaki yang kemudian diterbitkan sebagai buku. Ini adalah bacaan yang sangat dihargai dan sangat baik bagi siapa saja yang mempelajari beberapa FP untuk memahami bagaimana cara berpikir tentang pengorganisasian data dalam FP. Ini juga ditulis dengan baik dan mudah dibaca dan diikuti semuanya teks yang berkualitas.
Jimmy Hoffa

Jawaban:

24

Saya akan sedikit memperluas komentar saya. The List[T]struktur data, dari scala.collection.immutabledioptimalkan untuk bekerja dengan cara yang daftar kekal di lebih murni fungsional karya bahasa pemrograman. Hal ini telah sangat cepat tambahkan kali, dan diasumsikan bahwa Anda akan bekerja di kepala untuk hampir semua akses Anda.

Daftar yang tidak dapat diubah memiliki waktu prependensi yang sangat cepat karena fakta bahwa mereka memodelkan daftar tertaut mereka sebagai serangkaian "sel kontra". Sel mendefinisikan nilai tunggal, dan penunjuk ke sel berikutnya (gaya daftar tunggal-tertaut klasik):

Cell [Value| -> Nil]

Saat Anda menambahkan ke daftar, Anda benar-benar hanya membuat satu sel baru, dengan sisa daftar yang ada diarahkan ke:

Cell [NewValue| -> [Cell[Value| -> Nil]]

Karena daftar ini tidak dapat diubah, Anda aman untuk melakukan ini tanpa menyalin yang sebenarnya . Tidak ada bahaya perubahan daftar lama dan menyebabkan semua nilai dalam daftar baru Anda menjadi tidak valid. Namun, Anda kehilangan kemampuan untuk memiliki pointer yang bisa berubah ke akhir daftar Anda sebagai kompromi.

Ini cocok untuk bekerja secara rekursif pada daftar. Katakanlah Anda mendefinisikan versi Anda sendiri filter:

def deleteIf[T](list : List[T])(f : T => Boolean): List[T] = list match {
  case Nil => Nil
  case (x::xs) => f(x) match {
    case true => deleteIf(xs)(f)
    case false => x :: deleteIf(xs)(f)
  }
}

Itu adalah fungsi rekursif yang bekerja dari kepala daftar secara eksklusif, dan memanfaatkan pencocokan pola melalui :: extractor. Ini adalah sesuatu yang Anda lihat banyak dalam bahasa-bahasa seperti Haskell.

Jika Anda benar-benar ingin menambahkan cepat, Scala menyediakan banyak struktur data yang bisa berubah dan tidak dapat diubah untuk dipilih. Di sisi bisa berubah, Anda mungkin melihat ke dalam ListBuffer. Atau, Vectordari scala.collection.immutablememiliki waktu penambahan cepat.

KChaloux
sumber
sekarang saya mengerti! Masuk akal sepenuhnya.
DPM
Saya tidak tahu Scala, tetapi bukankah itu elseloop tak terbatas? Saya pikir itu harus seperti x::deleteIf(xs)(f).
svick
@vick Uh ... ya. Ya itu. Saya menulisnya dengan cepat dan tidak memverifikasi kode saya, karena saya punya pertemuan untuk pergi ke: p (Harus diperbaiki sekarang!)
KChaloux
@Jubbat Karena headdan tailakses dengan daftar seperti ini sangat cepat - lebih cepat daripada menggunakan peta atau array berbasis hash - ini adalah tipe yang sangat baik untuk fungsi rekursif. Ini adalah salah satu alasan mengapa daftar adalah tipe inti dalam sebagian besar bahasa fungsional (mis. Haskell atau Skema)
bruce
Jawaban yang sangat bagus. Saya mungkin akan menambahkan TL; DR yang hanya mengatakan "karena Anda harus menambahkan, tidak menambahkan" (dapat membantu menjernihkan asumsi dasar yang dimiliki sebagian besar pengembang tentang Lists dan menambahkan / menambahkan).
Daniel B