การเรียกซ้ำคืออะไรและฉันควรใช้เมื่อใด


121

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

ดังนั้นคำถามคือ:

  1. การเรียกซ้ำคืออะไร?
  2. ฉันจะใช้การเรียกซ้ำเมื่อใด
  3. ทำไมคนไม่ใช้การเรียกซ้ำ?

9
และอาจจะช่วยได้: stackoverflow.com/questions/126756/…
kennytm

3
สิ่งนี้อาจช่วยให้เข้าใจแนวคิด: ไปที่ลิงก์ที่ให้ไว้ในความคิดเห็นที่สองของคำถามในหน้านี้และทำในสิ่งที่ความคิดเห็นบอกว่าให้ทำ: stackoverflow.com/questions/3021/…
dtmland

คำตอบ:


86

มีคำอธิบายที่ดีมากมายเกี่ยวกับการเรียกซ้ำในเธรดนี้คำตอบนี้เกี่ยวกับสาเหตุที่คุณไม่ควรใช้ในภาษาส่วนใหญ่ , Ruby, Java และ C #) การวนซ้ำเป็นที่นิยมอย่างมากในการเรียกซ้ำ

หากต้องการดูสาเหตุให้ทำตามขั้นตอนที่ภาษาข้างต้นใช้เพื่อเรียกฟังก์ชัน:

  1. ช่องว่างถูกแกะออกบนสแต็กสำหรับอาร์กิวเมนต์ของฟังก์ชันและตัวแปรโลคัล
  2. อาร์กิวเมนต์ของฟังก์ชันจะถูกคัดลอกไปยังช่องว่างใหม่นี้
  3. ควบคุมข้ามไปที่ฟังก์ชัน
  4. โค้ดของฟังก์ชันทำงาน
  5. ผลลัพธ์ของฟังก์ชันจะถูกคัดลอกไปยังค่าที่ส่งคืน
  6. สแต็กจะย้อนกลับไปยังตำแหน่งก่อนหน้า
  7. การควบคุมจะกระโดดกลับไปยังตำแหน่งที่เรียกใช้ฟังก์ชัน

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

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

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

* ฉันชอบการเรียกซ้ำ ภาษาคงที่ที่ฉันชอบไม่ใช้ลูปเลยการเรียกซ้ำเป็นวิธีเดียวที่จะทำอะไรซ้ำ ๆ ฉันไม่คิดว่าการเรียกซ้ำโดยทั่วไปเป็นความคิดที่ดีในภาษาที่ไม่ได้รับการปรับแต่ง

** อย่างไรก็ตาม Mario ชื่อทั่วไปสำหรับฟังก์ชัน ArrangeString ของคุณคือ "เข้าร่วม" และฉันจะแปลกใจถ้าภาษาที่คุณเลือกยังไม่มีการใช้งาน


1
ดีที่จะเห็นคำอธิบายของค่าใช้จ่ายในการเรียกซ้ำโดยธรรมชาติ ฉันได้สัมผัสสิ่งนั้นในคำตอบของฉันเช่นกัน แต่สำหรับฉันจุดแข็งที่ยิ่งใหญ่ในการเรียกซ้ำคือสิ่งที่คุณสามารถทำได้กับ call stack คุณสามารถเขียนอัลกอริทึมแบบรวบรัดโดยมีการเรียกซ้ำซึ่งแตกกิ่งก้านซ้ำ ๆ ทำให้คุณทำสิ่งต่างๆเช่นลำดับชั้นการรวบรวมข้อมูล (ความสัมพันธ์แม่ / ลูก) ได้อย่างง่ายดาย ดูคำตอบของฉันสำหรับตัวอย่าง
Steve Wortham

7
ผิดหวังมากที่พบคำตอบยอดนิยมสำหรับคำถามที่ชื่อว่า "การเรียกซ้ำคืออะไรและฉันควรใช้เมื่อใด" ไม่ได้ตอบข้อใดข้อหนึ่งอย่างแท้จริงอย่ารังเกียจคำเตือนที่มีอคติอย่างยิ่งต่อการเรียกซ้ำแม้ว่าจะมีการใช้งานอย่างแพร่หลายในภาษาส่วนใหญ่ที่คุณกล่าวถึง (ไม่มีอะไรผิดปกติเกี่ยวกับสิ่งที่คุณพูด แต่ดูเหมือนว่าคุณจะพูดเกินจริงกับปัญหาและแสดงความเข้าใจน้อยเกินไป ประโยชน์).
Bernhard Barker

2
คุณอาจจะใช่ @Dukeling สำหรับบริบทเมื่อฉันเขียนคำตอบนี้มีคำอธิบายที่ดีมากมายเกี่ยวกับการเรียกซ้ำที่เขียนไว้แล้วและฉันเขียนสิ่งนี้โดยตั้งใจให้เป็นส่วนเสริมของข้อมูลนั้นไม่ใช่คำตอบอันดับต้น ๆ ในทางปฏิบัติเมื่อฉันต้องเดินบนต้นไม้หรือจัดการโครงสร้างข้อมูลที่ซ้อนกันอื่น ๆ ฉันมักจะหันไปใช้การเรียกซ้ำและฉันยังไม่เจอสแต็กล้นจากการสร้างของตัวเองในป่า
Peter Burns

