Day 12 (Sorting Advanced)

Day 12 (Sorting Advanced)

Today’s practice focused on diving into more advanced sorting algorithms. I implemented Merge Sort and Quick Sort, analyzing their time complexities in the process. Additionally, I worked on solving problems like finding the kth smallest/largest element using Quick Select, sorting characters by frequency, and calculating the minimum number of swaps needed to sort an array. These exercises helped deepen my understanding of divide-and-conquer algorithms and their practical applications.

Check out my progress in my GitHub repository: github.com/AayJayTee/100-Days-DSA.


Q1. Implement Merge Sort and analyze its time complexity.

    #include <iostream>
    using namespace std;

    void merge(int arr[], int beg, int mid, int end) {
        int n1 = mid - beg + 1; // Size of the left subarray
        int n2 = end - mid;     // Size of the right subarray

        // Create temporary arrays
        int left[n1], right[n2];

        // Copy data to temporary arrays
        for (int i = 0; i < n1; i++)
            left[i] = arr[beg + i];
        for (int j = 0; j < n2; j++)
            right[j] = arr[mid + 1 + j];

        // Merge the two subarrays
        int i = 0, j = 0, k = beg; // Initial indexes of left, right, and      merged subarrays
        while (i < n1 && j < n2) {
            if (left[i] <= right[j]) {
                arr[k] = left[i];
                i++;
            }
            else{
                arr[k] = right[j];
                j++;
            }
            k++;
        }

        // Copy any remaining elements of left subarray
        while (i < n1) {
            arr[k] = left[i];
            i++;
            k++;
        }

        // Copy any remaining elements of right subarray
        while (j < n2) {
            arr[k] = right[j];
            j++;
            k++;
        }
    }

    void mergeSort(int arr[],int beg,int end){
        if(beg<end){
            int mid=(beg+end)/2;
            mergeSort(arr,beg,mid);
            mergeSort(arr,mid+1,end);
            merge(arr,beg,mid,end);
        } 
    }

    int main(){
        int n;
        cout<<"Enter length of the array: ";
        cin>>n;
        int arr[n];
        cout<<"Enter the elements of the array: ";
        for(int i=0;i<n;i++){
            cin>>arr[i];
        }
        mergeSort(arr,0,n-1);
        cout << "Sorted array: ";
        for (int i = 0; i < n; i++)
            cout << arr[i] << " ";
        return 0;
    }

The Merge Sort algorithm is a divide-and-conquer sorting technique. The algorithm divides the input array into two halves, recursively sorts each half, and then merges the sorted halves back together. The merge function is responsible for combining two sorted subarrays into a single sorted array. It compares elements from two temporary arrays (left and right) and merges them in a sorted manner. The mergeSort function is the recursive function that divides the array until it reaches individual elements, then calls the merge function to combine them back into sorted order. The time complexity of Merge Sort is O(n log n), where n is the number of elements in the array, because the array is divided into two halves (log n divisions) and each element is processed (n comparisons and merges). Merge Sort has a space complexity of O(n) due to the auxiliary arrays used for merging.


Q2. Implement Quick Sort and analyze its time complexity.

#include <iostream>
using namespace std;

int Partition(int arr[], int lb, int ub) {
    int pivot = arr[lb]; // Choosing the first element as the pivot
    int start = lb; 
    int end = ub;

    while (start < end) {
        // Increment start index until we find an element greater than pivot
        while (arr[start] <= pivot && start < ub) {
            start++;
        }
        // Decrement end index until we find an element smaller than pivot
        while (arr[end] > pivot) {
            end--;
        }
        if (start < end) {
            swap(arr[start], arr[end]); // Swap the elements
        }
    }
    // Place pivot at the correct position
    swap(arr[lb], arr[end]);
    return end; // Return the pivot index
}


