Rekursi atau saat loop

123

Saya membaca tentang beberapa praktik pengembangan wawancara, khususnya tentang pertanyaan teknis dan tes yang diajukan pada wawancara dan saya telah beberapa kali tersandung kata-kata dari genre "Ok Anda memecahkan masalah dengan loop sementara, sekarang Anda dapat melakukannya dengan rekursi ", atau" semua orang bisa menyelesaikan ini dengan 100 baris sementara loop, tetapi bisakah mereka melakukannya dalam fungsi rekursif 5 garis? " dll.

Pertanyaan saya adalah, apakah rekursi umumnya lebih baik daripada jika / sementara / untuk konstruksi?

Sejujurnya saya selalu berpikir bahwa rekursi tidak disukai, karena terbatas pada memori tumpukan yang jauh lebih kecil daripada tumpukan, juga melakukan sejumlah besar panggilan fungsi / metode yang tidak optimal dari sudut pandang kinerja, tapi saya mungkin salah ...

Shivan Dragon
sumber
73
Mengenai masalah rekursi, ini terlihat cukup menarik.
dan_waterworth
4
@dan_waterworth juga, ini akan membantu: google.fr/… tapi saya selalu keliru mengeja: P
Shivan Dragon
@ShivanDragon Saya pikir begitu ^ _ ^ Sangat tepat bahwa saya mempostingnya kemarin :-)
Neal
2
Dalam lingkungan tertanam saya telah bekerja dalam rekursi disukai dan membuat Anda dicambuk secara publik. Ruang stack terbatas yang berlaku membuatnya ilegal.
Fred Thomsen

Jawaban:

192

Rekursi secara intrinsik tidak lebih baik atau lebih buruk daripada loop - masing-masing memiliki kelebihan dan kekurangan, dan mereka bahkan tergantung pada bahasa pemrograman (dan implementasi).

Secara teknis, loop berulang sesuai dengan sistem komputer tipikal yang lebih baik pada tingkat perangkat keras: pada tingkat kode mesin, satu lingkaran hanyalah sebuah tes dan lompatan bersyarat, sedangkan rekursi (diimplementasikan secara naif) melibatkan mendorong bingkai tumpukan, melompat, kembali, dan muncul kembali dari tumpukan. OTOH, banyak kasus rekursi (terutama yang sepele setara dengan loop berulang) dapat ditulis sehingga stack push / pop dapat dihindari; ini dimungkinkan ketika pemanggilan fungsi rekursif adalah hal terakhir yang terjadi dalam fungsi body sebelum kembali, dan itu umumnya dikenal sebagai pengoptimalan panggilan ekor (atau pengoptimalan pengulangan ekor ). Fungsi rekursif yang dioptimalkan tail-call-dioptimalkan sebagian besar setara dengan loop berulang pada tingkat kode mesin.

Pertimbangan lain adalah bahwa loop berulang membutuhkan pembaruan keadaan destruktif, yang membuatnya tidak kompatibel dengan semantik bahasa murni (efek samping). Ini adalah alasan mengapa bahasa murni seperti Haskell tidak memiliki konstruksi loop sama sekali, dan banyak bahasa pemrograman fungsional lainnya tidak memiliki mereka sepenuhnya atau menghindarinya sebanyak mungkin.

Alasan mengapa pertanyaan-pertanyaan ini muncul begitu banyak dalam wawancara, adalah karena untuk menjawabnya, Anda memerlukan pemahaman menyeluruh tentang banyak konsep pemrograman vital - variabel, pemanggilan fungsi, cakupan, dan tentu saja loop dan rekursi -, dan Anda memiliki untuk membawa fleksibilitas mental ke meja yang memungkinkan Anda untuk mendekati masalah dari dua sudut yang sangat berbeda, dan bergerak di antara berbagai manifestasi dari konsep yang sama.

