كان البحث عن ‘الأماكن القريبة’ يقتل الأداء: كيف أنقذتنا ‘شجرة الكواد’ (Quadtree) من جحيم البحث الخطي؟

يا جماعة الخير، السلام عليكم ورحمة الله.

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

المشكلة كانت واضحة زي عين الشمس: كنا بنستخدم بحث خطي (Linear Search). يعني عشان نلاقي المستخدمين القريبين من “أحمد”، كنا نمشي على كللل المستخدمين في قاعدة البيانات، واحد واحد، ونحسب المسافة بينه وبين أحمد. تخيل معي عندك 100,000 مستخدم، يعني 100,000 عملية حسابية لكل طلب بحث واحد! هاد هو اللي بسميه “جحيم البحث الخطي”.

في هذيك الليلة، وبعد ما يأست من كل الحلول التقليدية، لمعت في بالي فكرة من أيام الجامعة… محاضرة عن هياكل البيانات المكانية. كلمة ظلت ترن في أذني: “Quadtree”. وقتها ما كنت متخيل إنها رح تكون طوق النجاة اللي رح ينقذ المشروع كله.

الجحيم الذي كنا نعيش فيه: البحث الخطي (Linear Search)

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

تخيل عندك سوق كبير وغير منظم، وكل المحلات مصفوفة في خط واحد طويل. وإنت بدك تشتري زعتر من محل معين. ما في عندك خريطة ولا تقسيم، فالحل الوحيد هو إنك تمشي على السوق كله من أوله لآخره، وتسأل كل محل: “عندك زعتر؟”. هاد هو بالضبط البحث الخطي.

برمجياً، الكود كان بشبه هاد (مثال بسيط بلغة Python):


def find_nearby_linear(points, center, radius):
    nearby_points = []
    for point in points:
        #
        #
        distance = calculate_distance(point.coords, center.coords)
        if distance <= radius:
            nearby_points.append(point)
    return nearby_points

# تخيل لو points فيها مليون نقطة!
# كل عملية بحث رح تعمل مليون عملية حسابية للمسافة.

المشكلة هاي اسمها في علم الحاسوب تعقيد زمني (Time Complexity) من نوع O(N)، يعني الوقت اللي بتحتاجه العملية بزيد بشكل مباشر مع زيادة عدد النقاط (N). وهذا لا يمكن أن يستمر أو ينمو (Not Scalable) على الإطلاق.

الفجر يبزغ: ما هي شجرة الكواد (Quadtree)؟

الـ Quadtree، أو “الشجرة الرُباعية”، هي هيكل بيانات شجري (Tree Data Structure) مصمم خصيصًا لتنظيم البيانات في فضاء ثنائي الأبعاد (2D Space). الفكرة عبقرية في بساطتها: قسّم وتغلّب (Divide and Conquer).

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

كيف تعمل شجرة الكواد؟

لنبسّط الأمور، الشجرة بتتكون من “عُقد” (Nodes). كل عقدة بتمثل مساحة مستطيلة معينة.

  • العقدة الجذرية (Root Node): تمثل الخريطة كلها.
  • العقد الداخلية (Internal Nodes): كل عقدة داخلية لها أربعة “أبناء” (Children)، يمثل كل واحد منهم ربعًا من مساحة أبيه (شمال غربي، شمال شرقي، جنوب غربي، جنوب شرقي).
  • العقد الورقية (Leaf Nodes): هي نهاية الفروع. هاي العقد هي اللي بتحتوي على البيانات الفعلية (نقاط المستخدمين، المحلات، إلخ). لكل عقدة ورقية “سعة” (Capacity) معينة، يعني أقصى عدد من النقاط بتقدر تستوعبه.

عملية الإضافة (Insertion)

لما نضيف نقطة جديدة (مثلاً مستخدم جديد فتح التطبيق):

  1. نبدأ من العقدة الجذرية.
  2. نحدد في أي ربع من الأرباع الأربعة تقع النقطة الجديدة.
  3. ننتقل إلى العقدة الابن اللي بتمثل هذا الربع.
  4. نكرر العملية حتى نوصل لعقدة ورقية (Leaf Node).
  5. إذا كانت العقدة الورقية فيها مكان (أقل من سعتها القصوى)، بنضيف النقطة وانتهى.
  6. وهنا السحر: إذا كانت العقدة الورقية ممتلئة، نقوم بتقسيمها! نحولها لعقدة داخلية، وننشئ لها أربعة أبناء جدد (أربع عقد ورقية فارغة)، ثم نعيد توزيع النقاط القديمة + النقطة الجديدة على هؤلاء الأبناء الأربعة.

