Быстрая сортировка и с чем её едят
Всем привет! Я расскажу об алгоритме быстрой сортировки и покажу, как его можно реализовать программно.
Итак, быстрая сортировка, или, по названию функции в Си, Qsort — это алгоритм сортировки, сложность которого в среднем составляет O(n log(n)). Суть его предельно проста: выбирается так называемый опорный элемент, и массив делится на 3 подмассива: меньших опорного, равных опорному и больших опорного. Потом этот алгоритм применяется рекурсивно к подмассивам.
Алгоритм
- Выбираем опорный элемент
- Разбиваем массив на 3 части
- Создаём переменные l и r — индексы соответственно начала и конца рассматриваемого подмассива
- Увеличиваем l, пока l-й элемент меньше опорного
- Уменьшаем r, пока r-й элемент больше опорного
- Если l всё ещё меньше r, то меняем l-й и r-й элементы местами, инкрементируем l и декрементируем r
- Если l вдруг становится больше r, то прерываем цикл
- Повторяем рекурсивно, пока не дойдём до массива из 1 элемента
// qsort (0, n-1);
* This source code was highlighted with Source Code Highlighter .
Эта реализация имеет ряд недостатков, таких как возможное переполнение стека из-за большого количества вложенной рекурсии и то, что опорным элементом всегда берётся средний. Для примера это, может, и нормально, но при решении, например, олимпиадных задач, хитрое жюри может специально подобрать такие тесты, чтобы на них это решение работало слишком долго и не проходило в лимит. В принципе, в качестве опорного элемента можно брать любой, но лучше, чтобы он был максимально приближен к медиане, поэтому можно выбрать его случайно или взять средний по значению из первого, среднего и последнего. Зависимость быстродействия от опорного элемента — один из недостатков алгоритма, ничего с этим не поделать, но сильная деградация производительности происходит редко, обычно если сортируется специально подобранный набор чисел. Если всё-таки нужна сортировка, работающая гарантированно быстро, можно использовать, например, пирамидальную сортировку, всегда работающую строго за O(n log n). Обычно Qsort всё же выигрывает в производительности перед другими сортировками, не требует много дополнительной памяти и достаточно прост в реализации, поэтому пользуется заслуженной популярностью.
Quick Sort Explained | C++ STL
As the name suggests Quick Sort — is an Arrangement technique that can sequence the array of lower value to highest value or vice versa.
It is based upon the Divide and Conquer Technique and has a Time complexity of O (nlogn).
The primary nuts and screws of this sort can be framed as:
Pick an element as pivot and partition the given array around the picked pivot.
There are many different versions of quickSort that pick pivot in different ways.
- Always pick the first element as a pivot.
- Always pick the last element as a pivot.
- Pick a random element as a pivot.
- Pick median as a pivot.
In this explanation, we will be focusing just on picking the last element as a pivot to keep it clearer.
Simple Steps to Quick Sort.
The entire Quick Sort can be broken down into two steps:
- Partition the array.
- Divide the array around the pivot.
The key process of Sorting down the Array is to find the sorted place of the pivot.
Pivot Partition()
The most common way of finding partition is to start from the leftmost element and find elements that are smaller than the pivot and only then increment a dummy index i. Once we find the element that is a smaller element we swap out the current element with ith element and repeat the process.
Example courtesy of GeeksforGeeks:
Building the Divide and Conquer recursion.
This will be a basic recursion implementation, with the partition being made around the pivot.
Recursion Pseudo code:
Final Code.
Always picking the last element as pivot.
Time Complexity:
- Best case scenario: The best-case scenario occurs when the first pivot element will divide the entire Array into two equal halves and the subsequent pivot also do the same.
The calls will be we taking cn, cn/2, cn/4, and so on.
The best-case complexity of the quick sort algorithm is O(n logn)
Example of a Best Case Scenario:
[10,20,50,40,30]
[10,20] [30] [50,40]……. continues
- Worst case scenario: The worst case that can happen is when we have a completely opposite sorted Array or reverse order. The original call takes n iterations, subsequent one will take n-1, n-2 .. and so on.
[50,40,30,20,10]
[10] [50,40,30,20] …….. continues
The worst-case time complexity of Quick Sort would be O(n2).
Please Let us know if you find any discrepancies in this article and we will try out best to rectify them.
Быстрая сортировка
Быстрая сортировка представляет собой усовершенствованный метод сортировки, основанный на принципе обмена. Пузырьковая сортировка является самой неэффективной из всех алгоритмов прямой сортировки. Однако усовершенствованный алгоритм является лучшим из известных методом сортировки массивов. Он обладает столь блестящими характеристиками, что его изобретатель Ч. Хоар назвал его быстрой сортировкой.
Для достижения наибольшей эффективности желательно производить обмен элементов на больших расстояниях. В массиве выбирается некоторый элемент, называемый разрешающим . Затем он помещается в то место массива, где ему полагается быть после упорядочивания всех элементов. В процессе отыскания подходящего места для разрешающего элемента производятся перестановки элементов так, что слева от них находятся элементы, меньшие разрешающего, и справа — большие (предполагается, что массив сортируется по возрастанию).
Тем самым массив разбивается на две части:
- не отсортированные элементы слева от разрешающего элемента;
- не отсортированные элементы справа от разрешающего элемента.
Чтобы отсортировать эти два меньших подмассива, алгоритм рекурсивно вызывает сам себя.
Если требуется сортировать больше одного элемента, то нужно
- выбрать в массиве разрешающий элемент;
- переупорядочить массив, помещая элемент на его окончательное место;
- отсортировать рекурсивно элементы слева от разрешающего;
- отсортировать рекурсивно элементы справа от разрешающего.
Ключевым элементом быстрой сортировки является алгоритм переупорядочения .
Рассмотрим сортировку на примере массива:
10, 4, 2, 14, 67, 2, 11, 33, 1, 15.
Для реализации алгоритма переупорядочения используем указатель left на крайний левый элемент массива. Указатель движется вправо, пока элементы, на которые он показывает, остаются меньше разрешающего. Указатель right поставим на крайний правый элемент массива, и он движется влево, пока элементы, на которые он показывает, остаются больше разрешающего.
Пусть крайний левый элемент — разрешающий pivot . Установим указатель left на следующий за ним элемент; right — на последний. Алгоритм должен определить правильное положение элемента 10 и по ходу дела поменять местами неправильно расположенные элементы.
Движение указателей останавливается, как только встречаются элементы, порядок расположения которых относительно разрешающего элемента неправильный.
Указатель left перемещается до тех пор, пока не покажет элемент больше 10; right движется, пока не покажет элемент меньше 10.
Эти элементы меняются местами и движение указателей возобновляется.
Процесс продолжается до тех пор, пока right не окажется слева от left .
Тем самым будет определено правильное место разрешающего элемента.
Осуществляется перестановка разрешающего элемента с элементом, на который указывает right . 
Разрешающий элемент находится в нужном месте: элементы слева от него имеют меньшие значения; справа — большие. Алгоритм рекурсивно вызывается для сортировки подмассивов слева от разрешающего и справа от него.
Реализация алгоритма быстрой сортировки на Си
Результат выполнения 
Quicksort Algorithm in C#