63

ตัวอย่างภาษาอังกฤษง่ายๆของการเรียกซ้ำ

A child couldn't sleep, so her mother told her a story about a little frog,
    who couldn't sleep, so the frog's mother told her a story about a little bear,
         who couldn't sleep, so the bear's mother told her a story about a little weasel... 
            who fell asleep.
         ...and the little bear fell asleep;
    ...and the little frog fell asleep;
...and the child fell asleep.

1
ขึ้น + เพื่อสัมผัสหัวใจ :)
Suhail Mumtaz Awan

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

49

ในความหมายพื้นฐานทางวิทยาศาสตร์คอมพิวเตอร์การเรียกซ้ำเป็นฟังก์ชันที่เรียกตัวเองว่า สมมติว่าคุณมีโครงสร้างรายการที่เชื่อมโยง:

struct Node {
    Node* next;
};

และคุณต้องการทราบว่ารายการที่เชื่อมโยงนั้นคุณสามารถทำได้ด้วยการเรียกซ้ำ:

int length(const Node* list) {
    if (!list->next) {
        return 1;
    } else {
        return 1 + length(list->next);
    }
}

(แน่นอนว่าสิ่งนี้สามารถทำได้ด้วย for loop เช่นกัน แต่มีประโยชน์ในการเป็นภาพประกอบของแนวคิด)


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

2
คุณต้องการคำสั่งอื่นที่นี่จริงๆหรือ?
Adrien

1
ไม่ได้มีไว้เพื่อความชัดเจนเท่านั้น
Andreas Brinck

@SteveWortham: นี่ไม่ใช่หางซ้ำตามที่เขียนไว้ length(list->next)ยังคงต้องกลับไปlength(list)เพื่อให้หลังสามารถเพิ่ม 1 ในผลลัพธ์ได้ หากเขียนเพื่อส่งผ่านความยาวจนถึงตอนนี้เราก็ลืมได้แล้วว่ามีผู้โทรอยู่ ชอบint length(const Node* list, int count=0) { return (!list) ? count : length(list->next, count + 1); }.
cHao

46

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

ตัวอย่างที่ง่ายที่สุดคือการเรียกซ้ำหางโดยที่บรรทัดสุดท้ายของฟังก์ชันเป็นการเรียกตัวเอง:

int FloorByTen(int num)
{
    if (num % 10 == 0)
        return num;
    else
        return FloorByTen(num-1);
}

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

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

ใส่คำอธิบายภาพที่นี่

คุณสามารถวาดหนึ่งในสิ่งเหล่านี้ได้อย่างง่ายดายด้วยการเรียกซ้ำโดยที่ call stack แยกออกเป็น 3 ทิศทาง:

private void BuildVertices(double x, double y, double len)
{
    if (len > 0.002)
    {
        mesh.Positions.Add(new Point3D(x, y + len, -len));
        mesh.Positions.Add(new Point3D(x - len, y - len, -len));
        mesh.Positions.Add(new Point3D(x + len, y - len, -len));
        len *= 0.5;
        BuildVertices(x, y + len, len);
        BuildVertices(x - len, y - len, len);
        BuildVertices(x + len, y - len, len);
    }
}

หากคุณพยายามทำสิ่งเดียวกันกับการทำซ้ำฉันคิดว่าคุณจะพบว่าต้องใช้รหัสมากกว่านี้มากในการทำให้สำเร็จ

กรณีการใช้งานทั่วไปอื่น ๆ อาจรวมถึงลำดับชั้นการข้ามผ่านเช่นโปรแกรมรวบรวมข้อมูลเว็บไซต์การเปรียบเทียบไดเร็กทอรีเป็นต้น

ข้อสรุป

ในทางปฏิบัติการเรียกซ้ำมีความหมายมากที่สุดเมื่อใดก็ตามที่คุณต้องการการแตกแขนงซ้ำ ๆ


27

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

ตัวอย่างที่ยอมรับคือรูทีนในการสร้างแฟกทอเรียลของ n แฟกทอเรียลของ n คำนวณโดยการคูณตัวเลขทั้งหมดระหว่าง 1 ถึง n วิธีแก้ซ้ำใน C # มีลักษณะดังนี้:

public int Fact(int n)
{
  int fact = 1;

  for( int i = 2; i <= n; i++)
  {
    fact = fact * i;
  }

  return fact;
}

ไม่มีอะไรน่าแปลกใจเกี่ยวกับการแก้ปัญหาซ้ำและควรมีเหตุผลสำหรับทุกคนที่คุ้นเคยกับ C #

โซลูชันแบบวนซ้ำพบได้โดยการรับรู้ว่าแฟกทอเรียลที่ n คือ n * Fact (n-1) หรือจะพูดอีกอย่างถ้าคุณรู้ว่าตัวเลขแฟกทอเรียลคืออะไรคุณสามารถคำนวณตัวเลขถัดไปได้ นี่คือโซลูชันแบบวนซ้ำใน C #:

