Adakah yang bisa dilakukan dengan rekursi yang tidak bisa dilakukan dengan loop?


126

Ada saat-saat di mana menggunakan rekursi lebih baik daripada menggunakan loop, dan waktu di mana menggunakan loop lebih baik daripada menggunakan rekursi. Memilih yang "benar" dapat menghemat sumber daya dan / atau menghasilkan lebih sedikit baris kode.

Apakah ada kasus di mana tugas hanya dapat dilakukan dengan menggunakan rekursi, bukan loop?


13
Saya sangat meragukannya. Rekursi adalah loop yang dimuliakan.
Lightness Races in Orbit

6
Melihat arah yang berbeda ke mana jawaban pergi (dan baru saja gagal dalam memberikan yang lebih baik), Anda dapat melakukan siapa saja yang mencoba menjawab bantuan jika Anda memberikan latar belakang yang sedikit lebih banyak dan jawaban seperti apa yang Anda cari. Apakah Anda menginginkan bukti teoritis untuk mesin hipotetis (dengan penyimpanan tidak terbatas dan waktu berjalan)? Atau contoh praktis? (Di mana "akan sangat rumit" mungkin memenuhi syarat sebagai "tidak dapat dilakukan".) Atau sesuatu yang berbeda?
5gon12eder

8
@LightnessRacesinOrbit Di telinga saya yang bukan penutur asli bahasa Inggris, "Rekursi adalah loop yang dimuliakan" terdengar maksud Anda, "Sebaiknya Anda menggunakan konstruksi perulangan alih-alih panggilan rekursif di mana saja, dan konsepnya tidak benar-benar layak atas namanya sendiri" . Mungkin saya menafsirkan idiom "sesuatu yang mulia" itu salah.
hyde

13
Bagaimana dengan fungsi Ackermann? en.wikipedia.org/wiki/Ackermann_function , tidak terlalu berguna tetapi tidak mungkin dilakukan melalui perulangan. (Anda mungkin juga ingin memeriksa video ini youtube.com/watch?v=i7sm9dzFtEI oleh Computerphile)
WizardOfMenlo

8
@WizardOfMenlo kode befunge adalah implementasi dari solusi ERRE (yang juga merupakan solusi interaktif ... dengan tumpukan). Pendekatan berulang dengan tumpukan dapat meniru panggilan rekursif. Pada pemrograman apa pun yang sangat kuat, satu konstruksi perulangan dapat digunakan untuk meniru yang lain. Mesin register dengan instruksi INC (r), JZDEC (r, z)dapat mengimplementasikan mesin Turing. Ia tidak memiliki 'rekursi' - itu Lompatan jika Nol lain Dekrement. Jika fungsi Ackermann dapat dihitung (itu adalah), mesin register itu dapat melakukannya.

Jawaban:


164

Iya dan tidak. Pada akhirnya, tidak ada rekursi yang bisa menghitung perulangan itu tidak bisa, tetapi perulangan membutuhkan lebih banyak pipa ledeng. Oleh karena itu, satu hal yang dapat dilakukan rekursi agar loop tidak dapat membuat beberapa tugas menjadi sangat mudah.

Berjalanlah di pohon. Berjalan pohon dengan rekursi itu gampang-gampang susah. Itu hal yang paling alami di dunia. Berjalan pohon dengan loop jauh lebih mudah. Anda harus memelihara tumpukan atau struktur data lain untuk melacak apa yang telah Anda lakukan.

Seringkali, solusi rekursif untuk suatu masalah lebih cantik. Itu istilah teknis, dan itu penting.


120
Pada dasarnya, melakukan loop bukan rekursi berarti menangani tumpukan secara manual.
Silviu Burcea

15
... tumpukan . Situasi berikut mungkin lebih suka memiliki lebih dari satu tumpukan. Pertimbangkan satu fungsi rekursif Ayang menemukan sesuatu di pohon. Setiap kali Abertemu hal itu, ia meluncurkan fungsi rekursif lain Byang menemukan hal terkait di subtree pada posisi di mana ia diluncurkan A. Setelah Bmenyelesaikan rekursi itu kembali ke A, dan yang terakhir melanjutkan rekursi sendiri. Satu dapat mendeklarasikan satu tumpukan untuk Adan satu untuk B, atau meletakkan Btumpukan di dalam Aloop. Jika seseorang bersikeras menggunakan tumpukan tunggal, semuanya menjadi sangat rumit.
rwong

