أنماط حل المشكلات في المقابلات التقنية: شرح نمط عداد التكرار بالتفصيل

دقائق القراءة: 10
في عالم تطوير البرمجيات سريع التطور، تُعد المقابلات التقنية محطة حاسمة لتقييم مهارات المبرمجين. بعد أن استعرضنا سابقًا استراتيجيات التحضير لهذه المقابلات، ننتقل اليوم للغوص في صميم أحد أنماط حل المشكلات الأكثر فعالية وشيوعًا: نمط عداد التكرار (Frequency Counter). سيكشف هذا المقال عن جوهر هذا النمط، وكيف يمكنه تحسين كفاءة حلولك البرمجية، ويمنحك الأدوات اللازمة للتألق في أصعب التحديات التقنية.

ما هو نمط “عداد التكرار” (Frequency Counter)؟

يعتمد نمط عداد التكرار على استخدام كائن (object) أو مجموعة (set) لجمع القيم وتكراراتها. يُستخدم هذا النمط غالبًا مع مصفوفة (array) أو سلسلة نصية (string)، ويسمح لك بتجنب الحلقات المتداخلة (nested loops) التي تؤدي إلى تعقيد زمني تربيعي (O(n²)). بدلاً من ذلك، يمكن تحقيق حلول ذات تعقيد زمني خطي (O(n)) من خلال المرور على البيانات مرة واحدة أو مرتين على الأكثر، مما يحسن أداء الكود بشكل ملحوظ.

متى يجب استخدام نمط عداد التكرار؟

يكون نمط عداد التكرار مفيدًا للغاية عندما يكون لديك عدة أجزاء من البيانات وترغب في مقارنتها ببعضها البعض بطريقة فعالة. على سبيل المثال، إذا كنت بحاجة للتحقق مما إذا كانت مجموعتان من البيانات تحتويان على نفس العناصر بنفس التكرار، أو إذا كانت إحدى المجموعات هي تحويل للأخرى (مثل التربيع أو الترتيب). يبرز هذا النمط كحل أمثل لتجنب التعقيد غير الضروري الناتج عن المقارنات المباشرة أو الحلقات المتداخلة. دعني أوضح لك مثالًا عمليًا لترى نمط عداد التكرار في العمل.

تطبيق عملي: تمرين “sameSquared

وصف المشكلة

اكتب دالة تُسمى sameSquared تقبل مصفوفتين. يجب أن تُرجع الدالة true إذا كانت كل قيمة في المصفوفة الأولى لها قيمة مربعة مقابلة في المصفوفة الثانية. يجب أن يكون تكرار القيم هو نفسه.

النتائج المتوقعة

بعد كتابة الدالة، يجب أن نتوقع أن تُرجع دالة sameSquared القيم التالية:

sameSquared([1, 2, 3], [4, 1, 9]); // true
sameSquared([1, 2, 3], [1, 9]); // false
sameSquared([1, 2, 1], [4, 4, 1]); // false
sameSquared([2, 3, 6, 8, 8], [64, 36, 4, 9, 64]); // true

البدء في التنفيذ

أولاً، باستخدام الكلمة المفتاحية function، نُنشئ دالة بالمعرف sameSquared:

