วิธีที่จะไปจากการเรียกซ้ำไปซ้ำ


349

ฉันใช้การเรียกซ้ำหลายครั้งในการเขียนโปรแกรมเป็นเวลาหลายปีเพื่อแก้ไขปัญหาที่เรียบง่าย แต่ฉันรู้ดีว่าบางครั้งคุณจำเป็นต้องทำซ้ำเนื่องจากปัญหาหน่วยความจำ / ความเร็ว

ดังนั้นบางครั้งในอดีตที่ผ่านมาฉันลองไปหาว่ามี "รูปแบบ" หรือวิธีการแบบหนังสือหรือข้อความใด ๆ ในการเปลี่ยนวิธีการเรียกซ้ำโดยทั่วไปเพื่อทำซ้ำและไม่พบอะไรเลย หรืออย่างน้อยก็ไม่มีอะไรที่ฉันจำได้ว่ามันจะช่วย

  • มีกฎทั่วไปหรือไม่
  • มี "รูปแบบ" หรือไม่?

4
ฉันพบชุดข้อมูลนี้: blog.moertel.com/posts/2013-05-11-recursive-to-iterative.html
orionrush

คำตอบ:


334

โดยปกติแล้วฉันจะแทนที่อัลกอริทึมแบบเรียกซ้ำโดยอัลกอริทึมแบบวนซ้ำโดยการกดพารามิเตอร์ที่ปกติจะถูกส่งผ่านไปยังฟังก์ชันแบบเรียกซ้ำลงบนสแต็ก ในความเป็นจริงคุณกำลังแทนที่สแต็กโปรแกรมด้วยหนึ่งในของคุณเอง

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

  // Push other objects on the stack.

}

หมายเหตุ: หากคุณมีการโทรซ้ำภายในมากกว่าหนึ่งครั้งและคุณต้องการรักษาลำดับการโทรคุณต้องเพิ่มพวกเขาในลำดับที่กลับไปยังสแต็ก:

foo(first);
foo(second);

จะต้องถูกแทนที่ด้วย

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

แก้ไข: บทความการกำจัดการกองซ้อนและการเรียกซ้ำ (หรือลิงก์การสำรองข้อมูลบทความ ) มีรายละเอียดเพิ่มเติมเกี่ยวกับเรื่องนี้


4
หากคุณแทนที่สแต็กของคุณด้วยคิวจะไม่ช่วยแก้ปัญหาการย้อนกลับคำสั่งซื้อได้หรือไม่
SamuelWarren

2
ฉันเขียนมันลงบนกระดาษและมันเป็นสองสิ่งที่แตกต่างกัน หากคุณย้อนกลับคำสั่งซื้อที่คุณเพิ่มเข้าไปมันจะทำให้คุณเลื่อนไปข้างหน้าตามปกติ แต่การสำรวจเส้นทางของคุณยังเป็นการค้นหาในเชิงลึกก่อน แต่ถ้าคุณเปลี่ยนสิ่งทั้งหมดเป็นคิวตอนนี้คุณกำลังทำสิ่งที่กว้างกว่าไม่ใช่การสำรวจเส้นทางแรก
pete

1
ฉันเพิ่งทำอย่างนี้ในลักษณะทั่วไปโดยการเปลี่ยนฟังก์ชั่นการเยี่ยมชมโหนดของฉัน(node)->()กับที่ดำเนินการคือ(node)->[actions] () -> [actions]จากนั้นออกไปข้างนอกคุณเพียงแค่ป๊อปแอคชั่น / ต่อเนื่องออกจากสแต็คนำไปใช้ / ดำเนินการผลักดันการกระทำที่ส่งคืนบนสแต็กในลำดับที่กลับกันและทำซ้ำ โดยบังเอิญ / ซับซ้อน traversals คุณเพียงแค่จับสิ่งที่จะเป็นตัวแปรสแต็คในท้องถิ่นในพอยน์เตอร์นับอ้างอิงที่คุณปิดใน thunks ของคุณแล้ว thunks ที่ตามมาสามารถเกิดขึ้นกับผลลัพธ์ของ traversals ย่อยเป็นต้น
ประสบการณ์

6
บางครั้งเราหลีกเลี่ยงการเรียกซ้ำเพื่อหลีกเลี่ยง stackoverflow แต่การบำรุงรักษาสแต็กของเราเองก็จะทำให้เกิดการสแต็คโอเวอร์โฟลว์ เหตุใดเราจึงต้องการนำการสอบถามซ้ำมาใช้กับกองซ้อนของเราเอง
Zhu Li

