Cách đi từ đệ quy đến lặp


349

Tôi đã sử dụng đệ quy khá nhiều trong nhiều năm lập trình để giải quyết các vấn đề đơn giản, nhưng tôi hoàn toàn biết rằng đôi khi bạn cần lặp lại do vấn đề về bộ nhớ / tốc độ.

Vì vậy, đôi khi trong quá khứ tôi đã đi thử và tìm xem liệu có tồn tại bất kỳ "mẫu" hoặc cách nào trong sách giáo khoa để chuyển đổi một cách tiếp cận đệ quy phổ biến thành phép lặp và không tìm thấy gì. Hoặc ít nhất không có gì mà tôi có thể nhớ nó sẽ giúp ích.

  • Có những quy tắc chung?
  • Có một "mô hình"?

4

Câu trả lời:


333

Thông thường, tôi thay thế một thuật toán đệ quy bằng thuật toán lặp bằng cách đẩy các tham số thường được truyền cho hàm đệ quy lên ngăn xếp. Trong thực tế, bạn đang thay thế ngăn xếp chương trình bằng một trong những thứ của riêng bạn.

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

  // Push other objects on the stack.

}

Lưu ý: nếu bạn có nhiều hơn một cuộc gọi đệ quy bên trong và bạn muốn giữ nguyên thứ tự của các cuộc gọi, bạn phải thêm chúng theo thứ tự ngược lại với ngăn xếp:

foo(first);
foo(second);

phải được thay thế bởi

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

Chỉnh sửa: Bài viết Ngăn xếp và Loại bỏ đệ quy (hoặc liên kết Điều khoản dự phòng ) đi sâu vào chi tiết hơn về chủ đề này.


4
Nếu bạn thay thế ngăn xếp của mình bằng một hàng đợi không giải quyết được vấn đề đảo ngược thứ tự thêm?
SamuelWarren

2
Tôi đã làm nó ra giấy và chúng là hai thứ khác nhau. Nếu bạn đảo ngược thứ tự bạn đã thêm chúng, nó sẽ khiến bạn di chuyển về phía trước như bình thường, nhưng giao dịch của bạn vẫn là tìm kiếm theo chiều sâu. Nhưng nếu bạn thay đổi toàn bộ thành một hàng đợi thì bây giờ bạn đang thực hiện theo chiều rộng đầu tiên chứ không phải theo chiều sâu.
pete

1
Gần đây tôi đã làm điều này một cách tổng quát, bằng cách thay thế chức năng truy cập nút của tôi (node)->()bằng (node)->[actions]vị trí hành động () -> [actions]. Sau đó, ở bên ngoài, bạn chỉ cần bật một hành động / tiếp tục ra khỏi ngăn xếp, áp dụng / thực hiện nó, đẩy các hành động mà nó trả lại trên ngăn xếp theo thứ tự ngược lại và lặp lại. Ngũ / traversals phức tạp, bạn chỉ cần nắm bắt những gì có thể đã được stack biến địa phương trong việc gợi ý tham khảo-tính mà bạn gần gũi hơn trong thunks của bạn, sau đó thunks tiếp theo có thể tùy thuộc vào kết quả của tiểu traversals trước, vv
experquisite

6
Đôi khi chúng tôi tránh đệ quy để tránh stackoverflow. Nhưng duy trì ngăn xếp riêng của chúng tôi cũng sẽ gây ra stackoverflow. Vậy tại sao chúng ta muốn thực hiện đệ quy với ngăn xếp của riêng mình?
Zhu Li

8
@ZhuLi Nếu chúng ta sử dụng, newchúng ta có thể tạo một đối tượng trên heap thay vì stack. Không giống như stack, heap không có giới hạn bộ nhớ. Xem gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html
yuqli

77

Thực sự, cách phổ biến nhất để làm điều đó là giữ ngăn xếp của riêng bạn. Đây là một hàm quicksort đệ quy trong 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);
}

Đây là cách chúng ta có thể làm cho nó lặp đi lặp lại bằng cách giữ ngăn xếp của riêng mình:

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

