ตัวอย่างของ recursive code ที่ไม่มีเงื่อนไขการออก

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

มาเขียนโค้ดเพื่อคำนวณและแสดงตัวเลขเหล่านี้:


public class Fibonacci {
    public static void main(String[] args) {
        System.out.println(0);
        System.out.println(1);
        printFibonacci(0, 1);
    }

    private static void printFibonacci(long penultimate, long previous) {
        long current = penultimate + previous;
        System.out.println(current);
        printFibonacci(previous, current);
    }
}
    

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

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

จริงๆแล้วรหัสมีข้อผิดพลาดสองข้อ คุณสามารถดูได้หากคุณเรียกใช้รหัส

ข้อผิดพลาดแรกล้นประเภทยาว หมายเลข 104 ในลำดับของเราเป็นค่าลบ แสดงว่า ประเภท ยาวล้น

ข้อผิดพลาดที่สองแตกต่างกัน หลังจากคำนวณตัวเลขประมาณ 12,000 ตัวแล้ว เราจะได้:

ข้อยกเว้นในเธรด "หลัก" java.lang.StackOverflowError

ตอนนี้เป็นเวลาที่เหมาะสมในการเรียกคืนเมธอด call stack ใน Java เครื่อง Java เก็บบันทึกการเรียกใช้ฟังก์ชันทั้งหมด ในการทำเช่นนี้ จะใช้คอลเลกชันชนิดพิเศษที่เรียกว่าสแต็ค เมื่อฟังก์ชันหนึ่งเรียกใช้ฟังก์ชันอื่น เครื่อง Java จะพุช StackTraceElement ใหม่ไปยังสแต็ก เมื่อฟังก์ชันสิ้นสุดลง อิลิเมนต์นี้จะถูกลบออกจากสแต็ก ดังนั้น สแตกจะเก็บข้อมูลที่เป็นปัจจุบันเกี่ยวกับสถานะปัจจุบันของสแตกการเรียกใช้ฟังก์ชันเสมอ เอกสารประกอบสำหรับ StackTraceElement กล่าวว่า "โยนทิ้งเมื่อเกิดสแต็กล้นเนื่องจากแอปพลิเคชันเรียกซ้ำลึกเกินไป" JVM ที่รันอยู่มีพื้นที่หน่วยความจำพิเศษสำหรับจัดเก็บเมธอด call stack ขนาดของพื้นที่หน่วยความจำนี้ขึ้นอยู่กับการตั้งค่า OS และ JVM นอกจากเมธอด call stack แล้ว ตัวแปรดั้งเดิม (ค่าเฉพาะของพารามิเตอร์เมธอด) และแอดเดรสของตัวแปรอ้างอิง (ในพื้นที่หน่วยความจำที่เรียกว่า "ฮีป") จะถูกเก็บไว้ในพื้นที่หน่วยความจำพิเศษนี้ การเข้าถึง call stack เป็นไปตามหลักการ LIFO

ตัวอย่างที่แก้ไขด้วยเงื่อนไขการออก

เริ่มต้นด้วยการแก้ไขปัญหาที่สองในรหัส

มาลองแก้ปัญหากันแบบตัวต่อตัว: ถ้าสแต็กเล็กเกินไป มาทำให้มันใหญ่ขึ้นกันเถอะ ในการทำเช่นนี้ ให้เริ่มต้น JVM ด้วยแฟล็ก "-Xss" และระบุจำนวนหน่วยความจำที่จะจัดสรรสำหรับสแต็ก ลองจัดสรร 5 เมกะไบต์ นี่คือสิ่งที่จะมีลักษณะเหมือนใน IDEA:

เราจัดการเพื่อเพิ่มความยาวของเอาต์พุต ตอนนี้สามารถคำนวณลำดับได้มากกว่า 49,000 หมายเลขแทนที่จะถูกจำกัดไว้ที่ประมาณ 12,000 หมายเลข แต่ในบางจุด เรายังคงได้รับStackOverflowError

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

ลองเขียน เมธอด printFibonacciWithCondition ใหม่ ที่จะพิจารณาเงื่อนไขนี้ และเราจะเรียกวิธีการแก้ไขใหม่ในวิธีการหลัก


public class Fibonacci {
    public static void main(String[] args) {
        System.out.println(0);
        System.out.println(1);
//        printFibonacci(0, 1);
        printFibonacciWithCondition(0, 1);
    }