عملية البحث (Query) – هنا تتجلى القوة!

الآن، نعود لمشكلتنا الأصلية: كيف نجد كل النقاط داخل نطاق معين (مثلاً، دائرة نصف قطرها 5 كيلومتر)؟

بدل ما نبحث في كل النقاط، نحن نبحث في الشجرة:

  1. نبدأ من العقدة الجذرية.
  2. نسأل سؤال بسيط: هل مساحة البحث تبعتي (الـ Query Range) تتقاطع مع مساحة هذه العقدة؟
  3. إذا كان الجواب “لا”: يا سلام! نتجاهل هذه العقدة وكللللل ما تحتها من فروع ونقاط. هذا يوفر علينا آلاف العمليات الحسابية.
  4. إذا كان الجواب “نعم”:
    • إذا كانت هذه عقدة ورقية، نفحص النقاط اللي جواتها ونرى أي منها يقع بالفعل داخل نطاق البحث.
    • إذا كانت عقدة داخلية، نكرر نفس العملية على أبنائها الأربعة (نسأل كل ابن: هل نطاق البحث يتقاطع معك؟).

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

يلا نشوف الكود: مثال مبسط لشجرة الكواد

طبعًا في الواقع ما رح تبنيها من الصفر (إلا إذا كنت مثلي بتحب التحدي 😉). لكن فهم المبدأ مهم. هذا مثال بسيط جدًا بلغة JavaScript لتوضيح الفكرة:


// مجرد هيكل بسيط للتوضيح
class Point {
    constructor(x, y, data) {
        this.x = x;
        this.y = y;
        this.data = data; //
    }
}

class Rectangle {
    constructor(x, y, w, h) {
        this.x = x; // center x
        this.y = y; // center y
        this.w = w; // half-width
        this.h = h; // half-height
    }
    
    // هل هذه المنطقة تحتوي على نقطة؟
    contains(point) {
        return (point.x >= this.x - this.w &&
                point.x = this.y - this.h &&
                point.y  this.x + this.w ||
                 range.x + range.w  this.y + this.h ||
                 range.y + range.h < this.y - this.h);
    }
}

class QuadTree {
    constructor(boundary, capacity) {
        this.boundary = boundary; //
        this.capacity = capacity; //
        this.points = []; // النقاط في هذه العقدة
        this.divided = false;
    }

    subdivide() {
        let x = this.boundary.x;
        let y = this.boundary.y;
        let w = this.boundary.w / 2;
        let h = this.boundary.h / 2;

        // إنشاء الأرباع الأربعة
        let ne = new Rectangle(x + w, y - h, w, h);
        this.northeast = new QuadTree(ne, this.capacity);
        let nw = new Rectangle(x - w, y - h, w, h);
        this.northwest = new QuadTree(nw, this.capacity);
        let se = new Rectangle(x + w, y + h, w, h);
        this.southeast = new QuadTree(se, this.capacity);
        let sw = new Rectangle(x - w, y + h, w, h);
        this.southwest = new QuadTree(sw, this.capacity);

        this.divided = true;
    }

    insert(point) {
        if (!this.boundary.contains(point)) {
            return false;
        }

        if (this.points.length < this.capacity) {
            this.points.push(point);
            return true;
        } else {
            if (!this.divided) {
                this.subdivide();
            }
            // إعادة محاولة الإضافة في الربع الصحيح
            if (this.northeast.insert(point)) return true;
            if (this.northwest.insert(point)) return true;
            if (this.southeast.insert(point)) return true;
            if (this.southwest.insert(point)) return true;
        }
    }

    query(range, found) {
        if (!found) {
            found = [];
        }

        if (!this.boundary.intersects(range)) {
            // الأهم: لا تتقاطع، فتجاهل هذا الفرع بالكامل
            return found;
        } else {
            for (let p of this.points) {
                if (range.contains(p)) {
                    found.push(p);
                }
            }
            if (this.divided) {
                this.northwest.query(range, found);
                this.northeast.query(range, found);
                this.southwest.query(range, found);
                this.southeast.query(range, found);
            }
        }
        return found;
    }
}

ملاحظة: هذا الكود للتوضيح فقط. في المشاريع الحقيقية، استخدم مكتبة جاهزة ومختبرة جيدًا مثل d3-quadtree في عالم JavaScript أو ما يوازيها في لغتك المفضلة.