Rõ ràng, ví dụ này không kiểm tra ranh giới ngăn xếp ... và thực sự bạn có thể định kích thước ngăn xếp dựa trên trường hợp xấu nhất được đưa ra các giá trị trái và phải. Nhưng bạn hiểu ý rồi đấy.


1
Bất kỳ ý tưởng về cách làm việc ra ngăn xếp tối đa để phân bổ cho một đệ quy cụ thể?
vựng

@lexicalscope giả sử bạn có một thuật toán đệ quy O(N) = O(R*L), trong đó Ltổng của độ phức tạp "cho lớp r", ví dụ trong trường hợp này bạn có O(N)công việc ở mỗi bước thực hiện phân vùng, độ sâu đệ quy là O(R)trường hợp xấu nhất O(N), trường hợp trung bình O(logN)ở đây.
Caleth

48

Dường như không ai giải quyết được chức năng đệ quy gọi chính nó nhiều hơn một lần trong cơ thể và xử lý quay trở lại một điểm cụ thể trong đệ quy (tức là không phải đệ quy nguyên thủy). Người ta nói rằng mọi đệ quy có thể được chuyển thành lặp đi lặp lại , vì vậy có vẻ như điều này là có thể.

Tôi vừa đưa ra một ví dụ C # về cách làm điều này. Giả sử bạn có hàm đệ quy sau, hoạt động như một giao dịch theo thứ tự bưu điện và AbcTreeNode là một cây 3 lá có con trỏ 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
        }
}

Giải pháp lặp lại:

        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
Nó thực sự hữu ích, tôi đã phải viết phiên bản lặp lại của reccurence mà nó ngà ngà lần n, nhờ bài đăng của bạn tôi đã làm nó.
Wojciech Kulik

1
Đây phải là ví dụ tốt nhất mà tôi từng thấy về mô phỏng đệ quy ngăn xếp cuộc gọi cho các tình huống trong đó nhiều cuộc gọi đệ quy đang được thực hiện trong phương thức. Công việc tốt.
CCS

1
Bạn đã cho tôi tại "Dường như không ai giải quyết được chức năng đệ quy tự gọi mình nhiều lần trong cơ thể và xử lý quay trở lại một điểm cụ thể trong đệ quy" và sau đó tôi đã nâng cấp. OK, bây giờ tôi sẽ đọc phần còn lại của câu trả lời của bạn và xem liệu upvote sớm của tôi đã được chứng minh. (Bởi vì tôi rất cần biết câu trả lời cho điều đó).
mydoghasworms

1
@mydoghasworms - Quay trở lại với câu hỏi này sau khi để lâu, nó thậm chí còn mất tôi một chút thời gian để nhớ những gì tôi đã suy nghĩ. Hy vọng câu trả lời đã giúp.
T. Webster

1
Tôi thích ý tưởng của giải pháp này, nhưng nó có vẻ khó hiểu với tôi. Tôi đã viết phiên bản đơn giản hóa cho cây nhị phân trong python, có thể nó sẽ giúp ai đó hiểu ý tưởng: gist.github.com/azurkin/abb258a0e1a821cbb331f2696b37c3ac
azurkin

33

Phấn đấu thực hiện cuộc gọi đệ quy Tail Recursion (đệ quy trong đó câu lệnh cuối cùng là cuộc gọi đệ quy). Một khi bạn đã có, chuyển đổi nó thành lặp thường khá dễ dàng.


2
Một số đệ quy đuôi biến đổi của JIT: ibm.com/developerworks/java/l Library / j
Liran Orevi

Rất nhiều thông dịch viên (tức là Scheme được biết đến nhiều nhất) sẽ tối ưu hóa đệ quy đuôi tốt. Tôi biết rằng GCC, với một tối ưu hóa nhất định, thực hiện đệ quy đuôi (mặc dù C là một lựa chọn kỳ lạ cho tối ưu hóa như vậy).
new123456

19