    private static void printFibonacci(long penultimate, long previous) {
        long current = penultimate + previous;
        System.out.println(current);
        printFibonacci(previous, current);
    }

    private static void printFibonacciWithCondition(long penultimate, long previous) {
        long current = penultimate + previous;
        if (current > Integer.MAX_VALUE) {
            return;
        }
        System.out.println(current);
        printFibonacciWithCondition(previous, current);
    }
}
    

หลังจากรันโค้ดแล้ว เราจะเห็นว่าเอาต์พุตลงท้ายด้วยหมายเลข 1836311903 ตัวเลขก่อนหน้าตัวเลขนี้คือ 1134903170 ผลรวมของตัวเลขเหล่านี้คือ 2_971_215_073 ซึ่งมากกว่า Integer.MAX_VALUE (2_147_483_647 )จริงๆ

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

การสืบเชื้อสายแบบเรียกซ้ำและการคลี่คลาย

มาวิเคราะห์ทีละขั้นตอนว่าโค้ดของเราทำงานอย่างไร ในการทำเช่นนี้ เราจะเพิ่ม เมธอด echoและเรียกเมธอดก่อนและหลังการเรียกซ้ำของเมธอดprintFibonacciWithCondition


public class Fibonacci {
    public static void main(String[] args) {
        System.out.println(0);
        System.out.println(1);
        printFibonacciWithCondition(0, 1);
    }

    private static void printFibonacciWithCondition(long penultimate, long previous) {
        long current = penultimate + previous;
        if (current > Integer.MAX_VALUE) {
            return;
        }
        echo(true, penultimate, previous);
        System.out.println(current);
        printFibonacciWithCondition(previous, current);
        echo(false, penultimate, previous);
    }

    private static void echo(boolean isBeforeRecursiveCall, long penultimate, long previous) {
        if (isBeforeRecursiveCall) {
            System.out.printf("Before method call with args: %d, %d. Current number = ", penultimate, previous);
        } else {
            System.out.printf("After method call with args: %d, %d\n", penultimate, previous);
        }
    }
}
    

โปรแกรมให้ผลลัพธ์นี้แก่เรา:

