Cara untuk beralih dari rekursi ke iterasi


349

Saya telah menggunakan rekursi cukup banyak pada pemrograman saya selama bertahun-tahun untuk menyelesaikan masalah sederhana, tetapi saya sepenuhnya sadar bahwa kadang-kadang Anda membutuhkan iterasi karena masalah memori / kecepatan.

Jadi, suatu saat di masa lalu saya pergi untuk mencoba dan menemukan apakah ada "pola" atau cara buku teks untuk mengubah pendekatan rekursi umum untuk iterasi dan tidak menemukan apa pun. Atau setidaknya tidak ada yang saya ingat akan membantu.

  • Apakah ada aturan umum?
  • Apakah ada "pola"?

Jawaban:


333

Biasanya, saya mengganti algoritma rekursif dengan algoritma berulang dengan mendorong parameter yang biasanya dilewatkan ke fungsi rekursif ke tumpukan. Bahkan, Anda mengganti tumpukan program dengan salah satu dari Anda sendiri.

Stack<Object> stack;
stack.push(first_object);
while( !stack.isEmpty() ) {
   // Do something
   my_object = stack.pop();

  // Push other objects on the stack.

}

Catatan: jika Anda memiliki lebih dari satu panggilan rekursif di dalam dan Anda ingin mempertahankan urutan panggilan, Anda harus menambahkannya dalam urutan terbalik ke tumpukan:

foo(first);
foo(second);

harus diganti oleh

stack.push(second);
stack.push(first);

Sunting: Artikel Stacks and Elimination Recursion (atau Article Backup link ) menjelaskan lebih detail tentang hal ini.


4
Jika Anda mengganti tumpukan Anda dengan Antrean, apakah itu tidak memecahkan masalah membalikkan pesanan tambahan?
SamuelWarren

2
Saya mengerjakannya di atas kertas dan itu adalah dua hal yang berbeda. Jika Anda membalikkan urutan yang Anda tambahkan, itu membuat Anda melaju ke depan seperti biasa, tetapi traversal Anda masih pencarian yang lebih mendalam. Tetapi jika Anda mengubah semuanya menjadi antrian sekarang Anda melakukan traverse-first daripada traversal first-depth.
pete

1
Saya baru-baru ini melakukan ini secara umum, dengan mengganti fungsi kunjungan simpul saya (node)->()dengan di (node)->[actions]mana tindakan () -> [actions]. Kemudian di bagian luar, Anda cukup memunculkan tindakan / kelanjutan dari tumpukan, menerapkan / menjalankannya, mendorong tindakan yang dikembalikan pada tumpukan secara terbalik agar dan ulangi. Kontingensi / kompleks traversal, Anda hanya menangkap apa yang akan menjadi variabel stack lokal dalam pointer yang dihitung referensi yang Anda tutup di thunks Anda, kemudian thunks selanjutnya dapat bergantung pada hasil sub-traversal sebelumnya dll.
pengalaman

6
Terkadang kita menghindari rekursi untuk menghindari stackoverflow. Tetapi mempertahankan stack kita sendiri juga akan menyebabkan stackoverflow. Jadi mengapa kita ingin menerapkan rekursi dengan tumpukan kita sendiri?
Zhu Li

8
@ZhuLi Jika kita menggunakan newkita dapat membuat objek di heap, bukan stack. Tidak seperti stack, heap tidak memiliki batasan memori. Lihat gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html
yuqli

77

Sungguh, cara paling umum untuk melakukannya adalah dengan menyimpan tumpukan Anda sendiri. Inilah fungsi quicksort rekursif dalam C:

void quicksort(int* array, int left, int right)
{
    if(left >= right)
        return;

    int index = partition(array, left, right);
    quicksort(array, left, index - 1);
    quicksort(array, index + 1, right);
}

Inilah cara kami membuatnya berulang dengan menyimpan tumpukan kami sendiri:

void quicksort(int *array, int left, int right)
{
    int stack[1024];
    int i=0;

    stack[i++] = left;
    stack[i++] = right;

    while (i > 0)
    {
        right = stack[--i];
        left = stack[--i];

        if (left >= right)
             continue;

        int index = partition(array, left, right);
        stack[i++] = left;
        stack[i++] = index - 1;
        stack[i++] = index + 1;
        stack[i++] = right;
    }
}

