البحث في Rust: إيجاد فهرس عنصر مصفوفة بأقصى أداء

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

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

ابدأ الدورة

مقدمة: لماذا البحث عن فهرس في Rust ليس مجرد "ابحث وأرجع"؟

\n\n

كل مطور ينتقل إلى Rust من Python أو JavaScript، عاجلاً أم آجلاً، يتساءل: "كيف يمكنني إيجاد فهرس عنصر في مصفوفة أو متجه؟" في Python يتم ذلك بسطر واحد list.index(element)، وفي JavaScript array.indexOf(element). لكن Rust، بصفته مثالياً حقيقياً، لا يقدم طريقة واحدة، بل عدة طرق، لكل منها تفاصيلها الدقيقة في الأداء والأمان.

\n\n

دعنا نفهم كيفية إيجاد فهرس عنصر في مجموعات Rust (المصفوفات، المتجهات، والشرائح) بحيث يكون الكود ليس صحيحاً فحسب، بل سريعاً أيضاً. ففي البرمجة النظامية، كل نانوثانية لها حساب!

\n\n

الأساس: الطرق الأساسية للبحث عن فهرس في Rust

\n\n

في المكتبة القياسية لـ Rust، هناك "لاعبان" رئيسيان للبحث عن الفهرس: position() و rposition(). وهما متاحان لأي مُكرر، وبالتالي للمصفوفات والمتجهات والشرائح.

\n\n

طريقة position(): البحث من اليسار إلى اليمين

\n\n

position() تأخذ إغلاقاً (closure) وتُرجع Option<usize> — فهرس أول عنصر يحقق الشرط، أو None إذا لم يتم العثور على هذا العنصر.

\n\n
fn main() {\n    let numbers = vec![10, 20, 30, 40, 50];\n    \n    // نبحث عن أول ظهور للرقم 30\n    let index = numbers.iter().position(|&x| x == 30);\n    \n    match index {\n        Some(i) => println!("فهرس الرقم 30: {}", i), // الإخراج: 2\n        None => println!("العنصر غير موجود"),\n    }\n}\n
\n\n

لاحظ: نحن نستدعي iter() قبل position(). هذا مهم، لأن position() هي طريقة من سمة (trait) Iterator، وليست من المجموعة نفسها.

\n\n

طريقة rposition(): البحث من اليمين إلى اليسار

\n\n

إذا كنت بحاجة إلى آخر فهرس لظهور عنصر، استخدم rposition(). تعمل بشكل مشابه، لكنها تجتاز المجموعة من النهاية:

\n\n
fn main() {\n    let data = vec!['a', 'b', 'c', 'b', 'a'];\n    \n    // نجد آخر ظهور لـ 'b'\n    let last_b = data.iter().rposition(|&c| c == 'b');\n    \n    println!("آخر 'b' في الفهرس: {:?}", last_b); // Some(3)\n}\n
\n\n

قيد مهم: rposition() تعمل فقط مع المجموعات التي تدعم الاجتياز ثنائي الاتجاه (سمة DoubleEndedIterator). لحسن الحظ، جميع مجموعات Rust القياسية تفي بهذا المطلب.

\n\n

مقارنة الأداء: position() مقابل الاجتياز اليدوي

\n\n

يكتب العديد من المبتدئين حلقات يدوية، معتقدين أنها أسرع. دعنا نتحقق من هذا الافتراض عملياً.

\n\n

لننظر في طريقتين للبحث عن فهرس عنصر في متجه يحتوي على مليون عدد صحيح:

\n\n
use std::time::Instant;\n\nfn main() {\n    let large_vec: Vec<i32> = (0..1_000_000).collect();\n    let target = 999_999;\n    \n    // الطريقة 1: position()\n    let start = Instant::now();\n    let pos1 = large_vec.iter().position(|&x| x == target);\n    let duration1 = start.elapsed();\n    println!("position(): {:?}, الوقت: {:?}", pos1, duration1);\n    \n    // الطريقة 2: حلقة يدوية\n    let start = Instant::now();\n    let mut pos2 = None;\n    for (i, &val) in large_vec.iter().enumerate() {\n        if val == target {\n            pos2 = Some(i);\n            break;\n        }\n    }\n    let duration2 = start.elapsed();\n    println!("الحلقة اليدوية: {:?}, الوقت: {:?}", pos2, duration2);\n}\n
\n\n

النتائج على المعالجات الحديثة تُظهر عادةً أن position() تعمل بنفس سرعة الحلقة اليدوية، وأحياناً أسرع بفضل التحسينات الداخلية لمُجمّع Rust (LLVM). الخلاصة: استخدم position() — فهي اصطلاحية (idiomatic)، آمنة، ولا تقل في السرعة.

\n\n

البحث الثنائي: عندما تكون البيانات مرتبة

\n\n

إذا كانت مجموعتك مرتبة، فيجب عليك استخدام binary_se

المدونات

توصيات الكتب