Learn C Programming

lesson ১ / ৭ · Recursion

Module ৮ · Recursion

Recursion: যে function নিজেকেই call করে

Freeপড়া

এই lesson-এ যা শিখবে

  • Base case আর step দিয়ে recursive function লিখতে পারবে: countdown, n পর্যন্ত যোগফল, factorial।
  • Frame table দিয়ে recursion trace করতে পারবে, আর বলতে পারবে call-এর পরের কাজ কেন ফেরার পথে run হয়।
  • Base case না থাকলে বা কখনো না পৌঁছালে Playground আর gcc -Wall কী বলে, দেখে চিনতে পারবে।

Module 7-এ Bob শিখেছে, একটা function যেকোনো function-কে call করতে পারে। তারপর মাথায় একটা সাহসী প্রশ্ন আসে: function কি নিজেকেই call করতে পারে? ওর দরকার 1 + 2 + 3, আর ও খেয়াল করে, এটা আসলে 3 যোগ 1 + 2।

তাই ও এমন একটা function লেখে, যেটা n-এর সাথে n - 1-এর জন্য নিজেরই উত্তর যোগ করে। তারপর Run চাপে। কিছুই আসে না। 10 সেকেন্ড পরে Playground program-টা থামিয়ে দেয়, আর badge-এ লেখা ওঠে টাইম লিমিট পার হয়েছে।

ওর বইতে লেখা ছিল, এমন ভুলে নাটকীয় একটা "stack overflow" হবে। ও পেল শুধু চুপচাপ একটা ঘড়ি। Bob কোন লাইনটা বাদ দিয়েছে, আর ওই 10 সেকেন্ডে আসলে কী ঘটেছে, এই lesson সেটাই খুঁজে বের করবে।

Recursion: যে function নিজেকেই call করে

যে function নিজেকেই call করে, তাকে বলে recursive, আর পুরো ধারণাটার নাম recursion। এটা কাজ করে তখনই, যখন প্রতিটা call একই সমস্যার একটা ছোট রূপ পরের call-এর হাতে তুলে দেয়। Bob ঠিক যেভাবে run করেছিল, ওর program-টা এই:

#include <stdio.h>

int sum_to(int n);

int main(void)
{
    printf("%d\n", sum_to(3));
    return 0;
}

int sum_to(int n)
{
    return n + sum_to(n - 1);
}

হাতে একবার trace করে দেখো। sum_to(3)-এর লাগে sum_to(2), সেটার লাগে sum_to(1), আর সেটার লাগে sum_to(0)। sum_to(0)-কে থামতে কেউ বলে না, তাই সে চায় sum_to(-1), তারপর sum_to(-2), এভাবে শেষই হয় না।

টিকিট কাউন্টারের একটা লম্বা লাইনের কথা ভাবো। তুমি কত নম্বরে আছ জানতে চাও, তাই সামনের জনকে জিজ্ঞেস করো, আর তার উত্তরের সাথে এক যোগ করো। সে-ও একই কাজ করে। এই শিকল থামে একদম সামনের জনের কাছে গিয়ে: সে কাউকে জিজ্ঞেস না করেই জানে, সে 1 নম্বর।

তাই recursive function-এ এমন একটা call লাগবেই, যেটা কাউকে জিজ্ঞেস না করেই উত্তর দেয়। Bob-এর program-এ প্রতিটা call শুধু জিজ্ঞেসই করে, উত্তর দেয় না কেউ।

দুইটা নিয়ম: base case আর step

যে recursive function শেষ হয়, সেটা দুইটা নিয়ম মানে। Base case হলো সবচেয়ে ছোট input, যার উত্তর সরাসরি দেওয়া হয়, কোনো call ছাড়া। Step একই function-কে একটা ছোট input দিয়ে call করে, base case-এর দিকে এক ধাপ এগিয়ে। কোনো কোনো বই step-কে বলে recursive case।

Recursive function-এর গড়ন

type name(int n)
{
    if (n == smallest) {
        return answer;
    }
    return ... name(n - 1) ...;
}
  • if (n == smallest) হলো base case-এর পরীক্ষা। এটা আসে সবার আগে, কোনো call-এর আগেই।
  • return answer; সবচেয়ে ছোট input-এর উত্তর দেয় কোনো call ছাড়াই, তাই শিকলটা এখানেই থামে।
  • name(n - 1) হলো step: একই function, base case-এর আরও কাছের একটা মান দিয়ে।
  • ... অংশটা ছোট উত্তরটাকে এই call-এর উত্তরে বদলে দেয়, যেমন যোগফলের জন্য n +।

Bob হারানো নিয়মটা যোগ করে। 0 পর্যন্ত সংখ্যার যোগফল তো 0, তাই sum_to(0) কোনো call ছাড়াই 0 return করে। সাথে ও উত্তরটাকে long long বানায়, কারণ n = 65536 থেকে যোগফল INT_MAX পার হয়ে যায়।

#include <stdio.h>

long long sum_to(int n);

int main(void)
{
    printf("%lld\n", sum_to(3));
    printf("%lld\n", sum_to(100));
    return 0;
}

long long sum_to(int n)
{
    if (n == 0) {
        return 0;
    }
    return n + sum_to(n - 1);
}

Run করার আগে trace করো। Module 6-এ loop trace হতো প্রতিটা pass-এর জন্য এক সারি দিয়ে। Recursion-এ সারি হয় প্রতিটা call-এর জন্য একটা: call-টা, তার parameter, সে কীসের অপেক্ষায়, আর কী return করে। একে বলা যায় frame table।

Callnঅপেক্ষা করেReturn করে
sum_to(3)3sum_to(2)3 + 3 = 6
sum_to(2)2sum_to(1)2 + 1 = 3
sum_to(1)1sum_to(0)1 + 0 = 1
sum_to(0)0কারও না: এটাই base case0

Table-টা দুই দফায় ভরো। Call যেমন যেমন হয়, প্রথম তিনটা কলাম উপর থেকে নিচে ভরে যাও। তারপর শেষ কলামটা ভরো নিচ থেকে উপরে: নিচের সারি return না করা পর্যন্ত উপরের সারি শেষই হতে পারে না।

6
5050

তার মানে উত্তর শুরু হয় base case থেকেই। Bob-এর program-এর মতো base case না থাকলে কোনো সারি কখনো return করে না।

Countdown: call-এর আগের কাজ

Module 6-এর সেই rocket game-এ Bob-এর countdown-টা আবার লাগবে, এবার recursion দিয়ে। n থেকে countdown মানে: n বলো, তারপর n - 1 থেকে countdown করো। আর 0 থেকে countdown মানে শুধু "liftoff"।

#include <stdio.h>

void countdown(int n);

int main(void)
{
    countdown(3);
    return 0;
}

void countdown(int n)
{
    if (n == 0) {
        printf("liftoff\n");
        return;
    }
    printf("%d\n", n);
    countdown(n - 1);
}
Callnনিজের call-এর আগে print করেতারপর call করে
countdown(3)33countdown(2)
countdown(2)22countdown(1)
countdown(1)11countdown(0)
countdown(0)0liftoff, এটাই base caseকাউকে না
3
2
1
liftoff

প্রতিটা call আগে print করে, পরে call করে, তাই সংখ্যাগুলো আসে ঠিক সেই ক্রমে, যে ক্রমে call হয়। একদম নিচে base case print করে liftoff। void function কোনো মান return করে না, তাই ওর base case শেষ হয় শুধু return; দিয়ে।

তাই call-এর আগে রাখা কাজ হয় নামার পথে, call-গুলো যে ক্রমে হয় সেই ক্রমেই।

Call-এর পরের কাজ হয় ফেরার পথে

এবার কাজটা call-এর নিচে বসাও। Call-এর পরের লাইন ততক্ষণ run হতে পারে না, যতক্ষণ না ওই call return করছে। তাই call-এর পরের লাইনগুলো run হয় উল্টো ক্রমে, ফেরার পথে।

Alice দেখতে চায়, sum_to ধাপে ধাপে কীভাবে উত্তর বানায়। ও প্রতিটা যোগফল একটা local variable-এ রাখে, আর call-এর পরে সেটা print করে।

#include <stdio.h>

long long sum_to(int n);