Jelas, contoh ini tidak memeriksa batas-batas tumpukan ... dan Anda benar-benar dapat menentukan ukuran tumpukan berdasarkan kasus terburuk yang diberikan nilai kiri dan kanan. Tetapi Anda mendapatkan idenya.


1
Adakah ide tentang cara menyusun tumpukan maksimum untuk dialokasikan untuk rekursi tertentu?
lexicalscope

@lexicalscope misalkan Anda memiliki algoritma rekursif O(N) = O(R*L), di mana Ljumlah kompleksitas "untuk layer r", misalnya dalam hal ini Anda memiliki O(N)pekerjaan di setiap langkah melakukan partisi, kedalaman rekursif adalah O(R), yaitu kasus terburuk O(N), kasus rata-rata di O(logN)sini.
Caleth

48

Tampaknya tidak ada yang membahas di mana fungsi rekursif menyebut dirinya lebih dari satu kali dalam tubuh, dan menangani kembali ke titik tertentu dalam rekursi (yaitu tidak primitif-rekursif). Dikatakan bahwa setiap rekursi dapat diubah menjadi iterasi , sehingga tampaknya ini harus dimungkinkan.

Saya baru saja datang dengan contoh C # tentang bagaimana melakukan ini. Misalkan Anda memiliki fungsi rekursif berikut, yang bertindak seperti traversal postorder, dan AbcTreeNode adalah pohon 3-ary dengan pointer a, b, c.

public static void AbcRecursiveTraversal(this AbcTreeNode x, List<int> list) {
        if (x != null) {
            AbcRecursiveTraversal(x.a, list);
            AbcRecursiveTraversal(x.b, list);
            AbcRecursiveTraversal(x.c, list);
            list.Add(x.key);//finally visit root
        }
}

Solusi berulang:

        int? address = null;
        AbcTreeNode x = null;
        x = root;
        address = A;
        stack.Push(x);
        stack.Push(null)    

        while (stack.Count > 0) {
            bool @return = x == null;

            if (@return == false) {

                switch (address) {
                    case A://   
                        stack.Push(x);
                        stack.Push(B);
                        x = x.a;
                        address = A;
                        break;
                    case B:
                        stack.Push(x);
                        stack.Push(C);
                        x = x.b;
                        address = A;
                        break;
                    case C:
                        stack.Push(x);
                        stack.Push(null);
                        x = x.c;
                        address = A;
                        break;
                    case null:
                        list_iterative.Add(x.key);
                        @return = true;
                        break;
                }

            }


            if (@return == true) {
                address = (int?)stack.Pop();
                x = (AbcTreeNode)stack.Pop();
            }


        }

5
Ini benar-benar berguna, saya harus menulis versi berulang dari reccurence yang berulang kali, karena posting Anda, saya melakukannya.
Wojciech Kulik

1
Ini harus menjadi contoh terbaik yang pernah saya lihat dari meniru rekursi stack panggilan untuk situasi di mana beberapa panggilan rekursif dibuat dalam metode ini. Pekerjaan yang baik.
CCS

1
Anda memiliki saya di "Sepertinya tidak ada yang membahas di mana fungsi rekursif menyebut dirinya lebih dari satu kali dalam tubuh, dan menangani kembali ke titik tertentu dalam rekursi" dan kemudian saya sudah terbalik. OKE, sekarang saya akan membaca sisa jawaban Anda dan melihat apakah peningkatan prematur saya dibenarkan. (Karena aku sangat perlu tahu jawabannya).
mydoghasworms

1
@mydoghasworms - Kembali ke pertanyaan ini setelah begitu lama, bahkan mengambil saya sejenak untuk mengingat apa yang saya pikirkan. Semoga jawabannya membantu.
T. Webster

1
Saya menyukai ide solusi ini, tetapi tampaknya membingungkan bagi saya. Saya menulis versi yang disederhanakan untuk pohon biner dengan python, mungkin ini akan membantu seseorang untuk memahami ide tersebut: gist.github.com/azurkin/abb258a0e1a821cbb331f2696b37c3ac
azurkin

