Quick Sort in C

9 mins read1.9K Views Comment
Updated on Dec 31, 2023 00:49 IST

Have you ever wondered how large datasets are sorted efficiently in programming? Quick Sort in C is the answer, utilizing a divide-and-conquer strategy to sort arrays rapidly by partitioning them around a pivot and recursively sorting the partitions. Let's read more!

Quick Sort is a widely used sorting algorithm that follows the divide-and-conquer approach. It's known for its efficiency in sorting large datasets and is commonly used in various applications due to its good average-case performance. Quick Sort is used in scenarios where average-case performance is important. It's particularly effective for large datasets. However, in cases where worst-case performance is critical, other algorithms like Merge Sort or Heap Sort might be preferred due to their consistent O(n log n) performance. 

Selection Sort Algorithm in C
Selection Sort Algorithm in C
Ever wondered how your favorite music app arranges songs from least to most played, or how an online store lists products from cheapest to priciest? At the heart of such...read more

How Does Quick Sort Work? 

Quick Sort is the epitome of divide-and-conquer strategies in sorting algorithms. It operates on the principle of selecting a 'pivot' and then partitioning the array around this pivot. 

  • Pivot Selection: Choose an element from the array as the pivot.
  • Partitioning: Reorder the array so that all elements with values less than the pivot come before it, while all elements with values greater than the pivot come after it.
  • Divide and Conquer: Recursively apply the above steps to the sub-arrays of elements with smaller and larger values. 

The beauty of Quick Sort lies in its ability to perform sorting as it partitions, leading to efficient use of time and resources. 

Illustration

Imagine Quick Sort as a game of chess: 

  • The pivot is the strategic move that defines the game. 
  • Each partitioning is like a tactical play, dividing the board and conquering each section. 
  • The recursive nature of Quick Sort is like planning multiple moves ahead, each one setting up for the next. 

Quick Sort Algorithm 

Step 1: Initialize two pointers, i and j, at the beginning and end of the array (excluding the pivot). 

Step 2: Increment i until you find an element greater than or equal to the pivot. 

Step 3: Decrement j until you find an element less than or equal to the pivot. 

Step 4: If i is less than j, swap the elements at i and j. 

Step 5: Repeat steps 2-4 until i and j cross each other. 

Step 6: Swap the pivot element with the element at j. This places the pivot in its final position, dividing the array into two partitioned subarrays. 

Step 7: Apply Quick Sort to the sub-arrays (Continue recursion until each sub-array becomes sorted) 

Initial Array

We start with an unsorted array [10, 70, 40, 90, 50]. In Quick Sort, we select a 'pivot' element from the array, which we will use to divide the array into two parts. Elements less than the pivot go to the left, and elements greater than the pivot go to the right. 

Step 1: Choosing a Pivot: 

The first step in Quick Sort is to choose a pivot element. In this case we select 50 as our pivot element. The pivot can be chosen in different ways - either the first element, the last element, a random element, or the median among a few elements from the array. 

Step 2: Partitioning: 

We reorder the array so that all elements less than the pivot come before it and all elements greater than the pivot go after it. In the images, we can see that 70 and 40 are in place because they are less than the pivot, and we are moving 50 to the position just after the pivot because it is the next smallest element after the pivot. 

Step 3: Sorting the Subarrays: 

After partitioning, we have two subarrays around the pivot. We then apply the same Quick Sort process recursively to these subarrays. The subarray [10, 40] is already sorted because it contains all elements less than the pivot and only has two elements. For the subarray [90, 70, 50], we need to select a new pivot, partition this subarray, and continue the process. 

Step 4: Final Sorted Array: 

After sorting both subarrays and placing the pivot in its correct position, we combine the elements to form the sorted array [10, 40, 50, 70, 90]. Quick Sort doesn't literally combine arrays like Merge Sort; instead, the elements are sorted in place. 

 

The process of selecting a pivot and partitioning the array is repeated recursively on the subarrays, and this recursion continues until all the subarrays are sorted. 

Dry Run 

Step 

Array 

 

 

Pivot Element 

Subarrays 

1 

[10, 70, 40, 90, 50] 

0 

4 

50 

Full Array 

2 

[10, 40, 90, 70, 50] 

1 

3 

50 

Full Array 

3 

[10, 40, 50, 70, 90] 

2 

2 

50 

[10, 40] (50) [70, 90] 

4 

[10, 40] (50) [70, 90] 

- 

- 

50 

- 

5 

[10, 40] 

0 

1 

40 

[10] (40) 

6 

[10, 40] 

1 

0 

40 

- 

7 

[10, 40) 

- 

- 

40 

- 

8 

[90, 70] 

0 

1 

90 

(90)[70] 

9 

[70, 90] 

1 

0 

90 

- 

10 

[70, 90] 

 

- 

90 

- 

Pseudo Code

Before we code, let's outline our strategy with pseudo-code: 

function quickSort(array, low, high) 

    if low < high 

        pivotIndex = partition(array, low, high) 

        quickSort(array, low, pivotIndex - 1)  // Before pivot 

        quickSort(array, pivotIndex + 1, high) // After pivot 

 