Pengalaman dan penelitian menunjukkan bahwa ada garis antara orang yang memiliki kemampuan untuk memahami variabel, petunjuk, dan rekursi, dan mereka yang tidak. Hampir semua hal lain dalam pemrograman, termasuk kerangka kerja, API, bahasa pemrograman, dan kasus tepi mereka, dapat diperoleh melalui pembelajaran dan pengalaman, tetapi jika Anda tidak dapat mengembangkan intuisi untuk ketiga konsep inti ini, Anda tidak layak menjadi seorang programmer. Menerjemahkan loop berulang sederhana ke versi rekursif adalah tentang cara tercepat yang mungkin untuk menyaring non-programmer - bahkan seorang programmer yang agak tidak berpengalaman biasanya dapat melakukannya dalam 15 menit, dan ini merupakan masalah agnostik bahasa yang sangat, sehingga kandidat dapat memilih bahasa pilihan mereka alih-alih tersandung keanehan.

Jika Anda mendapatkan pertanyaan seperti ini dalam sebuah wawancara, itu pertanda baik: itu artinya calon majikan mencari orang yang bisa memprogram, bukan orang yang hafal manual alat pemrograman.

tammmer
sumber
3
Saya paling suka jawaban Anda, karena menyentuh pada aspek paling penting yang bisa dimiliki oleh jawaban atas pertanyaan ini, itu menjelaskan bagian teknis utama, dan juga memberikan pandangan yang diperbesar tentang bagaimana masalah ini cocok di bidang pemrograman.
Shivan Dragon
1
Saya juga memperhatikan pola dalam pemrograman sinkronisasi di mana loop dihindari demi panggilan rekursif pada iterator yang berisi metode .next (). Saya menganggap itu menjaga kode berjalan lama dari menjadi terlalu serakah CPU.
Evan Plaice
1
Versi iteratif juga melibatkan nilai push dan poping. Anda hanya perlu menulis kode secara manual untuk melakukan ini. Versi rekursif mendorong negara ke tumpukan dalam algoritma iteratif yang biasanya harus disimulasikan secara manual dengan mendorong negara ke dalam struktur tertentu. Hanya algoritma yang paling sepele yang tidak membutuhkan keadaan ini dan dalam kasus ini kompiler biasanya dapat melihat rekursi ekor dan menanam solusi berulang.
Martin York
1
@tdammers Bisakah Anda memberi tahu saya di mana saya bisa membaca studi yang Anda sebutkan "Pengalaman dan penelitian menunjukkan bahwa ada garis di antara orang-orang ..." Ini tampaknya sangat menarik bagi saya.
Yoo Matsuo
2
Satu hal yang Anda lupa sebutkan, kode iteratif cenderung berkinerja lebih baik ketika Anda berurusan dengan satu utas eksekusi, tetapi algoritma rekursif cenderung meminjamkan diri untuk dieksekusi di banyak utas.
GordonM
37

Tergantung.

  • Beberapa masalah sangat cocok untuk solusi rekursif misalnya quicksort
  • Beberapa bahasa tidak benar-benar mendukung rekursi misalnya FORTRAN awal
  • Beberapa bahasa menganggap rekursi sebagai sarana utama untuk pengulangan misalnya Haskell

Perlu juga dicatat bahwa dukungan untuk rekursi ekor membuat loop rekursif dan berulang berulang, yaitu, rekursi tidak selalu harus membuang tumpukan.

Juga, algoritma rekursif selalu dapat diimplementasikan secara iteratif dengan menggunakan stack eksplisit .

Akhirnya, saya perhatikan bahwa solusi lima baris mungkin selalu lebih baik daripada 100 baris satu (dengan asumsi mereka sebenarnya setara).

jk.
sumber
5
Jawaban yang bagus (+1). "solusi 5 baris mungkin selalu lebih baik daripada solusi 100 baris": Saya pikir keringkasan bukan satu-satunya keuntungan rekursi. Menggunakan panggilan rekursif memaksa Anda untuk membuat dependensi fungsional antara nilai-nilai dalam iterasi yang berbeda menjadi eksplisit.
Giorgio
4
Solusi yang lebih pendek memang cenderung lebih baik, tetapi ada yang namanya terlalu singkat.
dan_waterworth
5
@dan_waterworth dibandingkan dengan "100 line" agak sulit untuk menjadi terlalu singkat
agas
4
@ Giorgio, Anda dapat membuat program lebih kecil dengan menghapus kode yang tidak perlu atau dengan membuat hal-hal eksplisit tersirat. Selama Anda tetap pada yang pertama, Anda meningkatkan kualitas.
dan_waterworth
1
@ jk, saya pikir itu bentuk lain dari membuat informasi eksplisit tersirat. Informasi tentang apa variabel digunakan untuk dihapus dari namanya di mana itu eksplisit dan didorong ke dalam penggunaannya yang tersirat.
dan_waterworth
17

