مرحباً بالخوارزميات › تحليل التعقيد 4 / 19 السابق التالي

تمارين

مراجعة المفاهيم

التعقيد الزمني والتعقيد المكاني للتكرار والتعاود

تحسب الدالتان التاليتان كلتاهما 1+2+⋯+n1 + 2 + \dots + n (افترض أن n≥1n \ge 1). اجعل قيمة n تساوي 4، وأجب عن الأسئلة باتباع ترتيب تنفيذ البرنامج الفعلي، ثم قارن كفاءة الطريقتين.

Go

/* الجمع بالتكرار */
func sumIter(n int) int {
	res := 0
	for i := 1; i <= n; i++ {
		res += i
	}
	return res
}

TypeScript

/* الجمع بالتكرار */
function sumIter(n: number): number {
    let res = 0;
    for (let i = 1; i <= n; i++) {
        res += i;
    }
    return res;
}
  1. عند تشغيل الدالة التكرارية بقيمة n = 4، ما قيمة المجمّع res بعد كل دورة من دورات الحلقة؟
  2. عند تشغيل الدالة التعاودية بقيمة n = 4، ما القيم التي تأخذها الوسيطة n بالترتيب؟ وعندما تعود الاستدعاءات من أعمق مستوى، كيف نحصل على النتيجة؟
  3. ما التعقيد الزمني والتعقيد المكاني للطريقتين؟ اشرح استدلالك باستخدام عمليتي التنفيذ الواردتين في السؤالين 1 و2.

الإجابة

  1. يأخذ متغير الحلقة i القيم 1, 2, 3, 4. وبعد كل دورة يصبح res على الترتيب 1, 3, 6, 10، لذا تعيد الدالة التكرارية القيمة 10.

  2. تأخذ الوسيطة n القيم 4 → 3 → 2 → 1. ويعيد أعمق استدعاء القيمة 1. ثم تحصل الاستدعاءات المتبقية على 2 + 1 = 3 و3 + 3 = 6 و4 + 6 = 10 بهذا الترتيب. وعند أعمق نقطة، تكون استدعاءات الدالة الأربعة كلها ما تزال غير مكتملة.

  3. تؤدي الدالتان عدداً من دورات الحلقة أو الاستدعاءات يتناسب مع nn، لذا يكون التعقيد الزمني لكلتيهما O(n)O(n). أما التعقيد المكاني فيختلف بينهما. فالنسخة التكرارية تستخدم عدداً ثابتاً من المتغيرات فقط، لذا يكون تعقيدها المكاني O(1)O(1). وفي النسخة التعاودية، يجب أن تنتظر الاستدعاءات الأسبق نتيجةً قبل أن تعود، لذا يحمل مكدس الاستدعاءات حتى nn استدعاء في الوقت نفسه. وتعقيدها المكاني O(n)O(n).

    وعند تحليل التعقيد المكاني، تذكّر أن تشمل المساحة التي تستخدمها الاستدعاءات التعاودية إلى جانب المتغيرات المكتوبة في الشيفرة.

التعقيد الزمني لثلاث مقاطع شيفرة

يأخذ كل مقطع من مقاطع الشيفرة التالية عدداً صحيحاً موجباً nn كمدخل. رتّبها من الأدنى إلى الأعلى من حيث التعقيد الزمني، واذكر تعقيد كل واحد منها.

Go

/* حلقة خطية */
func linearLoop(n int) int {
	res := 0
	for i := 0; i < n; i++ {
		res += i
	}
	return res
}

TypeScript

/* حلقة خطية */
function linearLoop(n: number): number {
    let res = 0;
    for (let i = 0; i < n; i++) {
        res += i;
    }
    return res;
}

الإجابة

الترتيب من الأدنى إلى الأعلى هو: المقطع 3 بتعقيد O(log⁡n)O(\log n)، ثم المقطع 1 بتعقيد O(n)O(n)، ثم المقطع 2 بتعقيد O(n2)O(n^2). فالمقطع 3 ينصّف nn في كل دورة، لذا يعمل نحو log⁡2n\log_2 n مرة. أما حلقة المقطع 1 فتعمل nn مرة بالضبط. وتعمل الحلقة الداخلية في المقطع 2 n,n−1,…,1n,n-1,\dots,1 مرة، أي n(n+1)/2n(n+1)/2 في المجموع، لذا يكون تعقيده الزمني تربيعياً.

أي طريقة للعكس تستهلك مساحة أقل؟

توجد طريقتان لعكس جميع عناصر المصفوفة nums:

  1. أنشئ مصفوفة جديدة res بالطول نفسه، وانسخ العناصر إليها بترتيب معكوس، ثم أعِدها.

  2. حرّك فهرسين i وj إلى الداخل من البداية والنهاية، مع تبديل nums[i] وnums[j] في كل خطوة.

    ما التعقيد المكاني لكل طريقة؟ وأيّهما عملية «في المكان»؟

الإجابة

  1. تحتاج هذه الطريقة إلى مصفوفة مساعدة بالطول نفسه للمدخل، لذا يكون تعقيدها المكاني O(n)O(n).

  2. لا تستخدم هذه الطريقة سوى متغيري فهرس، لذا يكون تعقيدها المكاني O(1)O(1). وهي عملية في المكان.

    ولاحظ أن العكس في المكان يغيّر مصفوفة الإدخال، لذا لا يُفضَّل إلا عندما يكون تعديل الإدخال مسموحاً. أما إذا وجب الحفاظ على المصفوفة الأصلية، فلا مفرّ من تكلفة النسخ في الطريقة الأولى.

تمارين برمجية

عدد فيبوناتشي

تُعرَّف متتالية فيبوناتشي بـF(0)=0F(0)=0 وF(1)=1F(1)=1، ولأجل n≥2n\ge2 يكون F(n)=F(n−1)+F(n−2)F(n)=F(n-1)+F(n-2).

بمعطى عدد صحيح غير سالب n، استخدم حلقة لحساب F(n)F(n) وإعادته. ولا تستخدم التعاود.

تلميحات

  1. تعامل مع حالتي كون n تساوي 0 أو 1 على حدة
  2. لا نحتاج إلى حساب الحد التالي سوى إلى الحدين السابقين؛ فلا حاجة إلى تخزين المتتالية كاملة
  3. عند تحديث المتغيرين، احرص ألا تكتب فوق قيمة قديمة قبل استخدامها

LeetCode{ .rounded-button .exercise-button target="_blank" rel="noopener noreferrer" }