int main(void)
{
    long long total = sum_to(3);

    printf("total: %lld\n", total);
    return 0;
}

long long sum_to(int n)
{
    long long total = 0;

    if (n == 0) {
        return 0;
    }
    total = n + sum_to(n - 1);
    printf("sum to %d is %lld\n", n, total);
    return total;
}
Callnঅপেক্ষা করেঅপেক্ষার পরে print করেReturn করে
sum_to(3)3sum_to(2)sum to 3 is 66
sum_to(2)2sum_to(1)sum to 2 is 33
sum_to(1)1sum_to(0)sum to 1 is 11
sum_to(0)0কারও না: এটাই base caseকিছু না0

Call হয় উপরের সারি থেকে নিচের দিকে, কিন্তু print হয় নিচের সারি থেকে উপরের দিকে। সবার আগে print করে sum_to(1), কারণ উত্তর সবার আগে ওর হাতেই আসে।

sum to 1 is 1
sum to 2 is 3
sum to 3 is 6
total: 6

প্রতিটা call নিজের parameter আর local variable-এর জন্য নিজস্ব বাক্স পায় (Module 7)। এই বাক্সগুলো মিলে হয় call-টার frame। যে call-গুলো এখনো অপেক্ষায়, ওদের frame জমতে থাকে memory-র একটা অংশে, যার নাম stack, প্রতি call-এ একটা করে। Stack খুলে দেখাবে lesson 2; এখানে ছবিটা দেখো।

sum_to(3)-এর frame: call নামে নিচে, উত্তর ওঠে উপরে Call নামে নিচে, উত্তর ওঠে উপরে main sum_to(3)-এর অপেক্ষায় call করে sum_to(3) return করে 6 sum_to(3), n = 3 sum_to(2)-এর অপেক্ষা, তারপর 3 যোগ call করে sum_to(2) return করে 3 sum_to(2), n = 2 sum_to(1)-এর অপেক্ষা, তারপর 2 যোগ call করে sum_to(1) return করে 1 sum_to(1), n = 1 sum_to(0)-এর অপেক্ষা, তারপর 1 যোগ call করে sum_to(0) return করে 0 sum_to(0), n = 0 base case: 0 return করে, কোনো call নেই sum_to-এর চারটা frame একই সময়ে বেঁচে থাকে, প্রতিটার নিজের n। উত্তর ওঠে নিচের frame থেকে উপরে: 0, তারপর 1, তারপর 3, তারপর 6 পৌঁছায় main-এ।

বাঁ দিকের তীরগুলো call, নামার পথে। ডান দিকের তীরগুলো উত্তর, ফেরার পথে। কোনো call return করলে ওর frame-টা মুছে যায়।

তাই call-এর আগের কাজ চলে বাঁ দিকের তীর ধরে, আর পরের কাজ চলে ডান দিকের তীর ধরে।

Factorial, long long দিয়ে

Module 6 তোমার কাছে Kenji-র factorial চেয়েছিল একটা loop দিয়ে, আর কথা দিয়েছিল ওটা আবার ফিরবে। n-এর factorial, লেখা হয় n!, মানে 1 x 2 x ... x n, আর 0! হলো 1। এবার recursion-এর চোখে পড়ো: n! মানে n গুণ (n - 1)!, আর 0! হলো base case।

#include <stdio.h>

long long factorial(int n);

int main(void)
{
    for (int i = 0; i <= 20; i++) {
        printf("%d! = %lld\n", i, factorial(i));
    }
    return 0;
}

long long factorial(int n)
{
    if (n == 0) {
        return 1;
    }
    return n * factorial(n - 1);
}

ওই call-গুলোর একটা, factorial(4), তার frame table এরকম:

Callnঅপেক্ষা করেReturn করে
factorial(4)4factorial(3)4 x 6 = 24
factorial(3)3factorial(2)3 x 2 = 6
factorial(2)2factorial(1)2 x 1 = 2
factorial(1)1factorial(0)1 x 1 = 1
factorial(0)0কারও না: এটাই base case1
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800
11! = 39916800
12! = 479001600
13! = 6227020800
14! = 87178291200
15! = 1307674368000
16! = 20922789888000
17! = 355687428096000
18! = 6402373705728000
19! = 121645100408832000
20! = 2432902008176640000

