توثيق الخوارزميات الشامل

دليل مفصل للخوارزميات وهياكل البيانات باللغة العربية

نظرة عامة على الخوارزميات

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

الخصائص الأساسية للخوارزميات الجيدة:

  • الصحة: يجب أن تعطي الخوارزمية النتيجة الصحيحة لجميع المدخلات الصالحة.
  • الفعالية: يجب أن تستخدم الخوارزمية أقل قدر ممكن من الموارد (الوقت والذاكرة).
  • الوضوح: يجب أن تكون الخوارزمية واضحة وسهلة الفهم.
  • النهاية: يجب أن تنتهي الخوارزمية بعد عدد محدود من الخطوات.

تعقيد الخوارزميات

تعقيد الخوارزمية هو مقياس لكفاءتها وفعاليتها. يتم قياس التعقيد عادة من حيث:

تعقيد الوقت (Time Complexity)

يقيس مقدار الوقت الذي تستغرقه الخوارزمية كدالة في حجم المدخلات. يُعبّر عنه بالتدوين الكبير (Big O Notation):

  • O(1) تعقيد ثابت - العمليات لا تعتمد على حجم المدخلات
  • O(log n) تعقيد لوغاريتمي - فعال جدًا للمدخلات الكبيرة
  • O(n) تعقيد خطي - يتناسب طرديًا مع حجم المدخلات
  • O(n log n) تعقيد متوسط - شائع في خوارزميات الفرز الفعالة
  • O(n²) تعقيد تربيعي - أقل كفاءة مع المدخلات الكبيرة
  • O(2ⁿ) تعقيد أسي - غير عملي للمدخلات الكبيرة

تعقيد المساحة (Space Complexity)

يقيس مقدار الذاكرة التي تستهلكها الخوارزمية. يتم التعبير عنه أيضًا باستخدام التدوين الكبير (Big O Notation).

خوارزميات الفرز

فرز الفقاعة (Bubble Sort)

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

الخطوات:

  1. ابدأ من أول عنصرين في المصفوفة.
  2. إذا كان العنصر الأول أكبر من الثاني، بدلهما.
  3. انتقل للعنصرين التاليين وكرر العملية.
  4. بعد الوصول لنهاية المصفوفة، سيكون أكبر عنصر في آخر المصفوفة.
  5. كرر العملية لكن بدون النظر للعناصر التي تم ترتيبها في النهاية.

التعقيد الزمني:

  • الحالة المتوسطة: O(n²)
  • أسوأ حالة: O(n²)
  • أفضل حالة: O(n) (عندما تكون المصفوفة مرتبة أصلاً)

تنفيذ الخوارزمية بلغة JavaScript:

function bubbleSort(arr) {
    const n = arr.length;
    
    // تنفيذ فرز الفقاعة
    for (let i = 0; i < n - 1; i++) {
        // علم للتحقق من حدوث تبديل
        let swapped = false;
        
        for (let j = 0; j < n - i - 1; j++) {
            // قارن العناصر المتجاورة
            if (arr[j] > arr[j + 1]) {
                // تبديل العناصر
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
                swapped = true;
            }
        }
        
        // إذا لم يحدث تبديل في تكرار كامل، فالمصفوفة مرتبة
        if (!swapped) break;
    }
    
    return arr;
}

// مثال للاستخدام
const array = [64, 34, 25, 12, 22, 11, 90];
console.log("المصفوفة قبل الفرز:", array);
bubbleSort(array);
console.log("المصفوفة بعد الفرز:", array);

فرز الاختيار (Selection Sort)

خوارزمية فرز الاختيار تعمل عن طريق تقسيم المصفوفة إلى جزأين: جزء مرتب وآخر غير مرتب. في كل تكرار، تبحث عن أصغر عنصر في الجزء غير المرتب وتضيفه إلى نهاية الجزء المرتب.

الخطوات:

  1. ابدأ باعتبار المصفوفة بأكملها غير مرتبة.
  2. ابحث عن أصغر عنصر في المصفوفة غير المرتبة.
  3. بدل هذا العنصر مع العنصر الأول في المصفوفة غير المرتبة.
  4. اعتبر الآن أن العنصر الأول مرتب، وكرر العملية على المصفوفة المتبقية.

