C में प्रोग्रामिंग, तथा पुनरावर्तन

यह कंप्यूटर विज्ञान पेपर का खंड 4 है, और इससे पूर्व के दो खंडों की भाँति इसके लिए किसी अन्य सत्यापित पेपर का पाठ्यक्रम नहीं पढ़ा गया — अतः इन अध्यायों को अपना मानने से पूर्व अपना देखें। यह खंड शेष पेपर से इसलिए भिन्न है कि उसके प्रश्न अनुसरण हैं: आपको C की दस पंक्तियाँ दी जाती हैं और पूछा जाता है कि वे क्या छापती हैं, या कोई फलन कितनी बार बुलाया जाता है। कोई सूत्र नहीं है, और अंक उसे मिलते हैं जो कागज़ पर सावधान व्याख्याकार हो सके। तीन बातें अधिकांश हानि का कारण हैं। C सब कुछ मान से पास करता है, सदा — कोई संदर्भ प्राचल नहीं है, और जो फलन अपने बुलाने वाले का चर बदलता प्रतीत होता है वह वस्तुतः किसी संकेतक की प्रतिलिपि पाकर उसके माध्यम से लिखता है, इसीलिए swap(a, b) काम नहीं कर सकता और swap(&a, &b) कर सकता है। सरणी संकेतक नहीं है, चाहे a[i] की परिभाषा *(a + i) हो: सरणी का नाम परिवर्तनीय वस्तु नहीं है, sizeof संकेतक की चौड़ाई के बजाय सम्पूर्ण सरणी बताता है, और दोनों उसी क्षण अलग हो जाते हैं जब सरणी किसी फलन को दी जाए, जहाँ वह संकेतक में क्षीण होकर आकार-सूचना खो देती है। तथा पुनरावर्तन प्रश्न प्रायः सदा गणना के विषय में होते हैं, लौटाए गए मान के विषय में नहीं। भोला फिबोनाची मानक उदाहरण ठीक इसलिए है कि उसकी लागत दिखने से बहुत बुरी है: fib(5) निकालने में 15 कॉल होते हैं, fib(6) में 25, और fib(6) के भीतर fib(3) का मान तीन पृथक् बार पुनः निकाला जाता है — वही अतिरेक जिसे स्मृतिकरण हटाता है, और वही कारण कि भोला संस्करण चरघातांकी है जबकि पुनरावृत्तिमूलक रैखिक।

संकेतक, सरणियाँ, तथा वस्तुतः क्या पास होता है

सरणी व संकेतक कहाँ अलग हो जाते हैं
व्यंजकint a[10] हेतुint *p = a हेतु
a[i] / p[i]दोनों का अर्थ *(a + i) — समानवही
sizeof40 — सम्पूर्ण सरणी8 — एक संकेतक
निर्दिष्टीकरणa = ... अवैध हैp = ... ठीक है
फलन को दिया गयाint * में क्षीण — आकार लुप्तपहले से संकेतक
🎯 C में संदर्भ द्वारा कॉल क्यों नहीं, और उसकी आवश्यकता क्यों नहीं
C में प्रत्येक तर्क प्राचल में प्रतिलिपित होता है। अतः int पाता फलन बुलाने वाले का int नहीं बदल सकता, बस। जो संदर्भ द्वारा कॉल जैसा लगता है वह पते पर लगाया गया मान द्वारा कॉल है: &x दें, और फलन के पास पते की प्रतिलिपि है, जो मूल वस्तु में लिखने हेतु पर्याप्त है। यही प्रसिद्ध स्वैप प्रश्न की सम्पूर्ण व्याख्या है — swap(int a, int b) दो प्रतिलिपियाँ फेंटता है और बुलाने वाला कुछ नहीं देखता, जबकि swap(int *a, int *b) काम करता है। वही तर्क बताता है कि सरणियाँ संदर्भ द्वारा पास होती प्रतीत क्यों होती हैं: सरणी संकेतक में क्षीण होती है, संकेतक प्रतिलिपित होता है, और उसके माध्यम से लेखन बुलाने वाले की सरणी में पहुँचते हैं। सरणियों के साथ कुछ विशेष नहीं हो रहा; केवल क्षय हो रहा है।

पुनरावर्तन, प्रशंसा के बजाय गणना