33

Berusaha keras untuk membuat panggilan rekursif Tail Recursion (rekursi di mana pernyataan terakhir adalah panggilan rekursif). Setelah Anda memilikinya, mengonversinya menjadi iterasi umumnya cukup mudah.


2
Rekursi beberapa ekor transformasi JIT: ibm.com/developerworks/java/library/j-diag8.html
Liran Orevi

Banyak penafsir (yaitu Skema yang paling terkenal) akan mengoptimalkan rekursi ekor dengan baik. Saya tahu bahwa GCC, dengan optimasi tertentu, melakukan rekursi ekor (meskipun C adalah pilihan aneh untuk optimasi seperti itu).
new123456

19

Nah, secara umum, rekursi dapat ditiru sebagai iterasi dengan hanya menggunakan variabel penyimpanan. Perhatikan bahwa rekursi dan iterasi umumnya setara; yang satu hampir selalu dapat dikonversi ke yang lain. Fungsi rekursif ekor sangat mudah dikonversi menjadi fungsi berulang. Buat saja variabel akumulator menjadi variabel lokal, dan lakukan iterasi alih-alih berulang. Berikut adalah contoh dalam C ++ (C kalau bukan karena penggunaan argumen default):

// tail-recursive
int factorial (int n, int acc = 1)
{
  if (n == 1)
    return acc;
  else
    return factorial(n - 1, acc * n);
}

// iterative
int factorial (int n)
{
  int acc = 1;
  for (; n > 1; --n)
    acc *= n;
  return acc;
}

Mengenal saya, saya mungkin membuat kesalahan dalam kode, tetapi idenya ada di sana.


14

Bahkan menggunakan stack tidak akan mengubah algoritma rekursif menjadi iteratif. Rekursi normal adalah rekursi berbasis fungsi dan jika kita menggunakan stack maka itu menjadi rekursi berbasis stack. Tapi ini masih rekursi.

Untuk algoritma rekursif, kompleksitas ruang adalah O (N) dan kompleksitas waktu adalah O (N). Untuk algoritma iteratif, kompleksitas ruang adalah O (1) dan kompleksitas waktu adalah O (N).

Tetapi jika kita menggunakan tumpukan hal dalam hal kompleksitas tetap sama. Saya pikir hanya rekursi ekor yang dapat dikonversi menjadi iterasi.


1
Saya setuju dengan bit pertama Anda, tetapi saya pikir saya salah memahami paragraf kedua. Pertimbangkan untuk mengkloning array hanya dengan menyalin copy = new int[size]; for(int i=0; i<size; ++i) copy[i] = source[i];ruang memori dan kompleksitas waktu, keduanya O (N) berdasarkan pada ukuran data, tetapi itu jelas suatu algoritma iteratif.
Ponkadoodle

13

The tumpukan dan rekursi penghapusan pasal menangkap ide eksternalisasi stack frame di heap, tetapi tidak memberikan langsung dan berulang cara untuk mengkonversi. Di bawah ini adalah satu.

Saat mengonversi ke kode iteratif, orang harus sadar bahwa panggilan rekursif dapat terjadi dari blok kode yang sewenang-wenang. Ini bukan hanya parameter, tetapi juga titik untuk kembali ke logika yang masih harus dieksekusi dan keadaan variabel yang berpartisipasi dalam persyaratan berikutnya, yang penting. Di bawah ini adalah cara yang sangat sederhana untuk mengonversi ke kode berulang dengan sedikit perubahan.

Pertimbangkan kode rekursif ini:

struct tnode
{
    tnode(int n) : data(n), left(0), right(0) {}
    tnode *left, *right;
    int data;
};

void insertnode_recur(tnode *node, int num)
{
    if(node->data <= num)
    {
        if(node->right == NULL)
            node->right = new tnode(num);
        else
            insertnode(node->right, num);
    }
    else
    {
        if(node->left == NULL)
            node->left = new tnode(num);
        else
            insertnode(node->left, num);
    }    
}

Kode berulang:

