The definition of 'sorting' does not differ from its conventional meaning in the case of computer programming. It is essentially a way of taking a list of unsorted numbers and sorting them in a designated fashion, e.g. smallest-to-greatest or greatest-to-smallest. However, there is one key difference between sorting items via programming and sorting items intuitively, as we do with, say, a deck of cards; sorting items via an object-oriented programming language requires an algorithm. This algorithm provides the logic by which Python can systematically sort numbers. When we sort cards intuitively, there is not necessarily a clear logic by which we sort cards. We might see a small card and stick it in the front. We might see a large card and stick it in the back. We might deviate from these two patterns until we finally reach the desired outcome, a sorted list. However, programming languages do not have the benefit of being able to correct themselves. In this way, the programmer has to provide a perfectly logical algorithm for sorting items. There are multiple ways of doing this, each with pros and cons. In this post, I will primarily discuss three types of sorting: bubble sort, selection sort, and insertion sort.
Bubble sort employs a simple and transparent algorithm. It goes through every index in a list, and checks if the item at that index is greater than the item succeeding it. If it is, it swaps the item, this process continues until the function reaches the second last index, at which point the list is sorted. The diagram below illustrates the method of sorting.
Taken from http://mytutorialspace.com/bubble-sort/ |
Next, I will introduce selection sort. The first pass of this sorting algorithm takes the smallest item in the list and places it at index 0, i.e. at the start of the list. It dichotomizes the list by creating a sorted section and an unsorted section. After the first pass, the sorted section is comprised only of the item at index 0; this is the only item that we know for certain is sorted. The next pass will take the smallest item in the unsorted section and move it to index 0 of the unsorted section, i.e. one index after the end of the sorted section. In this way, the sorted section will be comprised of two sorted numbers in the next pass. The size of the sorted section will increase by one, and the unsorted section will decrease by one. This process continues until the length of the unsorted section equals one. This means that the item in the unsorted section is the largest number. Hence, the list is sorted. The diagram below clearly illustrates this concept.
Red: sorted, Blue: unsorted Taken from http://mytutorialspace.com/write-program-demonstrate-selection-sort-array/ |
Also, note that selection sort can also work in the opposite ways: it can move the largest items to the end of the list instead of moving the smallest items to the from of the list
Next, I will explain insertion sort. This algorithm starts by taking the first two indexes, 0 and 1, and comparing their sizes. It will swap them if index 0 is larger than index 1. Otherwise, it will do nothing. After this occurs, index 0 and 1 can be considered a sorted region. The rest, for all we know, are unsorted. The algorithm then moves forward one index at a time and checks if the item at that index is less than any of the items in the sorted region. If it is less, the sorting algorithm will move that item behind the item that it is closest to it in size, but still greater than it. Otherwise, the algorithm will proceed to the next index. This continues until the end of the list. The diagram below show this concept.
Red: sorted, Blue: unsorted Taken from http://www.stoimen.com/blog/2012/02/13/computer-algorithms-insertion-sort/ |
Now you may be wondering, why do we need all of these algorithms? They all do the same thing after all. The answer is efficiency. Each of these algorithms have different time complexities, that is, they require different amounts of time to output the desired result. This is a concept that I struggled with a lot at first. It was not always clear to me which sorting algorithm should have what time complexity, and I did not intuitively understand how to apply big O notation to an algorithm.
To overcome the hurdle of solving big O notation, I found it useful to break each algorithm down by looking at the code of the actual function. By looking at the code of the actual function, I was able to look for three things in particular: division (/), recursion, and nested for loops. These two things are worth paying attention to because they often revelatory of the time complexity of the algorithm. If we see division without recursion, we know that the function is logarithmic, because the number of passes is the quotient of n, the number of items in the list array. Hence, we end up with a time complexity of O(log(n)). If we see division and recursion, each level of recursion accounts the previous amount of items n divided by 2, for instance, because the function divides the number of items by 2. It does this n times, where n is the number of items in the original list array. Hence, we end up with O(n log(n)). If we see a nested for loop, we know that for each item, we are going to have to index another set of items. Hence we end up having to check the number of items n^2. Hence, we end up with a time complexity of O(n^2). Of course there is also O(n) and O(1). These however, are fairly intuitive: O(n) means the sorting algorithm indexes every item in the list, where n represents the amount of items. O(1) means that the sorting algorithm's time complexity. It is invariable, so we represent it using a constant value, i.e. 1.
I hope my approach to the issue of big O notation can help some other people in the class as we prepare for the final. For further exploration of sorting and efficiency, check out Zoe's post!