8
@ZhuLi ถ้าเราใช้newเราสามารถสร้างวัตถุบน heap แทนที่จะเป็น stack ฮีปไม่มีข้อ จำกัด หน่วยความจำ ดูgribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.html
yuqli

77

จริงๆแล้ววิธีที่ใช้กันโดยทั่วไปก็คือเก็บสแต็คของคุณเอง นี่คือฟังก์ชัน quicksort แบบเรียกซ้ำใน 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);
}

นี่คือวิธีที่เราสามารถทำซ้ำได้โดยเก็บสแต็คของเราเอง:

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

เห็นได้ชัดว่าตัวอย่างนี้ไม่ได้ตรวจสอบขอบเขตของสแต็ก ... และคุณสามารถปรับขนาดสแต็กตามกรณีที่เลวร้ายที่สุดที่กำหนดค่าซ้ายและขวา แต่คุณได้รับความคิด


1
แนวคิดใดบ้างเกี่ยวกับวิธีทำงานกับสแต็กสูงสุดเพื่อจัดสรรสำหรับการสอบถามซ้ำโดยเฉพาะ
lexicalscope

@lexicalscope สมมติว่าคุณมีขั้นตอนวิธีการเวียนเกิดในO(N) = O(R*L)ที่Lคือผลรวมของความซับซ้อน "สำหรับชั้น r" เช่นในกรณีนี้คุณต้องO(N)ทำงานในแต่ละขั้นตอนการทำ partitionings ที่ความลึก recursive เป็นO(R)เช่นกรณีที่เลวร้ายที่สุดO(N)กรณีเฉลี่ยO(logN)ที่นี่
Caleth

48

ดูเหมือนว่าไม่มีใครพูดถึงว่าฟังก์ชั่นวนซ้ำเรียกตัวเองว่ามากกว่าหนึ่งครั้งในร่างกายและจัดการกลับไปยังจุดที่ระบุในการเรียกซ้ำ (เช่นไม่ใช่แบบดั้งเดิม - แบบเรียกซ้ำ) ว่ากันว่าการเรียกซ้ำทุกครั้งสามารถเปลี่ยนเป็นการวนซ้ำได้ดังนั้นจึงดูเหมือนว่าสิ่งนี้จะเป็นไปได้

ฉันเพิ่งมาพร้อมกับตัวอย่าง C # ของวิธีการทำเช่นนี้ สมมติว่าคุณมีฟังก์ชัน recursive ต่อไปนี้ซึ่งทำหน้าที่เหมือน traversal postorder และ AbcTreeNode นั้นเป็นต้นไม้ 3-ary ที่มีตัวชี้ 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
        }
}

วิธีแก้ซ้ำ:

        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
มันมีประโยชน์จริง ๆ ฉันต้องเขียน reccurence ซ้ำ ๆ ซึ่งทำให้ตัวเอง n-times ขอบคุณโพสต์ของคุณที่ฉันทำ
วอยชิคคูลิ

1
นี่เป็นตัวอย่างที่ดีที่สุดที่ฉันเคยเห็นในการจำลองการโทรสแต็กแบบเรียกซ้ำสำหรับสถานการณ์ที่มีการโทรแบบเรียกซ้ำหลายครั้งภายในวิธีการ งานที่ดี.
CCS

1
คุณมีฉันที่ "ดูเหมือนว่าไม่มีใครพูดถึงว่าฟังก์ชั่นวนซ้ำเรียกตัวเองว่ามากกว่าหนึ่งครั้งในร่างกายและจัดการกลับไปยังจุดที่เฉพาะเจาะจงในการเรียกซ้ำ" แล้วฉันก็ upvoted แล้ว ตกลงตอนนี้ฉันจะอ่านคำตอบที่เหลือของคุณและดูว่า upvote ก่อนวัยอันควรของฉันเป็นธรรมหรือไม่ (เพราะฉันหมดความต้องการที่จะรู้คำตอบนั้น)
mydoghasworms

1
@mydoghasworms - กลับไปที่คำถามนี้หลังจากนานมันก็เอาฉันสักครู่จะจำสิ่งที่ผมคิด หวังว่าคำตอบช่วย
ต. เว็บสเตอร์

1
ฉันชอบความคิดของวิธีนี้ แต่มันทำให้ฉันสับสน ฉันเขียนเวอร์ชันแบบย่อสำหรับต้นไม้ไบนารีในหลามบางทีมันอาจช่วยให้ใครบางคนเข้าใจความคิด: gist.github.com/azurkin/abb258a0e1a821cbb331f2696b37c3ac
azurkin