भोला फिबोनाची, यथार्थ गिना गया
nfib(n) निकालने हेतु कुल कॉलसंवृत रूप 2·F(n+1) − 1
232×2 − 1 = 3
492×5 − 1 = 9
5152×8 − 1 = 15
6252×13 − 1 = 25
🧠 वृक्ष बनाकर कॉल गिनें, फिर उससे अतिरेक पढ़ें
fib(6) का पुनरावर्तन-वृक्ष fib(5) व fib(4) को संतान रखता है, fib(5) के नीचे fib(4) व fib(3), और इसी प्रकार — और वही उप-समस्याएँ कई शाखाओं पर आती हैं। fib(6) के भीतर fib(3) तीन पृथक् बार निकाला जाता है, fib(2) पाँच बार। वही पुनरावृत्ति चरघातांकी लागत है, और स्मृतिकरण उसी को मिटाता है: प्रत्येक परिणाम पहली बार संचित करने से काम रैखिक हो जाता है, क्योंकि n उप-समस्याओं में प्रत्येक एक बार हल होती है। इस अंतर्दृष्टि का परीक्षणीय रूप प्रायः जटिलता-प्रश्न के बजाय गणना-प्रश्न होता है, अतः व्यावहारिक कौशल वृक्ष बनाकर शीर्ष जोड़ना है। अंकगणित पर उपयोगी जाँच: कॉलों की संख्या सदा विषम होती है, क्योंकि प्रत्येक आंतरिक शीर्ष की ठीक दो संतानें हैं और कुल 2·F(n+1) − 1 है।

दो अन्य पुनरावर्तन कंठस्थ रखने योग्य हैं। हनोई की मीनार n चक्रिकाओं हेतु 2ⁿ − 1 चालें लेती है — तीन हेतु 7, दस हेतु 1023 — पुनरावृत्ति T(n) = 2T(n−1) + 1 से। तथा कॉल स्टैक की गहराई कॉलों की संख्या से पृथक् प्रश्न है: fib(n) चरघातांकी रूप से अनेक कॉल करता है परंतु उसका स्टैक केवल n गहरा है, क्योंकि शाखाएँ एक-एक कर खोजी जाती हैं। अधिकतम स्टैक गहराई पूछता प्रश्न वृक्ष की ऊँचाई चाहता है, उसका आकार नहीं।

मुख्य बिंदु

  • C सब कुछ मान से पास करता है; &x देना पते पर लगाया गया मान द्वारा कॉल है।
  • a[i] की परिभाषा *(a + i) है, पर सरणी-नाम निर्दिष्ट-योग्य नहीं और sizeof सम्पूर्ण सरणी बताता है।
  • फलन को दी गई सरणी संकेतक में क्षीण होती है और उसका आकार लुप्त हो जाता है।
  • भोला fib(n), 2·F(n+1) − 1 कॉल करता है: n = 5 हेतु 15, n = 6 हेतु 25।
  • fib(6) के भीतर fib(3) तीन बार पुनः निकाला जाता है — वही पुनरावृत्ति चरघातांकी लागत है।
  • स्मृतिकरण काम को रैखिक बनाता है क्योंकि तब n उप-समस्याओं में प्रत्येक एक बार हल होती है।
  • हनोई की मीनार को 2ⁿ − 1 चालें चाहिए, T(n) = 2T(n−1) + 1 से।
  • स्टैक गहराई पुनरावर्तन-वृक्ष की ऊँचाई है, कॉलों की संख्या नहीं — fib(n), n गहरा है।

अभ्यास प्रश्न (8)

उत्तर खोलने से पहले प्रत्येक प्रश्न हल करें। हर व्याख्या सही विकल्प के साथ लुभावना गलत विकल्प भी बताती है, क्योंकि अंक वहीं जाते हैं।

  1. भोला पुनरावर्ती फिबोनाची fib(n) = n यदि n < 2, अन्यथा fib(n−1) + fib(n−2) से परिभाषित है। fib(5) निकालते समय fib को कुल कितनी कॉल होती हैं, प्रारंभिक कॉल सहित?

    संख्यात्मक उत्तर — मान टाइप करें।

    उत्तर देखें

    उत्तर: 15

    पुनरावर्तन-वृक्ष बनाकर शीर्ष गिनने पर 15 मिलता है। संवृत रूप 2·F(n+1) − 1 है जहाँ F स्वयं फिबोनाची अनुक्रम है: F(6) = 8, अतः 2×8 − 1 = 15, और वही सूत्र fib(6) हेतु 25 व fib(4) हेतु 9 देता है। दो जाँचें अधिकांश त्रुटियाँ पकड़ती हैं: गणना सदा विषम होती है, क्योंकि प्रत्येक आंतरिक शीर्ष की ठीक दो संतानें हैं; और वह n से बहुत बड़ी है, और अभिप्राय यही है — भिन्न उप-समस्याएँ केवल 6 हैं, और उससे ऊपर सब पुनर्गणना है।
  2. 4-बाइट int व 8-बाइट संकेतक वाली मशीन पर int a[10]; हेतु sizeof(a) तथा sizeof(&a[0]) क्रमशः हैं:

    1. 40 व 8
    2. 8 व 8
    3. 40 व 40
    4. 10 व 8
    उत्तर देखें

    उत्तर: A — 40 व 8

    sizeof(a) सम्पूर्ण सरणी-वस्तु का आकार पूछता है: 10 × 4 = 40 बाइट। &a[0], int का संकेतक है, अतः उसका आकार संकेतक-चौड़ाई, 8 है। यही सर्वाधिक तीव्र स्थान है जहाँ सरणी व संकेतक भिन्न होते हैं, और इसीलिए सरणी पाता फलन उसकी लंबाई नहीं निकाल सकता — फलन के भीतर प्राचल वस्तुतः संकेतक है और sizeof, 8 लौटाता है। विकल्प B वह है जो अभ्यर्थी "सरणी संकेतक है" मानकर देता है; विकल्प D बाइटों के बजाय अवयव गिनता है, जो sizeof कभी नहीं करता।
  3. void swap(int a, int b) { int t = a; a = b; b = t; } अपने बुलाने वाले के चर क्यों नहीं बदल पाता?

    1. क्योंकि अस्थायी चर t क्षेत्र से बाहर हो जाता है
    2. क्योंकि C तर्कों की प्रतिलिपि बनाता है, अतः फलन प्रतिलिपियाँ बदलता है
    3. क्योंकि फलन void लौटाता है
    4. क्योंकि संकेतक प्रकार बिना int नहीं बदले जा सकते
    उत्तर देखें

    उत्तर: B — क्योंकि C तर्कों की प्रतिलिपि बनाता है, अतः फलन प्रतिलिपियाँ बदलता है

    C में ठीक एक प्राचल-पासन तंत्र है: तर्क प्राचल में प्रतिलिपित होता है। अतः फलन के भीतर a व b नई वस्तुएँ हैं, स्वैप उन पर सही होता है, और बुलाने वाले के चर कभी स्पर्श ही नहीं हुए। उपचार पते देना है — swap(int *a, int *b) भीतर *a/*b सहित — जो भिन्न तंत्र नहीं अपितु संकेतक-मानों पर लगा वही प्रतिलिपिकरण है। विकल्प A, t के विषय में वास्तविक तथ्य है और विफलता से अप्रासंगिक; t के static होने पर भी स्वैप समान रूप से विफल होता। विकल्प C लौटाए मान को प्राचलों से भ्रमित करता है, और void में कुछ भी फलन को संकेतक से लिखने से नहीं रोकता।
  4. हनोई की मीनार को 10 चक्रिकाओं हेतु कितनी चालें चाहिए?

    संख्यात्मक उत्तर — मान टाइप करें।

    उत्तर देखें

    उत्तर: 1023

    पुनरावृत्ति T(n) = 2T(n−1) + 1 है, T(1) = 1 सहित — n−1 चक्रिकाएँ पार ले जाएँ, सबसे बड़ी हिलाएँ, n−1 वापस लाएँ — जो 2ⁿ − 1 पर हल होती है, अतः 2¹⁰ − 1 = 1023। सर्वाधिक सामान्य गलत उत्तर 1024 है, घात याद रखकर −1 छोड़ने से; −1 वास्तविक है और n = 1 पर सहज दिखता है, जहाँ सूत्र को 2 के बजाय 1 देना चाहिए। यह भी ध्यान दें कि चालों की संख्या चरघातांकी है जबकि पुनरावर्तन गहराई केवल 10, और अगला प्रश्न उसी भेद पर घूमता है।
  5. भोला fib(20) निकालने में दस हज़ारों कॉल होते हैं। उस गणना के दौरान कॉल स्टैक की अधिकतम गहराई है:

    1. लगभग 2²⁰
    2. 20
    3. कॉलों की संख्या के बराबर
    4. लगभग 13529
    उत्तर देखें

    उत्तर: B — 20

    स्टैक गहराई पुनरावर्तन-वृक्ष की ऊँचाई है, उसका आकार नहीं। सबसे गहरी श्रृंखला fib(20) → fib(19) → … → fib(1) है, अतः गहराई लगभग 20 है, चाहे कॉलों की संख्या 2·F(21) − 1 = 21,891 हो। कारण यह है कि दोनों पुनरावर्ती शाखाएँ एक के बाद एक खोजी जाती हैं, साथ नहीं: बाएँ उपवृक्ष के लौटने पर उसके फ़्रेम दाएँ उपवृक्ष के आरंभ से पूर्व चले जाते हैं। इसीलिए भोला फिबोनाची समय की आपदा है, स्थान की नहीं, और प्रश्न दोनों लागतों को पृथक् करने हेतु मानक रूप से यही करता है। विकल्प A व D आकार को ऊँचाई से भ्रमित करते हैं।
  6. C में सरणियों व संकेतकों के विषय में निम्नलिखित में कौन सत्य हैं? (एक से अधिक सही हो सकते हैं।)

    1. a[i] व *(a + i) परिभाषा से तुल्य हैं
    2. सरणी-नाम को अन्यत्र संकेत करने हेतु पुनर्निर्दिष्ट किया जा सकता है
    3. सरणी प्राचल पाता फलन उसकी लंबाई निर्धारित नहीं कर सकता
    4. i[a] वाक्य-रचना त्रुटि है
    उत्तर देखें

    उत्तर: A — a[i] व *(a + i) परिभाषा से तुल्य हैं; C — सरणी प्राचल पाता फलन उसकी लंबाई निर्धारित नहीं कर सकता

    A व C। अनुलेखन की परिभाषा संकेतक-अंकगणित है, अतः a[i] का अर्थ *(a + i) है (A) — और चूँकि योग क्रमविनिमेय है, i[a] का अर्थ *(i + a) है, जो वही बात है और पूर्णतः वैध, अतः D असत्य है और वह प्रिय चालबाज़ प्रश्न है। C सत्य है और क्षय-नियम से निकलता है: प्राचल संकेतक है, sizeof संकेतक-चौड़ाई देता है, और लंबाई पृथक् रूप से देनी पड़ती है — इसीलिए सरणी लेता प्रत्येक C पुस्तकालय फलन एक गणना भी लेता है। B असत्य है: सरणी-नाम परिवर्तनीय lvalue नहीं है, और यही "सरणी संकेतक नहीं है" का दूसरा आधा है।
  7. भोले पुनरावर्ती फिबोनाची में स्मृतिकरण जोड़ने से उसकी समय-जटिलता बदलती है:

    1. चरघातांकी से रैखिक
    2. चरघातांकी से लघुगणकीय
    3. रैखिक से अचर
    4. वह जटिलता नहीं बदलता, केवल अचर
    उत्तर देखें

    उत्तर: A — चरघातांकी से रैखिक

    भोला संस्करण चरघातांकी है क्योंकि उप-समस्याएँ पुनः निकाली जाती हैं — fib(6) के भीतर fib(3) तीन बार, और ऊपर कहीं अधिक बुरा। स्मृतिकरण प्रत्येक परिणाम पहली बार उत्पन्न होने पर संचित करता है, अतः n भिन्न उप-समस्याओं में प्रत्येक ठीक एक बार हल होती है और प्रत्येक बाद की माँग एक खोज है: काम n में रैखिक हो जाता है। विकल्प B वह है जिस तक अभ्यर्थी विभाजन-और-जीत के साहचर्य से पहुँचता है, पर यहाँ कुछ भी समस्या आधी नहीं करता — n उप-समस्याएँ हैं और प्रत्येक अपने पूर्ववर्तियों के साथ O(1) है। साथ आने वाली स्थान-लागत ध्यान दें: सारणी हेतु O(n), जबकि भोला संस्करण केवल O(n) स्टैक प्रयोग करता था, अतः स्मृतिकरण यहाँ कुछ नहीं छोड़ता, जो असामान्य है और ध्यान देने योग्य।
  8. fib(6) की भोली गणना के भीतर fib(3) कितनी बार मूल्यांकित होता है?

    1. एक बार
    2. दो बार
    3. तीन बार
    4. पाँच बार
    उत्तर देखें

    उत्तर: C — तीन बार

    तीन बार। fib(6), fib(5) व fib(4) बुलाता है; fib(5), fib(4) व fib(3) बुलाता है; प्रत्येक fib(4), fib(3) व fib(2) बुलाता है। अतः fib(3) तक एक बार सीधे fib(5) से और एक-एक बार दोनों fib(4) कॉलों से पहुँचा जाता है — कुल तीन। इसे हाथ से अनुसरण करना ही वह कौशल है जो प्रश्न जाँच रहा है, और सामान्य प्रतिरूप देखने योग्य है: fib(n) के भीतर fib(k) कितनी बार मूल्यांकित होता है यह स्वयं एक फिबोनाची संख्या है, इसीलिए अतिरेक इतनी तेज़ी से बढ़ता है। उसी गणना से fib(2) पाँच बार मूल्यांकित होता है।