উত্তর খুব দ্রুত বাড়ে। 12! = 479001600 একটা int-এ এঁটে যায়, কিন্তু 13! = 6227020800 পার হয়ে যায় INT_MAX, মানে 2147483647। এজন্যই factorial return করে long long।

long long-এর দৌড় 20! পর্যন্ত। LLONG_MAX হলো 9223372036854775807, আর 20! তার মোটামুটি চার ভাগের এক ভাগ। 21! হলো 20!-এর 21 গুণ, প্রায় 5.1 x 10^19, মানে LLONG_MAX-এর পাঁচ গুণেরও বেশি। ওটা হিসাব করতে গেলেই signed overflow, যেটা undefined behaviour (Module 2), তাই এই lesson ওটা কখনো print করে না।

তাই recursive factorial-এর লাগে base case 0! = 1, আর উত্তর ধরার মতো বড় একটা type: 12! পর্যন্ত int, 20! পর্যন্ত long long।

Base case না থাকলে কী হয়, মেপে দেখা

আবার Bob-এর program-এ ফিরি। ওর বই বলে, base case ছাড়া function "Stack overflow" error দেখায়। C এমন কোনো বার্তা print করে না, আর Playground-এ Bob-এর program crash-ই করেনি। কারণটা দেখো।

Playground compile করে GCC 12 দিয়ে, -O2-তে, আর সেটা optimise করে। মানে তোমার program-কে একই কাজের দ্রুততর machine code হিসেবে নতুন করে লেখে। GCC 12 দেখেছে, প্রতিটা call পরের call-এর উত্তরের সাথে শুধু একটা সংখ্যা যোগ করে। তাই ও call-গুলোর জায়গায় একটা loop বসিয়ে দিয়েছে, আর সেই loop থেকে বেরোনোর কোনো পথ নেই।

এই machine code-কে বলে listing। Compiler Explorer-এ, Playground-এর -O2-তে, GCC 12 যেটা print করেছে সেটা এই:

main:
.L2:
        jmp     .L2
sum_to:
.L5:
        jmp     .L5

jmp .L2 মানে ".L2 label-এ লাফ দাও", আর .L2 ঠিক এই লাইনটাই। তাই main চিরকাল নিজের জায়গাতেই লাফাতে থাকে, নিজের printf পর্যন্ত কখনো পৌঁছায় না। কোনো call বাকি নেই, কিছু জমেও না, program শুধু ঘুরতে থাকে, যতক্ষণ না 10 সেকেন্ডের limit আসে।

তুমি কী দেখবে, সেটা নির্ভর করে function-টা নিজের call-এর আশপাশে কী করে তার উপর:

Base case ছাড়া function-O2-তে GCC 12 যা বানায়Playground যা দেখায়
Call-এর আশপাশে শুধু হিসাব, Bob-এর sum_to-এর মতোএকটা অশেষ loop(no output), আর 10090 ms পরে টাইম লিমিট পার হয়েছে
প্রতিটা call-এ print করে, if ছাড়া countdown-এর মতোএকটা অশেষ loop, যেটা print করতেই থাকেPlayground-এর 1 MB output cap ভরে যাওয়া পর্যন্ত print করে, তারপর run থামে Output limit পার হয়েছে (1 MB) badge দেখিয়ে।
Call-এর পরে এমন কাজ, যেটাকে GCC loop বানাতে পারে নাআসল call, প্রতিটার একটা করে frameProgram-এর 256,000 KB memory limit পর্যন্ত stack বাড়তে থাকে: Memory limit exceeded, আর (no output)। ঠিক কোথায় গিয়ে, সেটা lesson 2 মাপে।

বাংলা Playground page-এ Memory limit-এর badge-টা বাংলায় দেখায়, ফাঁকা output-এর লেখাটাও।

Machine code build-এর উপরও নির্ভর করে। Bob-এর file optimisation ছাড়া build করো, -O0-তে, তাহলে প্রতিটা call একটা আসল frame পায়। Compiler Explorer-এর GCC 12-এ একবার run করে দেখা গেছে, ওই build 193 ms-এই crash করে, আর বলে Program terminated with signal SIGSEGV (11)।