33

มุ่งมั่นที่จะทำการโทรซ้ำแบบเรียกซ้ำการเรียกซ้ำ (การสอบถามซ้ำโดยที่คำสั่งสุดท้ายคือการเรียกซ้ำ) เมื่อคุณมีแล้วการแปลงเป็นการทำซ้ำโดยทั่วไปจะค่อนข้างง่าย


2
การเรียกซ้ำการแปลงหางของ JIT บางอย่าง: ibm.com/developerworks/java/library/j-diag8.html
Liran Orevi

ล่ามจำนวนมาก (เช่นโครงการเป็นที่รู้จักกันดีที่สุด) จะเพิ่มการเรียกซ้ำหางได้ดีที่สุด ฉันรู้ว่า GCC ซึ่งมีการปรับให้เหมาะสมแล้วจะทำการเรียกซ้ำ (แม้ว่า C จะเป็นตัวเลือกที่แปลกสำหรับการปรับให้เหมาะสม)
new123456

19

โดยทั่วไปการเรียกซ้ำสามารถเลียนแบบเป็นการวนซ้ำโดยใช้ตัวแปรหน่วยเก็บข้อมูล โปรดทราบว่าการเรียกซ้ำและการทำซ้ำโดยทั่วไปจะเทียบเท่า สามารถแปลงเป็นอันหนึ่งได้ตลอดเวลา ฟังก์ชั่นหางแบบเรียกซ้ำจะถูกแปลงเป็นแบบวนซ้ำได้ง่ายมาก เพียงแค่ทำให้ตัวแปรตัวสะสมเป็นแบบโลคัลแล้ววนซ้ำแทนที่จะเป็นการรีเซ็ท นี่คือตัวอย่างใน C ++ (C ไม่ใช่สำหรับการใช้อาร์กิวเมนต์เริ่มต้น):

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

รู้จักฉันฉันอาจทำผิดในรหัส แต่ความคิดอยู่ที่นั่น


14

แม้แต่การใช้สแต็กจะไม่แปลงอัลกอริทึมแบบเรียกซ้ำเป็นซ้ำ การเรียกซ้ำปกติคือการเรียกซ้ำตามฟังก์ชันและถ้าเราใช้สแต็กมันจะกลายเป็นการเรียกซ้ำตามปกติ แต่มันก็ยังเรียกซ้ำ

สำหรับอัลกอริทึมแบบเรียกซ้ำความซับซ้อนของพื้นที่คือ O (N) และความซับซ้อนของเวลาคือ O (N) สำหรับอัลกอริทึมซ้ำความซับซ้อนของพื้นที่คือ O (1) และความซับซ้อนของเวลาคือ O (N)

แต่ถ้าเราใช้สิ่งต่าง ๆ ในแง่ของความซับซ้อนก็ยังคงเหมือนเดิม ฉันคิดว่าการวนรอบแบบวนซ้ำสามารถเปลี่ยนเป็นการวนซ้ำได้


1
ฉันเห็นด้วยกับบิตแรกของคุณ แต่ฉันคิดว่าฉันเข้าใจผิดในย่อหน้าที่สอง พิจารณาการโคลนอาร์เรย์โดยเพียงแค่คัดลอกcopy = new int[size]; for(int i=0; i<size; ++i) copy[i] = source[i];พื้นที่หน่วยความจำและความซับซ้อนของเวลาทั้ง O (N) ตามขนาดของข้อมูล แต่มันชัดเจนว่าเป็นอัลกอริทึมซ้ำ
Ponkadoodle

13

กองและการกำจัด recursionจับบทความความคิดของ externalizing กรอบสแต็คในกอง แต่ไม่ได้ให้ตรงไปตรงมาและทำซ้ำวิธีที่จะแปลง ด้านล่างเป็นหนึ่ง

ในขณะที่แปลงเป็นรหัสซ้ำคุณต้องทราบว่าการเรียกซ้ำอาจเกิดขึ้นจากการบล็อกรหัสที่มีความลึกโดยพลการ ไม่ใช่เพียงแค่พารามิเตอร์ แต่ยังรวมถึงจุดที่จะกลับไปสู่ตรรกะที่ยังคงต้องดำเนินการและสถานะของตัวแปรที่เข้าร่วมในเงื่อนไขที่ตามมาซึ่งมีความสำคัญ ด้านล่างเป็นวิธีที่ง่ายมากในการแปลงเป็นรหัสซ้ำโดยมีการเปลี่ยนแปลงน้อยที่สุด