Vâng, nói chung, đệ quy có thể được bắt chước như lặp lại bằng cách sử dụng một biến lưu trữ. Lưu ý rằng đệ quy và lặp thường là tương đương; người này hầu như luôn có thể được chuyển đổi sang người khác. Hàm đệ quy đuôi rất dễ dàng chuyển đổi thành hàm lặp. Chỉ cần biến bộ tích lũy thành một biến cục bộ và lặp lại thay vì lặp lại. Đây là một ví dụ trong C ++ (C không phải là để sử dụng đối số mặc định):

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

Biết tôi, có lẽ tôi đã mắc lỗi trong mã, nhưng ý tưởng là có.


14

Ngay cả khi sử dụng stack sẽ không chuyển đổi thuật toán đệ quy thành phép lặp. Đệ quy bình thường là đệ quy dựa trên hàm và nếu chúng ta sử dụng stack thì nó sẽ trở thành đệ quy dựa trên stack. Nhưng nó vẫn đệ quy.

Đối với các thuật toán đệ quy, độ phức tạp không gian là O (N) và độ phức tạp thời gian là O (N). Đối với các thuật toán lặp, độ phức tạp không gian là O (1) và độ phức tạp thời gian là O (N).

Nhưng nếu chúng ta sử dụng những thứ ngăn xếp về độ phức tạp vẫn như cũ. Tôi nghĩ rằng chỉ đệ quy đuôi có thể được chuyển đổi thành lặp.


1
Tôi đồng ý với bit đầu tiên của bạn, nhưng tôi nghĩ rằng tôi đang hiểu nhầm đoạn thứ hai. Xem xét nhân bản một mảng thông qua việc sao chép copy = new int[size]; for(int i=0; i<size; ++i) copy[i] = source[i];không gian bộ nhớ và độ phức tạp thời gian là cả O (N) dựa trên kích thước của dữ liệu, nhưng rõ ràng đó là một thuật toán lặp.
Ponkadoodle

13

Các ngăn xếp và loại bỏ đệ quy bài viết chụp ý tưởng externalizing khung ngăn xếp trên đống, nhưng không cung cấp một đơn giản và lặp lại cách để chuyển đổi. Dưới đây là một.

Trong khi chuyển đổi sang mã lặp, người ta phải biết rằng cuộc gọi đệ quy có thể xảy ra từ một khối mã sâu tùy ý. Nó không chỉ là các tham số, mà còn là điểm để trở về logic vẫn được thực thi và trạng thái của các biến tham gia vào các điều kiện tiếp theo, điều đó quan trọng. Dưới đây là một cách rất đơn giản để chuyển đổi sang mã lặp với ít thay đổi nhất.

Hãy xem xét mã đệ quy này:

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

Mã lặp:

// 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();
    }
}

Lưu ý cách cấu trúc của mã vẫn đúng với logic đệ quy và sửa đổi là tối thiểu, dẫn đến số lượng lỗi ít hơn. Để so sánh, tôi đã đánh dấu các thay đổi với ++ và -. Hầu hết các khối được chèn mới ngoại trừ v.push_back, là chung cho bất kỳ logic lặp được chuyển đổi nào

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();
    }

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

}

Điều này đã giúp tôi rất nhiều, nhưng có một vấn đề: stackitemcác đối tượng được phân bổ với giá trị rác cho ra. Mọi thứ vẫn hoạt động trong trường hợp giống nhất, nhưng nếu ratrùng hợp là 1 hoặc 2, bạn sẽ có hành vi không chính xác. Giải pháp là khởi tạo rathành 0.
JanX2

@ JanX2, stackitemkhông được đẩy mà không khởi tạo. Nhưng có, khởi tạo thành 0 sẽ bắt lỗi.
Chethan

Tại sao cả hai không trả lại địa chỉ được đặt thành v.pop_back()tuyên bố thay thế?
is7s

7

Tìm kiếm trên google cho "Phong cách tiếp tục đi qua." Có một quy trình chung để chuyển đổi sang kiểu đệ quy đuôi; đó cũng là một quy trình chung để biến các hàm đệ quy đuôi thành các vòng lặp.


6

Chỉ giết thời gian ... Một hàm đệ quy

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

có thể được chuyển đổi thành

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

}