public int FactRec(int n)
{
  if( n < 2 )
  {
    return 1;
  }

  return n * FactRec( n - 1 );
}

ส่วนแรกของฟังก์ชันนี้เรียกว่าBase Case (หรือบางครั้ง Guard Clause) และเป็นสิ่งที่ป้องกันไม่ให้อัลกอริทึมทำงานตลอดไป เพียงส่งคืนค่า 1 เมื่อใดก็ตามที่ฟังก์ชันถูกเรียกด้วยค่า 1 หรือน้อยกว่า ส่วนที่สองเป็นที่น่าสนใจมากขึ้นและเป็นที่รู้จักกันเป็นขั้นตอนซ้ำ ที่นี่เราเรียกวิธีการเดียวกันกับพารามิเตอร์ที่แก้ไขเล็กน้อย (เราลดลงด้วย 1) แล้วคูณผลลัพธ์ด้วยสำเนาของ n

เมื่อพบครั้งแรกสิ่งนี้อาจทำให้เกิดความสับสนดังนั้นจึงควรให้คำแนะนำในการตรวจสอบว่ามันทำงานอย่างไรเมื่อเรียกใช้ ลองนึกภาพว่าเราเรียก FactRec (5) เราเข้าสู่กิจวัตรไม่ได้รับกรณีฐานดังนั้นเราจึงลงเอยเช่นนี้:

// In FactRec(5)
return 5 * FactRec( 5 - 1 );

// which is
return 5 * FactRec(4);

หากเราป้อนเมธอดอีกครั้งด้วยพารามิเตอร์ 4 เราไม่ได้หยุดอีกครั้งโดยคำสั่งยามดังนั้นเราจึงจบลงที่:

// In FactRec(4)
return 4 * FactRec(3);

หากเราแทนค่าที่ส่งคืนนี้เป็นค่าส่งกลับด้านบนเราจะได้รับ

// In FactRec(5)
return 5 * (4 * FactRec(3));

สิ่งนี้ควรให้เบาะแสว่าวิธีการแก้ปัญหาขั้นสุดท้ายมาถึงอย่างไรดังนั้นเราจะติดตามอย่างรวดเร็วและแสดงแต่ละขั้นตอนในการลง:

return 5 * (4 * FactRec(3));
return 5 * (4 * (3 * FactRec(2)));
return 5 * (4 * (3 * (2 * FactRec(1))));
return 5 * (4 * (3 * (2 * (1))));

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

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


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

1
@SteveWortham: นี่ไม่ใช่การเรียกซ้ำหาง ในขั้นตอนการเรียกซ้ำผลลัพธ์ของFactRec()ต้องคูณด้วยnก่อนที่จะกลับมา
rvighne

12

การเรียกซ้ำเป็นการแก้ปัญหาด้วยฟังก์ชันที่เรียกตัวเอง ตัวอย่างที่ดีคือฟังก์ชันแฟกทอเรียล แฟกทอเรียลเป็นปัญหาทางคณิตศาสตร์ที่แฟกทอเรียลของ 5 เช่น 5 * 4 * 3 * 2 * 1 ฟังก์ชันนี้จะแก้ปัญหานี้ใน C # สำหรับจำนวนเต็มบวก (ไม่ได้ทดสอบ - อาจมีข้อผิดพลาด)

public int Factorial(int n)
{
    if (n <= 1)
        return 1;

    return n * Factorial(n - 1);
}

9

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

ยกตัวอย่างเช่นในการคำนวณปัจจัยสำหรับจำนวนหนึ่งสามารถแสดงเป็นX X times the factorial of X-1ดังนั้นวิธีการ "เรียกซ้ำ" เพื่อค้นหาแฟกทอเรียลของX-1แล้วคูณสิ่งที่ได้มาXเพื่อให้คำตอบสุดท้าย แน่นอนในการหาแฟกทอเรียลของX-1มันก่อนอื่นมันจะคำนวณแฟกทอเรียลของX-2และอื่น ๆ กรณีฐานจะเป็นตอนที่Xเป็น 0 หรือ 1 ในกรณีที่มันรู้ว่าจะกลับมาตั้งแต่10! = 1! = 1


1
ฉันคิดว่าสิ่งที่คุณอ้างถึงไม่ใช่การเรียกซ้ำ แต่เป็น <a href=" en.wikipedia.org/wiki/…และหลักการออกแบบอัลกอริทึม Conquer</a> ดูตัวอย่างได้ที่ <a href = " en.wikipedia org / wiki / Ackermann_function ">ฟังก์ชันAckermans </a>
Gabriel Ščerbák

2
ไม่ฉันไม่ได้หมายถึงคพ. คพ. บอกเป็นนัยว่ามีปัญหาย่อย 2 ปัญหาขึ้นไปการเรียกซ้ำด้วยตัวเองไม่ได้ (ตัวอย่างเช่นตัวอย่างแฟกทอเรียลที่ให้ไว้ที่นี่ไม่ใช่ D&C แต่เป็นเชิงเส้นโดยสมบูรณ์) D&C เป็นส่วนหนึ่งของการเรียกซ้ำ
แอมเบอร์