// Identify the stack variables that need to be preserved across stack 
// invocations, that is, across iterations and wrap them in an object
struct stackitem 
{ 
    stackitem(tnode *t, int n) : node(t), num(n), ra(0) {}
    tnode *node; int num;
    int ra; //to point of return
};

void insertnode_iter(tnode *node, int num) 
{
    vector<stackitem> v;
    //pushing a stackitem is equivalent to making a recursive call.
    v.push_back(stackitem(node, num));

    while(v.size()) 
    {
        // taking a modifiable reference to the stack item makes prepending 
        // 'si.' to auto variables in recursive logic suffice
        // e.g., instead of num, replace with si.num.
        stackitem &si = v.back(); 
        switch(si.ra)
        {
        // this jump simulates resuming execution after return from recursive 
        // call 
            case 1: goto ra1;
            case 2: goto ra2;
            default: break;
        } 

        if(si.node->data <= si.num)
        {
            if(si.node->right == NULL)
                si.node->right = new tnode(si.num);
            else
            {
                // replace a recursive call with below statements
                // (a) save return point, 
                // (b) push stack item with new stackitem, 
                // (c) continue statement to make loop pick up and start 
                //    processing new stack item, 
                // (d) a return point label
                // (e) optional semi-colon, if resume point is an end 
                // of a block.

                si.ra=1;
                v.push_back(stackitem(si.node->right, si.num));
                continue; 
ra1:            ;         
            }
        }
        else
        {
            if(si.node->left == NULL)
                si.node->left = new tnode(si.num);
            else
            {
                si.ra=2;                
                v.push_back(stackitem(si.node->left, si.num));
                continue;
ra2:            ;
            }
        }

        v.pop_back();
    }
}

Perhatikan bagaimana struktur kode masih tetap benar dengan logika rekursif dan modifikasi minimal, sehingga jumlah bug lebih sedikit. Sebagai perbandingan, saya telah menandai perubahan dengan ++ dan -. Sebagian besar blok yang dimasukkan baru kecuali v.push_back, umum untuk setiap logika iteratif yang dikonversi

void insertnode_iter(tnode *node, int num) 
{

+++++++++++++++++++++++++

    vector<stackitem> v;
    v.push_back(stackitem(node, num));

    while(v.size())
    {
        stackitem &si = v.back(); 
        switch(si.ra)
        {
            case 1: goto ra1;
            case 2: goto ra2;
            default: break;
        } 

------------------------

        if(si.node->data <= si.num)
        {
            if(si.node->right == NULL)
                si.node->right = new tnode(si.num);
            else
            {

+++++++++++++++++++++++++

                si.ra=1;
                v.push_back(stackitem(si.node->right, si.num));
                continue; 
ra1:            ;    

-------------------------

            }
        }
        else
        {
            if(si.node->left == NULL)
                si.node->left = new tnode(si.num);
            else
            {

+++++++++++++++++++++++++

                si.ra=2;                
                v.push_back(stackitem(si.node->left, si.num));
                continue;
ra2:            ;

-------------------------

            }
        }

+++++++++++++++++++++++++

        v.pop_back();
    }

-------------------------

}

Ini telah banyak membantu saya, tetapi ada masalah: stackitemobjek dialokasikan dengan nilai sampah ra. Semuanya masih berfungsi dalam kasus yang paling mirip, tetapi jika rakebetulan 1 atau 2 Anda akan mendapatkan perilaku yang salah. Solusinya adalah menginisialisasi rake 0.
JanX2

@ JanX2, stackitemtidak boleh didorong tanpa menginisialisasi. Tapi ya, menginisialisasi ke 0 akan menangkap kesalahan.
Chethan

Mengapa keduanya tidak mengembalikan alamat yang diatur ke v.pop_back()pernyataan?
is7s

7

Cari di Google untuk "Gaya kelanjutan lulus." Ada prosedur umum untuk mengubah ke gaya rekursif ekor; ada juga prosedur umum untuk mengubah fungsi rekursif ekor menjadi loop.


6

Hanya menghabiskan waktu ... Fungsi rekursif

void foo(Node* node)
{
    if(node == NULL)
       return;
    // Do something with node...
    foo(node->left);
    foo(node->right);
}