0
1
ก่อนการเรียกใช้เมธอดด้วย args: 0, 1. หมายเลขปัจจุบัน = 1
ก่อนการเรียกใช้เมธอดด้วย args: 1, 1. หมายเลขปัจจุบัน = 2
ก่อนการเรียกใช้เมธอดด้วย args: 1, 2. หมายเลขปัจจุบัน = 3
ก่อนการเรียกใช้เมธอดด้วย args: 2, 3. หมายเลขปัจจุบัน = 5
ก่อนการเรียกใช้เมธอดด้วย args: 3, 5. หมายเลขปัจจุบัน = 8
ก่อนการเรียกใช้เมธอดด้วย args: 5, 8. หมายเลขปัจจุบัน = 13
ก่อนการเรียกใช้เมธอดด้วย args: 8, 13. หมายเลขปัจจุบัน = 21
ก่อนการเรียกใช้เมธอดด้วย args: 13, 21. หมายเลขปัจจุบัน = 34
ก่อนการเรียกใช้เมธอดด้วย args: 21, 34. หมายเลขปัจจุบัน = 55
ก่อนการเรียกใช้เมธอดด้วย args: 34, 55. หมายเลขปัจจุบัน = 89
ก่อนการเรียกใช้เมธอดด้วย args: 55, 89. หมายเลขปัจจุบัน = 144
ก่อนการเรียกใช้เมธอดด้วย args: 89, 144 หมายเลขปัจจุบัน = 233
ก่อนการเรียกใช้เมธอดด้วย args: 144, 233 หมายเลขปัจจุบัน = 377
ก่อนการเรียกใช้เมธอดด้วย args: 233, 377 หมายเลขปัจจุบัน = 610
ก่อนการเรียกใช้เมธอดด้วย args: 377, 610. หมายเลขปัจจุบัน = 987
ก่อนการเรียกใช้เมธอดด้วย args: 610, 987 หมายเลขปัจจุบัน = 1597
ก่อนการเรียกใช้เมธอดด้วย args: 987, 1597 หมายเลขปัจจุบัน = 2584
ก่อนการเรียกใช้เมธอดด้วย args: 1597, 2584 หมายเลขปัจจุบัน = 4181
ก่อนเมธอด การโทรด้วย args: 2584, 4181 หมายเลขปัจจุบัน = 6765
ก่อนการเรียกใช้เมธอดด้วย args: 4181, 6765 หมายเลขปัจจุบัน = 10946
ก่อนการเรียกใช้เมธอดด้วย args: 6765, 10946 หมายเลขปัจจุบัน = 17711
ก่อนการเรียกใช้เมธอดด้วย args: 10946, 17711 หมายเลขปัจจุบัน = 28657
ก่อนการเรียกใช้เมธอดด้วย args: 17711, 28657 หมายเลขปัจจุบัน = 46368
ก่อนการเรียกใช้เมธอดด้วย args: 28657, 46368 หมายเลขปัจจุบัน = 75025
ก่อนการเรียกใช้เมธอดด้วย args: 46368, 75025 หมายเลขปัจจุบัน = 121393
ก่อนการเรียกใช้เมธอดด้วย args: 75025 121393 หมายเลขปัจจุบัน = 196418
ก่อนการเรียกใช้เมธอดด้วย args: 121393, 196418 หมายเลขปัจจุบัน = 317811
ก่อนการเรียกใช้เมธอดด้วย args: 196418, 317811 หมายเลขปัจจุบัน = 514229
ก่อนการเรียกใช้เมธอดด้วย args: 317811, 514229 หมายเลขปัจจุบัน = 83204 0
ก่อนวิธีการ โทรด้วย args: 514229, 832040 หมายเลขปัจจุบัน = 1346269
ก่อนเรียกใช้เมธอดด้วย args: 832040, 1346269 หมายเลขปัจจุบัน = 2178309
ก่อนเรียกใช้เมธอดด้วย args: 1346269, 2178309 หมายเลขปัจจุบัน = 3524578
ก่อนการเรียกใช้เมธอดด้วย args: 2178309, 3524578 หมายเลขปัจจุบัน = 5702887
ก่อนการเรียกใช้เมธอดด้วย args: 3524578, 5702887 หมายเลขปัจจุบัน = 9227465
ก่อนการเรียกใช้เมธอดด้วย args: 5702887, 9227465 หมายเลขปัจจุบัน = 14930352
ก่อนการเรียกใช้เมธอดด้วย args s: 9227465, 14930352 หมายเลขปัจจุบัน = 24157817
ก่อนการเรียกใช้เมธอดด้วย args: 14930352, 24157817 หมายเลขปัจจุบัน = 39088169
ก่อนการเรียกใช้เมธอดด้วย args: 24157817, 39088169 หมายเลขปัจจุบัน = 63245986 ก่อนการเรียก
ใช้เมธอดด้วย args: 39088169 , 63245986. หมายเลขปัจจุบัน = 102334155
วิธีก่อนหน้า โทรด้วย args: 63245986, 102334155 หมายเลขปัจจุบัน = 165580141
ก่อนเมธอดโทรด้วย args: 102334155, 165580141 หมายเลขปัจจุบัน = 267914296
ก่อนการเรียกใช้เมธอดด้วย args: 165580141, 267914296 หมายเลขปัจจุบัน = 433494437
ก่อนการเรียกใช้เมธอดด้วย args: 267914296, 433494437 หมายเลขปัจจุบัน = 701408733 ก่อนการเรียกใช้เมธอดด้วย args: 433494437, 701408733 หมายเลขปัจจุบัน = 1134903170
ก่อน
การเรียกใช้เมธอดด้วย args: 701408733, 1134903170 หมายเลขปัจจุบัน = 1836311903
หลังจากการเรียกใช้เมธอดด้วย args: 701408733, 113490317
หลังจากการเรียกใช้เมธอดด้วย args: 433494437, 701408733 หลังจากการเรียกใช้เมธอดด้วย args: 267914296, 433494437
หลังจาก การ
เรียกใช้เมธอดด้วย ar gs: 165580141, 267914296
หลังจากการเรียกใช้เมธอดด้วย args: 102334155, 165580141
หลังจากการเรียกใช้เมธอดด้วย args: 63245986, 102334155
หลังจากการเรียกใช้เมธอดด้วย args: 39088169, 63245986
หลังจากการเรียกใช้เมธอดด้วย args: 24157817, 39088169
หลังจากการเรียกใช้เมธอดด้วย args: 14930352, 24157817
หลังจากการเรียกใช้เมธอดด้วย args: 9227465, 14930352 หลังจากการเรียกใช้เมธอดด้วย args: 5702887, 9227465
หลังจาก
การเรียกใช้เมธอดด้วย args: 352 4578, 5702887
หลังจากการเรียกใช้เมธอดด้วย args : 2178309, 3524578
หลังจากการเรียกใช้เมธอดด้วย args: 1346269, 2178309
หลังจากการเรียกใช้เมธอดด้วย args: 832040, 1346269 หลังจาก
การเรียกใช้เมธอดด้วย args: 514229, 832040 หลังจากการเรียกใช้เมธอดด้วย args: 317811
, 514229
หลังจากการเรียกใช้เมธอดด้วย args หาเรื่อง: 196418, 317811
หลัง การเรียกใช้เมธอดด้วย args: 121393, 196418
หลังจากการเรียกใช้เมธอดด้วย args: 75025, 121393
หลังจากการเรียกใช้เมธอดด้วย args: 46368, 75025
หลังจากการเรียกใช้เมธอดด้วย args: 28657, 46368
หลังจากการเรียกใช้เมธอดด้วย args: 17711, 28657
หลังจากการเรียกใช้เมธอดด้วย args: 10946 , 17711 หลังจากการเรียก
ใช้เมธอดด้วย args: 6765, 10946
หลังจากการเรียกใช้เมธอดด้วย args: 4181, 6765
หลังจากการเรียกใช้เมธอดด้วย args : 2584, 4181
หลังจากการเรียกใช้เมธอดด้วย args: 1597, 2584
หลังจากการเรียกใช้เมธอดด้วย args: 987, 1597
หลังจากการเรียกใช้เมธอดด้วย args: 610, 987
หลังจากการเรียกใช้เมธอดด้วย args: 377, 610
หลังจากการเรียกใช้เมธอดด้วย args: 233, 377
หลังจาก การเรียกใช้เมธอดด้วย args: 144, 233
หลังจากการเรียกใช้เมธอดด้วย args: 89, 144
หลังจากการเรียกใช้เมธอดด้วย args: 55, 89
หลังจากการเรียกใช้เมธอดด้วย args: 34, 55
หลังจากการเรียกใช้เมธอดด้วย args: 21, 34
หลังจากการเรียกใช้เมธอดด้วย args: 13, 21
หลังจากการเรียกใช้เมธอดด้วย args: 8, 13
หลังจากการเรียกใช้เมธอดด้วย args: 5, 8
หลังจากการเรียกใช้เมธอดด้วย args: 3, 5
หลังจากการเรียกใช้เมธอดด้วย args: 2, 3
หลังจากการเรียกใช้เมธอดด้วย args : 1, 2
หลังจากการเรียกใช้เมธอดด้วย args: 1, 1
หลังจากการเรียกใช้เมธอดด้วย args: 0, 1