พิจารณารหัสซ้ำนี้:

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

รหัสซ้ำ:

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

สังเกตว่าโครงสร้างของรหัสยังคงเป็นจริงกับตรรกะแบบเรียกซ้ำและการปรับเปลี่ยนมีน้อยที่สุดทำให้มีจำนวนข้อบกพร่องน้อยลง สำหรับการเปรียบเทียบฉันได้ทำเครื่องหมายการเปลี่ยนแปลงด้วย ++ และ - บล็อกที่แทรกใหม่ส่วนใหญ่ยกเว้น v.push_back เป็นเรื่องธรรมดาสำหรับตรรกะการวนซ้ำใด ๆ ที่แปลงแล้ว

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

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

}

นี้ได้ช่วยให้ฉันมาก แต่มีปัญหาโรค: วัตถุที่มีการจัดสรรค่าขยะstackitem raทุกอย่างยังคงทำงานในกรณีที่ชอบมากที่สุด แต่raโดยบังเอิญเป็น 1 หรือ 2 คุณจะได้รับพฤติกรรมที่ไม่ถูกต้อง การแก้ปัญหาคือการเริ่มต้นraเพื่อ 0
JanX2

@ JanX2 stackitemต้องไม่ถูกผลักโดยไม่เริ่มต้น แต่ใช่การเริ่มต้นเป็น 0 จะจับข้อผิดพลาด
Chethan

เหตุใดจึงไม่ตั้งค่าที่อยู่ผู้ส่งทั้งสองให้เป็นv.pop_back()คำสั่งแทน
is7s

7

ค้นหา google สำหรับ "รูปแบบการส่งต่อ" มีขั้นตอนทั่วไปสำหรับการแปลงเป็นหางแบบเรียกซ้ำ; นอกจากนี้ยังมีขั้นตอนทั่วไปสำหรับการเปลี่ยนฟังก์ชั่นวนซ้ำหางเป็นลูป


6

แค่ฆ่าเวลา ... ฟังก์ชั่นวนซ้ำ

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

สามารถแปลงเป็น

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

}

ตัวอย่างข้างต้นเป็นตัวอย่างของการเรียกซ้ำไปซ้ำ DFS บนต้นไม้ค้นหาแบบทวิภาค :)
Amit

5

โดยทั่วไปเทคนิคในการหลีกเลี่ยงการล้นสแต็คสำหรับฟังก์ชั่นวนซ้ำเรียกว่าเทคนิคแทรมโพลีนซึ่งใช้กันอย่างแพร่หลายใน Java devs

อย่างไรก็ตามสำหรับ C # มีวิธีใช้ตัวช่วยเล็กน้อยที่นี่ซึ่งจะเปลี่ยนฟังก์ชั่นวนซ้ำเป็นซ้ำโดยไม่ต้องเปลี่ยนลอจิกหรือทำให้โค้ดไม่สามารถเข้าใจได้ C # เป็นภาษาที่ดีที่มีความเป็นไปได้

มันทำงานได้โดยการพันส่วนของวิธีด้วยวิธีการช่วยเหลือ ตัวอย่างเช่นฟังก์ชันเวียนเกิดต่อไปนี้:

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

กลายเป็น:

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

คิดถึงสิ่งต่าง ๆ ที่ต้องการกองซ้อน:

หากเราพิจารณารูปแบบของการเรียกซ้ำเป็น:

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
}

ตัวอย่างเช่นหอคอยคลาสสิคแห่งฮานอย

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
}

สิ่งนี้สามารถแปลเป็นลูปที่ทำงานบนสแต็กที่ชัดเจนโดยการเรียกคืนเป็น:

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

สำหรับหอคอยแห่งฮานอยสิ่งนี้จะกลายเป็น:

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

มีความยืดหยุ่นอย่างมากที่นี่เป็นวิธีกำหนดกองซ้อนของคุณ คุณสามารถทำให้สแต็กของคุณเป็นรายการของCommandวัตถุที่ทำสิ่งที่ซับซ้อน หรือคุณสามารถไปในทิศทางตรงกันข้ามและทำให้เป็นรายการประเภทที่ง่ายกว่า (เช่น "งาน" อาจเป็นองค์ประกอบ 4 รายการในสแต็กของintแทนที่จะเป็นองค์ประกอบหนึ่งรายการในสแต็กของTask )