Linux terminal-এ একই crash print করে Segmentation fault। কোনো program যে memory ছোঁয়ার অনুমতি পায়নি সেটা ছুঁলে, operating system এভাবেই জানায়। Signal আর stack-এর সীমার কথা আসবে lesson 5-এ।

এর কোনো ফলেই recursion-এর নাম নেই, তবে compiler নামটা বলতে পারে। Playground চুপ থাকে। নিজের মেশিনে GCC 12-এর gcc -Wall বলে warning: infinite recursion detected [-Winfinite-recursion], সাথে একটা note, যেটা দেখিয়ে দেয় call-টা কোথায়। এই warning GCC 12-এ নতুন।

তাই base case না থাকলে সেটা দেখতে time limit, output error বা memory limit-এর মতো লাগতে পারে। Run করার আগেই -Wall আসল সমস্যার নাম বলে দেয়।

Example 1: print করে এমন সবচেয়ে ছোট recursion

Base case চাইলে কিছুই না করে শুধু থামতে পারে। এটা প্রতি call-এ একটা করে তারা print করে।

#include <stdio.h>

void stars(int n);

int main(void)
{
    stars(3);
    printf("\n");
    return 0;
}

void stars(int n)
{
    if (n == 0) {
        return;
    }
    printf("*");
    stars(n - 1);
}
CallnPrint করেতারপর call করে
stars(3)3*stars(2)
stars(2)2*stars(1)
stars(1)1*stars(0)
stars(0)0কিছু না: এটাই base caseকাউকে না
***

প্রতিটা call একটা তারা print করে, আর সারির বাকিটা তুলে দেয় একটা ছোট call-এর হাতে। নতুন লাইনটা আসে main থেকে, শেষ call return করার পরে।

Run in Compiler
Example 2: Amara-র কমলার পিরামিড

Amara ওর দোকানে কমলা সাজায় বর্গাকার পিরামিড করে। সবচেয়ে উপরের স্তরে 1টা কমলা, পরেরটায় 4টা, তারপর 9টা: k নম্বর স্তরে k x k টা। n স্তরের পিরামিডে মোট কয়টা কমলা?

#include <stdio.h>

long long pyramid(int layers);

int main(void)
{
    printf("4 layers: %lld oranges\n", pyramid(4));
    printf("10 layers: %lld oranges\n", pyramid(10));
    return 0;
}

long long pyramid(int layers)
{
    if (layers == 0) {
        return 0;
    }
    return layers * layers + pyramid(layers - 1);
}
Calllayersঅপেক্ষা করেReturn করে
pyramid(4)4pyramid(3)16 + 14 = 30
pyramid(3)3pyramid(2)9 + 5 = 14
pyramid(2)2pyramid(1)4 + 1 = 5
pyramid(1)1pyramid(0)1 + 0 = 1
pyramid(0)0কারও না: এটাই base case0
4 layers: 30 oranges
10 layers: 385 oranges

Step-কে সব সময় n-ই যোগ করতে হবে, এমন না। এখানে সে সবচেয়ে নিচের স্তরটা যোগ করে, তারপর তার উপরে বসানো ছোট পিরামিডটা চায়। মোট সংখ্যাটা long long, কারণ 1861 স্তর থেকে এটা INT_MAX পার হয়ে যায়।

Run in Compiler
Example 3: input থেকে পড়া একটা range-এর যোগফল

Maria চায় a থেকে b পর্যন্ত প্রতিটা পূর্ণসংখ্যার যোগফল। এখানে ছোট সমস্যা মানে ছোট range: a যোগ করো, তারপর a + 1 থেকে b পর্যন্ত range-এর যোগফল নাও।

#include <stdio.h>

long long sum_range(int a, int b);

int main(void)
{
    int a = 0;
    int b = 0;

    scanf("%d %d", &a, &b);
    printf("%lld\n", sum_range(a, b));
    return 0;
}