Ví dụ trên là một ví dụ về đệ quy dfs lặp trên cây tìm kiếm nhị phân :)
Amit

5

Nói chung, kỹ thuật để tránh tràn ngăn xếp là dành cho các hàm đệ quy được gọi là kỹ thuật trampoline được các nhà phát triển Java áp dụng rộng rãi.

Tuy nhiên, đối với C #, có một phương thức trợ giúp nhỏ ở đây để chuyển hàm đệ quy của bạn thành phép lặp mà không yêu cầu thay đổi logic hoặc làm cho mã không thể hiểu được. C # là một ngôn ngữ tốt đẹp đến nỗi những thứ tuyệt vời có thể với nó.

Nó hoạt động bằng cách gói các phần của phương thức bằng một phương thức trợ giúp. Ví dụ: hàm đệ quy sau:

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

Trở thành:

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

Suy nghĩ về những thứ thực sự cần một ngăn xếp:

Nếu chúng ta xem xét mô hình đệ quy là:

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
}

Ví dụ: Tháp cổ điển Hà Nội

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
}

Điều này có thể được dịch thành một vòng lặp làm việc trên một ngăn xếp rõ ràng, bằng cách đặt lại nó là:

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

Đối với Tháp Hà Nội, điều này trở thành:

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()));
    }
}

Có sự linh hoạt đáng kể ở đây là cách bạn xác định ngăn xếp của mình. Bạn có thể làm cho ngăn xếp của mình một danh sách các Commandđối tượng làm những việc tinh vi. Hoặc bạn có thể đi theo hướng ngược lại và biến nó thành một danh sách các loại đơn giản hơn (ví dụ: "tác vụ" có thể là 4 phần tử trên một ngăn xếp int, thay vì một phần tử trên một ngăn xếp Task).

Tất cả điều này có nghĩa là bộ nhớ cho ngăn xếp nằm trong heap chứ không phải trong ngăn xếp thực thi Java, nhưng điều này có thể hữu ích ở chỗ bạn có nhiều quyền kiểm soát hơn.


3

Một mẫu để tìm là một cuộc gọi đệ quy ở cuối hàm (nên được gọi là đệ quy đuôi). Điều này có thể dễ dàng được thay thế trong một thời gian. Ví dụ: hàm foo:

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

kết thúc bằng một cuộc gọi đến foo. Điều này có thể được thay thế bằng:

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

trong đó loại bỏ cuộc gọi đệ quy thứ hai.


3
Vẫn có vẻ đệ quy với tôi ... :)
nathan

2
Vâng, vâng - nhưng đó là một nửa đệ quy. Loại bỏ các đệ quy khác sẽ yêu cầu sử dụng một kỹ thuật khác ...
Mark Bessey

2

Một câu hỏi đã được đóng lại như một bản sao của câu hỏi này có cấu trúc dữ liệu rất cụ thể:

nhập mô tả hình ảnh ở đây

Nút có cấu trúc như sau:

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

Hàm xóa đệ quy trông giống như:

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

Nói chung, không phải lúc nào cũng có thể tránh được một ngăn xếp cho các hàm đệ quy tự gọi nhiều lần (hoặc thậm chí một lần). Tuy nhiên, đối với cấu trúc đặc biệt này, nó là có thể. Ý tưởng là làm phẳng tất cả các nút thành một danh sách. Điều này được thực hiện bằng cách đặt nút hiện childtại ở cuối danh sách hàng trên cùng.

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

Kỹ thuật này có thể được áp dụng cho bất kỳ cấu trúc liên kết dữ liệu nào có thể được giảm xuống thành DAG với thứ tự tôpô xác định. Các nút con hiện tại được sắp xếp lại để con cuối cùng chấp nhận tất cả các con khác. Sau đó, nút hiện tại có thể bị xóa và sau đó có thể lặp lại cho con còn lại.


1