dapat dikonversi menjadi

void foo(Node* node)
{
    if(node == NULL)
       return;

    // Do something with node...

    stack.push(node->right);
    stack.push(node->left);

    while(!stack.empty()) {
         node1 = stack.pop();
         if(node1 == NULL)
            continue;
         // Do something with node1...
         stack.push(node1->right);             
         stack.push(node1->left);
    }

}

Contoh di atas adalah contoh rekursif untuk dfs berulang pada pohon pencarian biner :)
Amit

5

Secara umum teknik untuk menghindari stack overflow adalah untuk fungsi rekursif disebut teknik trampolin yang banyak diadopsi oleh Java devs.

Namun, untuk C # ada metode pembantu kecil di sini yang mengubah fungsi rekursif Anda menjadi berulang tanpa perlu mengubah logika atau membuat kode tidak bisa dipahami. C # adalah bahasa yang sangat bagus sehingga hal-hal yang luar biasa dimungkinkan dengannya.

Ia bekerja dengan membungkus bagian-bagian metode dengan metode helper. Misalnya fungsi rekursif berikut:

int Sum(int index, int[] array)
{
 //This is the termination condition
 if (int >= array.Length)
 //This is the returning value when termination condition is true
 return 0;

//This is the recursive call
 var sumofrest = Sum(index+1, array);

//This is the work to do with the current item and the
 //result of recursive call
 return array[index]+sumofrest;
}

Berubah menjadi:

int Sum(int[] ar)
{
 return RecursionHelper<int>.CreateSingular(i => i >= ar.Length, i => 0)
 .RecursiveCall((i, rv) => i + 1)
 .Do((i, rv) => ar[i] + rv)
 .Execute(0);
}

4

Memikirkan hal-hal yang sebenarnya membutuhkan tumpukan:

Jika kita menganggap pola rekursi sebagai:

if(task can be done directly) {
    return result of doing task directly
} else {
    split task into two or more parts
    solve for each part (possibly by recursing)
    return result constructed by combining these solutions
}

Misalnya, Menara klasik Hanoi

if(the number of discs to move is 1) {
    just move it
} else {
    move n-1 discs to the spare peg
    move the remaining disc to the target peg
    move n-1 discs from the spare peg to the target peg, using the current peg as a spare
}

Ini dapat diterjemahkan ke dalam loop yang bekerja pada stack eksplisit, dengan menyatakannya kembali sebagai:

place seed task on stack
while stack is not empty 
   take a task off the stack
   if(task can be done directly) {
      Do it
   } else {
      Split task into two or more parts
      Place task to consolidate results on stack
      Place each task on stack
   }
}

Untuk Menara Hanoi ini menjadi:

stack.push(new Task(size, from, to, spare));
while(! stack.isEmpty()) {
    task = stack.pop();
    if(task.size() = 1) {
        just move it
    } else {
        stack.push(new Task(task.size() -1, task.spare(), task,to(), task,from()));
        stack.push(new Task(1, task.from(), task.to(), task.spare()));
        stack.push(new Task(task.size() -1, task.from(), task.spare(), task.to()));
    }
}

Ada banyak fleksibilitas di sini mengenai bagaimana Anda mendefinisikan tumpukan Anda. Anda dapat membuat tumpukan Anda daftar Commandobjek yang melakukan hal-hal canggih. Atau Anda dapat pergi ke arah yang berlawanan dan membuatnya menjadi daftar jenis yang lebih sederhana (misalnya "tugas" mungkin 4 elemen pada tumpukan int, bukan satu elemen pada tumpukanTask ).

Semua ini berarti bahwa memori untuk tumpukan berada di tumpukan daripada di tumpukan eksekusi Java, tetapi ini bisa berguna karena Anda memiliki kontrol lebih besar atas tumpukan itu.


3

Satu pola yang harus dicari adalah panggilan rekursi di akhir fungsi (disebut ekor-rekursi). Ini dapat dengan mudah diganti dengan sementara. Misalnya, fungsi foo:

void foo(Node* node)
{
    if(node == NULL)
       return;
    // Do something with node...
    foo(node->left);
    foo(node->right);
}