long long sum_range(int a, int b)
{
    if (a > b) {
        return 0;
    }
    return a + sum_range(a + 1, b);
}
Callabঅপেক্ষা করেReturn করে
sum_range(3, 7)37sum_range(4, 7)3 + 22 = 25
sum_range(4, 7)47sum_range(5, 7)4 + 18 = 22
sum_range(5, 7)57sum_range(6, 7)5 + 13 = 18
sum_range(6, 7)67sum_range(7, 7)6 + 7 = 13
sum_range(7, 7)77sum_range(8, 7)7 + 0 = 7
sum_range(8, 7)87কারও না: এটাই base case0
25

ওই output-টা 3 7 input-এর জন্য। এখানে step a-কে বাড়ায়, কমায় না, আর base case হলো ফাঁকা range।

Zara আগে edge-গুলো চালিয়ে দেখে। 5 5 দিলে 5, এক সংখ্যার range। 5 3 দিলে সাথে সাথে 0, কারণ range-টা ফাঁকা। Base case a == b লিখলে সেটা 5 3-এর বেলায় কখনো মিলত না, কারণ a শুধু বাড়তেই থাকে।

Run in Compiler

এটা কোথায় কাজে লাগে

  • Folder-এর গাছ। একটা folder-এর size মানে ওর নিজের file-গুলো, সাথে ভিতরের প্রতিটা folder-এর size। GNU coreutils-এর du --help বলে, ও কাজ করে "recursively for directories", আর rm -r-এর -r মানে --recursive।
  • JSON parser। cJSON নামের ছোট একটা C library একটা JSON মান পড়ে parse_value দিয়ে। মানটা list বা object হলে parse_array বা parse_object ভিতরের প্রতিটা item-এর জন্য আবার parse_value-কে call করে। Stack ফুরানোর আগেই ওর CJSON_NESTING_LIMIT, মানে 1000, call থামিয়ে দেয়।
  • GCC-র নিজের C parser। 2006-এর GCC 4.1 থেকে GCC হাতে লেখা একটা recursive-descent parser দিয়ে C পড়ে। বন্ধনীর ভিতরের expression পড়া হয় পুরো expression পড়ার সেই একই function দিয়ে, শুধু এক call গভীরে।
  • Screen-এ fractal। Python-এর turtledemo package-এ fractalcurves নামে একটা demo আছে, যেটা Koch curve আঁকে। ওর function নিজেকেই চারবার call করে, প্রতিবার তিন ভাগের এক ভাগ লম্বা রেখার জন্য। Depth 0 হলে সোজা একটা রেখা আঁকে: এটাই base case।

যে ভুলগুলো সবাই করে

১. Step যখন base case টপকে যায়।

long long every_other(int n)
{
    if (n == 0) {
        return 0;
    }
    return n + every_other(n - 2);
}

কোনো command line-এই বার্তা নেই। 6 থেকে ঠিকঠাক চলে, কিন্তু 7 থেকে যায় 7, 5, 3, 1, -1, আর কখনো 0-তে নামে না। -O2-তে GCC 12 7-এর call-টাকে Bob-এর মতোই অশেষ লাফ বানিয়ে দিয়েছে। পরীক্ষা লেখো n <= 0, তাহলে 0 টপকে যাওয়া step-ও ধরা পড়ে। তুমি == 0 লিখবে, কারণ প্রথম যে input দিয়ে চালিয়েছিলে, তাতে ঠিক কাজ করেছিল।

২. Recursive লাইনে return নেই।

long long sum_to(int n)
{
    if (n == 0) {
        return 0;
    }
    n + sum_to(n - 1);
}

Playground-এ নীরব। নিজের মেশিনে GCC 12-এর gcc -Wall বলে warning: value computed is not used [-Wunused-value] আর warning: control reaches end of non-void function [-Wreturn-type]। যোগফলটা হিসাব হয়, তারপর ফেলে দেওয়া হয়, তাই main এরপর যা print করে, সেটা undefined behaviour। লেখো return n + sum_to(n - 1);। return তুমি ভুলবে, কারণ ওটা ছাড়াও অঙ্কটা পড়তে ঠিকঠাক লাগে।

৩. 0 বৈধ input, অথচ base case 1-এ।