35
Therefore, the one thing recursion can do that loops can't is make some tasks super easy. Dan satu hal yang dapat dilakukan loop agar rekursi tidak dapat membuat beberapa tugas menjadi sangat mudah. Pernahkah Anda melihat hal-hal buruk dan tidak intuitif yang harus Anda lakukan untuk mengubah sebagian besar masalah yang berulang secara alami dari rekursi naif menjadi rekursi ekor sehingga tidak akan menghancurkan tumpukan?
Mason Wheeler

10
@MasonWheeler 99% dari waktu "hal-hal" dapat lebih baik dikemas dalam operator rekursi seperti mapatau fold(pada kenyataannya jika Anda memilih untuk menganggap mereka primitif, saya pikir Anda dapat menggunakan fold/ unfoldsebagai alternatif ketiga untuk loop atau rekursi). Kecuali jika Anda menulis kode perpustakaan, tidak ada banyak kasus di mana Anda harus khawatir tentang implementasi iterasi, daripada tugas yang seharusnya dilakukan - dalam praktiknya, itu berarti loop eksplisit dan rekursi eksplisit sama-sama buruk. abstraksi yang harus dihindari di tingkat atas.
Leushenko

7
Anda dapat membandingkan dua string dengan membandingkan substring secara rekursif, tetapi hanya membandingkan setiap karakter, satu per satu, hingga Anda mendapatkan ketidakcocokan yang cenderung berkinerja lebih baik dan lebih jelas bagi pembaca.
Steven Burnap

78

Tidak.

Mendapatkan ke yang sangat dasar-dasar minimum yang diperlukan untuk menghitung, Anda hanya perlu untuk dapat lingkaran (ini saja tidak cukup, melainkan merupakan komponen yang diperlukan). Tidak masalah bagaimana caranya .

Bahasa pemrograman apa pun yang dapat menerapkan Mesin Turing, disebut Turing complete . Dan ada banyak bahasa yang turing lengkap.

Bahasa favorit saya di jalan di sana "yang benar-benar berfungsi?" Kelengkapan Turing adalah dari FRACTRAN , yang merupakan Turing lengkap . Ini memiliki satu struktur loop, dan Anda dapat menerapkan mesin Turing di dalamnya. Dengan demikian, apa pun yang dapat dihitung, dapat diimplementasikan dalam bahasa yang tidak memiliki rekursi. Oleh karena itu, tidak ada yang rekursi dapat berikan kepada Anda dalam hal komputabilitas yang tidak dapat dilakukan perulangan sederhana.

Ini sebenarnya bermuara pada beberapa poin:

  • Apa pun yang dapat dihitung dapat dihitung pada mesin Turing
  • Bahasa apa pun yang dapat mengimplementasikan mesin Turing (disebut Turing complete), dapat menghitung apa saja yang dapat dilakukan oleh bahasa lain
  • Karena ada mesin Turing dalam bahasa yang tidak memiliki rekursi (dan ada yang lain yang hanya memiliki rekursi ketika Anda masuk ke beberapa esolang lainnya), maka tentu benar bahwa tidak ada yang dapat Anda lakukan dengan rekursi yang tidak dapat Anda lakukan dengan loop (dan tidak ada yang dapat Anda lakukan dengan loop yang tidak dapat Anda lakukan dengan rekursi).

Ini bukan untuk mengatakan bahwa ada beberapa kelas masalah yang lebih mudah dianggap dengan rekursi daripada dengan perulangan, atau dengan perulangan daripada dengan rekursi. Namun, alat-alat ini juga sama kuatnya.