التعقيد الزمني:

  • الحالة المتوسطة: O(n²)
  • أسوأ حالة: O(n²)
  • أفضل حالة: O(n²)

تنفيذ الخوارزمية بلغة Python:

def selection_sort(arr):
    n = len(arr)
    
    # مرر خلال جميع عناصر المصفوفة
    for i in range(n):
        # ابحث عن الحد الأدنى في المصفوفة غير المرتبة
        min_idx = i
        for j in range(i + 1, n):
            if arr[j] < arr[min_idx]:
                min_idx = j
                
        # بدل العنصر الأصغر مع العنصر الأول
        arr[i], arr[min_idx] = arr[min_idx], arr[i]
    
    return arr

# مثال للاستخدام
arr = [64, 25, 12, 22, 11]
print("المصفوفة قبل الفرز:", arr)
selection_sort(arr)
print("المصفوفة بعد الفرز:", arr)

الفرز السريع (Quick Sort)

خوارزمية الفرز السريع هي خوارزمية فعالة تعتمد على استراتيجية "التقسيم والغزو". تعمل عن طريق اختيار عنصر محوري (pivot) وترتيب العناصر حوله، ثم تقسيم المصفوفة وتطبيق نفس العملية على الأجزاء.

الخطوات:

  1. اختر عنصر من المصفوفة ليكون المحور (Pivot).
  2. أعد ترتيب المصفوفة بحيث تكون العناصر الأصغر من المحور على يساره والأكبر على يمينه.
  3. طبق نفس العملية بشكل متكرر على الجزأين اليسار واليمين.

التعقيد الزمني:

  • الحالة المتوسطة: O(n log n)
  • أسوأ حالة: O(n²) (عندما يكون المحور دائماً أكبر أو أصغر عنصر)
  • أفضل حالة: O(n log n)

تنفيذ الخوارزمية بلغة C++:

#include <iostream>
#include <vector>
using namespace std;

// دالة للتقسيم
int partition(vector<int> &arr, int low, int high) {
    int pivot = arr[high];  // نختار آخر عنصر كمحور
    int i = (low - 1);  // مؤشر العنصر الأصغر
    
    for (int j = low; j <= high - 1; j++) {
        // إذا كان العنصر الحالي أصغر من أو يساوي المحور
        if (arr[j] <= pivot) {
            i++;  // زيادة مؤشر العنصر الأصغر
            swap(arr[i], arr[j]);
        }
    }
    swap(arr[i + 1], arr[high]);
    return (i + 1);
}