ความหมายทั้งหมดนี้คือหน่วยความจำสำหรับสแต็กอยู่ในฮีปมากกว่าในจาวาการเรียกใช้จาวา แต่จะมีประโยชน์ในการที่คุณสามารถควบคุมมันได้มากขึ้น


3

รูปแบบหนึ่งที่จะมองหาคือการเรียกซ้ำที่ท้ายฟังก์ชั่น (เรียกว่าการเรียกซ้ำแบบหาง) สิ่งนี้สามารถถูกแทนที่ได้อย่างง่ายดายในขณะที่ ตัวอย่างเช่นฟังก์ชั่น foo:

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

จบลงด้วยการโทรไปฝู สิ่งนี้สามารถแทนที่ด้วย:

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

ซึ่งกำจัดการโทรซ้ำที่สอง


3
ยังคงมีลักษณะเวียนเกิดให้ฉัน ... :)
นาธาน

2
เอ่อใช่ - แต่มันครึ่งเท่าแบบเรียกซ้ำได้ การกำจัดการเรียกซ้ำตัวอื่น ๆ นั้นจำเป็นต้องใช้เทคนิคอื่น ...
Mark Bessey

2

คำถามที่ได้รับการปิดเป็นซ้ำกับคนนี้มีโครงสร้างข้อมูลที่เฉพาะเจาะจงมาก:

ป้อนคำอธิบายรูปภาพที่นี่

โหนดมีโครงสร้างต่อไปนี้:

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

ฟังก์ชั่นการลบแบบเรียกซ้ำดูเหมือนว่า:

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

โดยทั่วไปแล้วมันเป็นไปไม่ได้เสมอที่จะหลีกเลี่ยงสแต็กสำหรับฟังก์ชั่นวนซ้ำที่เรียกใช้ตัวเองมากกว่าหนึ่งครั้ง (หรือแม้แต่ครั้งเดียว) อย่างไรก็ตามสำหรับโครงสร้างเฉพาะนี้เป็นไปได้ แนวคิดคือการทำให้โหนดทั้งหมดแบนลงในรายการเดียว สิ่งนี้สามารถทำได้โดยการใส่โหนดปัจจุบันchildที่ส่วนท้ายของรายการแถวบน

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

เทคนิคนี้สามารถนำไปใช้กับโครงสร้างการเชื่อมโยงข้อมูลใด ๆ ที่สามารถลดลงไปเป็น DAG ด้วยการจัดลำดับทอพอโลยีแบบกำหนดขึ้นได้ โหนดโหนดปัจจุบันถูกจัดเรียงใหม่เพื่อให้ลูกล่าสุดใช้ลูกอื่นทั้งหมด จากนั้นโหนดปัจจุบันสามารถลบได้และการแวะผ่านนั้นสามารถย้ำกับลูกที่เหลือ


1

การเรียกซ้ำไม่มีอะไรนอกจากกระบวนการเรียกใช้ฟังก์ชันหนึ่งจากอีกฟังก์ชันหนึ่งเท่านั้นกระบวนการนี้ทำได้โดยการเรียกใช้ฟังก์ชันด้วยตัวเอง อย่างที่เราทราบเมื่อฟังก์ชั่นหนึ่งเรียกใช้ฟังก์ชั่นอื่นฟังก์ชั่นแรกจะบันทึกสถานะของมัน (ตัวแปรของมัน) แล้วส่งผ่านการควบคุมไปยังฟังก์ชั่นที่เรียกว่า ฟังก์ชั่นที่เรียกว่าสามารถเรียกได้โดยใช้ชื่อเดียวกันของตัวแปรเช่น fun1 (a) สามารถเรียก fun2 (a) เมื่อเราโทรซ้ำแล้วก็ไม่มีอะไรใหม่เกิดขึ้น ฟังก์ชั่นหนึ่งเรียกตัวเองโดยผ่านประเภทเดียวกันและคล้ายกันในตัวแปรชื่อ (แต่เห็นได้ชัดว่าค่าที่เก็บไว้ในตัวแปรจะแตกต่างกันเพียงชื่อยังคงเหมือนกัน) เพื่อตัวเอง แต่ก่อนการโทรทุกครั้งฟังก์ชั่นจะบันทึกสถานะและขั้นตอนการบันทึกนี้จะดำเนินต่อไป การประหยัดทำได้สำเร็จ

ตอนนี้สแต็คมาเล่น