Dan sementara saya mengambil ini ke 'esolang' ekstrim (kebanyakan karena Anda dapat menemukan hal-hal yang Turing lengkap dan diimplementasikan dengan cara yang agak aneh), ini tidak berarti bahwa esolang dengan cara apa pun opsional. Ada seluruh daftar hal-hal yang secara tidak sengaja lengkap Turing termasuk Magic the Gathering, Sendmail, templat MediaWiki, dan sistem jenis Scala. Banyak dari ini jauh dari optimal ketika datang untuk benar-benar melakukan sesuatu yang praktis, hanya saja Anda dapat menghitung apa pun yang dapat dihitung menggunakan alat-alat ini.


Kesetaraan ini bisa sangat menarik ketika Anda masuk ke jenis rekursi tertentu yang dikenal sebagai panggilan ekor .

Jika Anda memiliki, katakanlah, metode faktorial yang ditulis sebagai:

int fact(int n) {
    return fact(n, 1);
}

int fact(int n, int accum) {
    if(n == 0) { return 1; }
    if(n == 1) { return accum; }
    return fact(n-1, n * accum);
}

Jenis rekursi ini akan ditulis ulang sebagai loop - tidak ada tumpukan yang digunakan. Pendekatan semacam itu memang seringkali lebih elegan dan lebih mudah dipahami daripada loop ekivalen yang ditulis, tetapi sekali lagi, untuk setiap panggilan rekursif dapat ada loop ekivalen yang ditulis dan untuk setiap loop dapat ada panggilan rekursif yang ditulis.

Ada juga saat-saat di mana mengubah loop sederhana menjadi panggilan rekursif panggilan ekor dapat berbelit-belit dan lebih sulit untuk dipahami.


Jika Anda ingin masuk ke sisi teori itu, lihat tesis Church Turing . Anda juga dapat menemukan tesis gereja-turing di CS.SE berguna.


29
Kelengkapan Turing terlempar terlalu banyak seperti itu penting. Banyak hal yang Turing Lengkap ( seperti Magic the Gathering ), tetapi itu tidak berarti itu sama dengan sesuatu yang Turing Lengkap. Setidaknya tidak pada level yang penting. Saya tidak ingin berjalan di pohon dengan Magic the Gathering.
Scant Roger

7
Setelah Anda dapat mengurangi masalah menjadi "ini memiliki kekuatan yang sama dengan mesin Turing" sudah cukup untuk mendapatkannya di sana. Mesin Turing adalah rintangan yang agak rendah, tetapi hanya itu yang dibutuhkan. Tidak ada yang dapat dilakukan loop yang rekursi tidak bisa lakukan, atau sebaliknya.

4
Pernyataan yang dibuat dalam jawaban ini tentu saja benar, tetapi saya berani mengatakan bahwa argumennya tidak terlalu meyakinkan. Mesin Turing tidak memiliki konsep rekursi langsung sehingga mengatakan "Anda dapat mensimulasikan mesin Turing tanpa rekursi" tidak benar-benar membuktikan apa pun. Apa yang harus Anda tunjukkan untuk membuktikan pernyataan itu adalah bahwa mesin Turing dapat mensimulasikan rekursi. Jika Anda tidak menunjukkan ini, Anda harus dengan setia mengasumsikan bahwa hipotesis Gereja-Turing juga berlaku untuk rekursi (yang memang terjadi) tetapi OP mempertanyakan hal ini.
5gon12eder

10
Pertanyaan OP adalah "bisa", bukan "terbaik", atau "paling efisien" atau kualifikasi lainnya. "Turing Complete" berarti segala sesuatu yang dapat dilakukan dengan rekursi juga dapat dilakukan dengan satu lingkaran. Apakah itu cara terbaik untuk melakukannya dalam implementasi bahasa tertentu adalah pertanyaan yang sama sekali berbeda.
Steven Burnap

7
"Bisa" sangat BUKAN sama dengan "terbaik". Ketika Anda salah mengira "tidak terbaik" karena "tidak bisa", Anda menjadi lumpuh karena tidak peduli bagaimana Anda melakukan sesuatu, hampir selalu ada cara yang lebih baik.
Steven Burnap

31

Apakah ada kasus di mana tugas hanya dapat dilakukan dengan menggunakan rekursi, bukan loop?

