Quick Sort in C#

Table of contents

Algorithms form the backbone of software development, and sorting algorithms hold immense importance in efficiently organizing data in a desired order. They are indispensable tools for developers seeking to arrange information systematically and optimize performance.

Few sorting algorithms are as renowned and widely used as the QuickSort algorithm. It offers a fast and efficient method for sorting data, with an average time complexity of O(n log n), positioning it as one of the fastest sorting algorithms available.

However, it is crucial to acknowledge that QuickSort may not always be optimal. In specific scenarios, such as when sorting an already sorted array, QuickSort can exhibit inefficiency and longer execution times. Its worst-case time complexity is O(n^2), representing a notably poor performance for certain input distributions.

While QuickSort possesses exceptional average-case performance, it is essential to consider other sorting algorithms based on specific use cases and input characteristics. Alternative algorithms like MergeSort or HeapSort might offer more consistent performance across various scenarios or provide better worst-case time complexity guarantees.

Therefore, selecting the most suitable sorting algorithm involves carefully assessing the data's characteristics, understanding the desired time complexity requirements, and considering trade-offs between efficiency and worst-case performance.

Implementation

To create an efficient QuickSort algorithm, you need to understand recursion and a good grasp of C# syntax. If you are unfamiliar with these topics, it would be beneficial to review them before proceeding.

Before diving into the code, let's recap what we aim to achieve with QuickSort. The goal is to sort a collection of data, in our case, an array of integers, in ascending order. To accomplish this, we select a pivot point within the array. While there are various approaches for choosing the pivot, a commonly used method is selecting the middle element, which helps reduce the time complexity in worst-case scenarios.

We utilize the pivot value to partition the initial array into two subarrays: the left array containing values smaller than the pivot and the right array containing values greater than the pivot. This partitioning process is recursively applied to each subarray until each subarray has only one element. Once all the values are separated into left and right subtrees, we merge them back into a single sorted array. If these concepts aren't immediately apparent, examining the code implementation should provide a clearer understanding.

We'll break the function down into four parts. First, we'll create a base case so the recursion doesn't run forever. Secondly, we'll select the pivot point and create the arrays. Third, we'll loop through every element, separating it based on the pivot value. Finally, we'll perform recursion and merge the arrays into a single array.

To begin, create a function that will return and accepts an array of integers, and let's perform the first step, creating a base case. You may be asking yourself, what is the base case? We're trying to get the subarrays down to a single value, so that would be the base case.

public static int[] QuickSort(int[] arr)
{
    if (arr.Length < 2){
        return arr;
    }
}

Next, we'll get the pivot and create the left and right array. In the example, we use lists, but that is only because adding values to a list over an array is easier.

public static int[] QuickSort(int[] arr)
{
    if (arr.Length < 2){
        return arr;
    }

    int pivot = arr[arr.Length / 2]; // Get pivot value and assign it to a variable
    List<int> left = new List<int>(); // Create left array
    List<int> right = new List<int>(); // Create right array
}

Everything so far should make sense. Next, we'll create a loop to iterate all the values in the original array, separating them based on size.

public static int[] QuickSort(int[] arr){
      if (arr.Length < 2){
          return arr;
      }

      int pivot = arr[arr.Length / 2];
      List<int> left = new List<int>();
      List<int> right = new List<int>();

      for (int i = 0; i < arr.Length; i++) // Loop through each index starting with 0 and ending with arr.Length - 1
      {
       if(i == arr.Length / 2){ // Skip over the pivot element
          continue;
        }

        if (arr[i] < pivot) // If the value is smaller than the pivot, add it to the left array
        {
        left.Add(arr[i]);
      }
      else if (arr[i] >= pivot) // If the value is larger or equal to the pivot, add it to the right array
      {
        right.Add(arr[i]);
      }
    }
}

This is the main section of the algorithm performing the sorting itself. As explained earlier, if the value is less than the pivot, we place the value in the left array. However, if the value equals or exceeds the pivot, it is placed in the right array.

public static int[] QuickSort(int[] arr){
      if (arr.Length < 2){
          return arr;
      }

      int pivot = arr[arr.Length / 2];
      List<int> left = new List<int>();
      List<int> right = new List<int>();

      for (int i = 0; i < arr.Length; i++) 
      {
        if(i == arr.Length / 2){
          continue;
        }

        if (arr[i] < pivot)
        {
        left.Add(arr[i]);
      }
        else if (arr[i] >= pivot)
      {
        right.Add(arr[i]);
        }

    }

    List<int> sortedArr = new List<int>(); // Create array to return
    sortedArr.AddRange(QuickSort(left.ToArray())); // Add left array & Recursion
    sortedArr.Add(pivot); // Add pivot value
    sortedArr.AddRange(QuickSort(right.ToArray())); // Add right array & Recursion

    return sortedArr.ToArray();
}

That finishes the quick sort. You should be able to run any arrays of integers through this and obtain an average of O(n log n).