void quickSort(int arr[], int lb, int ub) {
    if (lb < ub) { // Base case: when the subarray has one or zero elements
        int loc = Partition(arr, lb, ub); // Partition the array
        quickSort(arr, lb, loc - 1); // Recursively sort the left part
        quickSort(arr, loc + 1, ub); // Recursively sort the right part
    }
}

int main() {
    int n;
    cout << "Enter length of the array: ";
    cin >> n; 
    int arr[n];
    cout << "Enter the elements of the array: ";
    for (int i = 0; i < n; i++) {
        cin >> arr[i];
    }
    quickSort(arr, 0, n - 1);
    cout << "Sorted array: ";
    for (int i = 0; i < n; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;
    return 0;
}

This code implements the Quick Sort algorithm, a divide-and-conquer sorting technique. The quickSort function recursively divides the array into smaller subarrays by selecting a "pivot" element. The Partition function rearranges the array so that elements smaller than the pivot come before it, and elements greater than the pivot come after it. After partitioning, the algorithm recursively sorts the left and right subarrays. The time complexity of Quick Sort is O(n log n) in the best and average cases, where n is the number of elements, but it can degrade to O(n^2) in the worst case when the pivot is poorly chosen. The space complexity is O(log n) due to the recursion stack, making Quick Sort efficient in practice, especially for large datasets, though it can become less efficient with unbalanced partitions.


Q3. Solve the "find kth smallest/largest element in an array" problem using Quick Select.

#include <iostream>
using namespace std;

int Partition(int arr[], int lb, int ub) {
    int pivot = arr[ub]; // Use the last element as the pivot
    int i = lb - 1;

    for (int j = lb; j < ub; j++) {
        if (arr[j] <= pivot) {
            i++;
            swap(arr[i], arr[j]);
        }
    }
    swap(arr[i + 1], arr[ub]);
    return i + 1;
}

int quickSelect(int arr[], int lb, int ub, int k) {
    if (lb <= ub) {
        int pivotIndex = Partition(arr, lb, ub);

        if (pivotIndex == k) {
            return arr[pivotIndex]; // Found the k-th smallest element
        } else if (pivotIndex > k) {
            return quickSelect(arr, lb, pivotIndex - 1, k); // Search left
        } else {
            return quickSelect(arr, pivotIndex + 1, ub, k); // Search right
        }
    }
    return -1; // Error case (not expected for valid input)
}

int main() {
    int n, k, c;
    cout << "Enter length of the array: ";
    cin >> n;
    int arr[n];
    cout << "Enter the elements of the array: ";
    for (int i = 0; i < n; i++) {
        cin >> arr[i];
    }
    cout << "Do you want to find k-th smallest or largest? (1-smallest, 2-largest): ";
    cin >> c;
    cout << "What is the value of k?: ";
    cin >> k;
    if (c == 1) {
        cout << "The " << k << "th smallest element is: " 
             << quickSelect(arr, 0, n - 1, k - 1) << endl;
    }
    else if (c == 2) {
        cout << "The " << k << "th largest element is: " 
             << quickSelect(arr, 0, n - 1, n - k) << endl;
    }
    else {
        cout << "Invalid choice" << endl;
    }

    return 0;
}

This code implements the Quick Select algorithm to find the k-th smallest or largest element in an array. The quickSelect function uses a partitioning approach similar to Quick Sort. It selects a pivot, rearranges the array such that elements less than the pivot are on the left and those greater are on the right, and then recursively narrows the search based on the position of the pivot. If the pivot's index matches k, it returns the element at that position. For finding the k-th largest element, the position is adjusted by using n-k. The time complexity of Quick Select is O(n) on average but can degrade to O(n^2) in the worst case if poor pivots are selected. This makes it more efficient than fully sorting the array, which has a time complexity of O(n log n).


Q4. Solve the "sort characters by frequency" problem.

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

string sortCharactersByFrequency(string s) {
    // Create a frequency array for uppercase and lowercase English letters
    vector<int> freq(52, 0);

    // Count frequencies of characters (map a-z to 0-25 and A-Z to 26-51)
    for (char ch : s) {
        if ('a' <= ch && ch <= 'z') {
            freq[ch - 'a']++; // Lowercase letters
        } else if ('A' <= ch && ch <= 'Z') {
            freq[ch - 'A' + 26]++; // Uppercase letters
        }
    }

    // Create a vector of pairs (frequency, character)
    vector<pair<int, char>> charFreq;
    for (int i = 0; i < 52; i++) {
        if (freq[i] > 0) {
            char ch = (i < 26) ? 'a' + i : 'A' + (i - 26);
            charFreq.push_back({freq[i], ch});
        }
    }

    // Sort the vector by frequency in descending order
    sort(charFreq.begin(), charFreq.end(), [](pair<int, char>& a, pair<int, char>& b) {
        return a.first > b.first;
    });

    // Build the result string
    string result = "";
    for (auto& p : charFreq) {
        result += string(p.first, p.second); // Append the character 'frequency' times
    }

    return result;
}

int main() {
    string input;
    cout << "Enter the string: ";
    cin >> input;
    string result = sortCharactersByFrequency(input);
    cout << "Sorted by frequency: " << result << endl;
    return 0;
}

This code solves the "sort characters by frequency" problem by first counting the frequency of each character in the input string. It uses a vector of size 52 to track the frequency of each character, where the first 26 indices represent lowercase letters ('a' to 'z') and the last 26 represent uppercase letters ('A' to 'Z'). After counting, it stores each character and its frequency as a pair in a vector. This vector is then sorted in descending order based on the frequency of characters. Finally, the program constructs the result string by repeating each character according to its frequency. The time complexity of this approach is O(n + k log k), where n is the length of the input string and k is the number of distinct characters, which is constant (52 for English letters), so the overall time complexity is effectively O(n).


Q5. Solve the "minimum number of swaps required to sort an array" problem.

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

void countSwaps(vector<int>& arr) {
    int n = arr.size();
    vector<pair<int, int>> v;

    // Create pairs of element and its index
    for (int i = 0; i < n; i++) {
        v.push_back({arr[i], i});
    }

    // Sort the array
    sort(v.begin(), v.end());

    vector<bool> visited(n, false);
    int noofSwaps = 0;

    // Find cycles and count swaps
    for (int i = 0; i < n; i++) {
        // Skip already visited elements or already in correct position
        if (visited[i] || v[i].second == i) {
            continue;
        }

        // Calculate the size of the cycle
        int cycleSize = 0;
        int j = i;

        while (!visited[j]) {
            visited[j] = true;
            j = v[j].second; // Move to the next index in the cycle
            cycleSize++;
        }

        // If cycle size is n, then (n-1) swaps are required
        if (cycleSize > 1) {
            noofSwaps += (cycleSize - 1);
        }
    }

    cout << "No of swaps to sort the array are: " << noofSwaps << endl;
}

int main() {
    int n;
    cout << "Enter length of the array: ";
    cin >> n;
    vector<int> arr(n);
    cout << "Enter the elements of the array: ";
    for (int i = 0; i < n; i++) {
        cin >> arr[i];
    }
    countSwaps(arr);
    return 0;
}

This code implements a function to count the minimum number of swaps required to sort an array using the concept of cycle decomposition. It first creates a vector of pairs where each pair contains an element from the array and its original index. The array is then sorted, and a boolean vector is used to track visited elements. The algorithm iterates through the array, and for each unvisited element, it traces the cycle of elements that would result from sorting. For each cycle of size n, it requires n-1 swaps to place all elements in their correct positions. The total number of swaps is calculated by summing the swaps needed for each cycle. The time complexity is O(n log n) due to the sorting step.


Today's exercises strengthened my knowledge of advanced sorting algorithms and their real-world use cases. From mastering Merge and Quick Sort to solving problems involving selection and frequency analysis, I’m more confident in applying these techniques efficiently. Looking forward to tackling even more challenging topics in the upcoming days!