// دالة الفرز السريع
void quickSort(vector<int> &arr, int low, int high) {
    if (low < high) {
        // احصل على موضع التقسيم
        int pi = partition(arr, low, high);
        
        // فرز العناصر قبل وبعد التقسيم
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

// دالة للطباعة
void printArray(const vector<int> &arr) {
    for (int i : arr)
        cout << i << " ";
    cout << endl;
}

int main() {
    vector<int> arr = {10, 7, 8, 9, 1, 5};
    
    cout << "المصفوفة قبل الفرز: ";
    printArray(arr);
    
    quickSort(arr, 0, arr.size() - 1);
    
    cout << "المصفوفة بعد الفرز: ";
    printArray(arr);
    
    return 0;
}

فرز الدمج (Merge Sort)

خوارزمية فرز الدمج تعتمد أيضًا على استراتيجية "التقسيم والغزو". تقسم المصفوفة إلى نصفين، ثم تفرز كل نصف على حدة، وأخيرًا تدمج النصفين المرتبين.

الخطوات:

  1. قسم المصفوفة إلى نصفين بشكل متكرر حتى تصل إلى مصفوفات أحادية العنصر.
  2. ادمج المصفوفات المرتبة بدءًا من المصفوفات الأحادية.
  3. كرر عملية الدمج حتى تحصل على مصفوفة واحدة مرتبة.

التعقيد الزمني:

  • الحالة المتوسطة: O(n log n)
  • أسوأ حالة: O(n log n)
  • أفضل حالة: O(n log n)

تنفيذ الخوارزمية بلغة Java:

public class MergeSort {
    // دالة دمج مصفوفتين مرتبتين
    void merge(int arr[], int l, int m, int r) {
        // حساب أحجام المصفوفتين الفرعيتين
        int n1 = m - l + 1;
        int n2 = r - m;
 
        // إنشاء مصفوفات مؤقتة
        int L[] = new int[n1];
        int R[] = new int[n2];
 
        // نسخ البيانات إلى المصفوفات المؤقتة
        for (int i = 0; i < n1; ++i)
            L[i] = arr[l + i];
        for (int j = 0; j < n2; ++j)
            R[j] = arr[m + 1 + j];
 
        // دمج المصفوفات المؤقتة
        int i = 0, j = 0;
        int k = l;
        while (i < n1 && j < n2) {
            if (L[i] <= R[j]) {
                arr[k] = L[i];
                i++;
            } else {
                arr[k] = R[j];
                j++;
            }
            k++;
        }
 
        // نسخ العناصر المتبقية من L[]، إن وجدت
        while (i < n1) {
            arr[k] = L[i];
            i++;
            k++;
        }
 
        // نسخ العناصر المتبقية من R[]، إن وجدت
        while (j < n2) {
            arr[k] = R[j];
            j++;
            k++;
        }
    }
 
    // الدالة الرئيسية التي تقسم المصفوفة وتفرزها
    void sort(int arr[], int l, int r) {
        if (l < r) {
            // إيجاد نقطة المنتصف
            int m = l + (r - l) / 2;
 
            // فرز النصف الأول والثاني
            sort(arr, l, m);
            sort(arr, m + 1, r);
 
            // دمج النصفين المرتبين
            merge(arr, l, m, r);
        }
    }
 
    // دالة للطباعة
    static void printArray(int arr[]) {
        for (int i : arr)
            System.out.print(i + " ");
        System.out.println();
    }
 
    // دالة الاختبار الرئيسية
    public static void main(String args[]) {
        int arr[] = { 12, 11, 13, 5, 6, 7 };
 
        System.out.println("المصفوفة قبل الفرز:");
        printArray(arr);
 
        MergeSort ob = new MergeSort();
        ob.sort(arr, 0, arr.length - 1);
 
        System.out.println("المصفوفة بعد الفرز:");
        printArray(arr);
    }
}

خوارزميات البحث

هياكل البيانات

المصفوفات (Arrays)

المصفوفة هي هيكل بيانات يخزن عناصر متجانسة في مواقع متتالية في الذاكرة. تسمح المصفوفات بالوصول المباشر إلى أي عنصر باستخدام الفهرس (index).

الخصائص الأساسية:

  • العناصر متجاورة في الذاكرة.
  • وصول سريع للعناصر باستخدام الفهرس (O(1)).
  • حجم ثابت في معظم اللغات (ما عدا المصفوفات الديناميكية).
  • تستخدم لتخزين مجموعة من البيانات من نفس النوع.

تعقيد العمليات الأساسية:

العملية التعقيد الزمني
وصول (Access) O(1)
بحث (Search) O(n)
إدراج في النهاية (Insertion at end) O(1)
إدراج произвольный (Arbitrary insertion) O(n)
حذف (Deletion) O(n)

مثال لاستخدام المصفوفات بلغة JavaScript:

// إنشاء مصفوفة
let fruits = ['تفاح', 'موز', 'برتقال', 'فراولة'];

// الوصول إلى عنصر باستخدام الفهرس
console.log(fruits[0]);  // تفاح

// تعديل عنصر
fruits[1] = 'كيوي';
console.log(fruits);  // ['تفاح', 'كيوي', 'برتقال', 'فراولة']

// إضافة عنصر في النهاية
fruits.push('مانجو');
console.log(fruits);  // ['تفاح', 'كيوي', 'برتقال', 'فراولة', 'مانجو']

// حذف آخر عنصر
let lastFruit = fruits.pop();
console.log(lastFruit);  // مانجو
console.log(fruits);  // ['تفاح', 'كيوي', 'برتقال', 'فراولة']

// إضافة عنصر في البداية
fruits.unshift('عنب');
console.log(fruits);  // ['عنب', 'تفاح', 'كيوي', 'برتقال', 'فراولة']

// حذف أول عنصر
let firstFruit = fruits.shift();
console.log(firstFruit);  // عنب
console.log(fruits);  // ['تفاح', 'كيوي', 'برتقال', 'فراولة']

// الحصول على طول المصفوفة
console.log(fruits.length);  // 4

// البحث عن عنصر
let orangeIndex = fruits.indexOf('برتقال');
console.log(orangeIndex);  // 2

القوائم المتصلة (Linked Lists)

القائمة المتصلة هي هيكل بيانات ديناميكي يتكون من مجموعة من العُقد (nodes). كل عقدة تحتوي على قيمة ومؤشر (أو رابط) إلى العقدة التالية في السلسلة.

أنواع القوائم المتصلة:

  • القائمة المتصلة البسيطة (Singly Linked List): كل عقدة تشير إلى العقدة التالية فقط.
  • القائمة المتصلة المزدوجة (Doubly Linked List): كل عقدة تشير إلى العقدة التالية والسابقة.
  • القائمة المتصلة الدائرية (Circular Linked List): آخر عقدة تشير إلى العقدة الأولى، مكونة حلقة.

تعقيد العمليات الأساسية:

العملية التعقيد الزمني
وصول (Access) O(n)
بحث (Search) O(n)
إدراج في البداية (Insertion at beginning) O(1)
إدراج في موقع محدد (Insertion at position) O(n)
حذف من البداية (Deletion from beginning) O(1)
حذف من موقع محدد (Deletion from position) O(n)

تنفيذ قائمة متصلة بسيطة بلغة JavaScript:

// تعريف فئة العقدة
class Node {
    constructor(data) {
        this.data = data;  // القيمة المخزنة في العقدة
        this.next = null;  // مؤشر للعقدة التالية
    }
}

// تعريف فئة القائمة المتصلة البسيطة
class LinkedList {
    constructor() {
        this.head = null;  // رأس القائمة
        this.size = 0;     // حجم القائمة
    }
    
    // إضافة عنصر في نهاية القائمة
    append(data) {
        // إنشاء عقدة جديدة
        const newNode = new Node(data);
        
        // إذا كانت القائمة فارغة، اجعل العقدة الجديدة هي الرأس
        if (!this.head) {
            this.head = newNode;
        } else {
            // تصفح القائمة حتى آخر عقدة
            let current = this.head;
            while (current.next) {
                current = current.next;
            }
            
            // اجعل مؤشر آخر عقدة يشير إلى العقدة الجديدة
            current.next = newNode;
        }
        
        this.size++;
    }
    
    // إضافة عنصر في بداية القائمة
    prepend(data) {
        // إنشاء عقدة جديدة
        const newNode = new Node(data);
        
        // اجعل مؤشر العقدة الجديدة يشير إلى الرأس الحالي
        newNode.next = this.head;
        
        // اجعل العقدة الجديدة هي الرأس
        this.head = newNode;
        
        this.size++;
    }
    
    // عرض القائمة
    printList() {
        let current = this.head;
        const elements = [];
        
        while (current) {
            elements.push(current.data);
            current = current.next;
        }
        
        return elements.join(' -> ');
    }
    
    // الحصول على حجم القائمة
    getSize() {
        return this.size;
    }
}

// مثال للاستخدام
const list = new LinkedList();
list.append(10);
list.append(20);
list.append(30);
list.prepend(5);

console.log(list.printList());  // 5 -> 10 -> 20 -> 30
console.log(`حجم القائمة: ${list.getSize()}`);  // حجم القائمة: 4

المكدسات والطوابير (Stacks & Queues)

المكدس (Stack)

المكدس هو هيكل بيانات يتبع مبدأ LIFO (Last In, First Out) أو "الوارد أخيرًا، الصادر أولاً". العنصر الذي يتم إضافته أخيرًا هو أول عنصر يتم استخراجه.

العمليات الأساسية:
  • دفع (Push): إضافة عنصر إلى أعلى المكدس.
  • سحب (Pop): إزالة العنصر الموجود في أعلى المكدس.
  • قمة (Peek/Top): عرض العنصر الموجود في أعلى المكدس دون إزالته.
  • فارغ (isEmpty): التحقق مما إذا كان المكدس فارغًا.
تنفيذ المكدس بلغة JavaScript:
class Stack {
    constructor() {
        this.items = [];
    }
    
    // إضافة عنصر إلى أعلى المكدس
    push(element) {
        this.items.push(element);
    }
    
    // إزالة وإرجاع العنصر من أعلى المكدس
    pop() {
        if (this.isEmpty()) {
            return "المكدس فارغ";
        }
        return this.items.pop();
    }
    
    // عرض العنصر الموجود في أعلى المكدس
    peek() {
        if (this.isEmpty()) {
            return "المكدس فارغ";
        }
        return this.items[this.items.length - 1];
    }
    
    // التحقق إذا كان المكدس فارغًا
    isEmpty() {
        return this.items.length === 0;
    }
    
    // إرجاع حجم المكدس
    size() {
        return this.items.length;
    }
    
    // تفريغ المكدس
    clear() {
        this.items = [];
    }
    
    // عرض المكدس
    print() {
        return this.items.toString();
    }
}

// مثال للاستخدام
const stack = new Stack();
stack.push(10);
stack.push(20);
stack.push(30);

console.log(`المكدس: ${stack.print()}`);  // المكدس: 10,20,30
console.log(`العنصر الأعلى: ${stack.peek()}`);  // العنصر الأعلى: 30

console.log(`عنصر مسحوب: ${stack.pop()}`);  // عنصر مسحوب: 30
console.log(`المكدس بعد السحب: ${stack.print()}`);  // المكدس بعد السحب: 10,20

الطابور (Queue)

الطابور هو هيكل بيانات يتبع مبدأ FIFO (First In, First Out) أو "الوارد أولاً، الصادر أولاً". العنصر الذي يتم إضافته أولاً هو أول عنصر يتم استخراجه.

العمليات الأساسية:
  • إضافة (Enqueue): إضافة عنصر إلى نهاية الطابور.
  • إزالة (Dequeue): إزالة العنصر من مقدمة الطابور.
  • مقدمة (Front): عرض العنصر الموجود في مقدمة الطابور دون إزالته.
  • فارغ (isEmpty): التحقق مما إذا كان الطابور فارغًا.
تنفيذ الطابور بلغة JavaScript:
class Queue {
    constructor() {
        this.items = [];
    }
    
    // إضافة عنصر إلى نهاية الطابور
    enqueue(element) {
        this.items.push(element);
    }
    
    // إزالة وإرجاع العنصر من مقدمة الطابور
    dequeue() {
        if (this.isEmpty()) {
            return "الطابور فارغ";
        }
        return this.items.shift();
    }
    
    // عرض العنصر الموجود في مقدمة الطابور
    front() {
        if (this.isEmpty()) {
            return "الطابور فارغ";
        }
        return this.items[0];
    }
    
    // التحقق إذا كان الطابور فارغًا
    isEmpty() {
        return this.items.length === 0;
    }
    
    // إرجاع حجم الطابور
    size() {
        return this.items.length;
    }
    
    // تفريغ الطابور
    clear() {
        this.items = [];
    }
    
    // عرض الطابور
    print() {
        return this.items.toString();
    }
}

// مثال للاستخدام
const queue = new Queue();
queue.enqueue('أحمد');
queue.enqueue('محمد');
queue.enqueue('علي');

console.log(`الطابور: ${queue.print()}`);  // الطابور: أحمد,محمد,علي
console.log(`العنصر في المقدمة: ${queue.front()}`);  // العنصر في المقدمة: أحمد

console.log(`عنصر تمت إزالته: ${queue.dequeue()}`);  // عنصر تمت إزالته: أحمد
console.log(`الطابور بعد الإزالة: ${queue.print()}`);  // الطابور بعد الإزالة: محمد,علي

الأشجار (Trees)

الشجرة هي هيكل بيانات هرمي غير خطي يتكون من عقد (nodes) متصلة ببعضها البعض. كل شجرة تحتوي على عقدة جذر (root) وقد تحتوي على عقد فرعية (child nodes).

أنواع الأشجار:

  • شجرة ثنائية (Binary Tree): كل عقدة تحتوي على عقدتين فرعيتين على الأكثر (يسار ويمين).
  • شجرة ثنائية للبحث (Binary Search Tree): شجرة ثنائية حيث قيمة كل عقدة يسار أقل من قيمة الأب، وقيمة كل عقدة يمين أكبر من قيمة الأب.
  • شجرة AVL: شجرة ثنائية للبحث متوازنة ذاتيًا.
  • شجرة الكومة (Heap): شجرة ثنائية تستخدم لتنفيذ طابور الأولوية.

اجتياز الشجرة (Tree Traversal):

  • اجتياز مسبق (Pre-order): زيارة الجذر ثم الفرع الأيسر ثم الفرع الأيمن.
  • اجتياز وسطي (In-order): زيارة الفرع الأيسر ثم الجذر ثم الفرع الأيمن.
  • اجتياز لاحق (Post-order): زيارة الفرع الأيسر ثم الفرع الأيمن ثم الجذر.
  • اجتياز مستوى بمستوى (Level-order): زيارة العقد مستوى بمستوى من الجذر إلى الأسفل.

تنفيذ شجرة ثنائية للبحث بلغة Python:

class Node:
    def __init__(self, key):
        self.left = None
        self.right = None
        self.val = key

class BinarySearchTree:
    def __init__(self):
        self.root = None
    
    # إدراج عقدة في الشجرة
    def insert(self, root, key):
        # إذا كانت الشجرة فارغة، أرجع عقدة جديدة
        if root is None:
            return Node(key)
        
        # وإلا، تابع اجتياز الشجرة
        if key < root.val:
            root.left = self.insert(root.left, key)
        else:
            root.right = self.insert(root.right, key)
        
        return root
    
    # البحث عن عقدة في الشجرة
    def search(self, root, key):
        # الحالة الأساسية: إذا كان الجذر فارغًا أو يحتوي على المفتاح المطلوب
        if root is None or root.val == key:
            return root
        
        # المفتاح أكبر من قيمة الجذر
        if root.val < key:
            return self.search(root.right, key)
        
        # المفتاح أصغر من قيمة الجذر
        return self.search(root.left, key)
    
    # اجتياز الشجرة بطريقة In-order
    def inorder_traversal(self, root):
        result = []
        if root:
            result = self.inorder_traversal(root.left)
            result.append(root.val)
            result = result + self.inorder_traversal(root.right)
        return result

# مثال للاستخدام
bst = BinarySearchTree()
root = None

# إدراج عناصر في الشجرة
keys = [50, 30, 20, 40, 70, 60, 80]
for key in keys:
    root = bst.insert(root, key)

# اجتياز الشجرة بطريقة In-order
print("اجتياز الشجرة بطريقة In-order:")
print(bst.inorder_traversal(root))  # [20, 30, 40, 50, 60, 70, 80]

# البحث عن قيمة في الشجرة
search_key = 40
result = bst.search(root, search_key)
if result:
    print(f"العنصر {search_key} موجود في الشجرة")
else:
    print(f"العنصر {search_key} غير موجود في الشجرة")

الرسوم البيانية (Graphs)

الرسم البياني هو هيكل بيانات غير خطي يتكون من رؤوس (vertices) أو عقد (nodes) وحواف (edges) تربط بين هذه الرؤوس. تُستخدم الرسوم البيانية لتمثيل شبكات متصلة مثل الشبكات الاجتماعية، خرائط الطرق، أو شبكات الحاسوب.

أنواع الرسوم البيانية:

  • الرسم البياني الموجه (Directed Graph): الحواف لها اتجاه محدد.
  • الرسم البياني غير الموجه (Undirected Graph): الحواف ليس لها اتجاه محدد.
  • الرسم البياني الموزون (Weighted Graph): كل حافة لها وزن أو تكلفة.
  • الرسم البياني الكامل (Complete Graph): كل رأس متصل مباشرة بكل الرؤوس الأخرى.

طرق تمثيل الرسوم البيانية:

  • مصفوفة المجاورة (Adjacency Matrix): مصفوفة ثنائية الأبعاد حيث العنصر matrix[i][j] يمثل الحافة بين الرأسين i و j.
  • قائمة المجاورة (Adjacency List): مصفوفة من القوائم حيث القائمة في الموقع i تحتوي على جميع الرؤوس المتصلة بالرأس i.

تنفيذ رسم بياني باستخدام قائمة المجاورة بلغة C++:

#include <iostream>
#include <vector>
using namespace std;

// فئة الرسم البياني
class Graph {
private:
    int V;  // عدد الرؤوس
    vector<vector<int>> adj;  // قائمة المجاورة
    
public:
    // المنشئ
    Graph(int vertices) {
        V = vertices;
        adj.resize(V);
    }
    
    // إضافة حافة في الرسم البياني غير الموجه
    void addEdge(int u, int v) {
        adj[u].push_back(v);
        adj[v].push_back(u);  // يُحذف هذا السطر للرسم البياني الموجه
    }
    
    // طباعة قائمة المجاورة
    void printGraph() {
        for (int v = 0; v < V; ++v) {
            cout << "قائمة المجاورة للرأس " << v << ": ";
            for (auto x : adj[v]) {
                cout << "-> " << x;
            }
            cout << endl;
        }
    }
    
    // البحث في العمق (DFS)
    void DFSUtil(int v, vector<bool> &visited) {
        // تأشير الرأس الحالي كـ "تمت زيارته"
        visited[v] = true;
        cout << v << " ";
        
        // تكرار لجميع الرؤوس المجاورة للرأس الحالي
        for (auto x : adj[v]) {
            if (!visited[x]) {
                DFSUtil(x, visited);
            }
        }
    }
    
    // دالة البحث في العمق
    void DFS(int startVertex) {
        // إنشاء مصفوفة للتأشير على الرؤوس المزارة
        vector<bool> visited(V, false);
        
        cout << "نتيجة البحث في العمق بدءًا من الرأس " << startVertex << ": ";
        DFSUtil(startVertex, visited);
        cout << endl;
    }
    
    // البحث في العرض (BFS)
    void BFS(int startVertex) {
        // إنشاء مصفوفة للتأشير على الرؤوس المزارة
        vector<bool> visited(V, false);
        
        // إنشاء طابور للرؤوس
        vector<int> queue;
        
        // تأشير الرأس الابتدائي كـ "تمت زيارته" وإضافته للطابور
        visited[startVertex] = true;
        queue.push_back(startVertex);
        
        cout << "نتيجة البحث في العرض بدءًا من الرأس " << startVertex << ": ";
        
        while (!queue.empty()) {
            // إخراج رأس من الطابور وطباعته
            startVertex = queue.front();
            cout << startVertex << " ";
            queue.erase(queue.begin());
            
            // الحصول على جميع الرؤوس المجاورة للرأس الحالي
            // إذا لم تتم زيارته بعد، تأشيره كـ "تمت زيارته" وإضافته للطابور
            for (auto x : adj[startVertex]) {
                if (!visited[x]) {
                    visited[x] = true;
                    queue.push_back(x);
                }
            }
        }
        cout << endl;
    }
};

// الدالة الرئيسية
int main() {
    // إنشاء رسم بياني بـ 5 رؤوس
    Graph g(5);
    
    // إضافة حواف
    g.addEdge(0, 1);
    g.addEdge(0, 4);
    g.addEdge(1, 2);
    g.addEdge(1, 3);
    g.addEdge(1, 4);
    g.addEdge(2, 3);
    g.addEdge(3, 4);
    
    // طباعة تمثيل الرسم البياني
    cout << "تمثيل الرسم البياني باستخدام قائمة المجاورة:" << endl;
    g.printGraph();
    
    // تنفيذ البحث في العمق والعرض
    g.DFS(0);
    g.BFS(0);
    
    return 0;
}

البرمجة الديناميكية

مقدمة في البرمجة الديناميكية

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

خصائص المشكلات التي يمكن حلها باستخدام البرمجة الديناميكية:

  • التداخل (Overlapping Subproblems): المشكلة يمكن تقسيمها إلى مشكلات فرعية، وهذه المشكلات الفرعية تتكرر عدة مرات.
  • البنية المثالية (Optimal Substructure): الحل الأمثل للمشكلة يمكن بناؤه من الحلول المثلى للمشكلات الفرعية.

تقنيات البرمجة الديناميكية:

  • التخزين المؤقت (Memoization): النهج من أعلى إلى أسفل، حيث نحل المشكلة الكبيرة أولاً، ثم نخزن نتائج المشكلات الفرعية التي نصادفها.
  • النهج التصاعدي (Tabulation): النهج من أسفل إلى أعلى، حيث نبدأ بحل أصغر المشكلات الفرعية أولاً، ثم نستخدم هذه النتائج لحل المشكلة الكبرى.

أمثلة تطبيقية للبرمجة الديناميكية

مثال 1: متتالية فيبوناتشي (Fibonacci Sequence)

متتالية فيبوناتشي هي متتالية تبدأ بالأرقام 0 و 1، وكل رقم لاحق يكون مجموع الرقمين السابقين له. المتتالية: 0، 1، 1، 2، 3، 5، 8، 13، 21، ...

الحل التقليدي (المتكرر):
def fibonacci_recursive(n):
    if n <= 1:
        return n
    return fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)

# مثال للاستخدام
print(fibonacci_recursive(10))  # 55

التعقيد الزمني: O(2^n) - غير فعال للقيم الكبيرة.

الحل باستخدام البرمجة الديناميكية (التخزين المؤقت):
def fibonacci_memoization(n, memo={}):
    if n in memo:
        return memo[n]
    
    if n <= 1:
        return n
    
    memo[n] = fibonacci_memoization(n - 1, memo) + fibonacci_memoization(n - 2, memo)
    return memo[n]

# مثال للاستخدام
print(fibonacci_memoization(100))  # حساب فيبوناتشي للرقم 100 (قيمة كبيرة جداً)

التعقيد الزمني: O(n) - أكثر كفاءة للقيم الكبيرة.

الحل باستخدام البرمجة الديناميكية (النهج التصاعدي):
def fibonacci_tabulation(n):
    if n <= 1:
        return n
    
    # إنشاء مصفوفة لتخزين النتائج
    dp = [0] * (n + 1)
    dp[1] = 1
    
    # حساب القيم التالية
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    
    return dp[n]

# مثال للاستخدام
print(fibonacci_tabulation(100))  # حساب فيبوناتشي للرقم 100

التعقيد الزمني: O(n) - أكثر كفاءة من حيث استخدام المكدس.

مثال 2: مسألة الحقيبة (0/1 Knapsack Problem)

في مسألة الحقيبة، لدينا مجموعة من العناصر، كل منها له وزن وقيمة، والمطلوب هو تحديد مجموعة العناصر التي يجب وضعها في الحقيبة بحيث تكون القيمة الإجمالية أكبر ما يمكن دون تجاوز سعة الحقيبة.

الحل باستخدام البرمجة الديناميكية:
def knapsack(W, wt, val, n):
    """
    حل مسألة الحقيبة باستخدام البرمجة الديناميكية
    
    المعطيات:
    W -- سعة الحقيبة
    wt -- مصفوفة الأوزان
    val -- مصفوفة القيم
    n -- عدد العناصر
    
    إرجاع:
    أقصى قيمة يمكن وضعها في الحقيبة
    """
    # إنشاء جدول للبرمجة الديناميكية
    dp = [[0 for _ in range(W + 1)] for _ in range(n + 1)]
    
    # بناء الجدول dp[][] بطريقة تصاعدية
    for i in range(1, n + 1):
        for w in range(1, W + 1):
            # إذا كان وزن العنصر الحالي أكبر من سعة الحقيبة الفرعية
            if wt[i-1] > w:
                dp[i][w] = dp[i-1][w]
            # وإلا، نختار الأفضل: تضمين العنصر أو عدم تضمينه
            else:
                dp[i][w] = max(val[i-1] + dp[i-1][w-wt[i-1]], dp[i-1][w])
                
    return dp[n][W]

# مثال للاستخدام
val = [60, 100, 120]  # قيم العناصر
wt = [10, 20, 30]     # أوزان العناصر
W = 50                # سعة الحقيبة
n = len(val)          # عدد العناصر

result = knapsack(W, wt, val, n)
print(f"أقصى قيمة يمكن وضعها في الحقيبة: {result}")  # 220

التعقيد الزمني: O(n*W) - حيث n هو عدد العناصر و W هي سعة الحقيبة.