Module Chapter 10
Module Chapter 10
Disclaimer:
This learning material is provided in accordance with the modular learning approach adopted by the
University in response to the disruptions caused by Typhoon Pepito, which affected the delivery of
education in the province. The authors and publisher of the content are duly acknowledged. The
college and its faculty do not claim ownership of the sourced information. This learning material is
intended solely for instructional purposes and is not for commercial use.
Introduction
Sorting algorithms are a set of instructions that take an array or list as an input and arrange the items
into a particular order.
Sorts are most commonly in numerical or a form of alphabetical (or lexicographical) order, and can
be in ascending (A-Z, 0-9) or descending (Z-A, 9-0) order.
Since they can often reduce the complexity of a problem, sorting algorithms are very important in
computer science. These algorithms have direct applications in searching algorithms, database
algorithms, divide and conquer methods, data structure algorithms, and many more.
When choosing a sorting algorithm, some questions have to be asked – How big is the collection being
sorted? How much memory is available? Does the collection need to grow?
The answers to these questions may determine which algorithm is going to work best for each
situation. Some algorithms like merge sort may need a lot of space or memory to run, while insertion
sort is not always the fastest, but doesn't require many resources to run.
You should determine what your requirements are, and consider the limitations of your system before
deciding which sorting algorithm to use.
1. The number of swaps or inversions required: This is the number of times the algorithm swaps
elements to sort the input. Selection sort requires the minimum number of swaps.
2. The number of comparisons: This is the number of times the algorithm compares elements
to sort the input. Using Big-O notation, the sorting algorithm examples listed above require at
least O(nlogn) comparisons in the best case, and O(n^2) comparisons in the worst case for
most of the outputs.
3. Whether or not they use recursion: Some sorting algorithms, such as quick sort, use recursive
techniques to sort the input. Other sorting algorithms, such as selection sort or insertion sort,
use non-recursive techniques. Finally, some sorting algorithms, such as merge sort, make use
of both recursive as well as non-recursive techniques to sort the input.
4. Whether they are stable or unstable: Stable sorting algorithms maintain the relative order of
elements with equal values, or keys. Unstable sorting algorithms do not maintain the relative
order of elements with equal values / keys.
For example, imagine you have the input array [1, 2, 3, 2, 4]. And to help differentiate between the
two equal values, 2, let's update them to 2a and 2b, making the input array [1, 2a, 3, 2b, 4].
Stable sorting algorithms will maintain the order of 2a and 2b, meaning the output array will be [1, 2a,
2b, 3, 4]. Unstable sorting algorithms do not maintain the order of equal values, and the output array
may be [1, 2b, 2a, 3, 4].
Insertion sort, merge sort, and bubble sort are stable. Heap sort and quick sort are unstable.
5. The amount of extra space required: Some sorting algorithms can sort a list without creating
an entirely new list. These are known as in-place sorting algorithms, and require a
constant O(1) extra space for sorting. Meanwhile, out of place sorting algorithms create a new
list while sorting.
Insertion sort and quick sort are in place sorting algorithms, as elements are moved around a pivot
point, and do not use a separate array.
Merge sort is an example of an out of place sorting algorithm, as the size of the input must be allocated
beforehand to store the output during the sort process, which requires extra memory.
With a worst-case complexity of O(n^2), bubble sort is very slow compared to other sorting algorithms
like quicksort. The upside is that it is one of the easiest sorting algorithms to understand and code from
scratch.
From technical perspective, bubble sort is reasonable for sorting small-sized arrays or specially when
executing sort algorithms on computers with remarkably limited memory resources.
Example:
• Starting with [4, 2, 6, 3, 9], the algorithm compares the first two elements in the array, 4 and
2. It swaps them because 2 < 4: [2, 4, 6, 3, 9]
• It compares the next two values, 4 and 6. As 4 < 6, these are already in order, and the algorithm
moves on: [2, 4, 6, 3, 9]
• The next two values are also swapped because 3 < 6: [2, 4, 3, 6, 9]
• The last two values, 6 and 9, are already in order, so the algorithm does not swap them.
• The algorithm swaps the next two values because 3 < 4: [2, 3, 4, 6, 9]
The list is already sorted, but the bubble sort algorithm doesn't realize this. Rather, it needs to
complete an entire pass through the list without swapping any values to know the list is sorted.
Clearly bubble sort is far from the most efficient sorting algorithm. Still, it's simple to wrap your head
around and implement yourself.
Properties
• Stable: Yes
Example in C++
Example:
In Insertion sort, you compare the key element with the previous elements. If the previous elements
are greater than the key element, then you move the previous element to the next position.
[835142]
Step 1 :
In this case, `key` is compared with 8. since 8 > 3, move the element 8
Result: [ 3 8 5 1 4 2 ]
Step 2 :
Result: [ 3 5 8 1 4 2 ]
Step 3 :
Step 4 :
Step 5 :
The algorithm shown below is a slightly optimized version to avoid swapping the key element in every
iteration. Here, the key element will be swapped at the end of the iteration (step).
Properties:
For instance, the time complexity of Quick Sort is approximately O(nlog(n)) when the selection of
pivot divides original array into two nearly equal sized sub arrays.
On the other hand, if the algorithm, which selects of pivot element of the input arrays, consistently
outputs 2 sub arrays with a large difference in terms of array sizes, quick sort algorithm can achieve
the worst case time complexity of O(n^2).
• Choose an element to serve as a pivot, in this case, the last element of the array is the pivot.
• Partitioning: Sort the array in such a manner that all elements less than the pivot are to the
left, and all elements greater than the pivot are to the right.
• Call Quicksort recursively, taking into account the previous pivot to properly subdivide the
left and right arrays.
The space complexity of quick sort is O(n). This is an improvement over other divide and conquer
sorting algorithms, which take O(nlong(n)) space.
Quick sort achieves this by changing the order of elements within the given array. Compare this with
the merge sort algorithm which creates 2 arrays, each length n/2, in each function call.
However there does exist the problem of this sorting algorithm being of time O(n*n) if the pivot is
always kept at the middle. This can be overcomed by utilizing a random pivot
Complexity
Best, average, worst, memory: n log(n)n log(n)n 2log(n). It's not a stable algorithm, and quicksort is
usually done in-place with O(log(n)) stack space.
The space complexity of quick sort is O(n). This is an improvement over other divide and conquer
sorting algorithms, which take O(n log(n)) space.
Lesson 4: Merge Sort
Merge Sort is a Divide and Conquer algorithm. It divides input array in two halves, calls itself for the
two halves and then merges the two sorted halves. The major portion of the algorithm is given two
sorted arrays, and we have to merge them into a single sorted array. The whole process of sorting an
array of N integers can be summarized into three steps-
• Sort the left half and the right half using the same recurring algorithm.
There is something known as the Two Finger Algorithm that helps us merge two sorted arrays together.
Using this subroutine and calling the merge sort function on the array halves recursively will give us
the final sorted array we are looking for.
Since this is a recursion based algorithm, we have a recurrence relation for it. A recurrence relation is
simply a way of representing a problem in terms of its subproblems.
Putting it in plain English, we break down the subproblem into two parts at every step and we have
some linear amount of work that we have to do for merging the two sorted halves together at each
step.
Complexity
The biggest advantage of using Merge sort is that the time complexity is only n*log(n) to sort an entire
Array. It is a lot better than n^2 running time of bubble sort or insertion sort.
Before we write code, let us understand how merge sort works with the help of a diagram.
• Initially we have an array of 6 unsorted integers Arr(5, 8, 3, 9, 1, 2)
• We split the array into two halves Arr1 = (5, 8, 3) and Arr2 = (9, 1, 2).
• Again, we divide them into two halves: Arr3 = (5, 8) and Arr4 = (3) and Arr5 = (9, 1) and Arr6 =
(2)
• Again, we divide them into two halves: Arr7 = (5), Arr8 = (8), Arr9 = (9), Arr10 = (1) and Arr6 =
(2)
• We will now compare the elements in these sub arrays in order to merge them.
Properties:
• Time Complexity: O(n*log(n)). The time complexity for the Merge Sort might not be obvious
from the first glance. The log(n) factor that comes in is because of the recurrence relation we
have mentioned before.
• Sorting In Place: No in a typical implementation
• Stable: Yes
• Parallelizable: yes
C++ Implementation
Explanation:
Merge Process:
• A while loop runs as long as both token_a and token_b are within the bounds of their
respective arrays (A and B).
• At each step, compare the current element from A (A[token_a]) with the current element from
B (B[token_b]).
• Else:
o Increment token_c.
Once either A or B is fully traversed, the remaining elements of the other array are appended to C:
Example Walkthrough:
Given:
• B = {3, 5, 6, 9, 15}
Process:
This property can be leveraged to access the maximum element in the heap in O(logn) time using the
maxHeapify method. We perform this operation n times, each time moving the maximum element in
the heap to the top of the heap and extracting it from the heap and into a sorted array. Thus, after n
iterations we will have a sorted version of the input array.
The algorithm is not an in-place algorithm and would require a heap data structure to be constructed
first. The algorithm is also unstable, which means when comparing objects with same key, the original
ordering would not be preserved.
This algorithm runs in O(nlogn) time and O(1) additional space [O(n) including the space required to
store the input data] since all operations are performed entirely in-place.
The best, worst and average case time complexity of Heapsort is O(nlogn). Although heapsort has a
better worse-case complexity than quicksort, a well-implemented quicksort runs faster in practice. This
is a comparison-based algorithm so it can be used for non-numerical data sets insofar as some relation
(heap property) can be defined over the elements.
Heapify Function
The heapify function is the core of heap operations in the Heap Sort algorithm. Its purpose is to
maintain the max heap property for a subtree rooted at a specific index i.
o Compare it with its left child (index 2*i + 1) and right child (index 2*i + 2).
o Update largest to hold the index of the child if it is greater than the root.
o If the largest element is not the root, swap the root with the largest child.
o Recursively call heapify on the affected subtree to ensure the heap property is
maintained.
3. Base case:
o If the current node is already larger than its children (or is a leaf node), no further
action is required, and the recursion terminates.
Example Walkthrough:
Call: heapify(arr, 5, 1)
1. Initial state:
Call: heapify(arr, 5, 0)
1. Initial state:
o i = 0, root value = 4, left child = 10 (index 1), right child = 3 (index 2).
2. Swap:
3. Recursive call:
4. Reheapify:
• Start from the last non-leaf node (n / 2 - 1) and heapify each node in reverse order.
• After this step, the largest element is at the root of the heap (arr[0]).
• Swap the root (largest element) with the last element of the heap.
Example Execution:
Sort:
One important thing to remember is that counting sort can only be used when you know the range
of possible values in the input beforehand.
Example:
input = [2, 5, 3, 1, 4, 2]
First, you need to create a list of counts for each unique value in the input list. Since you know the
range of the input is from 0 to 5, you can create a list with five placeholders for the values 0 to 5,
respectively:
count = [0, 0, 0, 0, 0, 0]
// val: 0 1 2 3 4 5
Then, you go through the input list and iterate the index for each value by one.
For example, the first value in the input list is 2, so you add one to the value at the second index of
the count list, which represents the value 2:
count = [0, 0, 1, 0, 0, 0]
// val: 0 1 2 3 4 5
The next value in the input list is 5, so you add one to the value at the last index of the count list,
which represents the value 5:
count = [0, 0, 1, 0, 0, 1]
// val: 0 1 2 3 4 5
Continue until you have the total count for each value in the input list:
count = [0, 1, 2, 1, 1, 1]
// val: 0 1 2 3 4 5
Finally, since you know how many times each value in the input list appears, you can easily create a
sorted output list. Loop through the count list, and for each count, add the corresponding value (0 -
5) to the output array that many times.
For example, there were no 0's in the input list, but there was one occurrence of the value 1, so you
add that value to the output array one time:
output = [1]
Then there were two occurrences of the value 2, so you add those to the output list:
output = [1, 2, 2]
output = [1, 2, 2, 3, 4, 5]
Properties
C++ Implementation
Radix Sort uses stable sorting for each digit, ensuring that the order of equal digits is preserved
between passes. By progressively sorting each digit from the least significant to the most significant,
the entire array becomes sorted.
Simple Analogy:
Think of sorting a pile of envelopes by postal codes:
1. First, sort by the last digit of the postal code.
2. Then, sort by the second-to-last digit while keeping the previous sorting intact.
3. Repeat until all digits are sorted.
The Algorithm:
For each digit i where i varies from the least significant digit to the most significant digit of a number,
sort input array using countsort algorithm according to ith digit. We used count sort because it is a
stable sort.
Based on the algorithm, we will sort the input array according to the one's digit (least significant
digit).
0: 10
1: 21 11
2:
3: 123
4: 34 44 654
5:
6:
7: 17
8:
9:
So, the array becomes 10, 21, 11, 123, 24, 44, 654, 17
Now, the array becomes : 10, 11, 17, 21, 123, 34, 44, 654
Example Walkthrough:
Input Array: {0.897, 0.565, 0.656, 0.123, 0.665, 0.343}
Step 1: Create Buckets
Divide into 6 buckets (as n = 6):
• Bucket 0: [ ]
• Bucket 1: [0.123]
• Bucket 2: [ ]
• Bucket 3: [0.343, 0.565]
• Bucket 4: [0.656, 0.665]
• Bucket 5: [0.897]
Step 2: Sort Buckets
Sort each bucket:
• Bucket 1: [0.123]
• Bucket 3: [0.343, 0.565]
• Bucket 4: [0.656, 0.665]
• Bucket 5: [0.897]
Step 3: Merge Buckets
Concatenate the sorted buckets: Output Array: {0.123, 0.343, 0.565, 0.656, 0.665, 0.897}
Additional Source:
FreeCodeCamp - https://www.freecodecamp.org/news/sorting-algorithms-explained-with-examples-
in-python-java-and-c/
Programiz.com - https://www.programiz.com/dsa/sorting-algorithm
GeekforGeeks.org - https://www.programiz.com/dsa/sorting-algorithm