Anda selalu dapat mengubah algoritme rekursif menjadi loop, yang menggunakan struktur data Last-In-First-Out (AKA stack) untuk menyimpan keadaan sementara, karena panggilan rekursif persis seperti itu, menyimpan keadaan saat ini dalam tumpukan, melanjutkan dengan algoritma, kemudian mengembalikan keadaan. Jadi jawaban singkatnya adalah: Tidak, tidak ada kasus seperti itu .

Namun, argumen dapat dibuat untuk "ya". Mari kita ambil contoh konkret dan mudah: merge sort. Anda perlu membagi data menjadi dua bagian, menggabungkan mengurutkan bagian-bagian, dan kemudian menggabungkannya. Bahkan jika Anda tidak melakukan panggilan fungsi bahasa pemrograman aktual untuk menggabungkan pengurutan untuk melakukan penggabungan pengurutan pada bagian-bagian, Anda perlu mengimplementasikan fungsionalitas yang identik dengan benar-benar melakukan pemanggilan fungsi (push state ke stack Anda sendiri, lompat ke mulai dari loop dengan parameter awal yang berbeda, kemudian muncul status dari stack Anda).

Apakah ini rekursi, jika Anda menerapkan panggilan rekursi itu sendiri, sebagai langkah "push state" yang terpisah dan "lompat ke awal" dan "pop state"? Dan jawabannya adalah: Tidak, itu masih tidak disebut rekursi, itu disebut iterasi dengan tumpukan eksplisit (jika Anda ingin menggunakan terminologi yang sudah mapan).


Catatan, ini juga tergantung pada definisi "tugas". Jika tugas adalah untuk mengurutkan, maka Anda dapat melakukannya dengan banyak algoritma, banyak di antaranya tidak memerlukan jenis rekursi apa pun. Jika tugas adalah mengimplementasikan algoritma spesifik, seperti penggabungan sortir, maka ambiguitas di atas berlaku.

Jadi mari kita perhatikan pertanyaan, apakah ada tugas umum, yang hanya ada algoritma rekursi. Dari komentar @WizardOfMenlo di bawah pertanyaan, fungsi Ackermann adalah contoh sederhana dari itu. Jadi konsep rekursi berdiri sendiri, bahkan jika itu dapat diimplementasikan dengan konstruksi program komputer yang berbeda (iterasi dengan stack eksplisit).


2
Ketika berhadapan dengan perakitan untuk prosesor tanpa tumpukan, kedua teknik ini tiba-tiba menjadi satu.
Joshua

@ Yaua Memang! Ini adalah masalah tingkat abstraksi. Jika Anda naik satu atau dua tingkat lebih rendah, itu hanya gerbang logika ,.
hyde

2
Itu tidak sepenuhnya benar. Untuk meniru rekursi dengan iterasi, Anda memerlukan tumpukan di mana akses acak dimungkinkan. Tumpukan tunggal tanpa akses acak ditambah sejumlah terbatas memori yang dapat diakses langsung akan menjadi PDA, yang tidak menyelesaikan Turing.
Gilles

@Gilles Posting lama, tetapi mengapa tumpukan akses acak diperlukan? Juga, bukankah semua komputer nyata lebih rendah dari PDA, karena mereka hanya memiliki jumlah terbatas dari memori yang dapat diakses langsung, dan tidak ada tumpukan sama sekali (kecuali dengan menggunakan memori itu)? Ini tampaknya bukan abstraksi yang sangat praktis, jika dikatakan "kita tidak dapat melakukan rekursi dalam kenyataan".
hyde

20

Itu tergantung pada seberapa ketat Anda mendefinisikan "rekursi".

Jika kita benar-benar mengharuskannya untuk melibatkan tumpukan panggilan (atau mekanisme apa pun untuk mempertahankan status program yang digunakan), maka kita selalu dapat menggantinya dengan sesuatu yang tidak. Memang, bahasa yang secara alami mengarah pada penggunaan rekursi yang berat cenderung memiliki kompiler yang menggunakan optimisasi panggilan ekor, sehingga apa yang Anda tulis bersifat rekursif tetapi apa yang Anda jalankan adalah berulang.

Namun mari kita pertimbangkan kasus di mana kita melakukan panggilan rekursif dan menggunakan hasil dari panggilan rekursif untuk panggilan rekursif itu.

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  if (m == 0)
    return  n+1;
  if (n == 0)
    return Ackermann(m - 1, 1);
  else
    return Ackermann(m - 1, Ackermann(m, n - 1));
}