Tidak ada kesepakatan universal tentang definisi "lebih baik" dalam hal pemrograman, tetapi saya akan mengartikannya sebagai "lebih mudah untuk mempertahankan / membaca".

Rekursi memiliki kekuatan lebih ekspresif daripada konstruksi berulang berulang: Saya mengatakan ini karena loop sementara setara dengan fungsi rekursif ekor dan fungsi rekursif tidak perlu ekor rekursif. Konstruksi yang kuat biasanya merupakan hal yang buruk karena memungkinkan Anda melakukan hal-hal yang sulit dibaca. Namun, rekursi memberi Anda kemampuan untuk menulis loop tanpa menggunakan kemampuan berubah-ubah dan bagi saya, kemampuan berubah jauh lebih kuat daripada rekursi.

Jadi, dari daya ekspresif rendah hingga daya ekspresif tinggi, susunan looping disusun seperti ini:

  • Ekor fungsi rekursif yang menggunakan data tidak berubah,
  • Fungsi rekursif yang menggunakan data tidak berubah,
  • Sementara loop yang menggunakan data yang bisa diubah,
  • Ekor fungsi rekursif yang menggunakan data bisa berubah,
  • Fungsi rekursif yang menggunakan data yang bisa diubah,

Idealnya, Anda akan menggunakan konstruksi paling ekspresif yang Anda bisa. Tentu saja, jika bahasa Anda tidak mendukung pengoptimalan panggilan ekor, maka itu juga dapat memengaruhi pilihan Anda untuk membuat perulangan.

dan_waterworth
sumber
1
"a while loop setara dengan fungsi rekursif ekor dan fungsi rekursif tidak perlu menjadi rekursif ekor": +1. Anda dapat mensimulasikan rekursi melalui loop sementara + tumpukan.
Giorgio
1
Saya tidak yakin saya setuju 100% tapi itu tentu perspektif yang menarik jadi +1 untuk itu.
Konrad Rudolph
Memberi +1 untuk jawaban yang bagus dan juga untuk menyebutkan bahwa beberapa bahasa (atau kompiler) tidak melakukan optimasi panggilan ekor.
Shivan Dragon
@Iorgio, "Anda dapat mensimulasikan rekursi melalui loop sementara + setumpuk", Inilah sebabnya saya mengatakan kekuatan ekspresif. Secara komputasi, mereka sama kuatnya.
dan_waterworth
@dan_waterworth: Tepat, dan seperti yang Anda katakan dalam jawaban Anda, rekursi saja lebih ekspresif daripada loop sementara karena Anda perlu menambahkan tumpukan ke loop sementara untuk mensimulasikan rekursi.
Giorgio
7

Rekursi seringkali kurang jelas. Kurang jelas lebih sulit dipertahankan.

Jika Anda menulis for(i=0;i<ITER_LIMIT;i++){somefunction(i);}di arus utama, Anda membuatnya sangat jelas bahwa Anda sedang menulis loop. Jika Anda menulis, somefunction(ITER_LIMIT);Anda tidak benar-benar menjelaskan apa yang akan terjadi. Hanya melihat konten: somefunction(int x)panggilan itu somefunction(x-1)memberi tahu Anda bahwa itu sebenarnya sebuah loop menggunakan iterasi. Juga, Anda tidak dapat dengan mudah menempatkan kondisi pelarian dengan break;suatu tempat di tengah-tengah iterasi, Anda harus menambahkan kondisional yang akan dilewati semua jalan kembali, atau melemparkan pengecualian. (dan pengecualian lagi menambah kompleksitas ...)

Intinya, jika itu pilihan yang jelas antara iterasi dan rekursi, lakukan hal yang intuitif. Jika iterasi melakukan pekerjaan dengan mudah, menyimpan 2 baris jarang sepadan dengan sakit kepala yang mungkin terjadi dalam jangka panjang.

Tentu saja jika itu menghemat 98 baris, itu masalah yang sama sekali berbeda.