Quicksort is one of the most efficient algorithms that we can use to accomplish our sorting goals. In this article, we discuss how to implement Quicksort in C# as well as analyze its time and space complexity.
What is Quicksort Algorithm?
Just like merge sort, quicksort uses the “divide and conquer” strategy to sort elements in arrays or lists. It implements this strategy by choosing an element as a pivot and using it to partition the array.
The left subarray contains all elements that are less than the pivot. The right subarray contains all the elements that are greater than the pivot. We recursively repeat this process until we sort the array. We can select the pivot the algorithm uses during this process in different ways:
- The first element of the array
- The last element of the array
- A random element of the array
- Median element of the array
So, what is the best pivot to select when implementing the quicksort algorithm? The answer to this question is not that simple.
Selecting the middle element of the unsorted array seems to make sense as it divides the array into equal halves. However, the process of finding that middle element is difficult and time-consuming. Using this strategy involves calculating the array’s length in every iteration and halving it to determine the index of the element in the middle of the array.
On the other hand, when using the median element of the array as the pivot, we use the median-of-three technique where we select the pivot based on the median of three values such as the first, middle, and last elements of the array.
Therefore, selecting the first, last, random, or median element of the array as the pivot is the best approach.
Let’s take a deep dive and have and learn how quicksort works.
How Does Quicksort Algorithm Work?
To illustrate how the quicksort algorithm works, let’s assume we intend to sort this array:
In this article, let’s take the first element (52) as the pivot as we learn how to implement quicksort.
First Partition Level
We start traversing the array from the left and right indexes while comparing their elements against the pivot. 96 is greater than the pivot while 14 is less than the pivot so we swap their positions and the array becomes:
52 , 14 , 67, 71, 42, 38, 39, 40, 96
Next, we can see that 67 is greater than the pivot element while 40 is less than the pivot so we swap their positions:
As we traverse the array, 71 is greater than the pivot while 39 is less than the pivot so we swap their positions:
52 , 14 , 40 , 39 , 42, 38, 71 , 67 , 96
38 and 42 are both less than the pivot, which triggers the iteration to stop. The next step is to determine the array’s split point. 38 is less than the pivot so we swap their positions while 71 is greater than the pivot, which becomes the new split point.
38, 14 , 40 , 39 , 42, 52, 71 , 67 , 96
Given the fact that quicksort is recursive, in the next iteration, we are going to have two subarrays based on the identified splitting points.
Second Partition Level
The left subarray is 38, 14, 40, 39, 42 and the right subarray is 71 , 67 , 96
Let’s start with the right subarray and take 71 as the pivot. 67 is less than the pivot hence, we swap their positions. On the other hand, 96 is greater than the pivot so we don’t swap them and the array becomes sorted:
We are going to repeat the same process for the left subarray and select 38 as the pivot for that subarray. 14 is less than the pivot while the rest of the elements are greater than the pivot so the array becomes:
14 , 38 , 40, 39, 42
Third Partition Level
In the last iteration, quicksort takes 40 as the pivot. 39 is less than the pivot so we swap their positions but 42 is greater than the pivot, which completes the sorting process.
We can see that by using the divide and conquer strategy we complete the sorting process efficiently with the result being:
14, 38, 39, 40, 42, 67, 71, 96
Let’s learn how to implement the quicksort algorithm in C#.
SortArray() starts by assigning the values of the leftIndex and rightIndex to new variables i and j , which we are going to use when iterating through the array.
Next, we set the pivot as the leftmost element in the array:
var pivot = array[leftIndex];
The algorithm starts placing the pivot element at its correct position in the sorted array by dividing the array into two lists in the outermost while loop. Our goal is to place all smaller elements (smaller than the pivot) to the left of the pivot and all greater elements to the right of the pivot.
If the elements to the left of the pivot are less than the pivot element, we skip their positions:
Subsequently, if the elements to the right of the pivot are greater than the pivot element, we skip their positions as we loop through the right subarray:
As we loop through the array, if we find an element in the left subarray that is greater than the pivot and an element in the right subarray which is less than the pivot, we swap their positions. SortArray() swaps the positions of array[i] and array[j] and updates the subarrays’ counters accordingly:
Since the quicksort algorithm is recursive, the method calls itself to sort the left and right subarrays and returns a sorted array when the process is complete:
Finally, we can verify that the SortArray() method sorts a given unsorted array accurately:
Let’s learn quicksort’s time and space complexity and understand why it is considered to be one of the most efficient sorting algorithms.
Space Complexity of Quicksort Algorithm
As we’ve seen in the implementation section, the quicksort algorithm only needs extra space for handling recursive function calls and temporary variables when swapping array elements. This means quicksort calls itself on the order of log(N) times while the number of calls in worst-case scenarios is O(N).
At each partition level or recursive call, the algorithm has to allocate a new stack frame of constant size to handle the sorting process. Therefore, the space complexity of the quicksort algorithm is O(log N).
Time Complexity of Quicksort Algorithm
Quicksort is a “divide and conquer” algorithm as it subdivides a large unsorted array into smaller partitions that are easily sorted by comparing array elements with their pivots.
Best-Case Time Complexity
Quicksort achieves optimal performance if we always divide the arrays and subarrays into two partitions of equal size by selecting the middle element as the pivot. For example, when we want to sort an array that has four elements, we partition it twice since we use recursion to implement the algorithm.
This becomes quicksort’s best-case scenario, which we can explain using this recurrence relation:
From this relation, we can see that the size of the array is halved each time we partition the array. This means the number of partitioning levels is log2 N. Therefore since we have N array elements and log2 N partitioning levels, the best-case scenario of quicksort is O(N log N).
Average-Case Time Complexity
To come up with an accurate depiction of how quicksort handles average-case complexity, we need to consider all the possible array permutations and calculate the time the algorithm takes to sort each array permutation, which is not easy.
However, since the algorithm still has partitioning log2 N levels and sorting N elements, the average-case complexity of the quicksort algorithm is O(N log N).
Worst-Case Time Complexity
This scenario occurs if the selected pivot element is always the smallest or largest element of the array or its partitions. Such a scenario occurs when we keep choosing the last element in an array that has been sorted as the pivot element.
The partitioning process splits the array into two non-equal partitions. One partition has a length of zero (no element is larger than the pivot element). The other partition has a length of N-1 ( the rest of the elements except the pivot element).
Therefore, when we calculate the partitioning levels, the algorithm would need N partitioning levels with the partitions having size N, N-1, N-2, etc.
Quicksort’s worst-case complexity becomes O(N 2 ) as its partitioning effort decreases linearly from N to 0.
One of the strategies we can use to address this problem is shuffling. It introduces randomness in the array, making it possible for the algorithm to work efficiently.
Advantages of Quicksort Algorithm
To start with, the quicksort algorithm is fast and efficient, especially in best and average-case time complexity scenarios. This makes it better than other algorithms such as selection sort and bubble sort, which have O(N 2 ) time complexity.
Besides being fast and efficient, quicksort is memory-efficient. This algorithm sorts all the array elements in place, which makes it ideal for use in applications that have limited memory. On the other hand, other algorithms such as merge sort need auxiliary arrays to store values during the sorting process.
Disadvantages of Quicksort Algorithm
First, quicksort is an unstable algorithm, which means, it may not preserve the original order of key-value pairs. For example, when quicksort encounters two similar elements, their order could be reversed as the algorithm sorts them. Therefore, in situations where stability is an important factor, quicksort may not be the best algorithm to use.
Besides being unstable, we can see that quicksort has a worst-case complexity of O(N 2 ). Therefore, although the algorithm is fast and efficient in best and average-case scenarios, it is not efficient when it encounters large arrays that are sorted.
Performance Tests
Let’s verify the quicksort algorithm has a time complexity of O(N log N). We are going to accomplish this by measuring the time it takes for the algorithm to sort an array.
For these tests, we are going to use the first array element as the pivot element.
Next, we are going to define a method that generates a sequence of elements. This method simulates a scenario where we have a sorted array:
Next, we are going to create an object that holds different arrays that have random and sorted values:
Each object entry has four values: an integer array such as CreateRandomArray(200) , the index of the first value in the array (0), the index of the last element in the array (199), and a string object storing the name of that array (“Small Unsorted”).
The array objects have different sizes (to simulate time complexity scenarios) and hold random numbers that are added by the CreateRandomArray() method. The CreateSortedArray() method creates arrays that have values that are sorted.
Let’s assess the sample best, average, and worst-case complexity performance results of the algorithm:
We can see that quicksort performs well when sorting small and medium arrays. However, as we continue increasing the size of the array, the longer it takes to sort it. (5 times the array size, 30 times longer to sort it – medium to large unsorted)
From the benchmark, we can also see that it takes longer to sort presorted arrays than randomly generated arrays. At some point, quicksort’s worst-case time complexity may come into play resulting in the algorithm throwing a StackOverflowException .
Conclusion
In this article, we have learned how quicksort works in C#. It uses recursion and the “divide and conquer” strategy to make it efficient. Just like the other algorithms, it has some strengths and weaknesses.