diakhiri dengan panggilan ke foo. Ini bisa diganti dengan:

void foo(Node* node)
{
    while(node != NULL)
    {
        // Do something with node...
        foo(node->left);
        node = node->right;
     }
}

yang menghilangkan panggilan rekursif kedua.


3
Masih terlihat rekursif bagi saya ... :)
nathan

2
Ya, tapi itu setengah rekursif. Menyingkirkan rekursi lainnya akan membutuhkan menggunakan teknik lain ...
Mark Bessey

2

Sebuah pertanyaan yang telah ditutup sebagai duplikat dari yang satu ini memiliki struktur data yang sangat spesifik:

masukkan deskripsi gambar di sini

Node memiliki struktur berikut:

typedef struct {
    int32_t type;
    int32_t valueint;
    double  valuedouble;
    struct  cNODE *next;
    struct  cNODE *prev;
    struct  cNODE *child;
} cNODE;

Fungsi penghapusan rekursif tampak seperti:

void cNODE_Delete(cNODE *c) {
    cNODE*next;
    while (c) {
        next=c->next;
        if (c->child) { 
          cNODE_Delete(c->child)
        }
        free(c);
        c=next;
    }
}

Secara umum, tidak selalu mungkin untuk menghindari tumpukan untuk fungsi rekursif yang memanggil dirinya sendiri lebih dari satu kali (atau bahkan sekali). Namun, untuk struktur khusus ini, dimungkinkan. Idenya adalah untuk meratakan semua node menjadi satu daftar. Ini dilakukan dengan meletakkan node saat ini childdi akhir daftar baris atas.

void cNODE_Delete (cNODE *c) {
    cNODE *tmp, *last = c;
    while (c) {
        while (last->next) {
            last = last->next;   /* find last */
        }
        if ((tmp = c->child)) {
            c->child = NULL;     /* append child to last */
            last->next = tmp;
            tmp->prev = last;
        }
        tmp = c->next;           /* remove current */
        free(c);
        c = tmp;
    }
}

Teknik ini dapat diterapkan pada setiap struktur data tertaut yang dapat direduksi menjadi DAG dengan pemesanan topologi deterministik. Node anak saat ini disusun ulang sehingga anak terakhir mengadopsi semua anak lainnya. Kemudian node saat ini dapat dihapus dan traversal kemudian dapat beralih ke anak yang tersisa.


1

Rekursi tidak lain adalah proses memanggil satu fungsi dari yang lain hanya proses ini dilakukan dengan memanggil fungsi dengan sendirinya. Seperti yang kita ketahui ketika satu fungsi memanggil fungsi lainnya, fungsi pertama menyimpan statusnya (variabelnya) dan kemudian meneruskan kontrol ke fungsi yang dipanggil. Fungsi yang dipanggil dapat dipanggil dengan menggunakan nama variabel yang sama ex fun1 (a) dapat memanggil fun2 (a). Ketika kita melakukan panggilan rekursif, tidak ada hal baru yang terjadi. Satu fungsi memanggil dirinya sendiri dengan melewatkan tipe yang sama dan mirip dalam variabel nama (tetapi jelas nilai yang disimpan dalam variabel berbeda, hanya namanya tetap sama.) Untuk dirinya sendiri. Tetapi sebelum setiap panggilan fungsi menyimpan statusnya dan proses penyimpanan ini berlanjut. SAVING DILAKUKAN DENGAN STACK.

SEKARANG STACK DATANG KE MAIN.

Jadi jika Anda menulis program berulang dan menyimpan status di tumpukan setiap kali dan kemudian mengeluarkan nilai dari tumpukan saat diperlukan, Anda telah berhasil mengubah program rekursif menjadi program berulang!

Buktinya sederhana dan analitis.

Dalam rekursi komputer menyimpan stack dan dalam versi iteratif Anda harus mengelola stack secara manual.

Pikirkan, cukup konversikan program rekursif pencarian pertama (pada grafik) menjadi program berulang dfs.

Semua yang terbaik!


1

Contoh sederhana dan lengkap lainnya dari mengubah fungsi rekursif menjadi iteratif menggunakan stack.

