Course Notes: AlgoExpert - Sorting Algorithms
In this post, I summarize the various sorting algorithms discussed in AlgoExpert - Algorithm Questions Course and implement them in Python.
Bubble Sort
- In-place sort
- Last position of the list (or array) will have the largest number at the end of the first iteration
- We can optimize the code by reducing the loop by one. i.e. since the last position of the list has the largest number, we need not check it again, and we can only check till \(n - 1 \) elements.
- Space Complexity - \(O (1) \)
- Time Complexity (Worst and Average Case) - \(O(N^2) \)
- Time Complexity (Best Case = Already Sorted Array) - \(O(N) \)
def bubbleSort(array):
isSorted = False
counter = 0
while not isSorted:
isSorted = True
for i in range(len(array) - 1 - counter):
if array[i] > array[i+1]:
swap(i, i+1, array)
isSorted = False
counter += 1
return array
def swap(i, j, array):
array[i], array[j] = array[j], array[i]
Insertion Sort
- In-place sort
- Space Complexity - \(O (1) \)
- Time Complexity (Worst and Average Case) - \(O(N^2) \)
- Time Complexity (Best Case = Already Sorted Array) - \(O(N) \)
def insertionSort(array):
for i in range(1, len(array)):
j = i
while j > 0 and array[j] < array[j - 1]:
swap(j, j - 1, array)
j -= 1
return array
def swap(i, j, array):
array[i], array[j] = array[j], array[i]
Selection Sort
- In-place sort
- Space Complexity - \(O (1) \)
- Time Complexity (All Cases) - \(O(N^2) \)
def selectionSort(array):
currentIdx = 0
while currentIdx < len(array) - 1:
smallestIdx = currentIdx
for i in range(currentIdx + 1, len(array)):
if array[smallestIdx] > array[i]:
smallestIdx = i
swap(currentIdx, smallestIdx, array)
currentIdx += 1
return array
def swap(i, j, array):
array[i], array[j] = array[j], array[i]
Three Number Sort
Given an array nums
with n
objects colored red, white, or blue, sort them in-place so that objects of the same color are adjacent, with the colors in the order red, white, and blue.
We will use the integers 0
, 1
, and 2
to represent the color red, white, and blue, respectively.
Example 1:
Input: nums = [2,0,2,1,1,0]
Output: [0,0,1,1,2,2]
Example 2:
Input: nums = [2,0,1]
Output: [0,1,2]
- Space Complexity - \(O (1) \)
- Time Complexity (All Cases) - \(O(N) \)
def threeNumberSort(array, order):
valueCounts = [0, 0, 0]
for element in array:
orderIdx = order.index(element)
valueCounts[orderIdx] += 1
for i in range(3):
value = order[i]
count = valueCounts[i]
numElementsBefore = sum(valueCounts[:i])
for n in range(count):
currentIdx = numElementsBefore + n
array[currentIdx] = value
return array
def threeNumberSort(array, order):
firstValue = order[0]
thirdValue = order[2]
firstIdx = 0
for idx in range(len(array)):
if array[idx] == firstValue:
swap(firstIdx, idx, array)
firstIdx += 1
thirdIdx = len(array) - 1
for idx in range(len(array) - 1, -1, -1):
if array[idx] == thirdValue:
swap(thirdIdx, idx, array)
thirdIdx -= 1
return array
def swap(xi, j, array):
array[i], array[j] = array[j], array[i]
def threeNumberSort(array, order):
firstValue = order[0]
secondValue = order[1]
# Keep track of the indices where the values are stored
firstIdx, secondIdx, thirdIdx = 0, 0, len(array) - 1
while secondIdx <= thirdIdx:
value = array[secondIdx]
if value == firstValue:
swap(secondIdx, firstIdx, array)
firstIdx += 1
secondIdx += 1
elif value == secondValue:
secondIdx += 1
else:
swap(secondIdx, thirdIdx, array)
thirdIdx -= 1
return array
def swap(i, j, array):
array[i], array[j] = array[j], array[i]
Quick Sort
- Very famous, very fast, most commonly used in practice
- Time Complexity (Worst Case) - \(O(N^2) \)
- Time Complexity (Best Case) - \(O(N \log N) \)
- Time Complexity (Average Case) - \(O(N \log N) \)
- Space Complexity - \(O(\log N) \)
def quickSort(array):
quickSortHelper(array, 0, len(array) - 1)
return array
def quickSortHelper(array, startIdx, endIdx):
if startIdx >= endIdx:
return
# You could also pick random
# pivotIdx = getRandomBetween(startIdx, endIdx)
# But then you'll have to swap pivotIdx and startIdx
# swap(pivotIdx, startIdx, array)
pivotIdx = startIdx
leftIdx = startIdx + 1
rightIdx = endIdx
while rightIdx >= leftIdx:
if array[leftIdx] > array[pivotIdx] and array[rightIdx] < array[pivotIdx]:
swap(leftIdx, rightIdx, array)
if array[leftIdx] <= array[pivotIdx]:
leftIdx += 1
if array[rightIdx] >= array[pivotIdx]:
rightIdx -= 1
swap(pivotIdx, rightIdx, array)
# Call on smaller sub-array
leftSubarrayIsSmaller = (rightIdx - 1 - startIdx) < (endIdx - (rightIdx + 1))
if leftSubarrayIsSmaller:
quickSortHelper(array, startIdx, rightIdx - 1)
quickSortHelper(array, rightIdx + 1, endIdx)
else:
quickSortHelper(array, rightIdx + 1, endIdx)
quickSortHelper(array, startIdx, rightIdx - 1)
def swap(i, j, array):
array[i], array[j] = array[j], array[i]
Heap Sort
- Space Complexity - \(O(1) \)
- Time Complexity (All Cases) - \(O (N \log N) \)
def heapSort(array):
buildMaxHeap(array)
for endIdx in reversed(range(1, len(array))):
swap(0, endIdx, array)
siftDown(0, endIdx - 1, array)
return array
def buildMaxHeap(array):
firstParentIdx = (len(array) - 1) // 2
for currentIdx in reversed(range(firstParentIdx + 1)):
siftDown(currentIdx, len(array) - 1, array)
def siftDown(currentIdx, endIdx, heap):
childOneIdx = currentIdx * 2 + 1
while childOneIdx <= endIdx:
childTwoIdx = currentIdx * 2 + 2 if currentIdx * 2 + 2 <= endIdx else -1
if childTwoIdx > -1 and heap[childTwoIdx] > heap[childOneIdx]:
idxToSwap = childTwoIdx
else:
idxToSwap = childOneIdx
if heap[idxToSwap] > heap[currentIdx]:
swap(currentIdx, idxToSwap, heap)
currentIdx = idxToSwap
childOneIdx = currentIdx * 2 + 1
else:
return
def swap(i, j, array):
array[i], array[j] = array[j], array[i]
Radix Sort
- Time Complexity (All Cases) - \(O (D \times (N + B)) \)
- \(D \) is the largest number of digits in an input
- \(N \) is the number of elements in the input array
- \(B \) is the base of the numbering system of the elements i.e. if we are working with decimal numbers \(B = 10 \), but if we are working with Hexadecimal numbers then \(B = 16 \)
- Space Complexity - \(O(N + B) \)
- This code will not work with negative numbers, the logic is different
def radixSort(array):
if len(array) == 0:
return array
maxNumber = max(array)
digit = 0
while maxNumber / (10 ** digit) > 0:
countingSort(array, digit)
digit += 1
return array
def countingSort(array, digit):
sortedArray = [0] * len(array)
countArray = [0] * 10
digitColumn = 10 ** digit
for num in array:
countIdx = (num // digitColumn) % 10
countArray[countIdx] += 1
for idx in range(1, 10):
countArray[idx] += countArray[idx - 1]
# Traverse in reverse to make it a 'stable' sort
for idx in range(len(array) - 1, -1, -1):
countIdx = (array[idx] // digitColumn) % 10
countArray[countIdx] -= 1
sortedIdx = countArray[countIdx]
sortedArray[sortedIdx] = array[idx]
for idx in range(len(array)):
array[idx] = sortedArray[idx]
Merge Sort
- Time Complexity - \(O(N\log N) \)
- Space Complexity (First Method) - \(O(N \log N) \)
- Space Complexity (Second Method) - \(O(N) \)
# O(nlog(n)) time | O(nlog(n)) space
def mergeSort(array):
if len(array) == 1:
return array
middleIdx = len(array) // 2
leftHalf = array[:middleIdx]
rightHalf = array[middleIdx:]
return mergeSortedArrays(mergeSort(leftHalf), mergeSort(rightHalf))
def mergeSortedArrays(leftHalf, rightHalf):
sortedArray = [None] * (len(leftHalf) + len(rightHalf))
k = i = j = 0
while i < len(leftHalf) and j < len(rightHalf):
if leftHalf[i] <= rightHalf[j]:
sortedArray[k] = leftHalf[i]
i += 1
else:
sortedArray[k] = rightHalf[j]
j += 1
k += 1
while i < len(leftHalf):
sortedArray[k] = leftHalf[i]
i += 1
k += 1
while j < len(rightHalf):
sortedArray[k] = rightHalf[j]
j += 1
k += 1
return sortedArray
# O(nlog(n)) time | O(n) space
def mergeSort(array):
if len(array) <= 1:
return array
auxiliaryArray = array[:]
mergeSortHelper(array, 0, len(array) - 1, auxiliaryArray)
return array
def mergeSortHelper(mainArray, startIdx, endIdx, auxiliaryArray):
if startIdx == endIdx:
return
middleIdx = (startIdx + endIdx) // 2
mergeSortHelper(auxiliaryArray, startIdx, middleIdx, mainArray)
mergeSortHelper(auxiliaryArray, middleIdx + 1, endIdx, mainArray)
doMerge(mainArray, startIdx, middleIdx, endIdx, auxiliaryArray)
def doMerge(mainArray, startIdx, middleIdx, endIdx, auxiliaryArray):
k = startIdx
i = startIdx
j = middleIdx + 1
while i <= middleIdx and j <= endIdx:
if auxiliaryArray[i] <= auxiliaryArray[j]:
mainArray[k] = auxiliaryArray[i]
i += 1
else:
mainArray[k] = auxiliaryArray[j]
j += 1
k += 1
while i <= middleIdx:
mainArray[k] = auxiliaryArray[i]
i += 1
k += 1
while j <= endIdx:
mainArray[k] = auxiliaryArray[j]
j += 1
k += 1
Count Inversions
Inversion Count **for an array indicates – how far (or close) the array is from being sorted. If the array is already sorted, then the inversion count is 0, but if the array is sorted in reverse order, the inversion count is the maximum.
Given an array arr[]
. The task is to find the inversion count of arr[]
. Where two elements arr[i]
and arr[j]
form an inversion if a[i] > a[j]
and i < j
.
Examples
-
Input:
arr[] = {8, 4, 2, 1}
Output: 6
Explanation: Given array has six inversions: (8, 4), (4, 2), (8, 2), (8, 1), (4, 1), (2, 1).
-
Input:
arr[] = {1, 20, 6, 4, 5}
Output: 5
Explanation: Given array has five inversions: (20, 6), (20, 4), (20, 5), (6, 4), (6, 5).
- Time Complexity (using Merge Sort) - \(O(N \log N) \)
- Space Complexity - \(O(N) \)
def countInversions(array):
return countSubArrayInversions(array, 0, len(array))
def countSubArrayInversions(array, start, end):
if end - start <= 1:
return 0
middle = start + (end - start) // 2
leftInversions = countSubArrayInversions(array, start, middle)
rightInversions = countSubArrayInversions(array, middle, end)
mergedArrayInversions = mergeSortAndCountInversions(array, start, middle, end)
return leftInversions + rightInversions + mergedArrayInversions
def mergeSortAndCountInversions(array, start, middle, end):
sortedArray = []
left = start
right = middle
inversions = 0
while left < middle and right < end:
if array[left] <= array[right]:
sortedArray.append(array[left])
left += 1
else:
inversions += middle - left
sortedArray.append(array[right])
right += 1
sortedArray += array[left:middle] + array[right:end]
for idx, num in enumerate(sortedArray):
array[start + idx] = num
return inversions