النتائج: من حلزون إلى فهد 🐆

العودة لقصتنا… بعد ليلة طويلة من الترميز والتجربة، طبقنا الـ Quadtree على سيرفرات التطوير. النتائج كانت… مذهلة! عملية البحث اللي كانت تاخذ 3-5 ثواني، صارت تاخذ 20-50 ميللي ثانية. نعم، قرأتها صح. انتقلنا من ثوانٍ إلى أجزاء من الثانية. الحمل على المعالج (CPU) انخفض بشكل دراماتيكي.

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

نصائح أبو عمر العملية

من خبرتي في هذا المجال، اسمحوا لي أشارككم بعض النصائح العملية:

  • اعرف “سعة” عقدتك (Node Capacity)

    اختيار قيمة الـ `capacity` (كم نقطة تستوعبها العقدة الورقية قبل أن تنقسم) هو موازنة. سعة صغيرة تعني شجرة أعمق وعمليات تقسيم أكثر. سعة كبيرة تعني أنك ستقوم ببحث خطي أكثر داخل كل عقدة. القاعدة العامة هي البدء بقيمة صغيرة (مثل 4 أو 8) وقياس الأداء وتعديلها حسب الحاجة.

  • ليست للخرائط فقط

    أي مشكلة تتضمن تفاعلات بين كيانات في فضاء ثنائي الأبعاد هي مرشح جيد لشجرة الكواد. من أشهر الاستخدامات: كشف التصادم (Collision Detection) في الألعاب، محاكاة الجسيمات الفيزيائية، وحتى في معالجة الصور.

  • لا تخترع العجل من جديد (إلا للتعلم)

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

  • انتبه لتوزيع بياناتك

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

الخلاصة والزبدة الخلاصة

الدرس الأهم اللي تعلمته من هذيك التجربة هو أن اختيار هيكل البيانات الصحيح ليس رفاهية، بل هو أساس الأداء القوي والتطبيقات القابلة للنمو. الكود الذي “يعمل فقط” هو قنبلة موقوتة ستنفجر في وجهك عندما يبدأ مشروعك بالنجاح وجذب المزيد من المستخدمين.

لذلك، في المرة القادمة التي تواجه فيها مشكلة أداء، لا تهرع مباشرة لزيادة موارد السيرفر. خذ خطوة للوراء، اشرب فنجان قهوة، وفكر: هل أنا أستخدم الأداة المناسبة لهذه المهمة؟ هل هناك طريقة أذكى لتنظيم بياناتي؟

أحيانًا، الحل لا يكون في القوة الغاشمة، بل في التنظيم الذكي… تمامًا مثل شجرة الكواد. 😉

والله ولي التوفيق.

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

تجربة المستخدم والابداع البصري

كان تطبيقنا سجنًا رقميًا: كيف أنقذتنا ‘إمكانية الوصول’ (Accessibility) من جحيم استبعاد المستخدمين؟

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

14 مايو، 2026 قراءة المزيد
الحوسبة السحابية

كانت خوادمنا خاملة 90% من الوقت: كيف أنقذتنا ‘الحوسبة بدون خوادم’ (Serverless) من جحيم التكاليف المهدرة؟

أشارككم قصة حقيقية من تجربتي كمطور، كيف كنا ندفع مئات الدولارات على خوادم شبه نائمة، وكيف كانت معمارية "الحوسبة بدون خوادم" (Serverless) طوق النجاة الذي...

14 مايو، 2026 قراءة المزيد
التوظيف وبناء الهوية التقنية

كانت إجاباتي في المقابلات عشوائية: كيف أنقذتني منهجية STAR من جحيم أسئلة “حدثنا عن موقف…”؟

هل تجد نفسك تائهًا ومشتتًا عند الإجابة على أسئلة المقابلات السلوكية؟ في هذه المقالة، أشاركك تجربتي الشخصية مع منهجية STAR، الأداة التي حولت إجاباتي الفوضوية...

14 مايو، 2026 قراءة المزيد
التوسع والأداء العالي والأحمال

كيف أنقذ ‘موازن الحمل’ خادمنا الوحيد من الانهيار؟ قصة من قلب المعركة

هل يواجه تطبيقك بطئًا وتوقفًا مفاجئًا مع زيادة عدد المستخدمين؟ في هذه المقالة، أشارككم قصتي مع انهيار خادمنا الوحيد وكيف كان 'موازن الحمل' (Load Balancer)...

14 مايو، 2026 قراءة المزيد
البودكاست