ScholarQuill logoScholarQuillUniversity Notes
  • Notes
  • Past Papers
  • Blogs
  • Todo
Login
ScholarQuill logoScholarQuillUniversity Notes
Login
NotesPast PapersBlogsTodo
More
SubjectsDiscussionCGPA CalculatorGPA CalculatorStudent PortalCourse Outline
About
About usPrivacy PolicyReportContact
Notes
Past Papers
Blogs
Todo
Analytics
    Current Subject
    🧩
    Data Structures & Algorithms
    CC-213
    Progress0 / 37 topics
    Topics
    1. Abstract Data Types2. Complexity Analysis3. Big Oh Notation4. Stacks (Linked Lists and Array Implementations)5. Recursion and analyzing recursive algorithms6. Divide and Conquer Algorithms7. Sorting Algorithms8. Selection Sort9. Insertion Sort10. Merge Sort11. Quick Sort12. Bubble Sort13. Heap Sort14. Shell Sort15. Radix Sort16. Bucket Sort17. Queue18. Dequeuer19. Priority Queues (linked and array implementations of queues)20. Linked List and Its Various Types21. Sorted Linked List22. Searching an Unsorted Array23. Binary Search for Sorted Arrays24. Hashing and Indexing25. Open Addressing and Chaining26. Trees and Tree Traversals27. Binary Search Trees28. Heaps29. M-Way Trees30. Balanced Trees31. Graphs32. Breadth-First Traversal33. Depth-First Traversal34. Topological Order35. Shortest Path36. Adjacency Matrix and Adjacency List Implementations37. Memory Management and Garbage Collection
    CC-213›Divide and Conquer Algorithms
    Data Structures & AlgorithmsTopic 6 of 37

    Divide and Conquer Algorithms

    8 minread
    1,349words
    Intermediatelevel

    Divide and Conquer Algorithms

    What is Divide and Conquer?

    Divide and conquer is a powerful algorithm design paradigm used to solve complex problems by breaking them down into smaller subproblems, solving each subproblem independently, and then combining their solutions to solve the original problem. It typically involves three main steps:

    1. Divide: Split the problem into smaller subproblems that are easier to solve.
    2. Conquer: Solve each subproblem recursively.
    3. Combine: Merge the solutions of the subproblems to obtain the final solution.

    Key Characteristics of Divide and Conquer

    • Recursion: Divide and conquer algorithms often rely on recursion to solve subproblems.
    • Efficiency: These algorithms can be more efficient than iterative methods, especially for large inputs.
    • Parallelism: The independent nature of subproblems allows parallel processing, making them suitable for parallel computing.

    Common Divide and Conquer Algorithms

    Here are a few well-known algorithms that follow the divide and conquer strategy:

    1. Merge Sort
    2. Quick Sort
    3. Binary Search
    4. Closest Pair of Points
    5. Matrix Multiplication (Strassen's Algorithm)

    Let’s explore some of these algorithms in detail.

    1. Merge Sort

    Merge Sort is a sorting algorithm that uses the divide and conquer approach to sort an array or list of elements.

    Steps of Merge Sort:

    1. Divide: Split the array into two halves.
    2. Conquer: Recursively sort each half.
    3. Combine: Merge the two sorted halves into a single sorted array.

    Example:

    Consider an array [8, 3, 5, 4, 7, 1, 6, 2]. Here's how merge sort works:

    1. Divide:

      • Split into two arrays: [8, 3, 5, 4] and [7, 1, 6, 2]
      • Recursively divide further: [8, 3], [5, 4], [7, 1], [6, 2]
    2. Conquer:

      • Sort each pair: [3, 8], [4, 5], [1, 7], [2, 6]
      • Merge sorted halves: [3, 4, 5, 8] and [1, 2, 6, 7]
    3. Combine:

      • Merge the two sorted arrays to get the final sorted array: [1, 2, 3, 4, 5, 6, 7, 8]

    Merge Sort in C++:

    #include <iostream>
    using namespace std;
    
    void merge(int arr[], int left, int mid, int right) {
        int n1 = mid - left + 1;
        int n2 = right - mid;
        
        int L[n1], R[n2];
        
        for (int i = 0; i < n1; i++)
            L[i] = arr[left + i];
        for (int i = 0; i < n2; i++)
            R[i] = arr[mid + 1 + i];
        
        int i = 0, j = 0, k = left;
        while (i < n1 && j < n2) {
            if (L[i] <= R[j])
                arr[k++] = L[i++];
            else
                arr[k++] = R[j++];
        }
        
        while (i < n1)
            arr[k++] = L[i++];
        
        while (j < n2)
            arr[k++] = R[j++];
    }
    
    void mergeSort(int arr[], int left, int right) {
        if (left < right) {
            int mid = left + (right - left) / 2;
            mergeSort(arr, left, mid);
            mergeSort(arr, mid + 1, right);
            merge(arr, left, mid, right);
        }
    }
    
    int main() {
        int arr[] = {8, 3, 5, 4, 7, 1, 6, 2};
        int n = sizeof(arr)/sizeof(arr[0]);
        
        mergeSort(arr, 0, n - 1);
        
        cout << "Sorted array: ";
        for (int i = 0; i < n; i++)
            cout << arr[i] << " ";
        cout << endl;
        
        return 0;
    }
    

    Time Complexity of Merge Sort:

    • Divide: Splitting the array into two halves takes O(log n) time (because the array is divided log(n) times).
    • Conquer: Merging two halves takes O(n) time (since we need to merge n elements).
    • Overall Time Complexity: O(n log n).

    Merge Sort is an efficient sorting algorithm, with a time complexity of O(n log n), but it uses additional space for merging the subarrays.

    2. Quick Sort

    Quick Sort is another sorting algorithm that uses the divide and conquer technique. Unlike merge sort, quick sort selects a pivot element, and rearranges the elements around the pivot such that all elements smaller than the pivot are on the left, and all elements greater are on the right.

    Steps of Quick Sort:

    1. Divide: Choose a pivot element and partition the array around the pivot.
    2. Conquer: Recursively sort the left and right partitions.
    3. Combine: No need to explicitly combine, as the elements are already in place.

    Quick Sort in C++:

    #include <iostream>
    using namespace std;
    
    int partition(int arr[], int low, int high) {
        int pivot = arr[high];
        int i = low - 1;
    
        for (int j = low; j < high; j++) {
            if (arr[j] < pivot) {
                i++;
                swap(arr[i], arr[j]);
            }
        }
        swap(arr[i + 1], arr[high]);
        return i + 1;
    }
    
    void quickSort(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);
        }
    }
    
    int main() {
        int arr[] = {8, 3, 5, 4, 7, 1, 6, 2};
        int n = sizeof(arr)/sizeof(arr[0]);
    
        quickSort(arr, 0, n - 1);
    
        cout << "Sorted array: ";
        for (int i = 0; i < n; i++)
            cout << arr[i] << " ";
        cout << endl;
    
        return 0;
    }
    

    Time Complexity of Quick Sort:

    • Best/Average Case: O(n log n) (when the pivot divides the array evenly).
    • Worst Case: O(n²) (when the pivot divides the array unevenly, like choosing the smallest or largest element as the pivot).
    • Space Complexity: O(log n) (for recursive call stack).

    3. Binary Search

    Binary Search is a divide and conquer algorithm used to search for an element in a sorted array. It works by repeatedly dividing the search space in half.

    Steps of Binary Search:

    1. Divide: Find the middle element of the array.
    2. Conquer: If the middle element is equal to the target, return it. Otherwise, search in the left or right half depending on whether the target is smaller or larger than the middle element.
    3. Combine: No explicit combining is needed.

    Binary Search in C++:

    #include <iostream>
    using namespace std;
    
    int binarySearch(int arr[], int left, int right, int target) {
        while (left <= right) {
            int mid = left + (right - left) / 2;
            
            if (arr[mid] == target)
                return mid;
            if (arr[mid] < target)
                left = mid + 1;
            else
                right = mid - 1;
        }
        return -1; // Target not found
    }
    
    int main() {
        int arr[] = {1, 2, 3, 4, 5, 6, 7, 8};
        int n = sizeof(arr)/sizeof(arr[0]);
        int target = 5;
    
        int result = binarySearch(arr, 0, n - 1, target);
        if (result != -1)
            cout << "Element found at index " << result << endl;
        else
            cout << "Element not found" << endl;
    
        return 0;
    }
    

    Time Complexity of Binary Search:

    • Best Case: O(1) (when the middle element is the target).
    • Worst/Average Case: O(log n) (because the search space is halved with each iteration).
    • Space Complexity: O(1) (iterative implementation).

    Analyzing Divide and Conquer Algorithms

    To analyze the complexity of divide and conquer algorithms, we often use recurrence relations. These express the time complexity in terms of the time taken to solve smaller subproblems.

    Example: Merge Sort Recurrence Relation

    • T(n) = 2T(n/2) + O(n)

    Where:

    • 2T(n/2) represents the time taken to solve the two halves of size `n/

    2`.

    • O(n) represents the time taken to merge the two halves.

    Using the Master Theorem, we can solve this to get the overall time complexity of O(n log n).

    Conclusion

    Divide and conquer is a versatile approach that can optimize many algorithms by reducing the size of the problem at each step. It is particularly useful in sorting (e.g., merge sort and quick sort), searching (e.g., binary search), and various computational geometry problems.

    Previous topic 5
    Recursion and analyzing recursive algorithms
    Next topic 7
    Sorting Algorithms

    Past Papers

    Open this section to load past papers

    Click on Show Past Papers to see past papers.
    On This Page
      Reading Stats
      Est. reading time8 min
      Word count1,349
      Code examples0
      DifficultyIntermediate