3
อ้างจากบทความที่คุณเชื่อมโยง: "อัลกอริทึมการแบ่งและพิชิตทำงานโดยการแบ่งปัญหาซ้ำ ๆออกเป็นสองปัญหาย่อยที่เป็นประเภทเดียวกัน (หรือเกี่ยวข้อง)"
แอมเบอร์

ฉันไม่คิดว่านั่นเป็นคำอธิบายที่ดีเนื่องจากการพูดซ้ำ ๆ อย่างเคร่งครัดไม่จำเป็นต้องแก้ปัญหาเลย คุณสามารถเรียกตัวเองว่า (และล้น)
UK-AL

ฉันใช้คำอธิบายของคุณในบทความที่ฉันเขียนสำหรับ PHP Master แม้ว่าฉันจะไม่สามารถอ้างถึงคุณได้ หวังว่าคุณจะไม่รังเกียจ
หนาวจัด

9

พิจารณาปัญหาเก่าที่รู้จักกันดี :

ในทางคณิตศาสตร์ตัวหารร่วมที่ยิ่งใหญ่ที่สุด (gcd) …ของจำนวนเต็มที่ไม่ใช่ศูนย์ตั้งแต่สองตัวขึ้นไปเป็นจำนวนเต็มบวกที่ใหญ่ที่สุดที่หารจำนวนโดยไม่เหลือเศษ

คำจำกัดความของ gcd นั้นง่ายอย่างน่าประหลาดใจ:

นิยาม gcd

โดยที่ mod เป็นตัวดำเนินการโมดูโล (นั่นคือส่วนที่เหลือหลังจากการหารจำนวนเต็ม)

ในภาษาอังกฤษคำนิยามนี้กล่าวว่าตัวหารร่วมมากของจำนวนใด ๆ และเป็นศูนย์เป็นจำนวนนั้นและตัวหารร่วมมากของตัวเลขสองmและnเป็นตัวหารร่วมมากของnและส่วนที่เหลือหลังจากการหารเมตรโดยn

หากคุณต้องการที่จะรู้ว่าทำไมงานนี้ให้ดูที่บทความวิกิพีเดียในขั้นตอนวิธี Euclidean

ลองคำนวณ gcd (10, 8) เป็นตัวอย่าง แต่ละขั้นตอนจะเท่ากับขั้นตอนก่อนหน้า:

  1. gcd (10, 8)
  2. gcd (10, 10 สมัย 8)
  3. gcd (8, 2)
  4. gcd (8, 8 สมัย 2)
  5. gcd (2, 0)
  6. 2

ในขั้นตอนแรก 8 ไม่เท่ากับศูนย์ดังนั้นส่วนที่สองของคำจำกัดความจะใช้ 10 mod 8 = 2 เพราะ 8 ไปหาร 10 ครั้งเดียวโดยเหลือ 2 ในขั้นตอนที่ 3 ส่วนที่สองจะใช้อีกครั้ง แต่คราวนี้ 8 mod 2 = 0 เพราะ 2 หาร 8 โดยไม่มีส่วนที่เหลือ ในขั้นตอนที่ 5 อาร์กิวเมนต์ที่สองคือ 0 ดังนั้นคำตอบคือ 2

คุณสังเกตเห็นว่า gcd ปรากฏทั้งด้านซ้ายและด้านขวาของเครื่องหมายเท่ากับหรือไม่? นักคณิตศาสตร์จะบอกว่าคำจำกัดความนี้เป็นแบบวนซ้ำเนื่องจากนิพจน์ที่คุณกำหนดให้เกิดซ้ำภายในนิยามของมัน

คำจำกัดความแบบวนซ้ำมักจะสวยหรู ตัวอย่างเช่นคำจำกัดความแบบวนซ้ำสำหรับผลรวมของรายการคือ

sum l =
    if empty(l)
        return 0
    else
        return head(l) + sum(tail(l))

headองค์ประกอบแรกในรายการอยู่ที่ไหนและtailเป็นส่วนที่เหลือของรายการ โปรดทราบว่าsumเกิดซ้ำภายในคำจำกัดความในตอนท้าย

บางทีคุณอาจต้องการค่าสูงสุดในรายการแทน:

max l =
    if empty(l)
        error
    elsif length(l) = 1
        return head(l)
    else
        tailmax = max(tail(l))
        if head(l) > tailmax
            return head(l)
        else
            return tailmax

คุณอาจกำหนดการคูณของจำนวนเต็มที่ไม่เป็นลบซ้ำ ๆ เพื่อเปลี่ยนเป็นชุดของการเพิ่ม:

a * b =
    if b = 0
        return 0
    else
        return a + (a * (b - 1))

หากการแปลงการคูณเป็นชุดของการเพิ่มนั้นไม่สมเหตุสมผลให้ลองขยายตัวอย่างง่ายๆเพื่อดูว่ามันทำงานอย่างไร

