انتقل إلى المحتوى
Topos Uranos

ملخص

كثير من البيانات لا يشكّل صفًا واحدًا، بل جدولًا مربّعًا: درجات فصل دراسي لكل طالب ولكل اختبار، ورقعة لعب، ومصفوفة رياضية. يقدّم هذا الدرس الجدول، البنية ذات البعدين، في صورتيه في C++: المتجه من المتجهات، الذي يتقرر حجمه عند التنفيذ، والمصفوفة من المصفوفات، التي يُعرف حجمها عند الترجمة. ويلي ذلك المرور على الصفوف وعلى الأعمدة؛ والمنقولة وجداء المصفوفات الرياضية، ذو الكلفة التكعيبية؛ والخلايا المجاورة لخلية ما، التي تُلزم بالانتباه إلى الحواف؛ والجدول المخزَّن في متجه واحد بالفهرس i * columns + j، وهي الطريقة التي تراه بها الذاكرة.

أهداف التعلم

  1. تمثيل الجدول بوصفه std::vector<std::vector<T>> أو بوصفه std::array من std::array، والاختيار بينهما بحسب ما إذا كان الحجم معروفًا عند الترجمة أم لا.
  2. المرور على الجدول صفًا صفًا وعمودًا عمودًا، وحساب مجاميع كل صف وكل عمود ومتوسطاتها.
  3. تنفيذ عمليات على الجداول (المنقولة، وجداء المصفوفات الرياضية، وعدّ الخلايا المجاورة لخلية) مع الحرص على ألا يخرج أي فهرس عن الحواف.
  4. تمثيل الجدول في متجه مسطَّح بالفهرس i * columns + j، وشرح التقابل بين الخلية وموضعها بمثال.

الجداول

الصفوف والأعمدة

الجدول ذو mm من الصفوف وnn من الأعمدة مجموعة من m⋅nm \cdot n من البيانات من النوع نفسه، يُعيَّن كل منها بفهرسين: فهرس صفه، ii، مع 0≤i<m0 \leq i < m، وفهرس عموده، jj، مع 0≤j<n0 \leq j < n. وتسمّيه الرياضيات مصفوفة رياضية وتكتب aija_{ij}؛ وتدرس دورة «الجبر والهندسة 2» المصفوفات الرياضية وعملياتها، ويكتب هذا الدرس بعضها برامجَ. وترتيب الفهارس لا يقبل استثناءات: الصف أولًا، ثم العمود. والخلط بينهما هو أكثر الأخطاء شيوعًا مع الجداول، ولا يستطيع المترجم أن ينبّه إليه، لأن الفهرسين من النوع نفسه.

قدّم الدرس المتجهات التسلسل ذا الحجم المتغير. وإذا كان الأمر كذلك، فالجدول تسلسل من الصفوف، وكل صف بدوره تسلسل: متجه عناصره متجهات، std::vector<std::vector<int>>.

برنامج بلغة C++
#include <print>#include <vector>​int main(){    std::vector<std::vector<int>> table{{1, 2, 3}, {4, 5, 6}};    std::println("{}", table);    std::println("عدد الصفوف {}، عدد الأعمدة {}", table.size(), table[0].size());    std::println("الصف 1: {}؛ الصف 1، العمود 2: {}", table[1], table[1][2]);    table[0][1] = 20;    for (const std::vector<int>& row : table) {        for (const int value : row) {            std::print("{:>4}", value);        }        std::println("");    }    const std::vector<std::vector<int>> zeros(3, std::vector<int>(4));    std::println("{}", zeros);}
المخرجات
[[1, 2, 3], [4, 5, 6]]عدد الصفوف 2، عدد الأعمدة 3الصف 1: [4, 5, 6]؛ الصف 1، العمود 2: 6   1  20   3   4   5   6[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]

يكتب السطر 6 الجدول قائمةً من الصفوف، كل منها بين قوسيه المعقوفين، وتطبعه صياغة النطاقات بالشكل نفسه، بأقواس مربعة. وtable.size() هو عدد الصفوف، وtable[0].size() طول الصف الأول، وهو في جدول سليم البنية عدد الأعمدة. وtable[1] هو الصف 1 كله، أي std::vector<int>، وtable[1][2] هو العنصر 2 من ذلك الصف: فالفهرس المزدوج ليس عملية جديدة، بل وصولان متتاليان، يختار أولهما الصف ويختار الثاني، داخله، العمود. وتكتب الأسطر من 11 إلى 16 الجدول على هيئة مربّع بحلقتي for على النطاق متداخلتين؛ ويؤخذ الصف بالمرجع الثابت حتى لا يُنسخ في كل جولة. وينشئ السطر 17 جدولًا من 3 في 4 مملوءًا بالأصفار: بالأقواس الهلالية، كما في الدرس المتجهات، تُطلب 3 نسخ من صف من أربعة أصفار.

مثالالمرور على الصفوف وعلى الأعمدة

جدول من 3 في 4، يُمَرّ عليه صفًا صفًا (i في الحلقة الخارجية) ثم عمودًا عمودًا (بعد تبديل الحلقتين). وتعرض كل خلية رقم ترتيب زيارتها.