long long factorial(int n)
{
    if (n == 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

n input থেকে এলে কোনো command line-এই বার্তা নেই। 1 থেকে 20 পর্যন্ত ঠিক, কিন্তু factorial(0) চলে যায় -1, -2 আর আরও নিচে, 1 থেকে দূরে। শেষে int overflow করে, যেটা undefined behaviour, তাই এরপর কী হবে তার কোনো কথাই C দেয় না। Compiler Explorer-এর GCC 12-এ, Playground-এর -O2-তে, পুরো program একবার run করে দেখা গেছে: input 0-তে লেগেছে 7247 ms, আর উত্তর এসেছে ভুল।

Base case বসাও 0-তে, মানে সবচেয়ে ছোট বৈধ input-এ। তুমি 1 বেছে নেবে, কারণ 1! তো 1, আর গুণফল যেন ওখান থেকেই শুরু হয়।

মাথা খাটাও

Amara countdown-এর একটা লাইন সরিয়ে দেয়। printf এখন call-এর পরে, আর base case কিছুই print করে না।

#include <stdio.h>

void f(int n);

int main(void)
{
    f(3);
    printf("\n");
    return 0;
}

void f(int n)
{
    if (n == 0) {
        return;
    }
    f(n - 1);
    printf("%d ", n);
}

কোনো বার্তা ছাড়াই compile হয়। 3-এর জন্য এটা কী print করে? Countdown print করেছিল 3, 2, 1। এটা ক্রম উল্টে দেয় কেন?

একটা frame table বানাও, যার একটা কলামে থাকবে প্রতিটা call নিজের call return করার পরে কী print করে। তারপর ওই কলামটা নিচের সারি থেকে উপরে ভরো।

অনুশীলন ১সহজ

Bob এবার ওর যোগফল একেবারে ঠিক করবে। Recursion দিয়ে long long sum_to(int n) লেখো, আর player যে n টাইপ করে, তার যোগফল print করো।

Input. এক লাইনে একটা পূর্ণসংখ্যা n।

Output. এক লাইনে 1 + 2 + ... + n।

Constraints. 0 <= n <= 10000।

Sample. Input 3 দিলে 6। Input 0 দিলে 0।

#include <stdio.h>

long long sum_to(int n);

int main(void)
{
    int n = 0;
    scanf("%d", &n);

    printf("%lld\n", sum_to(n));
    return 0;
}

long long sum_to(int n)
{
    /* The base case comes first: what is the sum of the numbers up to 0?
       Then return n plus one call with a smaller n. */
    return 0;
}

আলাদা করে গ্রেড হয় না। Module 6-এর sum-to-n ঠিক এই output-টাই গ্রেড করে, আর judge শুধু output দেখে।

Run in Compiler
অনুশীলন ২মাঝারি

Alice গণিত ক্লাবের জন্য factorial print করে। সদস্যরা একটার পর একটা সংখ্যা বলতে থাকে, যতক্ষণ না ফুরায়, আর Alice প্রতিটার factorial print করে। Recursion দিয়ে long long factorial(int n) লেখো।

Input. Space বা নতুন লাইন দিয়ে আলাদা করা পূর্ণসংখ্যা, input-এর শেষ পর্যন্ত। অন্তত একটা থাকবেই।

Output. প্রতিটা পূর্ণসংখ্যা n-এর জন্য এক লাইনে n!, যে ক্রমে পড়া হয়েছে সেই ক্রমে।

Constraints. সর্বোচ্চ 21টা পূর্ণসংখ্যা, প্রতিটা 0 <= n <= 20।

Sample. Input 0 5 20 দিলে তিন লাইনে 1, 120 আর 2432902008176640000।

#include <stdio.h>

long long factorial(int n);

int main(void)
{
    int n = 0;

    while (scanf("%d", &n) == 1) {
        printf("%lld\n", factorial(n));
    }
    return 0;
}

long long factorial(int n)
{
    /* The base case comes first. Which n is the smallest one allowed?
       Every other n is n times the factorial of n - 1. */
    return 0;
}

factorials নামে গ্রেড হয়। Hidden test-এ আছে একা একটা 0, 13 (INT_MAX পার হওয়া প্রথম factorial), আর 20।

Run in Compiler
অনুশীলন ৩কঠিন

Bob-এর countdown 1 পর্যন্ত নামে, তারপর যেখান থেকে শুরু করেছিল সেখানে উঠে আসে। ও এটা চায় একটাই function থেকে, যেটা নিজের call-এর দুই পাশেই print করে। void down_and_up(int n) লেখো।

Input. এক লাইনে একটা পূর্ণসংখ্যা n।

Output. n, n - 1, ..., 1, তারপর 1, 2, ..., n, প্রতি লাইনে একটা সংখ্যা। তাই 1 আসে দুইবার।

Constraints. 1 <= n <= 1000।

Sample. Input 3 দিলে ছয় লাইনে 3, 2, 1, 1, 2 আর 3।

#include <stdio.h>

void down_and_up(int n);

int main(void)
{
    int n = 0;
    scanf("%d", &n);

    down_and_up(n);
    return 0;
}

void down_and_up(int n)
{
    /* One function that prints on both sides of its own call.
       How many times must 1 be printed? */
}

down-and-up নামে গ্রেড হয়। Hidden test-এ আছে n = 1, যেখানে 1 দুইবার print করতে হবে, আর n = 1000।

Run in Compiler

সচরাচর যে প্রশ্নগুলো আসে

  • Bob stack overflow না পেয়ে টাইম লিমিট পার হয়েছে পেল কেন?

    -O2-তে GCC 12 ওর recursion-কে একটা loop বানিয়ে দিয়েছে, আর loop-এর প্রতি pass-এ নতুন memory লাগে না। "Stack overflow" এমন কোনো বার্তা C আসলে print-ই করে না। Optimisation ছাড়া build করলে ঠিকই crash করে, -O0-এর run-এ যেমন দেখলে।

  • Recursion কি loop-এর চেয়ে ধীর?

    একটা আসল call-এর খরচ loop-এর এক pass-এর চেয়ে একটু বেশি। কিন্তু GCC 12 প্রায়ই সহজ recursion থেকে call-গুলো সরিয়ে দেয়। Playground-এ recursive sum_to 100000000-এর উত্তর দিয়েছে 70 ms-এ। দুইটাই মেপে দেখায় lesson 4।

  • প্রতিটা call-ই n নামটা ব্যবহার করে। তাহলে প্রতিটা call নিজের n আলাদা রাখে কীভাবে?

    প্রতিটা call ওর parameter-এর জন্য নতুন একটা বাক্স পায়, argument-এর একটা copy দিয়ে ভরা (Module 7)। তাই sum_to(3) n নামের চারটা বাক্স বানায়, প্রতি frame-এ একটা। Lesson 2 ওগুলো এঁকে দেখায়।

  • Base case কি 0-ই হতে হবে?

    না। এটা সেই সবচেয়ে ছোট input, যার উত্তর function-কে দিতে হবে। Factorial-এর base case 0, কারণ 0! জিজ্ঞেস করা একদম ন্যায্য প্রশ্ন। যে function-কে কখনো 1-এর কম কিছু জিজ্ঞেস করা হয় না, সেটা 1-এ থামতে পারে।

মূল কথা

  • Recursive function একটা ছোট সমস্যা নিয়ে নিজেকেই call করে, আর প্রতিটা call নিজের করা call-এর অপেক্ষায় থাকে।
  • এর লাগে একটা base case, যেটা call ছাড়াই উত্তর দেয় আর সবার আগে পরীক্ষা হয়, আর একটা step, যেটা ওর দিকে এগোয়।
  • Frame table-এ প্রতিটা call-এর জন্য এক সারি: call নামে সারি ধরে নিচে, উত্তর ওঠে উপরে।
  • Call-এর আগের কাজ হয় নামার পথে; পরের কাজ হয় ফেরার পথে, উল্টো ক্রমে।
  • factorial-এর লাগে long long: int-এ শেষ যেটা এঁটে যায় সেটা 12!, আর long long-এ 20!।
  • Base case না থাকলে সেটা time, output বা memory limit-এর মতো দেখায়; GCC 12-এর gcc -Wall আসল নামটা বলে দেয়।

এরপর Amara কাগজে frame আঁকে, প্রতি call-এ একটা বাক্স, আর Run চাপার আগেই output বলে দেয়।

lesson ১ শেষ

শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।

পরেরটা: Call stack, এক frame করে