การเรียงลำดับผสานมีคำจำกัดความแบบวนซ้ำที่น่ารัก:

sort(l) =
    if empty(l) or length(l) = 1
        return l
    else
        (left,right) = split l
        return merge(sort(left), sort(right))

คำจำกัดความแบบวนซ้ำอยู่รอบตัวหากคุณรู้ว่าจะหาอะไร สังเกตว่าคำจำกัดความเหล่านี้ทั้งหมดมีกรณีฐานอย่างง่ายเช่น gcd (m, 0) = m กรณีที่เกิดซ้ำจะลดลงที่ปัญหาเพื่อหาคำตอบที่ง่าย

ด้วยความเข้าใจนี้คุณสามารถชื่นชมอัลกอริทึมอื่น ๆ ในบทความของ Wikipedia เรื่องการเรียกซ้ำได้แล้ว !


8
  1. ฟังก์ชันที่เรียกตัวเอง
  2. เมื่อฟังก์ชันสามารถย่อยสลาย (อย่างง่ายดาย) เป็นการดำเนินการอย่างง่ายบวกกับฟังก์ชันเดียวกันในส่วนเล็ก ๆ ของปัญหา ฉันควรจะบอกว่านี่ทำให้เป็นตัวเลือกที่ดีสำหรับการเรียกซ้ำ
  3. พวกเขาทำ!

ตัวอย่างที่ยอมรับคือแฟกทอเรียลซึ่งมีลักษณะดังนี้:

int fact(int a) 
{
  if(a==1)
    return 1;

  return a*fact(a-1);
}

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


6

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

function cmdCheckAllClick {
    checkRecursively(TreeView1.RootNode);
}

function checkRecursively(Node n) {
    n.Checked = True;
    foreach ( n.Children as child ) {
        checkRecursively(child);
    }
}

ดังนั้นคุณจะเห็นว่า checkRecursively ก่อนอื่นจะตรวจสอบโหนดที่ถูกส่งผ่านจากนั้นเรียกตัวเองสำหรับแต่ละโหนดของโหนดลูกนั้น

คุณต้องระมัดระวังในการเรียกซ้ำ หากคุณเข้าสู่การวนซ้ำแบบไม่สิ้นสุดคุณจะได้รับข้อยกเว้น Stack Overflow :)

ฉันคิดไม่ออกว่าทำไมคนเราถึงไม่ควรใช้มันเมื่อเหมาะสม มันมีประโยชน์ในบางสถานการณ์ไม่ใช่ในบางสถานการณ์

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


5

การเรียกซ้ำคือนิพจน์ที่อ้างถึงตัวเองโดยตรงหรือโดยอ้อม

พิจารณาคำย่อแบบเรียกซ้ำเป็นตัวอย่างง่ายๆ:

  • GNUย่อมาจากNot Unix ของ GNU
  • PHPย่อมาจากPHP: Hypertext Preprocessor
  • YAMLย่อมาจากYAML Ain't Markup Language
  • WINEย่อมาจากWine Is Not an Emulator
  • VISAย่อมาจากVisa International Service Association

ตัวอย่างเพิ่มเติมใน Wikipedia


4

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

ผู้คนหลีกเลี่ยงการเรียกซ้ำด้วยเหตุผลหลายประการ:

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

  2. พวกเราที่ตัดฟันการเขียนโปรแกรมของเราเกี่ยวกับการเขียนโปรแกรมเชิงขั้นตอนหรือเชิงวัตถุมักได้รับคำสั่งให้หลีกเลี่ยงการเรียกซ้ำเนื่องจากมีข้อผิดพลาดได้ง่าย

  3. เรามักจะบอกว่าการเรียกซ้ำช้า การโทรเข้าและกลับจากกิจวัตรซ้ำ ๆ เกี่ยวข้องกับการผลักและการป๊อปสแต็กจำนวนมากซึ่งช้ากว่าการวนซ้ำ ฉันคิดว่าภาษาบางภาษาจัดการเรื่องนี้ได้ดีกว่าภาษาอื่น ๆ และภาษาเหล่านั้นมักจะไม่ใช่ภาษาที่กระบวนทัศน์ที่โดดเด่นเป็นขั้นตอนหรือเชิงวัตถุ

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


4

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

ตัวอย่างเช่นใช้แฟกทอเรียล:

factorial(6) = 6*5*4*3*2*1

แต่มันง่ายที่จะเห็นแฟกทอเรียล (6) ก็คือ:

6 * factorial(5) = 6*(5*4*3*2*1).

โดยทั่วไป:

factorial(n) = n*factorial(n-1)

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

ในตัวอย่างนี้เราสร้างกรณีพิเศษโดยกำหนดแฟกทอเรียล (1) = 1

ตอนนี้เราเห็นแล้วจากล่างขึ้นบน:

factorial(6) = 6*factorial(5)
                   = 6*5*factorial(4)
                   = 6*5*4*factorial(3) = 6*5*4*3*factorial(2) = 6*5*4*3*2*factorial(1) = 6*5*4*3*2*1