Membuat iteratif panggilan rekursif pertama adalah mudah:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
restart:
  if (m == 0)
    return  n+1;
  if (n == 0)
  {
    m--;
    n = 1;
    goto restart;
  }
  else
    return Ackermann(m - 1, Ackermann(m, n - 1));
}

Kami kemudian dapat membersihkan menghapus gotountuk menangkal velociraptors dan bayangan Dijkstra:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  while(m != 0)
  {
    if (n == 0)
    {
      m--;
      n = 1;
    }
    else
      return Ackermann(m - 1, Ackermann(m, n - 1));
  }
  return  n+1;
}

Tetapi untuk menghapus panggilan rekursif lainnya, kita harus menyimpan nilai beberapa panggilan ke stack:

public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
  Stack<BigInteger> stack = new Stack<BigInteger>();
  stack.Push(m);
  while(stack.Count != 0)
  {
    m = stack.Pop();
    if(m == 0)
      n = n + 1;
    else if(n == 0)
    {
      stack.Push(m - 1);
      n = 1;
    }
    else
    {
      stack.Push(m - 1);
      stack.Push(m);
      --n;
    }
  }
  return n;
}

Sekarang, ketika kita mempertimbangkan kode sumber, kita tentu mengubah metode rekursif kita menjadi yang berulang.

Mempertimbangkan apa yang telah dikompilasi, kami telah mengubah kode yang menggunakan panggilan stack untuk mengimplementasikan rekursi ke dalam kode yang tidak (dan dalam melakukannya mengubah kode yang akan membuang pengecualian stack-overflow bahkan untuk nilai yang sangat kecil ke dalam kode yang hanya akan butuh waktu yang sangat lama untuk kembali [lihat Bagaimana saya bisa mencegah fungsi Ackerman saya meluap tumpukan? untuk beberapa optimisasi lebih lanjut yang membuatnya benar-benar kembali untuk banyak input yang lebih mungkin]).

Mempertimbangkan bagaimana rekursi diimplementasikan secara umum, kami telah mengubah kode yang menggunakan panggilan-tumpukan menjadi kode yang menggunakan tumpukan yang berbeda untuk menahan operasi yang tertunda. Karena itu kita dapat berpendapat bahwa itu masih bersifat rekursif, ketika dipertimbangkan pada tingkat rendah itu.

Dan pada level itu, memang tidak ada cara lain di sekitarnya. Jadi jika Anda menganggap metode itu sebagai rekursif, maka memang ada hal-hal yang tidak dapat kita lakukan tanpanya. Meskipun secara umum kami tidak memberi label kode semacam itu sebagai rekursif. Istilah rekursi berguna karena mencakup serangkaian pendekatan tertentu dan memberi kita cara untuk membicarakannya, dan kita tidak lagi menggunakan salah satunya.

Tentu saja, semua ini mengasumsikan Anda punya pilihan. Ada kedua bahasa yang melarang panggilan rekursif, dan bahasa yang tidak memiliki struktur pengulangan yang diperlukan untuk iterasi.


Hanya mungkin untuk mengganti tumpukan panggilan dengan sesuatu yang setara jika tumpukan panggilan dibatasi atau seseorang memiliki akses ke memori tidak terbatas di luar tumpukan panggilan. Ada kelas masalah yang signifikan yang dipecahkan oleh automata push-down yang memiliki tumpukan panggilan tidak terbatas tetapi hanya dapat memiliki sejumlah negara terbatas.
supercat

Ini adalah jawaban terbaik, mungkin satu-satunya jawaban yang benar. Bahkan contoh kedua masih rekursif, dan pada level ini, jawaban untuk pertanyaan awal adalah tidak . Dengan definisi rekursi yang lebih luas, rekursi untuk fungsi Ackermann tidak mungkin untuk dihindari.
gerrit

@gerrit dan dengan lebih sempit, ia menghindarinya. Pada akhirnya itu turun ke tepi hanya apa yang kita lakukan atau tidak menerapkan label yang berguna ini kita gunakan untuk kode tertentu.
Jon Hanna