Đệ quy không là gì ngoài quá trình gọi một chức năng từ chức năng khác, chỉ có quá trình này được thực hiện bằng cách tự gọi một chức năng. Như chúng ta biết khi một hàm gọi hàm kia, hàm đầu tiên lưu trạng thái của nó (các biến của nó) và sau đó chuyển điều khiển sang hàm được gọi. Hàm được gọi có thể được gọi bằng cách sử dụng cùng tên của biến ex fun1 (a) có thể gọi fun2 (a). Khi chúng tôi thực hiện cuộc gọi đệ quy không có gì mới xảy ra. Một hàm gọi chính nó bằng cách chuyển cùng loại và tương tự trong các biến tên (nhưng rõ ràng các giá trị được lưu trữ trong các biến là khác nhau, chỉ có tên vẫn giữ nguyên.) Cho chính nó. Nhưng trước mỗi cuộc gọi, chức năng sẽ lưu trạng thái của nó và quá trình lưu này tiếp tục. TIẾT KIỆM LÀ ĐÁNG TIN CẬY.

NGAY BÂY GIỜ THEO D COMI VÀO CHƠI.

Vì vậy, nếu bạn viết một chương trình lặp và lưu trạng thái trên một ngăn xếp mỗi lần và sau đó bật ra các giá trị từ ngăn xếp khi cần, bạn đã chuyển đổi thành công một chương trình đệ quy thành một lần lặp!

Bằng chứng là đơn giản và phân tích.

Trong đệ quy, máy tính duy trì một ngăn xếp và trong phiên bản lặp, bạn sẽ phải duy trì thủ công ngăn xếp.

Hãy suy nghĩ về nó, chỉ cần chuyển đổi một chương trình đệ quy chuyên sâu (trên biểu đồ) thành chương trình lặp dfs.

Tất cả là tốt nhất!


1

Một ví dụ đơn giản và đầy đủ khác về việc biến hàm đệ quy thành phép lặp bằng cách sử dụng ngăn xếp.

#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

Một mô tả sơ bộ về cách một hệ thống lấy bất kỳ hàm đệ quy nào và thực thi nó bằng cách sử dụng ngăn xếp:

Điều này nhằm thể hiện ý tưởng mà không có chi tiết. Hãy xem xét chức năng này sẽ in ra các nút của biểu đồ:

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

Ví dụ biểu đồ: A-> B A-> C hiển thị (A) sẽ in B, A, C

Các cuộc gọi chức năng có nghĩa là lưu trạng thái cục bộ và điểm tiếp tục để bạn có thể quay lại, sau đó nhảy chức năng bạn muốn gọi.

Ví dụ: giả sử chương trình (A) bắt đầu chạy. Hàm gọi trên dòng 3. show (B) có nghĩa là - Thêm mục vào ngăn xếp có nghĩa là "bạn sẽ cần tiếp tục ở dòng 2 với nút trạng thái biến cục bộ = A" - Dòng Goto 0 với nút = B.

Để thực thi mã, hệ thống chạy qua các hướng dẫn. Khi gặp một cuộc gọi chức năng, hệ thống sẽ đẩy thông tin cần quay lại vị trí của nó, chạy mã chức năng và khi chức năng hoàn thành, sẽ bật thông tin về nơi cần tiếp tục.


0

Liên kết này cung cấp một số giải thích và đề xuất ý tưởng giữ "vị trí" để có thể đến địa điểm chính xác giữa một số cuộc gọi đệ quy:

Tuy nhiên, tất cả các ví dụ này mô tả các tình huống trong đó một cuộc gọi đệ quy được thực hiện một số lần cố định . Mọi thứ trở nên phức tạp hơn khi bạn có một cái gì đó như:

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


0

Ví dụ của tôi là trong Clojure, nhưng nên khá dễ dịch sang bất kỳ ngôn ngữ nào.

Cho hàm này StackOverflows cho các giá trị lớn của n:

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

chúng ta có thể định nghĩa một phiên bản sử dụng ngăn xếp của riêng mình theo cách sau:

(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)))))

nơi returnđược định nghĩa là:

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

Điều này cũng hoạt động cho các hàm phức tạp hơn, ví dụ như hàm ackermann :

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

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

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

có thể được chuyển thành:

(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)))))
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.