เนื่องจากเรากำหนดแฟกทอเรียล (1) = 1 เราจึงมาถึง "ด้านล่าง"

โดยทั่วไปขั้นตอนการเรียกซ้ำมีสองส่วน:

1) ส่วนที่เรียกซ้ำซึ่งกำหนดขั้นตอนบางอย่างในแง่ของอินพุตใหม่รวมกับสิ่งที่คุณ "ทำไปแล้ว" ผ่านขั้นตอนเดียวกัน (กล่าวคือfactorial(n) = n*factorial(n-1))

2) ส่วนฐานซึ่งทำให้แน่ใจว่ากระบวนการจะไม่ทำซ้ำตลอดไปโดยให้จุดเริ่มต้น (เช่นfactorial(1) = 1)

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

หวังว่านี่จะช่วยได้ ...


4

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

ฉันยังชอบการอภิปรายของ Steve McConnells เกี่ยวกับการเรียกซ้ำใน Code Complete ซึ่งเขาวิจารณ์ตัวอย่างที่ใช้ในหนังสือวิทยาการคอมพิวเตอร์เรื่อง Recursion

อย่าใช้การเรียกซ้ำสำหรับแฟกทอเรียลหรือหมายเลขฟีโบนักชี

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

ฉันคิดว่านี่เป็นจุดที่น่าสนใจมากที่จะเพิ่มและอาจเป็นเหตุผลว่าทำไมการเรียกซ้ำมักจะเข้าใจผิด

แก้ไข: นี่ไม่ใช่คำตอบของ Dav - ฉันไม่เห็นคำตอบนั้นเมื่อโพสต์สิ่งนี้


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

ฉันเห็นด้วย - ฉันเพิ่งพบเมื่อฉันอ่านหนังสือเล่มนี้ว่ามันเป็นจุดที่น่าสนใจที่จะยกขึ้นตรงกลางส่วนเกี่ยวกับการเรียกซ้ำ
Robben_Ford_Fan_boy

4

1. ) วิธีการเรียกซ้ำหากสามารถเรียกตัวเองได้ โดยตรง:

void f() {
   ... f() ... 
}

หรือทางอ้อม:

void f() {
    ... g() ...
}

void g() {
   ... f() ...
}

2. ) เมื่อใดควรใช้การเรียกซ้ำ

Q: Does using recursion usually make your code faster? 
A: No.
Q: Does using recursion usually use less memory? 
A: No.
Q: Then why use recursion? 
A: It sometimes makes your code much simpler!

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


สิ่งที่เกี่ยวกับการลดความซับซ้อนเมื่อแบ่งและพิชิตเกี่ยวกับความสมบูรณ์แบบ?
mfrachet

4

นี่คือตัวอย่างง่ายๆ: จำนวนองค์ประกอบในชุด (มีวิธีที่ดีกว่าในการนับสิ่งต่าง ๆ แต่นี่เป็นตัวอย่างการเรียกซ้ำง่ายๆที่ดี)

ขั้นแรกเราต้องมีกฎสองข้อ:

  1. หากชุดนั้นว่างเปล่าจำนวนรายการในชุดจะเป็นศูนย์ (duh!)
  2. หากชุดไม่ว่างเปล่าการนับจะเป็นหนึ่งบวกกับจำนวนรายการในชุดหลังจากนำออกหนึ่งรายการ

สมมติว่าคุณมีชุดดังนี้: [xxx] ลองนับดูว่ามีกี่รายการ

  1. ชุดคือ [xxx] ซึ่งไม่ว่างดังนั้นเราจึงใช้กฎข้อ 2 จำนวนรายการคือหนึ่งบวกจำนวนรายการใน [xx] (เช่นเราลบรายการออก)
  2. ชุดคือ [xx] ดังนั้นเราจึงใช้กฎ 2 อีกครั้ง: หนึ่ง + จำนวนรายการใน [x]
  3. ชุดคือ [x] ซึ่งยังคงตรงกับกฎ 2: หนึ่ง + จำนวนรายการใน []
  4. ตอนนี้เซ็ตคือ [] ซึ่งตรงกับกฎ 1: จำนวนนับเป็นศูนย์!
  5. ตอนนี้เรารู้คำตอบแล้วในขั้นตอนที่ 4 (0) เราสามารถแก้ขั้นตอนที่ 3 (1 + 0)
  6. ในทำนองเดียวกันตอนนี้เรารู้คำตอบในขั้นตอนที่ 3 (1) แล้วเราสามารถแก้ขั้นตอนที่ 2 (1 + 1) ได้
  7. และในที่สุดเมื่อเรารู้คำตอบแล้วในขั้นตอนที่ 2 (2) เราสามารถแก้ขั้นตอนที่ 1 (1 + 2) และรับจำนวนไอเทมใน [xxx] ซึ่งก็คือ 3 ไชโย!

เราสามารถแทนค่านี้เป็น:

count of [x x x] = 1 + count of [x x]
                 = 1 + (1 + count of [x])
                 = 1 + (1 + (1 + count of []))
                 = 1 + (1 + (1 + 0)))
                 = 1 + (1 + (1))
                 = 1 + (2)
                 = 3