ดังนั้นถ้าคุณเขียนโปรแกรมวนซ้ำและบันทึกสถานะลงบนสแต็กแต่ละครั้งจากนั้นป๊อปอัพค่าจากสแต็กเมื่อจำเป็นคุณได้แปลงโปรแกรมแบบเรียกซ้ำให้เป็นตัววนซ้ำได้สำเร็จ!

พิสูจน์ได้ง่ายและวิเคราะห์

ในการเรียกใช้ซ้ำคอมพิวเตอร์จะรักษาสแต็กและในเวอร์ชันที่ซ้ำกันคุณจะต้องดูแลสแต็กด้วยตนเอง

ลองคิดดูสิเพียงแค่แปลงความลึกในการค้นหาครั้งแรก (บนกราฟ) โปรแกรมเรียกซ้ำเป็นโปรแกรมวนซ้ำ dfs

ดีที่สุด!


1

อีกตัวอย่างที่ง่ายและสมบูรณ์ของการเปลี่ยนฟังก์ชั่นวนซ้ำเป็นหนึ่งซ้ำโดยใช้สแต็ก

#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

คำอธิบายคร่าวๆเกี่ยวกับวิธีที่ระบบใช้ฟังก์ชันแบบเรียกซ้ำและดำเนินการโดยใช้สแต็ก:

สิ่งนี้มีวัตถุประสงค์เพื่อแสดงความคิดโดยไม่มีรายละเอียด พิจารณาฟังก์ชั่นนี้ที่จะพิมพ์ปมของกราฟ:

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

ตัวอย่างกราฟ: A-> B A-> C show (A) จะพิมพ์ B, A, C

การเรียกใช้ฟังก์ชั่นหมายถึงบันทึกสถานะท้องถิ่นและจุดต่อเนื่องเพื่อให้คุณสามารถกลับมาแล้วข้ามฟังก์ชั่นที่คุณต้องการโทร

ตัวอย่างเช่นสมมติว่า show (A) เริ่มทำงาน การเรียกใช้ฟังก์ชันบนบรรทัดที่ 3 แสดง (B) หมายถึง - เพิ่มรายการลงในสแต็กความหมาย "คุณจะต้องดำเนินการต่อที่บรรทัดที่ 2 พร้อมโหนดสถานะตัวแปรท้องถิ่น = A" - ไปที่บรรทัด 0 พร้อมโหนด = B

เพื่อรันโค้ดระบบจะทำงานตามคำแนะนำ เมื่อพบการเรียกใช้ฟังก์ชันระบบจะส่งข้อมูลที่ต้องการกลับไปยังตำแหน่งเดิมเรียกใช้รหัสฟังก์ชันและเมื่อฟังก์ชันเสร็จสมบูรณ์จะปรากฏข้อมูลที่ต้องการเพื่อดำเนินการต่อ


0

ลิงก์นี้ให้คำอธิบายและเสนอแนวคิดในการรักษา "ตำแหน่ง" เพื่อให้สามารถไปยังตำแหน่งที่แน่นอนระหว่างการโทรซ้ำหลายครั้ง:

อย่างไรก็ตามตัวอย่างเหล่านี้ทั้งหมดอธิบายสถานการณ์ที่มีการโทรซ้ำแบบเรียกซ้ำเป็นจำนวนคงที่ สิ่งต่าง ๆ มีความซับซ้อนมากขึ้นเมื่อคุณมีสิ่งที่ชอบ:

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

0

มีวิธีทั่วไปในการแปลงเส้นทางวนซ้ำแบบวนซ้ำเป็นตัววนซ้ำโดยใช้ตัววนซ้ำแบบขี้เกียจซึ่งเชื่อมต่อซัพพลายเออร์ตัววนซ้ำหลายตัวเข้าด้วยกัน (การแสดงออกแลมบ์ดาที่ส่งคืนตัววนซ้ำ) ดูของฉันแปลงซ้ำข้ามผ่านไป Iterator


0

ตัวอย่างของฉันอยู่ใน Clojure แต่น่าจะง่ายต่อการแปลเป็นภาษาใด ๆ

รับฟังก์ชั่นนี้ว่าStackOverflows สำหรับค่าขนาดใหญ่ของ n:

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

เราสามารถกำหนดเวอร์ชันที่ใช้สแต็กของตัวเองในลักษณะดังต่อไปนี้:

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

ที่returnถูกกำหนดเป็น:

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

มันใช้งานได้กับฟังก์ชั่นที่ซับซ้อนมากขึ้นเช่นกันเช่นฟังก์ชั่น ackermann :

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

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

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

สามารถเปลี่ยนเป็น:

(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)))))
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.