#include <iostream>
#include <stack>
using namespace std;

int GCD(int a, int b) { return b == 0 ? a : GCD(b, a % b); }

struct Par
{
    int a, b;
    Par() : Par(0, 0) {}
    Par(int _a, int _b) : a(_a), b(_b) {}
};

int GCDIter(int a, int b)
{
    stack<Par> rcstack;

    if (b == 0)
        return a;
    rcstack.push(Par(b, a % b));

    Par p;
    while (!rcstack.empty()) 
    {
        p = rcstack.top();
        rcstack.pop();
        if (p.b == 0)
            continue;
        rcstack.push(Par(p.b, p.a % p.b));
    }

    return p.a;
}

int main()
{
    //cout << GCD(24, 36) << endl;
    cout << GCDIter(81, 36) << endl;

    cin.get();
    return 0;
}

0

Deskripsi kasar tentang bagaimana suatu sistem mengambil fungsi rekursif dan menjalankannya menggunakan tumpukan:

Ini dimaksudkan untuk menunjukkan ide tanpa detail. Pertimbangkan fungsi ini yang akan mencetak titik-titik pada grafik:

function show(node)
0. if isleaf(node):
1.  print node.name
2. else:
3.  show(node.left)
4.  show(node)
5.  show(node.right)

Misalnya grafik: A-> B A-> C show (A) akan mencetak B, A, C

Panggilan fungsi berarti menyimpan status lokal dan titik kelanjutan agar Anda dapat kembali, dan kemudian melompat fungsi yang ingin Anda panggil.

Misalnya, misalkan acara (A) mulai berjalan. Panggilan fungsi pada baris 3. show (B) berarti - Tambahkan item ke tumpukan yang berarti "Anda harus melanjutkan di baris 2 dengan simpul status variabel lokal = A" - Goto line 0 dengan node = B.

Untuk mengeksekusi kode, sistem menjalankan instruksi. Ketika panggilan fungsi ditemui, sistem mendorong informasi yang diperlukan untuk kembali ke tempat sebelumnya, menjalankan kode fungsi, dan ketika fungsi selesai, muncul informasi tentang ke mana ia harus pergi untuk melanjutkan.


0

Tautan ini memberikan beberapa penjelasan dan mengusulkan gagasan untuk menjaga "lokasi" agar dapat sampai ke tempat yang tepat di antara beberapa panggilan rekursif:

Namun, semua contoh ini menggambarkan skenario di mana panggilan rekursif dibuat dalam jumlah tetap . Hal-hal menjadi lebih rumit ketika Anda memiliki sesuatu seperti:

function rec(...) {
  for/while loop {
    var x = rec(...)
    // make a side effect involving return value x
  }
}


0

Contoh saya ada di Clojure, tetapi harus cukup mudah diterjemahkan ke bahasa apa pun.

Diberikan fungsi ini StackOverflowuntuk nilai besar n:

(defn factorial [n]
  (if (< n 2)
    1
    (*' n (factorial (dec n)))))

kita dapat mendefinisikan versi yang menggunakan tumpukannya sendiri dengan cara berikut:

(defn factorial [n]
  (loop [n n
         stack []]
    (if (< n 2)
      (return 1 stack)
      ;; else loop with new values
      (recur (dec n)
             ;; push function onto stack
             (cons (fn [n-1!]
                     (*' n n-1!))
                   stack)))))

di mana returndidefinisikan sebagai:

(defn return
  [v stack]
  (reduce (fn [acc f]
            (f acc))
          v
          stack))

Ini berfungsi untuk fungsi yang lebih kompleks juga, misalnya fungsi ackermann :

(defn ackermann [m n]
  (cond
    (zero? m)
    (inc n)

    (zero? n)
    (recur (dec m) 1)

    :else
    (recur (dec m)
           (ackermann m (dec n)))))

dapat diubah menjadi:

(defn ackermann [m n]
  (loop [m m
         n n
         stack []]
    (cond
      (zero? m)
      (return (inc n) stack)

      (zero? n)
      (recur (dec m) 1 stack)

      :else
      (recur m
             (dec n)
             (cons #(ackermann (dec m) %)
                   stack)))))
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.