เมื่อใช้โซลูชันแบบวนซ้ำคุณมักจะมีกฎอย่างน้อย 2 ข้อ:

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

หากเราแปลข้างต้นเป็นรหัสเทียมเราจะได้รับ:

numberOfItems(set)
    if set is empty
        return 0
    else
        remove 1 item from set
        return 1 + numberOfItems(set)

มีตัวอย่างที่เป็นประโยชน์มากมาย (เช่นการข้ามต้นไม้) ซึ่งฉันมั่นใจว่าคนอื่นจะพูดถึง


3

นั่นเป็นคำจำกัดความที่ดีทีเดียวที่คุณมี และวิกิพีเดียก็มีคำจำกัดความที่ดีเช่นกัน ดังนั้นฉันจะเพิ่มคำนิยามอื่น (อาจแย่กว่านั้น) ให้คุณ

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


3

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


2

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


2

เป็นภาษาอังกฤษธรรมดาสมมติว่าคุณทำได้ 3 อย่าง:

  1. ใช้แอปเปิ้ลหนึ่งลูก
  2. เขียนเครื่องหมายนับ
  3. นับเครื่องหมายนับ

คุณมีแอปเปิ้ลจำนวนมากอยู่ตรงหน้าคุณบนโต๊ะและคุณต้องการทราบว่ามีแอปเปิ้ลกี่ลูก

start
  Is the table empty?
  yes: Count the tally marks and cheer like it's your birthday!
  no:  Take 1 apple and put it aside
       Write down a tally mark
       goto start

กระบวนการทำซ้ำสิ่งเดียวกันจนเสร็จเรียกว่าการเรียกซ้ำ

ฉันหวังว่านี่คือคำตอบ "ภาษาอังกฤษธรรมดา" ที่คุณกำลังมองหา!


1
เดี๋ยวก่อนฉันมีเครื่องหมายนับจำนวนมากอยู่ตรงหน้าฉันบนโต๊ะและตอนนี้ฉันต้องการทราบว่ามีเครื่องหมายนับจำนวนเท่าใด ฉันสามารถใช้แอปเปิ้ลสำหรับสิ่งนี้ได้หรือไม่?
Christoffer Hammarström

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

2

ฟังก์ชันเรียกซ้ำคือฟังก์ชันที่มีการเรียกตัวเอง โครงสร้างแบบวนซ้ำคือโครงสร้างที่มีอินสแตนซ์ของตัวมันเอง คุณสามารถรวมทั้งสองเป็นคลาสซ้ำ ส่วนสำคัญของรายการเรียกซ้ำคือมันมีอินสแตนซ์ / การเรียกของตัวเอง

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

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


2

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

ฉันใช้ Java ในการทำงานและ Java ไม่รองรับฟังก์ชันซ้อนกัน ดังนั้นหากฉันต้องการทำการเรียกซ้ำฉันอาจต้องกำหนดฟังก์ชันภายนอก (ซึ่งมีอยู่เพียงเพราะโค้ดของฉันกระแทกกับกฎราชการของ Java) หรือฉันอาจต้อง refactor รหัสทั้งหมด (ซึ่งฉันไม่ชอบทำจริงๆ)

ดังนั้นฉันมักจะหลีกเลี่ยงการเรียกซ้ำและใช้การดำเนินการสแต็กแทนเนื่องจากการเรียกซ้ำนั้นโดยพื้นฐานแล้วเป็นการดำเนินการแบบสแต็ก


1

คุณต้องการใช้เมื่อใดก็ได้ที่คุณมีโครงสร้างต้นไม้ มีประโยชน์มากในการอ่าน XML


1

การเรียกซ้ำตามที่ใช้กับการเขียนโปรแกรมนั้นโดยพื้นฐานแล้วการเรียกใช้ฟังก์ชันจากภายในนิยามของตัวเอง (ภายในตัวมันเอง) โดยมีพารามิเตอร์ที่แตกต่างกันเพื่อให้งานสำเร็จ


1

"ถ้าฉันมีค้อนจงทำทุกอย่างให้เหมือนตะปู"

การเรียกซ้ำเป็นกลยุทธ์ในการแก้ปัญหาสำหรับปัญหาใหญ่โดยทุกขั้นตอนเพียงแค่ "เปลี่ยน 2 สิ่งเล็ก ๆ ให้กลายเป็นสิ่งที่ใหญ่กว่า" ทุกครั้งด้วยค้อนอันเดียวกัน

ตัวอย่าง