นี่คือการแสดงภาพของสิ่งที่เกิดขึ้น

พูดกันอีกครั้ง: เมธอด printFibonacciWithConditionถูกเรียก มันคำนวณจำนวนปัจจุบัน ถ้ามันเหมาะกับเรา เราจะแสดงมันและเรียกเมธอดprintFibonacciWithConditionอีกครั้งพร้อมกับอาร์กิวเมนต์ใหม่

ตราบใดที่มีการเรียกใช้ recursive method สิ่งนี้เรียกว่า "recursive destination" เมื่อการเรียกซ้ำสิ้นสุดลงและการเรียกเมธอดกลับมา เราบอกว่า call stack กำลัง "คลาย"

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


public class Fibonacci {
    public static void main(String[] args) {
        printFibonacciWithCondition(0, 1);
        System.out.println(1);
        System.out.println(0);
    }

    private static void printFibonacciWithCondition(long penultimate, long previous) {
        long current = penultimate + previous;
        if (current > Integer.MAX_VALUE) {
            return;
        }
        printFibonacciWithCondition(previous, current);
        System.out.println(current);
    }
}
    

ผลลัพธ์จะเป็น:

1836311903
1134903170
701408733
433494437
267914296
165580141
102334155
63245986
39088169
24157817
14930352
9227465
5702887
3524578
2178309
1346269
832040
514229
317811
196418
121393
75025
46368
28657
17711
10946
6765
4181
2584
1597
987
610
377
233
144
89
55
34
21
13
8
5
3
2
1
1
0