Cách chung để chuyển đổi một vòng lặp (while / for) thành đệ quy hoặc từ một đệ quy thành một vòng lặp?


23

Vấn đề này chủ yếu tập trung vào thuật toán, có thể là một cái gì đó trừu tượng và mang tính học thuật hơn.

Ví dụ đưa ra một ý nghĩ, tôi muốn một cách chung chung, vì vậy ví dụ chỉ được sử dụng để làm cho chúng tôi rõ hơn về suy nghĩ của bạn.

Nói chung, một vòng lặp có thể được chuyển đổi thành một đệ quy.

ví dụ:

for(int i=1;i<=100;++i){sum+=i;}

Và đệ quy liên quan của nó là:

int GetTotal(int number)
{
   if (number==1) return 1;   //The end number
   return number+GetTotal(number-1); //The inner recursive
}

Và cuối cùng để đơn giản hóa điều này, một đệ quy đuôi là cần thiết:

int GetTotal (int number, int sum)
{
    if(number==1) return sum;
    return GetTotal(number-1,sum+number);
}

Tuy nhiên, hầu hết các trường hợp không dễ dàng để trả lời và phân tích. Những gì tôi muốn biết là:

1) Chúng ta có thể có một "cách chung chung" để chuyển đổi một vòng lặp (cho / trong khi) thành một đệ quy không? Và những loại điều chúng ta nên chú ý trong khi thực hiện chuyển đổi? Sẽ tốt hơn nếu viết thông tin chi tiết với một số mẫu và lý thuyết sai lầm của bạn cũng như quá trình chuyển đổi.

2) "Đệ quy" có hai dạng: Đệ quy hoàn toàn và Đệ quy đuôi. Vì vậy, tốt hơn để chuyển đổi? "Quy tắc" nào chúng ta nên nắm vững?

3) Đôi khi chúng ta cần giữ "lịch sử" đệ quy, điều này dễ dàng được thực hiện trong một câu lệnh lặp:

ví dụ:

List<string> history = new List<string>();
int sum=0;
for (int i=1;i<=100;++i)
{
   if(i==1) history.Add(i.ToString()+"'s result is:1.");
   else
   {
     StringBuilder sub = new StringBuilder();

      for(int j=1;j<=i;++j)
      {
          if(j==i) sbu.Append(j.ToString());
          else
          {
            sub.Append(j.ToString()+"+");
          }
      }
    sum +=i;
    sbu.Append("'s result is:"+sum+Environment.NewLine);
   }
}

Kết quả dưới đây là:

Kết quả 1 là 1.

Kết quả 1 + 2 là 3.

Kết quả 1 + 2 + 3 là 6 trận

Tuy nhiên, tôi nghĩ rằng thật khó để giữ lịch sử theo cách đệ quy, bởi vì thuật toán dựa trên đệ quy tập trung vào việc lấy kết quả cuối cùng và thực hiện trả lại cuộc gọi lại. Vì vậy, tất cả những điều này được thực hiện thông qua ngăn xếp được duy trì bởi ngôn ngữ lập trình gán bộ nhớ ở dạng ngăn xếp tự động. Và làm thế nào chúng ta có thể "thủ công" lấy từng "giá trị ngăn xếp" ra và trả về nhiều giá trị thông qua thuật toán đệ quy?

Và những gì về "từ một thuật toán đệ quy đến một vòng lặp"? Họ có thể được chuyển đổi cho nhau (tôi nghĩ rằng nó nên được thực hiện trên lý thuyết, nhưng tôi muốn những điều chính xác hơn để chứng minh suy nghĩ của tôi) .


"Persudo" nghĩa là gì?
gnat

Câu trả lời:


30

Trên thực tế bạn nên phá vỡ chức năng đầu tiên:

Một vòng lặp có một vài phần:

  1. tiêu đề và xử lý trước vòng lặp. Có thể khai báo một số biến mới

  2. điều kiện, khi dừng vòng lặp.

  3. cơ thể vòng lặp thực tế. Nó thay đổi một số biến tiêu đề và / hoặc các tham số được truyền vào.

  4. cái đuôi; Điều gì xảy ra sau vòng lặp và kết quả trả về.

Hoặc để viết nó ra:

foo_iterative(params){
    header
    while(condition){
        loop_body
    }
    return tail
}

Sử dụng các khối này để thực hiện cuộc gọi đệ quy khá đơn giản:

foo_recursive(params){
    header
    return foo_recursion(params, header_vars)
}

foo_recursion(params, header_vars){
    if(!condition){
        return tail
    }

    loop_body
    return foo_recursion(params, modified_header_vars)
}

Et voilà; một phiên bản đệ quy đuôi của bất kỳ vòng lặp. breaks và continues trong thân vòng lặp sẽ vẫn phải được thay thế return tailvà trả về foo_recursion(params, modified_header_vars)khi cần nhưng điều đó đủ đơn giản.


Đi theo con đường khác phức tạp hơn; một phần vì có thể có nhiều cuộc gọi đệ quy. Điều này có nghĩa là mỗi lần chúng ta bật một khung stack có thể có nhiều nơi chúng ta cần tiếp tục. Ngoài ra, có thể có các biến mà chúng ta cần lưu trong cuộc gọi đệ quy và các tham số ban đầu của cuộc gọi.

Chúng ta có thể sử dụng một công tắc để làm việc xung quanh đó:

bar_recurse(params){
    if(baseCase){
        finalize
        return
    }
    body1
    bar_recurse(mod_params)
    body2
    bar_recurse(mod_params)
    body3
}


bar_iterative(params){
    stack.push({init, params})

    while(!stack.empty){
        stackFrame = stack.pop()

        switch(stackFrame.resumPoint){
        case init:
            if(baseCase){
                finalize
                break;
            }
            body1
            stack.push({resum1, params, variables})
            stack.push({init, modified_params})
            break;
        case resum1:
            body2
            stack.push({resum2, params, variables})
            stack.push({init, modified_params})
            break;
        case resum2:
            body3
            break;
        }
    }
}

0

Theo dõi câu trả lời của @ratchet freak, tôi đã tạo ra ví dụ này về cách hàm Fibonacci có thể được viết lại thành một vòng lặp while trong Java. Lưu ý rằng Có một cách đơn giản hơn (và hiệu quả) để viết lại Fibonacci bằng một vòng lặp while.

class CallContext { //this class is similar to the stack frame

    Object[] args;

    List<Object> vars = new LinkedList<>();

    int resumePoint = 0;

    public CallContext(Object[] args) {
        this.args = args;
    }

}


static int fibonacci(int fibNumber) {
    Deque<CallContext> callStack = new LinkedList<>();
    callStack.add(new CallContext(new Object[]{fibNumber}));
    Object lastReturn = null; //value of last object returned (when stack frame was dropped)
    while (!callStack.isEmpty()) {
        CallContext callContext = callStack.peekLast();
        Object[] args = callContext.args;
        //actual logic starts here
        int arg = (int) args[0];
        if (arg == 0 || arg == 1) {
            lastReturn = arg;
            callStack.removeLast();
        } else {
            switch (callContext.resumePoint) {
                case 0: //calculate fib(n-1)
                    callStack.add(new CallContext(new Object[]{arg - 1}));
                    callContext.resumePoint++;
                    break;
                case 1: //calculate fib(n-2)
                    callContext.vars.add(lastReturn); //fib1
                    callStack.add(new CallContext(new Object[]{arg - 2}));
                    callContext.resumePoint++;
                    break;
                case 2: // fib(n-1) + fib(n-2)
                    callContext.vars.add(lastReturn); //fib2
                    lastReturn = (int) callContext.vars.get(0) + (int) callContext.vars.get(1);
                    callStack.removeLast();
                    break;
            }
        }
    }
    return (int) lastReturn;
}
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.