Implementation in C


 
#include <stdio.h>
// Function to swap two elements
void swap(int* a, int* b) {
int t = *a;
*a = *b;
*b = t;
}
// Function to partition the array on the basis of pivot
int partition (int arr[], int low, int high) {
int pivot = arr[high]; // pivot
int i = (low - 1); // Index of smaller element
for (int j = low; j <= high - 1; j++) {
// If current element is smaller than or equal to pivot
if (arr[j] <= pivot) {
i++; // increment index of smaller element
swap(&arr[i], &arr[j]);
}
}
swap(&arr[i + 1], &arr[high]);
return (i + 1);
}
// Function to implement Quick Sort
void quickSort(int arr[], int low, int high) {
if (low < high) {
/* pi is partitioning index, arr[pi] is now at right place */
int pi = partition(arr, low, high);
// Separately sort elements before partition and after partition
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
// A utility function to print array of size n
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}
int main() {
int arr[] = {10, 7, 8, 9, 1, 5};
int n = sizeof(arr) / sizeof(arr[0]);
printf("Unsorted array: \n");
printArray(arr, n);
quickSort(arr, 0, n - 1);
printf("Sorted array: \n");
printArray(arr, n);
return 0;
}
Copy code

Output

Unsorted array: 
10 7 8 9 1 5 
Sorted array: 
1 5 7 8 9 10 

Time Complexity 

The speed of an algorithm is often captured by its time complexity, which expresses the relationship between the input size and the time required to complete the task. 

  • Best and Average Case: In the best and average scenarios, where the pivot element is chosen to divide the array into proportionate halves consistently, the time complexity is O(n log n). This is because the array is halved with each pass (log n), and this operation is performed for each n element. 
  • Worst Case: The worst case occurs when the pivot selection results in one partition containing all but one of the elements, leading to O(n^2). This situation commonly happens when the pivot is the smallest or largest element in the array, as it would be in an already sorted or reverse-sorted array. 

Space Complexity 

Space complexity refers to the amount of memory space required by an algorithm in its life cycle. 

  • In-Place Sorting: Quick Sort is an in-place sorting algorithm. However, it requires space for the stack frames of the recursive calls. In the best case, this leads to a space complexity of O(log n), representing the height of the balanced partition tree. 
  • Worst Case: In the worst case, with unbalanced partitions, the space complexity can degrade to O(n), where n is the depth of the recursive call stack, one for each array element. 

Real-World Problem 

Problem Visualization: Imagine a busy airport with hundreds of passengers waiting to board their flights. The boarding process must be optimized to minimize passenger waiting time and ensure efficient boarding. 

Task: Sort an unsorted array of passenger boarding pass numbers in ascending order to determine the boarding sequence. 

Solution: Use Quick Sort to sort the boarding pass numbers efficiently. 


 
#include <stdio.h>
// Function to swap two elements
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
// Function to partition the array around a pivot
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // Choose the last element as pivot
int i = (low - 1); // Index of the smaller element
// Rearrange elements around the pivot
for (int j = low; j <= high - 1; j++) {
if (arr[j] <= pivot) {
i++;
swap(&arr[i], &arr[j]);
}
}
swap(&arr[i + 1], &arr[high]); // Place pivot at the right position
return (i + 1); // Return the partitioning index
}
// Function to implement Quick Sort
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high); // Partitioning index
// Recursively sort elements before and after partition
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
int main() {
int passengerNumbers[] = {35, 12, 87, 23, 61};
int n = sizeof(passengerNumbers) / sizeof(passengerNumbers[0]);
printf("Unsorted passenger numbers:\n");
for (int i = 0; i < n; i++) {
printf("%d ", passengerNumbers[i]);
}
quickSort(passengerNumbers, 0, n - 1); // Sort the array
printf("\nSorted boarding sequence:\n");
for (int i = 0; i < n; i++) {
printf("%d ", passengerNumbers[i]);
}
return 0;
}
Copy code

Output

Unsorted passenger numbers:
35 12 87 23 61 
Sorted boarding sequence:
12 23 35 61 87 

Advantages of Quick Sort

  • Efficient average-case complexity: Quick Sort has an average-case time complexity of O(n log n), making it efficient for large datasets.
  • In-place sorting: It doesn't require additional memory beyond the existing array, reducing space complexity.
  • Parallelizable: The algorithm can be easily parallelized across multiple processors or cores, further improving its speed.
  • Cache-friendly: Quick Sort works well with modern processor caches due to its locality of reference, enhancing performance.
  • No recursion depth limit: Unlike some other recursive algorithms, Quick Sort doesn't have a bound on its recursion depth, making it suitable for very large datasets. 

Disadvantages of Quick Sort

  • Worst-case complexity: In the worst-case scenario, Quick Sort's time complexity can deteriorate to O(n^2), which can occur when the chosen pivot elements are poorly selected.
  • Unstable sorting: Quick Sort does not preserve the relative order of equal elements, which might be important for specific applications.
  • High constant factor: Compared to other sorting algorithms like Merge Sort, Quick Sort might have a higher constant factor in its time complexity, leading to slightly slower performance in some cases.
  • Not ideal for small datasets: For small datasets, the overhead of partitioning and recursion might outweigh the benefits, making simpler algorithms like Bubble Sort or Insertion Sort more efficient. 

Contributed By: Manali Bhave

About the Author

This is a collection of insightful articles from domain experts in the fields of Cloud Computing, DevOps, AWS, Data Science, Machine Learning, AI, and Natural Language Processing. The range of topics caters to upski... Read Full Bio