Quick sort#
Is sorting algorithm that is based on recursive procedure which divides an array into two parts - less than some pivot element and greater than some pivot element, and applies itself to each of the parts.
import random
from random import randint
from warnings import warn
Partition#
The core part of the algorithm is a procedure that allows elements less than the pivot to be pre-positioned to the left of the array, and values greater than the pivot to be pre-positioned to the right of the array.
For simplicity, I will use left to denote a subset of elements such that it is smaller than pivot
, and right to denote a subset of elements such that it is larger than pivot
.
def partition(arr, low, high, pivot):
'''
Function shuffle elements in place array
so that all elements lower than
pivot have less indexes than elements
whose value is greater than pivot.
Parameters
----------
arr : list
array, which elements need to be shuffled;
low : int
left border index that will be
considered by funtion;
high : int
right border index that will be
consedered by funtion;
pivot : number
such a number that all elements of the array
less will be collected in the left part of
the array, and all elements of the array
more will be collected in the right part of
the array.
Returns
----------
out : int
index of the half-next element among those
that are less than pivot
'''
i = low
for j in range(low, high+1):
if arr[j] <= pivot:
arr[i], arr[j] = arr[j], arr[i]
i+=1
return i-1
Now let’s see how this works in the most general case. So in the following cell there is an input array and partition
is applied from the i-th to the len-i-th elements, with pivot 10. And before execution funtion under consideration is printed results:
Elements that were in the input array before
low
don’t change their order;Elements that were in the input array after `high’ don’t change their order either;
Elements between the
low
andhigh
indices are split into two arrays:less or equal than
pivot
;more than
pivot
.
random.seed(10)
arr_size = 20
arr = [randint(0, 20) for i in range(arr_size)]
low = 3
high = arr_size - 3
ind = partition(arr, low, high, 10)
print("Input array", arr, end="\n\n")
print(
"before left:", arr[:low], "\n",
"left:", arr[low:ind+1], "\n",
"right:", arr[ind+1:high], "\n",
"after right:", arr[high:], "\n",
sep = ""
)
Input array [18, 1, 13, 0, 6, 8, 5, 1, 10, 2, 7, 18, 14, 16, 15, 15, 15, 20, 11, 1]
before left:[18, 1, 13]
left:[0, 6, 8, 5, 1, 10, 2, 7]
right:[18, 14, 16, 15, 15, 15]
after right:[20, 11, 1]
Also let’s check some boundary cases:
Left contains only one lement:
arr = [3,2,1,5,4]
print("Input array", arr)
ind = partition(arr, 0, 4, 1)
print("Result:", arr, f"with separation in {ind}-th position.")
Input array [3, 2, 1, 5, 4]
Result: [1, 2, 3, 5, 4] with separation in 0-th position.
Left is empty;
arr = [3,2,1,5,4]
print("Input array", arr)
ind = partition(arr, 0, 4, 0)
print("Result:", arr, f"with separation in {ind}-th position.")
Input array [3, 2, 1, 5, 4]
Result: [3, 2, 1, 5, 4] with separation in -1-th position.
Right contains only one element;
arr = [3,2,1,5,4]
print("Input array", arr)
ind = partition(arr, 0, 4, 4)
print("Result:", arr, f"with separation in {ind}-th position.")
Input array [3, 2, 1, 5, 4]
Result: [3, 2, 1, 4, 5] with separation in 3-th position.
Right is empty;
arr = [3,2,1,5,4]
print("Input array", arr)
ind = partition(arr, 0, 4, 10)
print("Result:", arr, f"with separation in {ind}-th position.")
Input array [3, 2, 1, 5, 4]
Result: [3, 2, 1, 5, 4] with separation in 4-th position.
Classic sort#
Here is the basic realisation of this algorithm, which is a key to algorithms for different tasks based on the same idea.
def quick_sort(arr, low=0, high=None):
'''
Sort in ascending order in place.
This is a recursive function that only
replaces elements in the elements only
in the specified range.
Parameters
----------
arr : list
the object will be sorted by the
passed reference;
low : int
is the lower index of the subarray
under consideration;
high : int
is the highes index of the subarry
under consideration.
'''
if len(arr)<=1:
return arr
if not high: high=len(arr)-1
ind = partition(arr, low, high, arr[high])
if ind-1>low:quick_sort(arr, low=low, high=ind-1)
if ind+1<high: quick_sort(arr, low=ind+1, high=high)
Now let’s check how it works:
random.seed(20)
arr_size = 20
arr = [randint(0, 20) for i in range(arr_size)]
print("Input array:", arr)
quick_sort(arr)
print("Sorted array:", arr)
Input array: [4, 8, 20, 3, 10, 18, 5, 0, 13, 13, 2, 3, 4, 10, 15, 18, 14, 13, 6, 6]
Sorted array: [0, 2, 3, 3, 4, 4, 5, 6, 6, 8, 10, 10, 13, 13, 13, 14, 15, 18, 18, 20]
And finally, a little “stress test” - we’ll compare the results of the function quick_sort
and the built-in function sorted
.
arr_size = 20
for i in range(1000):
arr = [randint(0, 20) for i in range(arr_size)]
classic_res = sorted(arr)
quick_sort(arr)
if classic_res != arr:
warn("there is some problems with your realisation!!")
Quick search#
There is another application of the same idea - quick search. This algorithm allows you to find the k-th largest (or smallest) elements in an array.
Idea#
For brevity, we will describe the problem of finding the k-th largest element.
As with quick sort algorithm, we divide array into left and right at each step of the algorithm. Here we won’t include pivot to the left. There are few options from this point:
Number of elements in right equal \(k-1\) - it means that pivot is element we search for, we can just return it as answer;
Number of elements in right greater than or equal to \(k\) - it guarantees that the \(k\)-th largerst element of the input array is somewhere in the right. We have to continue search of \(k\)-th in the left;
Number of elements in right less than \(k-1\) - it means that \(k\)-th largest element of input array somewhere in left. But elements in right and pivot are bigger than element under consideration, so if element in input array was k-th biggest in left it must be \((k-m-1)\)-th (where \(m\) number of elements in the right). We have to keep looking for the \(k-m-1\)-th largest in the left.
Recursive implementation#
This is my original implementation of the quick search. It is based on recursion.
def quick_search_rec(arr, k, low=0, high=None):
'''
Quick search algorithm implementation.
Finds k-th biggest element in the arr
between low and high indeces.
Parameters
----------
arr : list
array to be searched;
k : int
number of biggest element to be found;
low : int
lower boundary of the subarray to be searched;
high : int
upper boundary of the subarray to be searched.
Subarray will include element with index high.
Returns
----------
out : number
k-th biggest element of the subarray of arr
between indeces low and high.
'''
# boundary cases
if len(arr) < 1: return None
if k<=0 : k=1
if not high: high=len(arr)-1
ind = partition(arr, low, high, arr[high])
# number of elements in right
right_number = high-ind
if right_number == k-1: return arr[ind]
elif right_number>=k: return quick_search_rec(arr, k, ind+1, high)
elif right_number<k: return quick_search_rec(
arr, k-right_number-1, low, ind-1
)
Here is a simple example to show that it works:
random.seed(11)
arr = [random.randint(0, 20) for i in range(10)]
print("Input:", arr)
quick_search_rec(arr, 3)
Input: [14, 17, 14, 14, 16, 18, 6, 5, 16, 15]
16
Here is a simple stress test where I compare result of selecting \(-k\)-th element of array sorted by python and result of the quick_sarch_rec
.
array_size = 20
for i in range(1000):
arr = [random.randint(0, 20) for i in range(array_size)]
k = random.randint(1, array_size//2)
build_in = sorted(arr)[-k]
my = quick_search_rec(arr, k)
if my != build_in:
warn("Mistake!")
break
while
implementation#
Sometimes recursion isn’t acceptable solution - you can get RecursionError
. I faced in my practice case when solution showen above isn’t acceptable, so in the following cell there is code that realise fast search without recursion. Here is just a subarray moved from step to step - high
and low
changed to suit this case:
def quick_search_while(arr, k):
'''
Quick search algorithm implementation.
Finds k-th biggest element in the arr.
Parameters
----------
arr : list
array to be searched;
k : int
number of biggest element to be found.
Returns
----------
out : number
k-th biggest element of the arr.
'''
# boundary cases
if len(arr) < 1: return None
if k<=0 : k=1
low = 0
high = len(arr)-1
while True:
ind = partition(arr, low, high, arr[high])
# number of elements in right
right_number = high-ind
if right_number == k-1: return arr[ind]
elif right_number>=k: low=ind+1
elif right_number<k: k=k-right_number-1;high=ind-1
Here is a simple example to show that it works:
random.seed(12)
arr = [random.randint(0, 20) for i in range(10)]
print("Input:", arr)
quick_search_rec(arr, 3)
Input: [15, 8, 16, 11, 4, 12, 0, 11, 15, 8]
15
Here is a simple stress test where I compare result of selecting \(-k\)-th element of array sorted by python and result of the quick_sarch_while
.
array_size = 20
for i in range(1000):
arr = [random.randint(0, 20) for i in range(array_size)]
k = random.randint(1, array_size//2)
build_in = sorted(arr)[-k]
my = quick_search_while(arr, k)
if my != build_in:
warn("Mistake!")
break