Both algorithms are based on recursion and divide-and-conquer strategy. Both are efficient for large datasets, but they have different performance characteristics.
Recursion is a programming technique where a function calls itself. It is a powerful tool to solve problems that can be divided into smaller problems of the same type.
The recursion has two main parts:
Base case: the condition that stops the recursion;
Recursive case: the condition that calls the function again.
Let's see an example of a recursive function to calculate the factorial of a number:
The algorthims will keep dividing the array (in red) until it reaches the base case, where the array has only one element(in gray). Then it will merge the two sorted subarrays (in green).
#include<iostream>#include<vector>#include<queue>// inplace merge without extra spacetemplate<typenameT>requiresstd::is_arithmetic<T>::value// C++20voidmergeInplace(std::vector<T>&arr,constsize_tstart,size_tmid,constsize_tend){size_tleft=start;size_tright=mid+1;while(left<=mid&&right<=end){if(arr[left]<=arr[right]){left++;}else{Ttemp=arr[right];for(size_ti=right;i>left;i--){arr[i]=arr[i-1];}arr[left]=temp;left++;mid++;right++;}}}// Merge two sorted halvestemplate<typenameT>requiresstd::is_arithmetic<T>::value// C++20voidmerge(std::vector<T>&arr,constsize_tstart,constsize_tmid,constsize_tend){// create a temporary array to store the merged arraystd::vector<T>temp(end-start+1);// indexes for the subarrays:constsize_tleftStart=start;constsize_tleftEnd=mid;constsize_trightStart=mid+1;constsize_trightEnd=end;// indexes forsize_ttempIdx=0;size_tleftIdx=leftStart;size_trightIdx=rightStart;// merge the subarrayswhile(leftIdx<=leftEnd&&rightIdx<=rightEnd){if(arr[leftIdx]<arr[rightIdx])temp[tempIdx++]=arr[leftIdx++];elsetemp[tempIdx++]=arr[rightIdx++];}// copy the remaining elements of the left subarraywhile(leftIdx<=leftEnd)temp[tempIdx++]=arr[leftIdx++];// copy the remaining elements of the right subarraywhile(rightIdx<=rightEnd)temp[tempIdx++]=arr[rightIdx++];// copy the merged array back to the original arraystd::copy(temp.begin(),temp.end(),arr.begin()+start);}// recursive mergesorttemplate<typenameT>requiresstd::is_arithmetic<T>::value// C++20voidmergesortRecursive(std::vector<T>&arr,size_tleft,size_tright){if(right-left>0){size_tmid=(left+right)/2;mergesortRecursive(arr,left,mid);mergesortRecursive(arr,mid+1,right);merge(arr,left,mid,right);// if the memory is limited, use the inplace merge at the cost of performance// mergeInplace(arr, left, mid - 1, right - 1);}}// interactive mergesorttemplate<typenameT>requiresstd::is_arithmetic<T>::value// C++20voidmergesortInteractive(std::vector<T>&arr){for(size_twidth=1;width<arr.size();width*=2){for(size_tleft=0;left<arr.size();left+=2*width){size_tmid=std::min(left+width,arr.size());size_tright=std::min(left+2*width,arr.size());merge(arr,left,mid-1,right-1);// if the memory is limited, use the inplace merge at the cost of performance// mergeInplace(arr, left, mid - 1, right - 1);}}}intmain(){std::vector<int>arr1;for(inti=1000;i>0;i--)arr1.push_back(rand()%1000);std::vector<int>arr2=arr1;for(autoi:arr1)std::cout<<i<<" ";std::cout<<std::endl;mergesortRecursive(arr1,0,arr1.size()-1);for(autoi:arr1)std::cout<<i<<" ";std::cout<<std::endl;mergesortInteractive(arr2);for(autoi:arr2)std::cout<<i<<" ";std::cout<<std::endl;return0;}
Recursive: the function calls itself for the two halves;
Iterative: the function uses a loop to merge the two sorted halves.
The interactive version is more efficient than the recursive version, but it is more complex to understand. But both uses the same core algorithm to merge the two sorted halves.
The main issue with Mergesort is that it requires extra space O(n) to merge the subarrays, which can be problem for large datasets.
The recursive version will increase the call stack by O(lg(n) and can potentially cause a stack overflow;
The iterative version does not add pressure to the stack;
Quicksort is prtetty similar to mergesort, but it solves the extra memory allocation at expense of stability. So quicksort is an in-place and unstable sorting algorithm.
One of the core strategy of quicksort is pivoting. It will be selected and the array will be partitioned in two subarrays: one with elements smaller than the pivot (left) and the other with elements greater than the pivot (right).
The partitioning process consists of the following steps:
Select a pivotIndex (left, right, random or median);
Swap the pivot with the leftmost element (this can be delayed to the end of the step);
Set the pivot to the leftmost element (assuming you swapped);
Set the left and right indexes;
While the left index is less than or equal to the right index:
If the left element is less than or equal to the pivot, increment the left index;
If the right element is greater than the pivot, decrement the right index;
If the left element is greater than the pivot and the right element is less than or equal to the pivot, swap the left and right elements;
The main issue with quicksort is that it can degrade to O(n^2) in an already sorted array. To solve this issue, we can select a random pivot, or the median of the first, middle and last element of the array. This can increase the stability of the algorithm at the expense of performance. The best case scenario is O(n*lg(n)) and the average case is O(n*lg(n)).
#include<iostream>#include<vector>#include<utility>#include<stack>#include<random>usingstd::stack;usingstd::swap;usingstd::pair;usingstd::vector;usingstd::cout;// Function to generate a random pivot index within the range [left, right]template<typenameT>requiresstd::integral<T>// c++20TrandomRange(Tleft,Tright){staticstd::random_devicerd;staticstd::mt19937gen(rd());std::uniform_int_distribution<T>dist(left,right);returndist(gen);}// partitiontemplate<typenameT>requiresstd::is_arithmetic_v<T>size_tpartition(std::vector<T>&arr,size_tleft,size_tright){// random pivot to increase stability at the cost of performance by random callsize_tpivotIndex=randomRange(left,right);swap(arr[left],arr[pivotIndex]);size_tpivot=left;size_tl=left+1;size_tr=right;while(l<=r){if(arr[l]<=arr[pivot])l++;elseif(arr[r]>arr[pivot])r--;elseswap(arr[l],arr[r]);}swap(arr[pivot],arr[r]);returnr;}// quicksort recursivetemplate<typenameT>requiresstd::is_arithmetic_v<T>voidquicksortRecursive(std::vector<T>&arr,size_tleft,size_tright){if(left<right){// partition the arraysize_tpivot=partition(arr,left,right);// recursive call to left and right subarrayquicksortRecursive(arr,left,pivot-1);quicksortRecursive(arr,pivot+1,right);}}// quicksort interactivetemplate<typenameT>requiresstd::is_arithmetic_v<T>voidquicksortInteractive(std::vector<T>&arr,size_tleft,size_tright){// simulate recursive call and avoid potential stack overflow// std::stack allocate memory to hold data content on heap.stack<pair<size_t,size_t>>stack;// produce the initial statestack.emplace(left,right);// iteratewhile(!stack.empty()){// consumeauto[left,right]=stack.top();// C++17stack.pop();if(left<right){autopivot=partition(arr,left,right);// producestack.emplace(left,pivot-1);stack.emplace(pivot+1,right);}}}intmain(){std::vector<int>arr1;for(inti=0;i<100;i++)arr1.push_back(rand()%100);vector<int>arr2=arr1;for(auto&i:arr1)cout<<i<<" ";cout<<std::endl;quicksortRecursive(arr1,0,arr1.size()-1);for(auto&i:arr1)cout<<i<<" ";cout<<std::endl;quicksortInteractive(arr2,0,arr2.size()-1);for(auto&i:arr2)cout<<i<<" ";cout<<std::endl;return0;}
The recursive version of quicksort can increase the function call stack by O(lg(n)) on average, but it can degrade to O(n) in the worst case scenario. Potentially causing a stack overflow;
The interactive version of quicksort avoids the function call stack issue, avoiding stack overflow. But the memory required for the replacement is still O(lg(n)) on average and O(n) for the indexes in the worst case scenario.