สมมติว่าโต๊ะของคุณเต็มไปด้วยกระดาษ 1024 แผ่นที่ไม่เป็นระเบียบ คุณจะทำกองกระดาษที่เป็นระเบียบและสะอาดจากระเบียบโดยใช้การเรียกซ้ำได้อย่างไร?

  1. แบ่ง:กระจายแผ่นงานทั้งหมดออกดังนั้นคุณจึงมีเพียงแผ่นเดียวในแต่ละ "กอง"
  2. พิชิต:
    1. ไปรอบ ๆ วางแต่ละแผ่นไว้ด้านบนของอีกแผ่นหนึ่ง ตอนนี้คุณมี 2 กอง
    2. เดินไปรอบ ๆ โดยวาง 2 กองซ้อนทับอีก 2 กอง ตอนนี้คุณมีกอง 4
    3. ไปรอบ ๆ โดยวาง 4 กองซ้อนทับอีก 4 กอง ตอนนี้คุณมีสแต็ค 8
    4. ... ซ้ำแล้วซ้ำอีก ...
    5. ตอนนี้คุณมีกองใหญ่ 1024 แผ่น!

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


6
คุณกำลังอธิบายถึงการแบ่งแยกและการพิชิต แม้ว่านี่จะเป็นตัวอย่างของการเรียกซ้ำ แต่ก็ไม่ได้เป็นเพียงวิธีเดียว
Konrad Rudolph

ไม่เป็นไร. ฉันไม่ได้พยายามจับ [โลกแห่งการเรียกซ้ำ] [1] ในประโยคที่นี่ ฉันต้องการคำอธิบายที่เข้าใจง่าย [1]: facebook.com/pages/Recursion-Fairy/269711978049
Andres Jaan Tack

1

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


1

เดี๋ยวก่อนขอโทษถ้าความคิดเห็นของฉันเห็นด้วยกับใครบางคนฉันแค่พยายามอธิบายการเรียกซ้ำเป็นภาษาอังกฤษธรรมดา

สมมติว่าคุณมีผู้จัดการสามคน - แจ็คจอห์นและมอร์แกน แจ็คจัดการโปรแกรมเมอร์ 2 คนคือจอห์น - 3 และมอร์แกน - 5. คุณจะให้ผู้จัดการทุกคน 300 $ และอยากรู้ว่ามันต้องเสียค่าอะไร คำตอบนั้นชัดเจน - แต่จะเกิดอะไรขึ้นถ้าพนักงานของมอร์แกน 2 คนเป็นผู้จัดการด้วย?

การเรียกซ้ำมาที่นี่ คุณเริ่มจากด้านบนสุดของลำดับชั้น ค่าใช้จ่ายในช่วงฤดูร้อนคือ 0 $ คุณเริ่มต้นด้วยแจ็คจากนั้นตรวจสอบว่าเขามีผู้จัดการเป็นพนักงานหรือไม่ หากคุณพบว่ามีผู้จัดการเป็นพนักงานหรือไม่และอื่น ๆ เพิ่ม 300 $ ให้กับค่าใช้จ่ายในช่วงฤดูร้อนทุกครั้งที่คุณพบผู้จัดการ เมื่อคุณคุยกับแจ็คเสร็จแล้วให้ไปหาจอห์นพนักงานของเขาแล้วไปที่มอร์แกน

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

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


1

ในภาษาอังกฤษธรรมดาการเรียกซ้ำหมายถึงการทำซ้ำซ้ำแล้วซ้ำเล่า

ในการเขียนโปรแกรมตัวอย่างหนึ่งคือการเรียกใช้ฟังก์ชันภายในตัวเอง

ดูตัวอย่างการคำนวณแฟกทอเรียลของตัวเลขต่อไปนี้:

public int fact(int n)
{
    if (n==0) return 1;
    else return n*fact(n-1)
}

1
ในภาษาอังกฤษธรรมดาการทำบางสิ่งซ้ำแล้วซ้ำอีกเรียกว่าการทำซ้ำ
toon81

1

อัลกอริทึมใด ๆ แสดงการเรียกซ้ำโครงสร้างบนประเภทข้อมูลหากโดยพื้นฐานแล้วประกอบด้วยคำสั่งสวิตช์พร้อมเคสสำหรับแต่ละกรณีของประเภทข้อมูล

ตัวอย่างเช่นเมื่อคุณทำงานกับประเภท

  tree = null 
       | leaf(value:integer) 
       | node(left: tree, right:tree)

อัลกอริธึมแบบวนซ้ำโครงสร้างจะมีรูปแบบ

 function computeSomething(x : tree) =
   if x is null: base case
   if x is leaf: do something with x.value
   if x is node: do something with x.left,
                 do something with x.right,
                 combine the results

นี่เป็นวิธีที่ชัดเจนที่สุดในการเขียนอัลกอริทึมใด ๆ ที่ทำงานกับโครงสร้างข้อมูล

ตอนนี้เมื่อคุณดูจำนวนเต็ม (ก็คือจำนวนธรรมชาติ) ตามที่กำหนดโดยใช้สัจพจน์ของ Peano

 integer = 0 | succ(integer)

คุณจะเห็นว่าอัลกอริธึมแบบวนซ้ำโครงสร้างบนจำนวนเต็มมีลักษณะเช่นนี้

 function computeSomething(x : integer) =
   if x is 0 : base case
   if x is succ(prev) : do something with prev

ฟังก์ชันแฟกทอเรียลที่รู้จักกันดีมากเกินไปเป็นตัวอย่างที่ไม่สำคัญที่สุดของแบบฟอร์มนี้


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