Ada beberapa situasi di mana rekursi sangat cocok, dan mereka tidak terlalu jarang. Traversal struktur pohon, jaringan multiply-linked, struktur yang dapat mengandung tipenya sendiri, array bergerigi multi-dimensi, pada dasarnya segala sesuatu yang bukan vektor langsung atau array dimensi tetap. Jika Anda melintasi jalur yang dikenal dan lurus, lakukan iterate. Jika Anda menyelam ke yang tidak diketahui, kambuh.

Pada dasarnya, jika somefunction(x-1)dipanggil dari dalam dirinya sendiri lebih dari sekali per level, lupakan iterasi.

... Menulis secara iteratif berfungsi untuk tugas-tugas yang paling baik dilakukan dengan rekursi mungkin tetapi tidak menyenangkan. Di mana pun Anda menggunakan int, Anda membutuhkan sesuatu seperti stack<int>. Saya melakukannya sekali, lebih sebagai latihan daripada untuk tujuan praktis. Saya dapat meyakinkan Anda begitu Anda menghadapi tugas seperti itu Anda tidak akan memiliki keraguan seperti yang Anda ungkapkan.

SF.
sumber
10
Apa yang jelas dan apa yang kurang jelas sebagian tergantung pada apa yang Anda terbiasa. Iterasi telah digunakan lebih sering dalam bahasa pemrograman karena lebih dekat dengan cara kerja CPU (yaitu menggunakan lebih sedikit memori dan mengeksekusi lebih cepat). Tetapi jika Anda terbiasa berpikir secara induktif, rekursi bisa sama intuitifnya.
Giorgio
5
"Jika mereka melihat kata kunci loop, mereka tahu itu adalah loop. Tetapi tidak ada kata kunci berulang, mereka hanya dapat mengenalinya dengan melihat f (x-1) di dalam f (x).": Ketika Anda memanggil fungsi rekursif yang Anda lakukan tidak ingin tahu bahwa itu adalah rekursif. Demikian pula, ketika Anda memanggil fungsi yang berisi loop, Anda tidak ingin tahu bahwa itu berisi loop.
Giorgio
3
@ SF: Ya tetapi Anda hanya dapat melihat ini jika Anda melihat isi dari fungsi tersebut. Dalam kasus loop, Anda melihat loop, dalam kasus rekursi, Anda melihat bahwa fungsi itu memanggil dirinya sendiri.
Giorgio
5
@ SF: Sepertinya sedikit alasan melingkar kepada saya: "Jika dalam intuisi saya itu adalah loop, maka itu adalah loop." mapdapat didefinisikan sebagai fungsi rekursif (lihat misalnya haskell.org/tutorial/functions.html ), bahkan jika secara intuitif jelas bahwa itu melintasi daftar dan menerapkan fungsi untuk setiap anggota daftar.
Giorgio
5
@ SF, mapbukan kata kunci, ini adalah fungsi biasa, tapi itu sedikit tidak relevan. Ketika programmer fungsional menggunakan rekursi, biasanya itu bukan karena mereka ingin melakukan serangkaian tindakan, itu karena masalah yang dipecahkan dapat dinyatakan sebagai fungsi dan daftar argumen. Masalahnya kemudian dapat dikurangi menjadi fungsi lain dan daftar argumen lainnya. Akhirnya, Anda memiliki masalah yang bisa diselesaikan dengan mudah.
dan_waterworth
6

Seperti biasa, ini tidak dapat dijawab secara umum karena ada faktor-faktor tambahan, yang dalam praktiknya secara luas tidak sama antara kasus dan tidak sama satu sama lain dalam kasus penggunaan. Inilah beberapa tekanannya.

  • Kode pendek dan elegan pada umumnya lebih baik daripada kode panjang dan rumit.
  • Namun, poin terakhir agak batal jika basis pengembang Anda tidak terbiasa dengan rekursi dan tidak mau / tidak bisa belajar. Bahkan mungkin menjadi sedikit negatif daripada positif.
  • Rekursi dapat berakibat buruk bagi efisiensi jika Anda akan membutuhkan panggilan yang bersarang dalam praktik dan Anda tidak dapat menggunakan rekursi ekor (atau lingkungan Anda tidak dapat mengoptimalkan rekursi ekor).
  • Rekursi juga buruk dalam banyak kasus jika Anda tidak dapat melakukan cache hasil menengah dengan benar. Sebagai contoh, contoh umum menggunakan rekursi pohon untuk menghitung angka Fibonacci berkinerja sangat buruk jika Anda tidak melakukan cache. Jika Anda melakukan cache, itu sederhana, cepat, elegan, dan sekaligus luar biasa indah.
  • Rekursi tidak berlaku untuk beberapa kasus, sama baiknya dengan iterasi pada kasus lain, dan mutlak diperlukan pada kasus lainnya. Memanjat melalui rantai panjang aturan bisnis biasanya tidak tertolong oleh rekursi sama sekali. Iterasi melalui aliran data dapat bermanfaat dilakukan dengan rekursi. Iterasi lebih dari struktur data dinamis multidimensi (misalnya labirin, pohon objek ...) hampir tidak mungkin tanpa rekursi, eksplisit atau implisit. Perhatikan bahwa dalam kasus-kasus ini, rekursi eksplisit jauh lebih baik daripada implisit - tidak ada yang lebih menyakitkan daripada membaca kode di mana seseorang menerapkan tumpukan ad-hoc, tidak lengkap, dan tidak stabil dalam bahasa hanya untuk menghindari kata-R yang menakutkan.