البرهان

  1. الجدول، وفهارس الصفوف على يساره وفهارس الأعمدة فوقه.

  2. على الصفوف: مع i=0i = 0، تمرّ الحلقة الداخلية على j=0,1,2,3j = 0, 1, 2, 3.

  3. مع i=1i = 1، تبدأ الحلقة الداخلية من جديد عند j=0j = 0.

  4. مع i=2i = 2: مُرَّ على الجدول كما يُقرأ النص، سطرًا بعد سطر.

  5. على الأعمدة: مع j=0j = 0، تمرّ الحلقة الداخلية على i=0,1,2i = 0, 1, 2.

  6. مع j=1j = 1، العمود الثاني.

  7. مع j=2j = 2.

  8. مع j=3j = 3. زِيرت كل خلية مرة أخرى مرة واحدة بالضبط: تبديل الحلقتين لا يغيّر ما يُزار، بل متى يُزار.

صفوف بأطوال مختلفة

لا شيء يُلزم صفوف المتجه من المتجهات بأن تكون بالطول نفسه: فكل منها متجه مستقل. وقد يكون هذا هو المطلوب أحيانًا، كما في مثلث باسكال؛ لكن في الجدول، وهو مستطيل بالتعريف، يكون الصف الأقصر خطأً في البيانات، والبرنامج الذي يستعمل table[0].size() عددًا للأعمدة يقرأ خارجه. ولذلك يجب التحقق من الجدول الآتي من الخارج.

برنامج بلغة C++
#include <print>#include <vector>​// الشرط المسبق: لا شيء.// الشرط اللاحق: يعيد true إذا كان في table صف واحد على الأقل وكانت كل// صفوفه بطول الصف الأول.bool isRectangular(const std::vector<std::vector<int>>& table){    if (table.empty()) {        return false;    }    for (const std::vector<int>& row : table) {        if (row.size() != table[0].size()) {            return false;        }    }    return true;}​int main(){    const std::vector<std::vector<int>> pascal{{1}, {1, 1}, {1, 2, 1}, {1, 3, 3, 1}};    const std::vector<std::vector<int>> grid{{1, 2, 3}, {4, 5, 6}};    std::println("{} {}", pascal, isRectangular(pascal));    std::println("{} {}", grid, isRectangular(grid));}
المخرجات
[[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]] false[[1, 2, 3], [4, 5, 6]] true

تستبعد الدالة الجدول الخالي من الصفوف، الذي لا يوجد فيه table[0]. ودوال هذا الدرس التي تقرأ table[0].size() تشترط في شرطها المسبق جدولًا مستطيلًا وغير فارغ، وisRectangular تتيح ضمان ذلك.

المصفوفة من المصفوفات

حين تكون الأبعاد معروفة عند الترجمة (رقعة شطرنج، مربع سحري)، يمكن أن يكون الجدول std::array من std::array: وstd::array<std::array<int, 3>, 3> يُقرأ من الداخل إلى الخارج، «3 صفوف من 3 أعداد صحيحة». ويصلح مثالًا المربعُ السحري من الرتبة 3، الذي مجموع كل صف وكل عمود وكل قطر فيه 15.

برنامج بلغة C++
#include <array>#include <print>​int main(){    constexpr std::array<std::array<int, 3>, 3> magic{{        {2, 7, 6},        {9, 5, 1},        {4, 3, 8},    }};    std::println("{}", magic);    std::println("المركز {}، {} بايت", magic[1][1], sizeof(magic));}
المخرجات
[[2, 7, 6], [9, 5, 1], [4, 3, 8]]المركز 5، 36 بايت

الأقواس المعقوفة ثلاثة عند كل طرف. فلـ std::array عضو واحد فقط، هو مصفوفة C، ولذلك فالقوس الخارجي هو قوس std::array، والثاني قوس تلك المصفوفة الداخلية، والثالث وحده قوس كل صف. وبقوسين اثنين، يؤخذ الصف الأول مهيِّئًا للمصفوفة الداخلية، ولا يبقى للثاني ما يهيّئه.

برنامج بلغة C++
#include <array>#include <print>​int main(){    const std::array<std::array<int, 3>, 2> table{{1, 2, 3}, {4, 5, 6}};    std::println("{}", table);}
رسائل الخطأ من المترجم
main.cpp: In function ‘int main()’:main.cpp:6:71: error: too many initializers for ‘const std::array<std::array<int, 3>, 2>’    6 |     const std::array<std::array<int, 3>, 2> table{{1, 2, 3}, {4, 5, 6}};      |                                                                       ^

Too many initializers: القائمة {4, 5, 6} زائدة. والقائمة المسطَّحة، {1, 2, 3, 4, 5, 6}، تُترجم هي أيضًا وتملأ الصفوف بالترتيب، لكنها تُخفي شكل الجدول؛ وتكتب الدورة الأقواس الثلاثة.

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

  • إذا عُرف البعدان كلاهما عند الترجمة، فالمصفوفة من المصفوفات: كتلة واحدة، وشكل مستطيل يضمنه النوع، وإمكان constexpr.
  • إذا لم يُعرف أحدهما إلا عند التنفيذ، فالمتجه من المتجهات، مع وجوب التحقق من أن الصفوف بالطول نفسه، أو المتجه المسطَّح في القسم الأخير.