1
Bergabung dengan situs untuk memilih ini. Fungsi Ackermann / bersifat / rekursif. Menerapkan struktur rekursif dengan loop dan stack tidak menjadikannya solusi berulang, Anda baru saja memindahkan rekursi ke ruang pengguna.
Aaron McMillin

9

Jawaban klasiknya adalah "tidak", tetapi izinkan saya untuk menjelaskan mengapa saya berpikir "ya" adalah jawaban yang lebih baik.


Sebelum melanjutkan, mari kita dapatkan sesuatu: dari sudut pandang komputabilitas & kompleksitas:

  • Jawabannya adalah "tidak" jika Anda diizinkan memiliki tumpukan tambahan saat mengulang.
  • Jawabannya adalah "ya" jika Anda tidak diizinkan data tambahan saat perulangan.

Oke, sekarang, mari kita meletakkan satu kaki di tanah praktik, menjaga kaki lainnya di tanah teori.


Tumpukan panggilan adalah struktur kontrol , sedangkan tumpukan manual adalah struktur data . Kontrol dan data bukanlah konsep yang sama, tetapi mereka setara dalam arti bahwa mereka dapat direduksi satu sama lain (atau "ditiru" satu sama lain) dari sudut pandang komputabilitas atau kompleksitas.

Kapan perbedaan ini penting? Ketika Anda bekerja dengan alat dunia nyata. Ini sebuah contoh:

Katakanlah Anda menerapkan N-way mergesort. Anda mungkin memiliki forloop yang melewati masing-masing Nsegmen, memanggil mergesortmereka secara terpisah, kemudian menggabungkan hasilnya.

Bagaimana Anda memparalelkan ini dengan OpenMP?

Di bidang rekursif, ini sangat sederhana: cukup letakkan #pragma omp parallel forlingkaran Anda yang bergerak dari 1 ke N, dan Anda selesai. Di ranah iteratif, Anda tidak bisa melakukan ini. Anda harus menelurkan utas secara manual dan memberikan data yang sesuai secara manual sehingga mereka tahu apa yang harus dilakukan.

Di sisi lain, ada alat lain (seperti vektorizers otomatis, misalnya #pragma vector) yang bekerja dengan loop tetapi sama sekali tidak berguna dengan rekursi.

Intinya, hanya karena Anda dapat membuktikan kedua paradigma itu setara secara matematis, itu tidak berarti mereka sama dalam praktik. Masalah yang mungkin sepele untuk diotomatisasi dalam satu paradigma (katakanlah, paralelisasi loop) mungkin jauh lebih sulit untuk diselesaikan dalam paradigma lain.

yaitu: Alat untuk satu paradigma tidak secara otomatis diterjemahkan ke paradigma lain.

Akibatnya, jika Anda memerlukan alat untuk memecahkan masalah, kemungkinan alat tersebut hanya akan bekerja dengan satu jenis pendekatan tertentu, dan akibatnya Anda akan gagal menyelesaikan masalah dengan pendekatan yang berbeda, bahkan jika Anda dapat membuktikan secara matematis masalah tersebut dapat dipecahkan dengan cara baik.


Bahkan lebih dari itu, pertimbangkan bahwa set masalah yang dapat diselesaikan dengan otomat push-down lebih besar dari set yang dapat diselesaikan dengan otomat terbatas (baik deterministik atau non) tetapi lebih kecil dari set yang dapat diselesaikan dengan Mesin turing.
supercat

8

Mengesampingkan alasan teoretis, mari kita lihat seperti apa rekursi dan loop dari sudut pandang mesin (perangkat keras atau virtual). Rekursi adalah kombinasi dari aliran kontrol yang memungkinkan untuk memulai eksekusi beberapa kode dan untuk kembali pada penyelesaian (dalam tampilan sederhana ketika sinyal dan pengecualian diabaikan) dan data yang diteruskan ke kode lain (argumen) dan yang dikembalikan dari itu (hasil). Biasanya tidak ada manajemen memori eksplisit yang terlibat, namun ada alokasi implisit memori stack untuk menyimpan alamat, argumen, hasil dan data lokal menengah.

Loop adalah kombinasi aliran kontrol dan data lokal. Membandingkan ini dengan rekursi, kita dapat melihat bahwa jumlah data dalam kasus ini adalah tetap. Satu-satunya cara untuk memecahkan batasan ini adalah dengan menggunakan memori dinamis (juga dikenal sebagai heap ) yang dapat dialokasikan (dan dibebaskan) kapan pun diperlukan.

Untuk meringkas:

  • Kasing rekursi = Aliran kontrol + Stack (+ Heap)
  • Kasus lingkaran = Aliran kontrol + Heap

Dengan asumsi bahwa bagian aliran kontrol cukup kuat, satu-satunya perbedaan adalah pada tipe memori yang tersedia. Jadi, kita dibiarkan dengan 4 case (kekuatan ekspresif terdaftar dalam tanda kurung):

  1. Tidak ada tumpukan, tidak ada tumpukan: rekursi dan struktur dinamis tidak mungkin. (rekursi = loop)
  2. Stack, no heap: rekursi OK, struktur dinamis tidak mungkin. (rekursi> lingkaran)
  3. Tidak ada tumpukan, tumpukan: rekursi tidak mungkin, struktur dinamis OK. (rekursi = loop)
  4. Stack, heap: rekursi dan struktur dinamis OK. (rekursi = loop)

Jika aturan mainnya sedikit lebih ketat dan implementasi rekursif tidak diizinkan untuk menggunakan loop, kita malah mendapatkan ini:

  1. Tidak ada tumpukan, tidak ada tumpukan: rekursi dan struktur dinamis tidak mungkin. (rekursi <loop)
  2. Stack, no heap: rekursi OK, struktur dinamis tidak mungkin. (rekursi> lingkaran)
  3. Tidak ada tumpukan, tumpukan: rekursi tidak mungkin, struktur dinamis OK. (rekursi <loop)
  4. Stack, heap: rekursi dan struktur dinamis OK. (rekursi = loop)

Perbedaan utama dengan skenario sebelumnya adalah kurangnya memori stack tidak memungkinkan rekursi tanpa loop untuk melakukan lebih banyak langkah selama eksekusi daripada ada baris kode.


2

Iya. Ada beberapa tugas umum yang mudah diselesaikan menggunakan rekursi tetapi tidak mungkin hanya dengan loop:

  • Menyebabkan tumpukan meluap.
  • Pemrogram pemula yang benar-benar membingungkan.
  • Membuat fungsi yang tampak cepat yang sebenarnya adalah O (n ^ n).

3
Tolong, ini sangat mudah dengan loop, saya melihat mereka sepanjang waktu. Heck, dengan sedikit usaha Anda bahkan tidak perlu loop. Bahkan jika rekursi lebih mudah.
AviD

1
sebenarnya, A (0, n) = n + 1; A (m, 0) = A (m-1,1) jika m> 0; A (m, n) = A (m-1, A (m, n-1)) jika m> 0, n> 0 tumbuh sedikit lebih cepat daripada O (n ^ n) (untuk m = n) :)
John Donn

1
@ JohnDonn Lebih dari sedikit, ini super eksponensial. untuk n = 3 n ^ n ^ n untuk n = 4 n ^ n ^ n ^ n ^ n dan seterusnya. n ke n power n kali.
Aaron McMillin

1

Ada perbedaan antara fungsi rekursif dan fungsi rekursif primitif. Fungsi rekursif primitif adalah fungsi yang dihitung menggunakan loop, di mana jumlah iterasi maksimum setiap loop dihitung sebelum eksekusi loop dimulai. (Dan "rekursif" di sini tidak ada hubungannya dengan penggunaan rekursi).

Fungsi rekursif primitif benar-benar kurang kuat daripada fungsi rekursif. Anda akan mendapatkan hasil yang sama jika Anda mengambil fungsi yang menggunakan rekursi, di mana kedalaman maksimum rekursi harus dihitung sebelumnya.


3
Saya tidak yakin bagaimana ini berlaku untuk pertanyaan di atas? Bisakah Anda membuat koneksi itu lebih jelas?
Yakk

1
Mengganti "loop" yang tidak tepat dengan perbedaan penting antara "loop dengan jumlah iterasi terbatas" dan "loop dengan jumlah iterasi tak terbatas", yang saya pikir semua orang akan tahu dari CS 101.
gnasher729

tentu, tapi itu masih tidak berlaku untuk pertanyaan. Pertanyaannya adalah tentang perulangan dan rekursi, bukan rekursi dan rekursi primitif. Bayangkan jika seseorang bertanya tentang perbedaan C / C ++, dan Anda menjawab tentang perbedaan antara K&R C dan Ansi C. Tentu itu membuat segalanya lebih tepat, tetapi tidak menjawab pertanyaan.
Yakk

1

Jika Anda memprogram dalam c ++, dan menggunakan c ++ 11, maka ada satu hal yang harus dilakukan dengan menggunakan rekursi: fungsi constexpr. Tetapi standar membatasi ini hingga 512, seperti yang dijelaskan dalam jawaban ini . Menggunakan loop dalam hal ini tidak mungkin, karena dalam kasus ini fungsi tidak bisa menjadi constexpr, tetapi ini diubah dalam c ++ 14.


0
  • Jika panggilan rekursif adalah pernyataan pertama atau terakhir (tidak termasuk pengecekan kondisi) dari fungsi rekursif, cukup mudah untuk diterjemahkan ke dalam struktur pengulangan.
  • Tetapi jika fungsi melakukan beberapa hal lain sebelum dan sesudah panggilan rekursif , maka akan sulit untuk mengubahnya menjadi loop.
  • Jika fungsinya memiliki beberapa panggilan rekursif, mengonversinya menjadi kode yang hanya menggunakan loop akan sangat tidak mungkin. Beberapa tumpukan akan diperlukan untuk mengikuti data. Dalam rekursi tumpukan panggilan itu sendiri akan berfungsi sebagai tumpukan data.

Tree walk memiliki beberapa panggilan rekursif (satu untuk setiap anak), namun itu sepele berubah menjadi loop menggunakan tumpukan eksplisit. Parser di sisi lain sering menjengkelkan untuk diubah.
CodesInChaos

@CodesInChaos Diedit.
Gulshan

-6

Saya setuju dengan pertanyaan lain. Tidak ada yang dapat Anda lakukan dengan rekursi yang tidak dapat Anda lakukan dengan satu lingkaran.

TETAPI , menurut saya rekursi bisa sangat berbahaya. Pertama, bagi sebagian orang lebih sulit untuk memahami apa yang sebenarnya terjadi dalam kode. Kedua, setidaknya untuk C ++ (Java saya tidak yakin) setiap langkah rekursi berdampak pada memori karena setiap pemanggilan metode menyebabkan akumulasi memori dan inisialisasi header metode. Dengan cara ini Anda dapat meledakkan tumpukan Anda. Cukup coba rekursi angka Fibonacci dengan nilai input tinggi.


2
Implementasi rekursif naif dari angka-angka Fibonacci dengan rekursi akan kehabisan waktu sebelum kehabisan ruang stack. Saya kira ada masalah lain yang lebih baik untuk contoh ini. Juga, untuk banyak masalah versi loop memiliki dampak memori yang sama dengan yang berulang, hanya pada heap dan bukan stack (jika bahasa pemrograman Anda membedakannya).
Paŭlo Ebermann

6
Loop juga bisa "sangat berbahaya" jika Anda hanya lupa untuk menambah variabel loop ...
h22

2
Jadi, memang, sengaja menghasilkan stack overflow adalah tugas yang menjadi sangat rumit tanpa menggunakan rekursi.
5gon12eder

@ 5gon12eder yang membawa kita ke Metode apa yang ada untuk menghindari stack overflow dalam algoritma rekursif? - menulis untuk melibatkan TCO, atau Memoisation dapat bermanfaat. Pendekatan Iteratif vs Rekursif juga menarik karena berkaitan dengan dua pendekatan rekursif berbeda untuk Fibonacci.

1
Sebagian besar waktu jika Anda mendapatkan stack overflow pada rekursi, Anda akan memiliki hang pada versi iteratif. Setidaknya bekas lemparan dengan jejak tumpukan.
Jon Hanna
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.