Kilian Foth
sumber
Apa yang Anda maksud dengan caching sehubungan dengan rekursi?
Giorgio
@Giorgio mungkin memoisasi
jk.
Feh. Jika lingkungan Anda tidak mengoptimalkan tail tail, Anda harus menemukan lingkungan yang lebih baik, dan jika pengembang Anda tidak terbiasa dengan rekursi, Anda harus menemukan pengembang yang lebih baik. Memiliki beberapa standar, orang-orang!
CA McCann
1

Rekursi adalah tentang mengulangi panggilan ke fungsi, loop adalah tentang pengulangan melompat ke tempat di memori.

Harus disebutkan juga tentang stack overflow - http://en.wikipedia.org/wiki/Stack_overflow

okobaka
sumber
1
Rekursi berarti suatu fungsi yang definisi memerlukan pemanggilan itu sendiri.
hardmath
1
Meskipun definisi sederhana Anda tidak 100% akurat, hanya Anda satu-satunya yang menyebutkan stack overflow.
Qix
1

Itu sangat tergantung pada kenyamanan atau persyaratan:

Jika Anda menggunakan bahasa pemrograman Python , ia mendukung rekursi, tetapi secara default ada batas untuk kedalaman rekursi (1000). Jika melebihi batas, kami akan mendapatkan kesalahan atau pengecualian. Batas itu dapat diubah, tetapi jika kita melakukannya kita mungkin mengalami situasi abnormal dalam bahasa.

Pada saat ini (jumlah panggilan lebih dari kedalaman rekursi), kita perlu memilih konstruksi lingkaran. Maksud saya, jika ukuran stack tidak cukup, kita harus memilih konstruksi loop.

usernaveen
sumber
3
Berikut ini adalah blog oleh Guido van Rossum tentang mengapa ia tidak ingin pengoptimalan pengulangan ekor di Python (mendukung gagasan bahwa bahasa yang berbeda menggunakan pendekatan taktis yang berbeda).
Hardmath
-1

Gunakan pola desain strategi.

  • Rekursi bersih
  • Loop (bisa dibilang) efisien

Bergantung pada beban Anda (dan / atau kondisi lainnya), pilih satu.

Pravin Sonawane
sumber
5
Tunggu apa? Bagaimana pola strategi cocok di sini? Dan kalimat kedua terdengar seperti frasa kosong.
Konrad Rudolph
@KonradRudolph Saya akan melakukan rekursi. Untuk set data yang sangat besar, saya akan beralih ke loop. Itu yang saya maksud. Saya minta maaf jika tidak jelas.
Pravin Sonawane
3
Ah. Yah, saya masih tidak yakin bahwa ini bisa disebut "pola desain strategi", yang memiliki arti yang sangat tetap dan Anda menggunakannya secara metaforis. Tapi sekarang setidaknya saya melihat ke mana Anda pergi.
Konrad Rudolph
@KonradRudolph mendapat pelajaran penting. Sangat jelaskan apa yang ingin Anda katakan .. Terima kasih .. itu membantu .. :)
Pravin Sonawane
2
@Pravin Sonawane: Jika Anda dapat menggunakan pengoptimalan rekursi ekor, Anda juga dapat menggunakan rekursi pada kumpulan data besar.
Giorgio