إزالة التكرارات من مصفوفة JavaScript: تحليل الأداء تحت الغطاء

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

من أين تأتي التكرارات ولماذا تمثل مشكلة؟

\n

تخيل أنك تجمع بيانات من API، أو تحلل ملف CSV، أو تعالج مدخلات المستخدم. في 90% من الحالات، ستواجه مصفوفات تحتوي على نفس العنصر مكررًا عدة مرات. هذه ليست مجرد مشكلة جمالية — فالتكرارات يمكن أن تشوه الإحصائيات، وتعطل عرض القوائم، أو تؤدي إلى حلقات لا نهائية في الخوارزميات.

\n

سؤال "إزالة القيم المكررة من مصفوفة JavaScript" على Stack Overflow حصد آلاف المشاهدات لسبب وجيه. إنها عملية أساسية ولكنها بالغة الأهمية يجب أن يعرفها كل مطور. اليوم سننظر تحت الغطاء ونحلل كيفية عمل الطرق المختلفة — من البسيطة إلى الأكثر كفاءة.

\n\n

الطريقة رقم 1: Set — الأكثر أناقة وسرعة

\n

إذا سألت مطور JavaScript حديث: "كيف تزيل التكرارات؟"، أول ما سيقوله هو Set. وهذا صحيح. ولكن كيف يعمل؟

\n\n
const numbers = [1, 2, 2, 3, 4, 4, 5];\nconst uniqueNumbers = [...new Set(numbers)];\nconsole.log(uniqueNumbers); // [1, 2, 3, 4, 5]
\n\n

ماذا يحدث تحت غطاء Set؟

\n

كائن Set يخزن قيمًا فريدة من أي نوع. عندما تمرر مصفوفة إلى المُنشئ new Set(numbers)، يقوم محرك JavaScript (V8, SpiderMonkey إلخ) بتنفيذ الخطوات التالية:

\n
    \n
  • التكرار عبر المصفوفة: يمر المحرك على كل عنصر من المصفوفة الأصلية.
  • \n
  • التحقق من التفرد: لكل عنصر، يتم استدعاء دالة تجزئة داخلية. إذا كان العنصر موجودًا بالفعل في "السلة" (bucket)، يتم تجاهله. إذا لم يكن موجودًا، يتم إضافته.
  • \n
  • تجميع النتيجة: عامل الانتشار [...set] (spread) يفك الكائن القابل للتكرار Set ويعيده إلى مصفوفة.
  • \n
\n

ملاحظة مهمة: Set يستخدم خوارزمية المقارنة SameValueZero. هذا يعني أن NaN يُعتبر مساويًا لـ NaN (على عكس ===)، بينما +0 و -0 يُعتبران قيمتين مختلفتين. عمليًا، نادرًا ما يكون لهذا أهمية، لكن من الجيد معرفته.

\n\n

أداء Set

\n

تعقيد العملية هو O(n). يتم فحص كل عنصر مرة واحدة فقط. هذا هو أفضل مؤشر بين جميع الطرق. للمصفوفات التي تحتوي على 10,000 عنصر، يمكن أن يصل الفرق بين Set والطرق الأخرى إلى عشرات الميلي ثانية — وهو أمر بالغ الأهمية للتطبيقات عالية التحميل.

\n\n

الطريقة رقم 2: filter + indexOf — الكلاسيكية

\n

قبل ظهور ES6 (2015)، كان المطورون يستخدمون مزيجًا من filter و indexOf. لا تزال هذه الطريقة موجودة في الكود القديم (legacy code):

\n\n
const numbers = [1, 2, 2, 3, 4, 4, 5];\nconst uniqueNumbers = numbers.filter((value, index, self) => {\n    return self.indexOf(value) === index;\n});\nconsole.log(uniqueNumbers); // [1, 2, 3, 4, 5]
\n\n

كيف تعمل؟

\n
    \n
  • filter يمر على كل عنصر في المصفوفة.
  • \n
  • لكل عنصر، يتم استدعاء indexOf(value) الذي يعيد أول فهرس توجد فيه هذه القيمة.
  • \n
  • إذا كان فهرس العنصر الحالي يطابق أول فهرس تم العثور عليه — فهذا يعني أن هذا هو أول ظهور، ويتم الاحتفاظ بالعنصر. إذا لم يطابق — فهذا تكرار، ويتم تجاهله.
  • \n
\n\n

المزالق

\n

المشكلة الرئيسية هي الأداء. indexOf داخليًا يستخدم بحثًا خطيًا (O(n) لكل استدعاء). في النهاية، لمصفوفة من N عنصر، يصبح التعقيد O(n²). مع 1000 عنصر، لا يزال هذا مقبولاً، ولكن مع 100,000 عنصر، يصبح البطء ملحوظًا.

\n

عيب آخر: indexOf لا يعمل بشكل صحيح مع NaN. إذا كانت المصفوفة تحتوي على NaN، فإن indexOf(NaN) سيعيد دائمًا -1، وسيتم إضافة العنصر مرة أخرى. للتحقق من NaN، تحتاج إلى استخدام

المدونات

توصيات الكتب