function sameSquared ( ) {

تحتاج دالتنا sameSquared إلى معاملين: مصفوفة أولى (firstArr) ومصفوفة ثانية (secondArr). في هذا المثال، سنمرر القيم [1, 2, 3] و [4, 1, 9].

function sameSquared ( firstArr, secondArr ) {

التحقق من الحالات الهامشية (Edge Cases)

داخل كتلة الدالة، نريد معالجة بعض الحالات الهامشية. أولاً، نحتاج إلى التحقق من أن كلا المعاملين لهما قيم صحيحة (truthy values)، أي ليسا null أو undefined وما إلى ذلك. يمكننا التحقق من وجود قيمة خاطئة (falsy value) باستخدام المعامل !. إذا كانت firstArr أو secondArr خاطئة، فإننا نُرجع false.

function sameSquared ( firstArr, secondArr ) {
    if (!firstArr || !secondArr) return false;
}

الحالة الهامشية التالية التي نريد أخذها في الاعتبار هي التأكد من أن طول كلتا المصفوفتين متماثل. إذا كانتا مختلفتين، فإننا نعلم أنهما لا يمكن أن تحتويان على عدد متساوٍ من القيم المشتركة. من خلال التحقق من الخاصية length في كلا المعاملين، يمكننا تحديد ما إذا كانتا متماثلتين. إذا لم تكونا كذلك، نُرجع false.

function sameSquared ( firstArr, secondArr ) {
    if (!firstArr || !secondArr) return false;
    if (firstArr.length !== secondArr.length) return false;
}

بناء “قاموس” لتجنب الحلقات المتداخلة

نحتاج إلى تتبع جميع القيم في واحدة على الأقل من المصفوفتين. للقيام بذلك، ولتجنب حلقة متداخلة، يمكننا تخزين هذه القيم في جدول تجزئة (hash table) أو كائن (object). سأسميه lookup.

function sameSquared ( firstArr, secondArr ) {
    if (!firstArr || !secondArr) return false;
    if (firstArr.length !== secondArr.length) return false;
    const lookup = {};
}

باستخدام حلقة for...of، نُكرر عبر المصفوفة firstArr. داخل كتلة for...of، نُعيّن المفتاح ليكون نتيجة value * value (القيمة مربعة). ستكون القيمة في زوج المفتاح/القيمة هذا عبارة عن عداد تكرار يعكس عدد المرات التي شوهدت فيها قيمة معينة في firstArr. أولاً، نتحقق مما إذا كان lookup يحتوي على إدخال لـ value * value، إذا كان كذلك، نضيف 1 إليه. إذا لم يكن كذلك، نُعيّن القيمة إلى 0 ثم نضيف 1.

function sameSquared ( firstArr, secondArr ) {
    if (!firstArr || !secondArr) return false;
    if (firstArr.length !== secondArr.length) return false;
    const lookup = {};

    for (const value of firstArr) {
        lookup[value * value] = (lookup[value * value] || 0) + 1;
    }
}

بمجرد انتهاء الحلقة عبر firstArr، يجب أن يحتوي الكائن lookup على هذه القيم (بافتراض المدخلات [1, 2, 3]):

{ 1: 1, 4: 1, 9: 1 }

مقارنة قيم المصفوفات

الآن بعد أن كررنا عبر جميع القيم في firstArr وخزناها كقيمها المربعة، نريد مقارنة تلك القيم بالقيم في secondArr. نبدأ بإنشاء حلقة for...of أخرى. في السطر الأول داخل كتلة for...of الجديدة، نكتب عبارة شرطية للتحقق مما إذا كانت القيمة الحالية من secondArr غير موجودة داخل lookup أو أن تكرارها أصبح صفرًا. إذا لم تكن كذلك، نتوقف عن التكرار ونُرجع false. إذا كانت القيمة من secondArr موجودة في lookup ولها تكرار أكبر من صفر، فإننا نريد إنقاص قيمة هذا الإدخال.

function sameSquared ( firstArr, secondArr ) {
    if (!firstArr || !secondArr) return false;
    if (firstArr.length !== secondArr.length) return false;
    const lookup = {};

    for (const value of firstArr) {
        lookup[value * value] = (lookup[value * value] || 0) + 1;
    }

    for (const secondValue of secondArr) {
        if (!lookup[secondValue]) {
            return false;
        }
        lookup[secondValue] -= 1;
    }
}

بعد الانتهاء من التكرار عبر secondArr، يجب أن يحتوي الكائن lookup على هذه القيم (بافتراض المدخلات [4, 1, 9]):

{ 1: 0, 4: 0, 9: 0 }

إنهاء دالة “sameSquared

إذا انتهينا من التكرار عبر secondArr دون إرجاع false، فهذا يعني أن firstArr تحتوي على جميع القيم التي هي في حالة مربعة في secondArr، وبنفس التكرار. لذلك، نُرجع true خارج حلقة for...of.

function sameSquared ( firstArr, secondArr ) {
    if (!firstArr || !secondArr) return false;
    if (firstArr.length !== secondArr.length) return false;
    const lookup = {};

    for (const value of firstArr) {
        lookup[value * value] = (lookup[value * value] || 0) + 1;
    }

    for (const secondValue of secondArr) {
        if (!lookup[secondValue]) {
            return false;
        }
        lookup[secondValue] -= 1;
    }

    return true;
}

مثال آخر: تمرين “isAnagram

دعني أوضح لك مثالًا آخر يُستخدم بشكل شائع جدًا في تقييمات البرمجة (لذا ربما تكون قد رأيت هذه المشكلة من قبل).

وصف المشكلة

اكتب دالة تُسمى isAnagram تقبل سلسلتين نصيتين (strings). يجب أن تُرجع الدالة true إذا كانت السلسلتان النصيتان المُمررتان إحداهما هي جناس (anagram) للأخرى.

النتائج المتوقعة

بعد كتابة الدالة، يجب أن نتوقع أن تُرجع دالة isAnagram القيم التالية:

isAnagram("silent", "listen"); // true
isAnagram("martin", "nitram"); // true
isAnagram("cat", "tag"); // false
isAnagram("rat", "tar"); // true

البدء في التنفيذ

أولاً، باستخدام الكلمة المفتاحية function، نُنشئ دالة بالمعرف isAnagram:

function isAnagram ( ) {

تحتاج دالتنا isAnagram إلى معاملين: سلسلة نصية أولى (firstStr) وسلسلة نصية ثانية (secondStr). في هذا المثال، سنمرر القيم silent و listen.

function isAnagram ( firstStr, secondStr ) {

التحقق من الحالات الهامشية

في الأسطر القليلة الأولى من كتلة الدالة، نريد معالجة بعض الحالات الهامشية، تمامًا كما في المثال الأول. على غرار sameSquared، نحتاج إلى التحقق من أن كلا المعاملين لهما قيم صحيحة (truthy values). يمكننا التحقق من وجود قيمة خاطئة باستخدام المعامل !. إذا كانت firstStr أو secondStr خاطئة، فإننا نُرجع false.

function isAnagram ( firstStr, secondStr ) {
    if (!firstStr || !secondStr) return false;
}

الحالة الهامشية التالية التي نريد أخذها في الاعتبار هي التأكد من أن طول كلتا السلسلتين النصيتين متماثل. إذا كانتا مختلفتين، فإننا نعلم أنهما لا يمكن أن تكونا جناسًا لبعضهما البعض. من خلال التحقق من الخاصية length في كلا المعاملين، يمكننا تحديد ما إذا كانتا متماثلتين. إذا لم تكونا كذلك، نُرجع false.

function isAnagram ( firstStr, secondStr ) {
    if (!firstStr || !secondStr) return false;
    if (firstStr.length !== secondStr.length) return false;
}

بناء “قاموس” لتجنب الحلقات المتداخلة

تذكر، نحن نستخدم نمط عداد التكرار ونحتاج إلى تتبع جميع القيم في واحدة على الأقل من السلاسل النصية. الآن نعلم أن أفضل طريقة للتعامل مع هذا هي تخزين هذه القيم في جدول تجزئة (hash table) أو كائن (object). للحفاظ على الاتساق، سأسميه lookup مرة أخرى.

function isAnagram ( firstStr, secondStr ) {
    if (!firstStr || !secondStr) return false;
    if (firstStr.length !== secondStr.length) return false;
    const lookup = {};
}

باستخدام حلقة for...of، نُكرر عبر السلسلة النصية firstStr. داخل كتلة for...of، نُعيّن المفتاح ليكون الحرف الحالي. ستكون القيمة في زوج المفتاح/القيمة هذا عبارة عن عداد تكرار يعكس عدد المرات التي شوهد فيها حرف معين في firstStr. نستخدم التعبير (lookup[char] || 0) + 1 لزيادة قيمة الحرف بمقدار 1، أو تعيينها إلى 1 إذا لم تكن موجودة مسبقًا.

function isAnagram ( firstStr, secondStr ) {
    if (!firstStr || !secondStr) return false;
    if (firstStr.length !== secondStr.length) return false;
    const lookup = {};

    for (const char of firstStr) {
        lookup[char] = (lookup[char] || 0) + 1;
    }
}

بمجرد انتهاء الحلقة عبر firstStr، يجب أن يحتوي الكائن lookup على هذه القيم (بافتراض المدخل silent):

{ s: 1, i: 1, l: 1, e: 1, n: 1, t: 1 }

مقارنة قيم السلاسل النصية

الآن بعد أن كررنا عبر جميع القيم في firstStr وخزنا تكراراتها، نريد مقارنة تلك القيم بالقيم في secondStr. نبدأ بإنشاء حلقة for...of أخرى. في السطر الأول داخل كتلة for...of الجديدة، نكتب عبارة شرطية للتحقق مما إذا كانت القيمة الحالية من secondStr غير موجودة داخل lookup أو أن تكرارها أصبح صفرًا (مما يعني أنها لم تعد موجودة بالعدد الكافي). إذا لم تكن كذلك، نريد إيقاف التكرار ونُرجع false. بخلاف ذلك، إذا كانت القيمة من secondStr موجودة في lookup ولها تكرار أكبر من صفر، فإننا نريد إنقاص قيمة هذا الإدخال.

function isAnagram ( firstStr, secondStr ) {
    if (!firstStr || !secondStr) return false;
    if (firstStr.length !== secondStr.length) return false;
    const lookup = {};

    for (const char of firstStr) {
        lookup[char] = (lookup[char] || 0) + 1;
    }

    for (const char of secondStr) {
        if (!lookup[char]) {
            return false;
        }
        lookup[char] -= 1;
    }
}

بعد الانتهاء من التكرار عبر secondStr، يجب أن يحتوي الكائن lookup على هذه القيم (بافتراض المدخل listen):

{ s: 0, i: 0, l: 0, e: 0, n: 0, t: 0 }

إنهاء دالة “isAnagram

إذا انتهينا من التكرار عبر secondStr دون إرجاع false، فهذا يعني أن firstStr تحتوي على جميع الأحرف الموجودة في secondStr، وبنفس التكرار، مما يجعلها جناسًا. لذلك، نُرجع true خارج حلقة for...of.

function isAnagram ( firstStr, secondStr ) {
    if (!firstStr || !secondStr) return false;
    if (firstStr.length !== secondStr.length) return false;
    const lookup = {};

    for (const char of firstStr) {
        lookup[char] = (lookup[char] || 0) + 1;
    }

    for (const char of secondStr) {
        if (!lookup[char]) {
            return false;
        }
        lookup[char] -= 1;
    }

    return true;
}

ملخص

آمل أن يكون هذا الاستعراض المتعمق لنمط عداد التكرار (Frequency Counter) مفيدًا. الآن بعد أن عرفت كيف يعمل هذا النمط، أنا واثق من أنك ستتمكن من إبهار القائمين على المقابلات من خلال عرض مهاراتك على مستوى أعلى. في مقالي التالي، سأناقش نمطًا آخر شائعًا لحل المشكلات يُسمى النافذة المنزلقة (Sliding Window). شكرًا للقراءة، ومقابلات موفقة!

الخلاصة التقنية

يُعد نمط عداد التكرار (Frequency Counter) أداة برمجية قوية لا غنى عنها في صندوق أدوات أي مطور، خاصة عند التعامل مع المشكلات التي تتطلب مقارنة تكرارات العناصر داخل هياكل البيانات. تكمن قوته الرئيسية في قدرته على تحويل التعقيد الزمني من تربيعي (O(n²)) إلى خطي (O(n)) عن طريق الاستفادة من جداول التجزئة (hash tables) أو الكائنات (objects) لتخزين التكرارات. هذا التحسين لا يقلل فقط من وقت تنفيذ الكود بشكل كبير، بل يجعل الحلول أكثر قابلية للتوسع وفعالية في استهلاك الموارد. إتقان هذا النمط يعكس فهمًا عميقًا لكفاءة الخوارزميات وهياكل البيانات، وهي مهارة أساسية للنجاح في المقابلات التقنية وفي بناء أنظمة برمجية عالية الأداء.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *