Chapter 5: Sorting

In 1880 the United States began counting its population, and the counting went badly. Clerks worked through stacks of handwritten returns, copying, grouping and re-grouping them by hand. The results took roughly seven years to compile — so long that the figures were stale before they were published, and the next census was already close.

A young engineer named Herman Hollerith proposed a different approach. Each person’s details would be punched as holes into a card, and machines would read the cards electrically. The important part is what sat beside his tabulating machine: a sorting box. Cards were fed through, and the machine directed each one into the compartment where it belonged. Only after the cards had been brought into order could they be counted meaningfully — all the cards for one state together, all the cards for one age group together.

The 1890 count was completed in a fraction of the earlier time. What made that possible was not faster counting. It was ordering.

The same lesson holds inside a computer today. A list of ten numbers can be arranged in almost any way you like and nobody notices. A list of ten lakh cannot. How you put those elements in order decides whether an answer arrives in a moment or never arrives at all.

Fig. 5.0

Fig. 5.0 – Sorting transforms unordered data into an ordered arrangement.

NoteCuriosity Corner

Q1. If a list of six numbers is already in the correct order, does a sorting method still do work on it? Q2. Bubble sort compares only neighbours. How can comparing neighbours ever move a very small number from the end of a list all the way to the front? Q3. Two methods both sort the same list correctly. Is there any sense in which one of them is better? Q4. When names are added to a class list one at a time and the list must stay in order after every addition, which sorting idea is really being used? Q5. Why does the choice of sorting method hardly matter for ten elements, yet matter enormously for ten lakh?

Keep these five questions in mind as you read. Each one is answered somewhere in this chapter.

5.1 Introduction

Open any dictionary and you will find the words running from a towards z. Walk into an examination hall and the seats are laid out by roll number. Look at a shop’s rate list and the items are grouped so that a price can be found without reading every line. None of these arrangements happened by accident. Somebody decided the order first, and everything that follows became easier because of it.

A computer works with the same advantage. Data stored in a jumble is still data — every value is there, nothing is lost — but almost every useful question asked about it becomes slower to answer. Putting the values into a chosen order before the questions arrive is one of the oldest and most useful ideas in computing, and it has a name.

5.1.1 What It Means to Sort a Collection

Sorting is the process of arranging the elements of a collection into a definite order decided in advance. The elements themselves do not change; only their positions do. A list holding the numbers 8, 7, 13, 1 and 4 still holds exactly those five numbers after sorting — but now they sit as 1, 4, 7, 8 and 13.

For numbers, two orders are used constantly. In ascending order each element is smaller than or equal to the one after it, so the list runs from the smallest value to the largest. In descending order the arrangement is reversed, and the list runs from the largest value down to the smallest. Nothing in the idea of sorting forces one choice over the other; the problem being solved decides it. A merit list of marks is usually wanted in descending order, while a list of bus departure times is wanted in ascending order.

NoteDefinition

Sorting is the process of rearranging the elements of a given collection so that they appear in a specified order, without adding, removing or altering any element.

Strings can be ordered too. A list of names can be arranged alphabetically from a to z, or from z to a, or — if the problem calls for it — by the length of each name. Notice that these are three different orders on the same collection, which tells you something important: sorting is never fully described until you say what “in order” is going to mean.

Sorting Happens Inside the List

In this chapter the collection is a Python list, and the sorting is done inside that same list. Lists are mutable, which means an element can be replaced by assigning to its index — numbers[2] = 40 changes the third element in place, and no new list is created. Every technique you are about to study relies on this. Elements are moved about within the original list by assignment and by exchanging values, and when the work finishes it is the original list that has come into order.

Exchanging two elements is used so often that it has a name of its own. To swap two elements means to make each of them take the other’s position. Python allows this in a single statement, without a helper variable:

numbers = [8, 7, 13, 1, 4]
numbers[0], numbers[1] = numbers[1], numbers[0]
print(numbers)
[7, 8, 13, 1, 4]

The right-hand side is evaluated first and packed into a tuple, so both original values are safely captured before either assignment takes place. Written the other way round, as two separate assignment statements, the first assignment would overwrite one of the values and both positions would end up holding the same element.

swapping in two steps and losing a value What students write:

numbers[0] = numbers[1]
numbers[1] = numbers[0]

What happens: the first statement overwrites the element at index 0, so by the time the second statement runs, the original value is gone. Both positions end up holding the same number — no error message appears, and the list is silently wrong. The fix: exchange in one statement, numbers[0], numbers[1] = numbers[1], numbers[0], or hold the first value in a temporary variable before overwriting it. A swap must remember what it is about to destroy.

5.1.2 Why Ordered Data Is Easier to Work With

Sorting is rarely the goal in itself. It is done because the operations that come afterwards become cheaper. Consider looking up the meaning of a word in a dictionary whose pages carry words in no particular order. There would be no clever way to proceed — you would read entry after entry until the word appeared, and a word that is absent would be discovered only after reading everything. Because dictionaries are ordered, you can open a page in the middle, decide from a single glance whether the word lies before or after, and discard half the book without looking at it.

Fig. 5.1 shows the same contrast for a list of numbers held in a computer.

Fig. 5.1

Fig. 5.1 – Searching ordered versus unordered data.

Ordering helps far beyond searching. Once a list is sorted, the smallest and the largest elements are simply the first and the last, and no scanning is needed to find them. Equal values come to lie next to each other, so duplicates can be detected by comparing neighbours. The median sits at the middle position. Two sorted lists can be merged into one sorted list in a single sweep. Printed output — a rank list, a fee-defaulters list, a stock report — is readable in a way that unordered output never is.

Q. A list holds the marks of 50 students, and only the highest mark is required. Is sorting the right tool here?

A. No. One pass through the list, keeping track of the largest value seen so far, answers the question after 50 comparisons. Sorting does far more work — it puts all 50 marks in order when only one of them was ever wanted. Sorting pays for itself when the ordered list will be used many times, not when a single fact is needed once.

5.1.3 The Sort Key and the Order of Sorting

When a collection holds plain numbers, there is nothing to choose: the value of the element decides its position. Real data is rarely so simple. A record for a student carries a name, a roll number, a height and a set of marks, and any of these could decide the order. The field on whose value the ordering is decided is called the sort key.

NoteDefinition

A sort key is the value, drawn from each element, that is used to decide that element’s position in the sorted order.

Table 5.1 holds five records from a school sports register, printed in order of name.

Table 5.1 – five entries from a sports register; the columns hold the participant’s name and height in centimetres | Name | Height (cm) | |—|—| | Aarav | 163 | | Bhavna | 158 | | Ishaan | 171 | | Meera | 166 | | Ravi | 154 |

The same five records ordered by height instead would run Ravi, Bhavna, Aarav, Meera, Ishaan. Nothing was added or removed — only the sort key changed, and with it the entire arrangement. This is why a specification such as “sort the list” is incomplete. A usable specification says what the key is and whether the order is ascending or descending.

Comparing Strings

Ordering names alphabetically works because Python can compare strings with the same operators used for numbers. Comparison proceeds character by character from the left; at the first position where the two strings differ, the character with the smaller underlying code decides the result. If one string runs out first and everything matched up to that point, the shorter string is treated as the smaller.

>>> "Anita" < "Bhavna"
True
>>> "anita" < "Bhavna"
False

The second result surprises most students. It is correct: in the character codes Python uses, every capital letter comes before every small letter, so "B" is smaller than "a" and the comparison is decided at the very first character. Alphabetical ordering in the everyday sense is recovered by comparing the strings in one consistent case.

Everything the three techniques in this chapter do rests on one comparison of the form “is this element greater than that one?” Change what that comparison means, and the same technique sorts by a different key or in the opposite order, without a single change to its structure.

5.1.4 Sorting as an Overhead Worth Paying

Sorting is not free. Elements must be compared with one another, and many of them must be moved, and for a large collection this takes a substantial amount of time. Time spent on work that is not the final answer, but that makes the final answer cheaper to obtain, is called overhead.

The question is whether the overhead is worth paying, and the honest answer is: it depends on how the sorted data will be used. Sorting a list to answer one question about it is usually wasteful, as the sidebar above showed. Sorting a list that will be searched a thousand times, printed as a report, checked for duplicates and merged with another list is an obvious bargain — the cost is paid once and recovered again and again.

This is exactly the calculation a school makes when it maintains its library register in order of accession number, or a shop makes when it keeps its stock ledger in order of item code. Somebody spends effort keeping the order; everybody who consults it afterwards spends less.

What This Chapter Covers

Because sorting is needed so widely, many different methods of doing it have been designed and studied, and computer scientists compare them by how their running time grows as the collection grows. A method for sorting, expressed as a definite sequence of steps, is called a sorting algorithm.

This chapter takes up three of them. Section 5.2 introduces bubble sort, which repeatedly compares neighbouring elements and exchanges them when they are out of order. Section 5.3 takes up selection sort, which finds the smallest remaining element and puts it directly where it belongs. Section 5.4 takes up insertion sort, which takes elements one at a time and slides each into its correct place among those already arranged. Each is stated first as an algorithm, then implemented in Python, then traced by hand on a small list so that you can see exactly what it does. Section 5.5 then asks the question that separates the three: how does the work grow when the list grows?

Write any eight two-digit numbers on eight paper slips and place them face up in a row. Arrange them in ascending order using only one kind of move: exchanging two slips that are next to each other. Count the exchanges you make. Then shuffle the same slips, arrange them again, and see whether the count is the same. Keep both counts — section 5.2 will explain why they differ.

5.2 Bubble Sort

The first technique takes the simplest possible view of the problem. If a list is out of order, then somewhere in it there must be two neighbouring elements standing the wrong way round. Find such a pair, exchange them, and the list has become a little less disordered than it was. Keep doing that until no such pair is left, and the list must be sorted.

That single observation is the whole of bubble sort. Everything else is bookkeeping — deciding which pairs to check, in what order, and how to know when to stop.

5.2.1 Comparing Adjacent Elements and Swapping

Bubble sort never compares two elements that are far apart. It looks only at adjacent elements, that is, at a pair sitting in consecutive positions — the element at index j and the element at index j+1. If the first of the two is greater than the second, the two are swapped; otherwise nothing is done and the technique moves one step to the right.

Take the list [42, 17, 63, 9, 28], holding the marks of five students out of 70, and start at the left end. The elements at indices 0 and 1 are 42 and 17. Since \(42 > 17\), they are the wrong way round for ascending order, and they are exchanged, giving [17, 42, 63, 9, 28].

The comparison now moves one position right, to indices 1 and 2, which hold 42 and 63. These are already in order, so nothing happens. Then indices 2 and 3 hold 63 and 9, which are exchanged; then indices 3 and 4 hold 63 and 28, which are exchanged as well. The list has become [17, 42, 9, 28, 63].

Fig. 5.2 shows this sequence of comparisons step by step.

Fig. 5.2

Fig. 5.2 – Step-by-step trace of the first pass of bubble sort.

Notice what happened to 63. It was compared at indices 2 and 3, won that comparison and moved right; it was compared again at indices 3 and 4, won again and moved right again. A large element, once met, is carried along to the right until something larger stops it — or until the end of the list is reached.

5.2.2 The Idea of a Pass

One complete sweep from the left end of the list to the point where the comparisons stop is called a pass. The four comparisons just made — at index pairs (0,1), (1,2), (2,3) and (3,4) — form the first pass over a five-element list.

NoteDefinition

A pass is one complete traversal of the list under consideration, during which every required pair of adjacent elements is compared once and swapped if it is out of order.

A single pass does not sort the list, and it is not meant to. What it guarantees is much narrower and much more useful: at the end of the first pass, the largest element of the list is standing in the last position. In the example above, 63 finished at index 4, and that is where it belongs in the final answer.

Because that position is now correct and will never need to change, the next pass need not look at it. The second pass therefore examines only the first four elements, the third pass only the first three, and so on. The region still being worked on shrinks by one element after every pass.

5.2.3 How the Largest Element Bubbles Up

The reason a pass always deposits the largest element at the end is worth stating carefully, because it is the correctness argument for the whole method. Consider the largest element of the list, wherever it happens to be. The moment the sweep reaches it, that element is compared with its right-hand neighbour. Being the largest, it is greater than that neighbour, so a swap occurs and it moves one place right. The sweep also moves one place right, so the very next comparison involves the same element again. It therefore keeps winning and keeps moving until the sweep runs off the end of the list — and there is nowhere for it to stop except the last position.

This steady rise of the largest value to the top of the list is the image that gives the technique its name: the value behaves like a bubble rising through water. The remaining elements are not left untouched in the meantime. Every out-of-order neighbouring pair the sweep happens to meet is corrected on the way, so the list ends each pass in better shape than it began.

Q2. Bubble sort compares only neighbours. How can comparing neighbours ever move a very small number from the end of a list all the way to the front?

A. By moving one step at a time, over several passes. A large element travels a long way in a single pass because the sweep chases it rightwards. A small element has no such luck — it moves left by exactly one position per pass, since after it is swapped leftwards the sweep has already moved past it. So a small element sitting at the far right takes as many passes to reach the front as there are positions to cross. It gets there, but slowly, and this is precisely why bubble sort needs many passes rather than one.

Continuing the example, the second pass works on [17, 42, 9, 28, 63] but ignores the settled 63. It compares 17 with 42 and leaves them; swaps 42 and 9; swaps 42 and 28. The list becomes [17, 9, 28, 42, 63], and 42 has settled into the second-last position.

The third pass works on the first three elements. It swaps 17 and 9, then leaves 17 and 28 alone, giving [9, 17, 28, 42, 63]. The list is now sorted, although the technique as stated does not yet know that. Fig. 5.3 shows the state of the list at the end of each pass, with the settled tail growing from the right.

Fig. 5.3

Fig. 5.3 – The list at the end of each pass of bubble sort.

Bubble sort Statement: Repeatedly sweep through the list comparing each pair of adjacent elements and swapping them when they are out of order; after the \(k\)-th sweep the \(k\) largest elements occupy the last \(k\) positions. In words: keep exchanging neighbours that are the wrong way round, one sweep at a time, until nothing is left to exchange. Why it holds: within a sweep, the largest element of the region being examined is pushed rightwards by every comparison it takes part in, so it must finish at the right end of that region. That position is then final, and the region shrinks by one. Used for: arranging a list in ascending or descending order in place, with no extra list required. Watch out: a small element moves left by only one position per pass, so a list whose smallest values sit at the far right forces the maximum number of passes.

5.2.4 Algorithm for Bubble Sort

The method can now be written as a definite sequence of steps. Two counters are needed: one, i, to count the passes, and another, j, to walk across the list within a pass. The inner walk must stop before the settled tail, which after i completed passes holds i elements — hence the limit \(n-i-1\).

Bubble sort on a list of n elements Input: a list numList of n elements that can be compared with one another Output: the same list, rearranged in ascending order Method:

BUBBLESORT(numList, n)
Step 1: SET i = 0
Step 2: WHILE i < n-1, REPEAT STEPS 3 TO 8
Step 3:     SET j = 0
Step 4:     WHILE j < n-i-1, REPEAT STEPS 5 TO 7
Step 5:         IF numList[j] > numList[j+1] THEN
Step 6:             SWAP numList[j] AND numList[j+1]
Step 7:         SET j = j+1
Step 8:     SET i = i+1
Step 9: STOP

Why it works: each execution of Step 4 completes one pass, and a completed pass leaves the largest element of the unsettled region at the right end of that region. After $n-1 passes, \(n-1\) elements are in their final positions, and a single remaining element has nowhere else to be. Watch out: the inner limit is \(n-i-1\) and not \(n-1\). Using \(n-1\) would compare numList[n-1] with numList[n], an index that does not exist, and the program would stop with an error.

Only \(n-1\) passes are needed, not \(n\). Each pass settles one element, so after \(n-1\) of them exactly one element remains unsettled, and a single element compared with nothing is already in order. A list of five elements is therefore sorted in four passes.

5.2.5 Implementing Bubble Sort in Python

Translating the algorithm into Python is direct. The two WHILE loops become for loops over ranges, because in both cases the number of repetitions is known in advance. range(n - 1) produces the pass numbers \(0, 1, \dots, n-2\), and range(n - 1 - i) produces the index positions examined during pass i.

Sorting a list of marks in ascending order using bubble sort Problem: given a list of marks, rearrange the list so that the marks appear in ascending order, using the bubble sort technique. The list must be sorted in place, without building a second list.

def bubble_sort(data):
    n = len(data)
    for i in range(n - 1):
        for j in range(n - 1 - i):
            if data[j] > data[j + 1]:
                data[j], data[j + 1] = data[j + 1], data[j]

marks = [42, 17, 63, 9, 28]
bubble_sort(marks)
print("Sorted marks:", marks)

Sample output:

Sorted marks: [9, 17, 28, 42, 63]

Identifier description:

[Table:] Table 5.2 – identifiers used in the bubble sort program, with their type and purpose | Identifier | Type | Purpose | |—|—|—| | data | list | the list received by the function and sorted in place | | n | int | number of elements in data | | i | int | pass counter; also the number of elements already settled at the right | | j | int | index of the left element of the pair being compared | | marks | list | the list of marks passed to the function | [/Table]

Notes: the function returns nothing, and it does not need to. Because a list is mutable and is passed to the function as a reference to the same object, the changes made inside bubble_sort are visible to marks outside it. Writing return data as well would do no harm, but a function that both modifies its argument and returns it invites the reader to think a fresh list has been produced, which is not the case.

bubble_sort on the list [42, 17, 63, 9, 28] | i | j | data[j] | data[j+1] | Swap? | List after this step | |—|—|—|—|—|—| | 0 | 0 | 42 | 17 | yes | [17, 42, 63, 9, 28] | | 0 | 1 | 42 | 63 | no | [17, 42, 63, 9, 28] | | 0 | 2 | 63 | 9 | yes | [17, 42, 9, 63, 28] | | 0 | 3 | 63 | 28 | yes | [17, 42, 9, 28, 63] | | 1 | 0 | 17 | 42 | no | [17, 42, 9, 28, 63] | | 1 | 1 | 42 | 9 | yes | [17, 9, 42, 28, 63] | | 1 | 2 | 42 | 28 | yes | [17, 9, 28, 42, 63] | | 2 | 0 | 17 | 9 | yes | [9, 17, 28, 42, 63] | | 2 | 1 | 17 | 28 | no | [9, 17, 28, 42, 63] | | 3 | 0 | 9 | 17 | no | [9, 17, 28, 42, 63] | Result: the list ends as [9, 17, 28, 42, 63]. The trace shows that the list was already fully sorted at the end of pass i = 2, yet pass i = 3 still ran and made a comparison that changed nothing.

writing the inner range as range(n) What students write:

for j in range(n):
    if data[j] > data[j + 1]:
        data[j], data[j + 1] = data[j + 1], data[j]

What happens: on the last repetition j equals n - 1, so data[j + 1] refers to index n, which is one past the end of the list. Python stops the program with IndexError: list index out of range. The fix: the inner loop must stop one position earlier, and earlier still as the settled tail grows — for j in range(n - 1 - i). A loop that reads data[j + 1] may only let j reach n - 2.

5.2.6 Counting the Passes and the Comparisons

How much work has been done? The first pass over a list of \(n\) elements makes \(n-1\) comparisons, the second makes \(n-2\), and so on down to the last pass, which makes a single comparison. Table 5.3 sets this out for the five-element list traced above.

Table 5.3 – comparisons made in each pass of bubble sort on a list of five elements, with the swaps actually performed in the traced example | Pass (i) | Comparisons made | Swaps in the traced example | |—|—|—| | 0 | 4 | 3 | | 1 | 3 | 2 | | 2 | 2 | 1 | | 3 | 1 | 0 | | Total | 10 | 6 |

Adding the column gives the total number of comparisons for any list of \(n\) elements:

\[(n-1) + (n-2) + \cdots + 2 + 1 = \frac{n(n-1)}{2}\]

For \(n = 5\) this is \(\frac{5 \times 4}{2} = 10\), which matches the trace exactly. The important feature of this expression is not its precise value but how it grows: doubling the number of elements multiplies the comparisons by roughly four. Section 5.5 returns to this point.

The number of comparisons is fixed — it does not depend on the data at all. The number of swaps does. A list that is already in ascending order needs no swaps whatsoever; this is the best case. A list arranged in exactly descending order needs a swap at every single comparison, giving \(\frac{n(n-1)}{2}\) swaps; this is the worst case. Ordinary unsorted data falls somewhere between the two.

Q. Two students sort the same eight numbers by hand using bubble sort and report different numbers of swaps. Must one of them be wrong?

A. Not necessarily — but only if they started from different arrangements of those numbers. For a given starting list, the technique is completely determined: the same comparisons happen in the same order and the same swaps result. Two different starting arrangements of the same eight values can easily need different numbers of swaps, which is what the paper-slip activity in section 5.1 was meant to reveal.

5.2.7 Detecting a List That Is Already Sorted

The dry run exposed an obvious waste. The list was fully sorted at the end of pass 2, yet pass 3 ran anyway, because the loop was told to make \(n-1\) passes and it obediently made them.

There is a clean signal available. If an entire pass completes without performing a single swap, then no adjacent pair was out of order — and a list in which no adjacent pair is out of order is, by definition, sorted. The moment such a pass occurs, the remaining passes can only repeat the same finding, so the work can stop.

The signal is recorded in an ordinary variable used as a flag: a variable whose value stands for a yes-or-no fact about what has happened. It is set to False at the start of each pass and turned to True the instant a swap takes place. After the pass, its value answers the question “did anything move?”

Bubble sort with early termination Input: a list numList of n elements Output: the same list in ascending order, with sorting abandoned as soon as a swap-free pass occurs Method:

Step 1: SET i = 0
Step 2: WHILE i < n-1, REPEAT STEPS 3 TO 9
Step 3:     SET swapped = FALSE
Step 4:     SET j = 0
Step 5:     WHILE j < n-i-1, REPEAT STEPS 6 TO 8
Step 6:         IF numList[j] > numList[j+1] THEN
Step 7:             SWAP numList[j] AND numList[j+1] AND SET swapped = TRUE
Step 8:         SET j = j+1
Step 9:     IF swapped = FALSE THEN STOP
Step 10: STOP

Why it works: a pass that performs no swap has verified every adjacent pair in the unsettled region and found each one correctly ordered, which is exactly the condition for that region to be sorted. Since the settled tail is already correct, the whole list is sorted. Watch out: the flag must be reset to False at the beginning of every pass. Set once before the outer loop, it would record only whether any swap ever occurred, and the early stop would never trigger.

Bubble sort that stops as soon as the list is in order Problem: sort a list of item prices in ascending order using bubble sort, stopping early if a pass makes no swap, and report how many passes were actually needed.

def bubble_sort_early(data):
    n = len(data)
    passes = 0
    for i in range(n - 1):
        swapped = False
        passes = passes + 1
        for j in range(n - 1 - i):
            if data[j] > data[j + 1]:
                data[j], data[j + 1] = data[j + 1], data[j]
                swapped = True
        if not swapped:
            break
    return passes

prices = [12, 25, 40, 55, 70]
count = bubble_sort_early(prices)
print(prices)
print("Passes made:", count)

Sample output:

[12, 25, 40, 55, 70]
Passes made: 1

Identifier description:

[Table:] Table 5.4 – identifiers used in the early-stopping bubble sort program | Identifier | Type | Purpose | |—|—|—| | data | list | the list sorted in place | | n | int | number of elements in data | | passes | int | count of passes actually carried out | | swapped | bool | flag recording whether the current pass performed any swap | | i, j | int | pass counter and comparison index, as before | | prices | list | the list of prices passed to the function | | count | int | value returned by the function | [/Table]

Notes: break leaves the outer loop immediately, abandoning the passes that remain. The list here was already in ascending order, so the first pass made its four comparisons, swapped nothing, and the work ended — where the plain version of section 5.2.5 would have gone on to make six more comparisons across three further passes.

Q1. If a list of six numbers is already in the correct order, does a sorting method still do work on it?

A. Yes, but how much work depends on how the method is written. Plain bubble sort has no way of noticing that the list is in order, so it carries out all five passes and all fifteen comparisons regardless. The version with a flag notices after one pass: it makes five comparisons, finds nothing to swap and stops. Neither version can do no work — some checking is unavoidable, because a list cannot be declared sorted without looking at it.

Remove the two lines that use swapped from the program above and run it again on prices, printing the value of passes. Then run both versions on [70, 55, 40, 25, 12] and compare the pass counts. Explain in one sentence why the flag helps enormously in one case and not at all in the other.

5.2.8 Sorting in Descending Order

Nothing in bubble sort assumes that smaller elements belong on the left. That assumption lives in exactly one place — the condition inside the if statement — and reversing it reverses the sorted order.

To sort in descending order, an element should move right when it is smaller than its neighbour. The comparison data[j] > data[j + 1] therefore becomes data[j] < data[j + 1], and now it is the smallest element that is carried to the end of the list in each pass.

def bubble_sort_desc(data):
    n = len(data)
    for i in range(n - 1):
        for j in range(n - 1 - i):
            if data[j] < data[j + 1]:
                data[j], data[j + 1] = data[j + 1], data[j]

scores = [55, 12, 70, 33]
bubble_sort_desc(scores)
print(scores)
[70, 55, 33, 12]

The structure of the loops, the number of passes and the number of comparisons are all unchanged. This is a general point about sorting techniques, and it is worth carrying forward to the next two sections: the technique decides how elements are moved about, while a single comparison decides what counts as being in order. Change the comparison and you change the ordering; change nothing else.

5.3 Selection Sort

Bubble sort works by correcting local mistakes: it keeps fixing neighbouring pairs and trusts that the list will come into order as a result. It reaches the right answer, but it moves elements about a great deal on the way — a single element may be picked up and put down again in pass after pass.

The next technique takes the opposite view. Instead of nudging elements towards their positions, it decides a position outright and fills it once, correctly, and never touches it again. This is selection sort, so called because each pass selects the element that belongs in the next position.

5.3.1 Dividing the List into a Sorted and an Unsorted Part

Think of the list as being split into two regions by an imaginary line. Everything to the left of the line is finished: those elements are in their final positions and in the correct order. Everything to the right is still unexamined. Initially the line sits at the very beginning, so the finished region is empty and the unfinished region is the whole list.

Each pass moves the line one position to the right. To do that, the pass must work out which of the remaining elements deserves the position immediately to the right of the line — and for ascending order, that is the smallest element of the unfinished region.

Fig. 5.4 shows the first pass on the list [35, 12, 48, 20, 27], holding five bus fares in rupees.

Fig. 5.4

Fig. 5.4 – The first pass of selection sort.

Notice how differently this behaves from bubble sort. There, an element crept one position at a time. Here, 12 travelled from index 1 to index 0 in a single move, and it could just as easily have travelled from index 4 to index 0 in the same single move. Selection sort does not care how far an element has to go.

5.3.2 Selecting the Smallest Element in Each Pass

Finding the smallest element of the unfinished region is a small task in its own right, and it is worth stating on its own because it is used constantly in programming. You cannot know the smallest value without looking at every candidate, so the region is scanned from left to right while a record is kept of the best value seen so far.

In practice it is the position of the smallest element that is recorded, not the value. The position is what the swap will need, and from the position the value can always be recovered by indexing.

So the pass proceeds like this. Assume, to begin with, that the leftmost unfinished element is the smallest, and remember its index. Compare each later element with the element at the remembered index; whenever a smaller one appears, forget the old index and remember the new one. When the scan reaches the end of the list, the remembered index is certainly the position of the smallest element, because every element was compared against the best candidate available at the time.

Then one swap puts that element where it belongs, and whatever was occupying that position is displaced into the gap left behind. It is still in the unfinished region, so it will be considered again later.

Continuing the fares example, the second pass scans indices 1 to 4 of [12, 35, 48, 20, 27] and finds 20 to be the smallest, so 20 and 35 are exchanged, giving [12, 20, 48, 35, 27]. The third pass scans indices 2 to 4, finds 27, and exchanges it with 48, giving [12, 20, 27, 35, 48]. The fourth pass scans indices 3 to 4, finds that 35 is already the smaller of the two, and does nothing at all. Fig. 5.5 shows the boundary advancing across the list.

Fig. 5.5

Fig. 5.5 – The list at the end of each pass of selection sort.

Selection sort Statement: repeatedly select the smallest element of the unsorted region of the list and exchange it with the leftmost element of that region, moving the boundary one position right after each selection. In words: find the smallest of what is left, put it straight into the next free position, and repeat. Why it holds: after the \(k\)-th pass, the first \(k\) positions hold the \(k\) smallest elements of the list in increasing order, because each pass takes the smallest of everything that remains. When only one element remains unfinished, nothing in the list can be smaller than it, so it is already in place. Used for: sorting in place when moving data is expensive, since each pass performs at most one exchange. Watch out: the number of comparisons does not depend on the data at all — an already sorted list is scanned just as thoroughly as a reversed one.

5.3.3 Algorithm for Selection Sort

Two counters are needed again. The outer counter i marks the boundary — it is the position being filled in this pass. The inner counter j scans the unfinished region, which runs from i+1 to the end of the list. A third variable holds the index of the smallest element found so far.

Selection sort on a list of n elements Input: a list numList of n elements that can be compared with one another Output: the same list, rearranged in ascending order Method:

SELECTIONSORT(numList, n)
Step 1: SET i = 0
Step 2: WHILE i < n-1, REPEAT STEPS 3 TO 9
Step 3:     SET min_pos = i
Step 4:     SET j = i+1
Step 5:     WHILE j < n, REPEAT STEPS 6 TO 7
Step 6:         IF numList[j] < numList[min_pos] THEN SET min_pos = j
Step 7:         SET j = j+1
Step 8:     IF min_pos != i THEN
Step 9:         SWAP numList[i] AND numList[min_pos]
Step 10:    SET i = i+1
Step 11: STOP

Why it works: Steps 3 to 7 examine every element from index i onwards and leave min_pos holding the index of the smallest of them, since a candidate is replaced only by something smaller. Step 9 then places that smallest element at index i, which is correct because everything before index i is already smaller and already settled. Watch out: min_pos must be reset to i at the start of every pass. Carrying it over from the previous pass would leave it pointing into the settled region, and the swap would destroy an element already in its final position.

Only \(n-1\) passes are needed here as well, and for the same reason as before: once \(n-1\) positions have been filled correctly, the single element left over has no other place to be.

5.3.4 Implementing Selection Sort in Python

The inner scan is written as range(i + 1, n), which is Python’s way of saying “from the position just after the boundary up to the end of the list”. Both loop limits change from pass to pass, and both depend on i.

Sorting a list of bus fares in ascending order using selection sort Problem: given a list of bus fares in rupees, rearrange the list in ascending order using the selection sort technique, sorting in place and performing at most one exchange per pass.

def selection_sort(data):
    n = len(data)
    for i in range(n - 1):
        min_pos = i
        for j in range(i + 1, n):
            if data[j] < data[min_pos]:
                min_pos = j
        if min_pos != i:
            data[i], data[min_pos] = data[min_pos], data[i]

fares = [35, 12, 48, 20, 27]
selection_sort(fares)
print("Sorted fares:", fares)

Sample output:

Sorted fares: [12, 20, 27, 35, 48]

Identifier description:

[Table:] Table 5.5 – identifiers used in the selection sort program, with their type and purpose | Identifier | Type | Purpose | |—|—|—| | data | list | the list received by the function and sorted in place | | n | int | number of elements in data | | i | int | pass counter; the position being filled in this pass | | min_pos | int | index of the smallest element found so far in the unsorted region | | j | int | index used to scan the unsorted region | | fares | list | the list of fares passed to the function | [/Table]

Notes: the exchange sits outside the inner loop, not inside it. That placement is the whole point of the technique — the entire region is examined first, a decision is taken once, and only then is any data moved. The test if min_pos != i skips the exchange when the smallest element is already in the right place; without it the program would still be correct, but it would swap an element with itself.

selection_sort on the list [35, 12, 48, 20, 27] | i | j | data[j] | data[min_pos] | min_pos after this step | List at end of pass | |—|—|—|—|—|—| | 0 | 1 | 12 | 35 | 1 | | | 0 | 2 | 48 | 12 | 1 | | | 0 | 3 | 20 | 12 | 1 | | | 0 | 4 | 27 | 12 | 1 | [12, 35, 48, 20, 27] | | 1 | 2 | 48 | 35 | 1 | | | 1 | 3 | 20 | 35 | 3 | | | 1 | 4 | 27 | 20 | 3 | [12, 20, 48, 35, 27] | | 2 | 3 | 35 | 48 | 3 | | | 2 | 4 | 27 | 35 | 4 | [12, 20, 27, 35, 48] | | 3 | 4 | 48 | 35 | 3 | [12, 20, 27, 35, 48] | Result: the list ends as [12, 20, 27, 35, 48]. The trace shows ten comparisons but only three exchanges — and the fourth pass, having found min_pos equal to i, moved nothing at all.

Q. In the last pass the smallest remaining element was already in position. Would the program be wrong if the test if min_pos != i were removed?

A. No, it would still be correct. With min_pos equal to i, the statement data[i], data[min_pos] = data[min_pos], data[i] exchanges the element with itself and leaves the list unchanged. The test is there to avoid a pointless assignment, not to protect the result. It matters when the data being moved is large, and it matters for the honest count of exchanges reported in the next subsection.

naming the variable min What students write:

min = i
for j in range(i + 1, n):
    if data[j] < data[min]:
        min = j
smallest = min(data)

What happens: min is the name of a built-in Python function. Assigning to it inside the function creates a local variable that hides the built-in, so the later call fails with TypeError: 'int' object is not callable. The fix: choose a name that describes the thing being stored and does not collide with a built-in — min_pos says clearly that a position is being held, and it leaves min() available.

5.3.5 Comparisons and Swaps in Selection Sort

The first pass compares the leftmost unfinished element with \(n-1\) others, the second with \(n-2\) others, and so on, down to a single comparison in the last pass. The total is therefore

\[(n-1) + (n-2) + \cdots + 1 = \frac{n(n-1)}{2}\]

which for the five fares gives \(\frac{5 \times 4}{2} = 10\) — exactly the number of rows in the dry run above, and exactly the same count that bubble sort needed for a list of five.

What is striking is that this count is completely fixed. Selection sort has no idea whether the list handed to it is already sorted, badly jumbled or perfectly reversed, and it performs the same ten comparisons in every case. There is no best case and no worst case to speak of, and no flag can rescue it, because a pass that finds nothing to move has still had to look at everything to be sure.

The exchanges tell a different story. Each pass performs at most one, so a list of \(n\) elements needs at most \(n-1\) exchanges however disordered it is — and often fewer, as the trace showed with three. Compare that with bubble sort, which on a reversed list performs \(\frac{n(n-1)}{2}\) exchanges. For a list of 100 elements, that is at most 99 exchanges against as many as 4,950.

5.3.6 Bubble Sort and Selection Sort Contrasted

The two techniques are close relatives. Both work in place, both need no second list, both make the same number of comparisons, and both settle one element per pass. The differences lie in which element gets settled, and in how much data has to move to settle it.

Table 5.6 – bubble sort compared with selection sort | Feature | Bubble sort | Selection sort | |—|—|—| | Basic action in a pass | compare and swap adjacent elements repeatedly | scan for the smallest element, then swap once | | Element settled by a pass | the largest of the unsorted region, at its right end | the smallest of the unsorted region, at its left end | | Where the sorted region grows | from the right end of the list | from the left end of the list | | Comparisons for n elements | \(\frac{n(n-1)}{2}\), or fewer if the early stop is used | \(\frac{n(n-1)}{2}\) always | | Exchanges performed | none in the best case, up to \(\frac{n(n-1)}{2}\) in the worst | at most \(n-1\), whatever the data | | Sensitive to the data given? | yes — an ordered list can be recognised in one pass | no — the same comparisons happen every time | | Distance an element moves per pass | one position when moving left | any distance, in a single exchange |

Read the fifth row carefully, because it is the practical difference between the two. Comparing two values is cheap. Moving data can be expensive — if each element were a long record rather than a number, every exchange would mean copying a great deal of memory. Selection sort was designed for exactly that situation: it spends comparisons freely in order to keep exchanges to a minimum.

Q3. Two methods both sort the same list correctly. Is there any sense in which one of them is better?

A. Yes. Correctness is not the only measure — the cost of reaching the answer counts too, and the two techniques do not pay the same price. On a list of five, both made ten comparisons, but bubble sort performed six exchanges to selection sort’s three, and on a reversed list the gap becomes enormous. Bubble sort, on the other hand, can recognise a list that is already in order and stop after one pass, which selection sort can never do. So “better” is not a property of a technique on its own; it depends on what the data looks like and on whether comparing or moving is the more expensive operation in the situation at hand.

Add a counter to both bubble_sort and selection_sort that increases by one every time an exchange takes place, and have each function return it. Run both on [9, 7, 5, 3, 1] and then on [1, 3, 5, 7, 9], and record the four counts in a small table. Which technique is affected more by the starting arrangement?

5.4 Insertion Sort

Watch somebody arrange a hand of playing cards. They do not scan the whole hand looking for the smallest card, and they do not repeatedly swap neighbouring cards. They pick up one card at a time, look along the cards already held, and push it into the gap where it belongs — sliding the others aside to make room.

That is insertion sort, and of the three techniques in this chapter it is the one closest to how people actually order things by hand.

5.4.1 Placing Each Element Where It Belongs

Like selection sort, insertion sort keeps the list divided into a finished region on the left and an unfinished region on the right. The resemblance ends there, and the difference matters.

In selection sort, the left region held elements that were in their final positions — an element placed there was never moved again. In insertion sort the left region holds elements that are in order among themselves, but not necessarily in their final positions. An element sitting comfortably at index 0 may well be pushed to index 1 later, when something smaller turns up.

The technique starts with a finished region of exactly one element. A single element, having nothing to be out of order with, is trivially in order. Every pass then takes the first element of the unfinished region and inserts it into its correct place among the elements on the left, which enlarges the ordered region by one.

Take the list [46, 15, 39, 8, 22], holding the runs scored by five players in a match. The ordered region begins as [46] and the first element to be placed is 15. Since \(15 < 46\), it belongs before 46, so 46 moves one position right and 15 takes its place: [15, 46, 39, 8, 22].

5.4.2 Shifting Elements to Make Space

Making room is where insertion sort does its real work, and it is done by shifting rather than swapping. To shift an element is to copy it into the position immediately to its right, one element at a time, so that a gap opens up where the new element can be written.

NoteDefinition

Shifting means copying an element one position to the right within the same list, so as to vacate a position for another element; unlike a swap, it moves one element rather than exchanging two.

The order of operations here needs care. Before anything is shifted, the element being placed must be copied into a separate variable — conventionally called temp — because the very first shift will overwrite the position it was occupying. With the value safely held, the scan can walk leftwards through the ordered region, shifting each element that is larger than temp one place right, and stopping the moment it meets an element that is not larger, or runs off the left end of the list. Whatever position the scan has vacated is then where temp is written.

Fig. 5.6 shows this happening for the value 8, the fourth element of the example list.

Fig. 5.6

Fig. 5.6 – Shifting elements to insert a value into the ordered region.

Three elements moved right, and then 8 was written into the vacated position at index 0. Notice that only one value entered the list from outside — the one held in temp — while everything else simply slid along. A swap-based method would have needed three separate exchanges to achieve the same thing, each involving two writes instead of one.

Continuing to the last element, 22 is held in temp; 46 and 39 are both larger and shift right; 15 is not larger, so the scan stops there and 22 is written into the gap at index 2, giving [8, 15, 22, 39, 46]. Fig. 5.7 shows the ordered region growing across the four passes.

Fig. 5.7

Fig. 5.7 – The list at the end of each pass of insertion sort.

Insertion sort Statement: treat the first element as an ordered region of size one; then take each remaining element in turn and insert it into the ordered region at its correct place, shifting larger elements one position right to make room. In words: build the sorted list one element at a time, the way a hand of cards is arranged. Why it holds: at the start of a pass the first \(k\) elements are in order among themselves. The pass inserts the next element so that everything larger than it lies to its right and everything smaller to its left, which leaves the first \(k+1\) elements in order. After the last pass, \(k+1\) equals the length of the list. Used for: sorting small lists, and lists that are already nearly in order, and lists that grow as data arrives. Watch out: each element still travels leftwards one position at a time, so a list in reverse order forces the maximum amount of shifting.

5.4.3 Algorithm for Insertion Sort

The outer counter i marks the element currently being placed, and it starts at 1 rather than 0, because the element at index 0 already forms the initial ordered region. The inner counter j walks leftwards from i-1, and it must stop for either of two reasons — running past the left end of the list, or meeting an element that is not larger than the value being inserted.

Insertion sort on a list of n elements Input: a list numList of n elements that can be compared with one another Output: the same list, rearranged in ascending order Method:

INSERTIONSORT(numList, n)
Step 1: SET i = 1
Step 2: WHILE i < n, REPEAT STEPS 3 TO 9
Step 3:     SET temp = numList[i]
Step 4:     SET j = i-1
Step 5:     WHILE j >= 0 AND numList[j] > temp, REPEAT STEPS 6 TO 7
Step 6:         SET numList[j+1] = numList[j]
Step 7:         SET j = j-1
Step 8:     SET numList[j+1] = temp
Step 9:     SET i = i+1
Step 10: STOP

Why it works: Step 5 stops only when every element still to the left of position j+1 is less than or equal to temp, and every element that was shifted was greater than temp. Writing temp at j+1 therefore leaves the region from index 0 to index i correctly ordered. Watch out: Step 8 lies outside the inner loop and must be executed even when no shifting took place. If the loop stops immediately, j+1 equals i, and the statement simply writes the value back where it came from.

The condition in Step 5 has two parts joined by AND, and the order in which they are written is not arbitrary. j >= 0 must be tested first, because the second test reads numList[j] and that reading is only meaningful while j is a valid index.

5.4.4 Implementing Insertion Sort in Python

The inner loop is written as a while rather than a for, because the number of repetitions is not known in advance — it depends entirely on how far leftwards the value has to travel. This is the first of the three techniques whose inner loop can finish early.

Sorting a list of runs scored in ascending order using insertion sort Problem: given a list of runs scored by five players, rearrange the list in ascending order using the insertion sort technique, sorting in place.

def insertion_sort(data):
    n = len(data)
    for i in range(1, n):
        temp = data[i]
        j = i - 1
        while j >= 0 and data[j] > temp:
            data[j + 1] = data[j]
            j = j - 1
        data[j + 1] = temp

runs = [46, 15, 39, 8, 22]
insertion_sort(runs)
print("Sorted runs:", runs)

Sample output:

Sorted runs: [8, 15, 22, 39, 46]

Identifier description:

[Table:] Table 5.7 – identifiers used in the insertion sort program, with their type and purpose | Identifier | Type | Purpose | |—|—|—| | data | list | the list received by the function and sorted in place | | n | int | number of elements in data | | i | int | index of the element currently being inserted | | temp | int | a copy of the element being inserted, held safe while shifting | | j | int | index used to walk leftwards through the ordered region | | runs | list | the list of runs passed to the function | [/Table]

Notes: Python evaluates and from left to right and stops as soon as the answer is decided, so when j becomes -1 the test j >= 0 fails and data[j] is never read. The loop counts down rather than up, which is unusual enough to be worth saying aloud when you read the code: j = j - 1 moves the search towards the front of the list.

insertion_sort on the list [46, 15, 39, 8, 22] | i | temp | j | data[j] | Action taken | List after this step | |—|—|—|—|—|—| | 1 | 15 | 0 | 46 | 46 > 15, shift right | [46, 46, 39, 8, 22] | | 1 | 15 | -1 | — | j < 0, write temp at index 0 | [15, 46, 39, 8, 22] | | 2 | 39 | 1 | 46 | 46 > 39, shift right | [15, 46, 46, 8, 22] | | 2 | 39 | 0 | 15 | 15 not > 39, stop | [15, 46, 46, 8, 22] | | 2 | 39 | 0 | — | write temp at index 1 | [15, 39, 46, 8, 22] | | 3 | 8 | 2 | 46 | 46 > 8, shift right | [15, 39, 46, 46, 22] | | 3 | 8 | 1 | 39 | 39 > 8, shift right | [15, 39, 39, 46, 22] | | 3 | 8 | 0 | 15 | 15 > 8, shift right | [15, 15, 39, 46, 22] | | 3 | 8 | -1 | — | j < 0, write temp at index 0 | [8, 15, 39, 46, 22] | | 4 | 22 | 3 | 46 | 46 > 22, shift right | [8, 15, 39, 46, 46] | | 4 | 22 | 2 | 39 | 39 > 22, shift right | [8, 15, 39, 39, 46] | | 4 | 22 | 1 | 15 | 15 not > 22, stop | [8, 15, 39, 39, 46] | | 4 | 22 | 1 | — | write temp at index 2 | [8, 15, 22, 39, 46] | Result: the list ends as [8, 15, 22, 39, 46]. Nine comparisons were made in all — fewer than the ten that bubble sort and selection sort each need for five elements — because two of the four passes stopped early instead of scanning the whole ordered region.

The duplicated values that appear in the middle rows of the trace are not errors. During a shift the same value genuinely occupies two positions for a moment, and the copy left behind is overwritten either by the next shift or by the final write of temp.

Q. What happens when the element being inserted is already larger than everything on its left?

A. The while condition fails at the very first test, so no shifting occurs at all, and j still holds i - 1. The statement data[j + 1] = temp then writes the value back into index i, exactly where it came from. One comparison, one harmless assignment, and the pass is over — which is why an already sorted list is handled so cheaply.

writing the two conditions in the wrong order What students write:

while data[j] > temp and j >= 0:
    data[j + 1] = data[j]
    j = j - 1

What happens: when j reaches -1, the first test is evaluated before the guard. In Python data[-1] is not an error — it is the last element of the list. The comparison quietly succeeds or fails against the wrong element, values get copied to the wrong end of the list, and the list is corrupted with no error message. Only if j falls further than -len(data) does Python finally report IndexError. The fix: test the index first — while j >= 0 and data[j] > temp:. Because and stops as soon as the left side is false, data[j] is then read only when j is a valid position.

5.4.5 Behaviour on Nearly Sorted and Reversed Lists

Insertion sort is the first technique in this chapter whose work depends heavily on the data it is given, and the dependence is worth understanding because it decides when the technique should be chosen.

Suppose the list is already in ascending order. Every pass makes exactly one comparison, finds the value already larger than its left neighbour, shifts nothing, and ends. For \(n\) elements that is \(n-1\) comparisons and no shifting whatever — the best case, and it is remarkably cheap. Selection sort on the same list would still grind through all \(\frac{n(n-1)}{2}\) comparisons.

Now suppose the list is in exactly reverse order. Every element being inserted is smaller than everything to its left, so every pass scans the entire ordered region and shifts every element in it. The \(i\)-th pass then makes \(i\) comparisons, and the total climbs to \(\frac{n(n-1)}{2}\) — the worst case, and no better than the other two techniques.

Between these extremes lies the case that matters in practice. Data is often nearly sorted: a fresh set of marks appended to a sorted list, a register with a few late entries, a file that was sorted last week and has since had a handful of additions. In such a list each element has only a short distance to travel, so each pass stops after a couple of comparisons, and insertion sort finishes far faster than its worst case suggests.

Q. On the reversed list [9, 7, 5, 3, 1], does insertion sort do more work than bubble sort?

A. They do comparable work, and neither escapes. Insertion sort makes \(1+2+3+4 = 10\) comparisons and shifts on every one of them; bubble sort makes the same ten comparisons and swaps at every one. The reversed list is the situation both techniques handle worst. The difference between them appears at the other extreme — on an already ordered list, insertion sort needs four comparisons and plain bubble sort still needs ten.

5.4.6 Sorting Data as It Arrives

There is one situation the other two techniques cannot handle gracefully at all, and insertion sort handles naturally. Suppose the data does not exist as a complete list to begin with. Names are entered at an admission counter one at a time, readings arrive from an instrument through the day, entries are added to a register as visitors sign in — and the collection must be in order after every addition, not only at the end.

Bubble sort and selection sort both assume the whole list is present before they begin. Insertion sort makes no such assumption. Its every pass is already an “insert one more element into an ordered list” operation, so a single pass, run once per arrival, keeps the list permanently in order.

Inserting a new value into an already ordered list Input: a list names already in ascending order, and a new value new_name to be added Output: the same list, still in ascending order, now containing the new value Method:

Step 1: APPEND new_name to names, so that the list has one more position
Step 2: SET j = length of names - 2
Step 3: WHILE j >= 0 AND names[j] > new_name, REPEAT STEPS 4 TO 5
Step 4:     SET names[j+1] = names[j]
Step 5:     SET j = j-1
Step 6: SET names[j+1] = new_name
Step 7: STOP

Why it works: this is exactly one pass of insertion sort, applied to a list whose first n-1 elements are known to be in order. The append creates the extra position that the shifting needs; the scan then finds the correct place for the new value in the same way as before. Watch out: j begins at the second-last index, not the last. The last position is the one just created by the append and holds a copy of the new value, which must not be compared with itself.

Building a class list that stays in alphabetical order as names are entered Problem: names are entered one after another at an admission counter. After each entry, the list of names must be in alphabetical order. Write a function that inserts one name into an already ordered list, and show the list after each of four entries.

def insert_name(names, new_name):
    names.append(new_name)
    j = len(names) - 2
    while j >= 0 and names[j] > new_name:
        names[j + 1] = names[j]
        j = j - 1
    names[j + 1] = new_name

class_list = []
for entry in ["Nisha", "Farhan", "Kabir", "Anjali"]:
    insert_name(class_list, entry)
    print(class_list)

Sample output:

['Nisha']
['Farhan', 'Nisha']
['Farhan', 'Kabir', 'Nisha']
['Anjali', 'Farhan', 'Kabir', 'Nisha']

Identifier description:

[Table:] Table 5.8 – identifiers used in the ordered-insertion program | Identifier | Type | Purpose | |—|—|—| | names | list | the ordered list of names, modified in place | | new_name | str | the name being inserted in this call | | j | int | index used to walk leftwards through the existing names | | class_list | list | the list built up across the four entries | | entry | str | the name currently being read from the sequence of entries | [/Table]

Notes: the function works for an empty list too. With names empty, the append makes its length 1, j becomes -1, the loop is skipped, and the name is written at index 0. Comparing strings with > gives alphabetical order here because all four names begin with a capital letter and continue in small letters; mixing cases would need the comparison to be made on a single consistent case, as section 5.1 explained.

Q4. When names are added to a class list one at a time and the list must stay in order after every addition, which sorting idea is really being used?

A. Insertion sort. Each new name is placed into a list that is already in order, by walking leftwards past every name that should come after it and shifting those names one position right to open a gap. That is precisely one pass of insertion sort, and running it once per arrival keeps the list ordered at every moment. Neither of the other two techniques fits, because both begin by assuming that all the data is already present.

Practical link: insertion sort implemented as a user-defined function; file this under Python programs in your practical report file. Write a program that reads a count n from the user, then reads n integers into a list, and passes the list to a user-defined function sort_marks(data) which arranges it in ascending order using insertion sort. The program should print the list before and after sorting, and also print the number of shifts the function performed. Test it on [70, 55, 40, 25, 10] and note how the shift count for that input compares with the count for the same five values entered in increasing order. Include the identifier description table for your function in the report.

5.5 Time Complexity of Algorithms

Three techniques have now been studied, and all three do the job. On a list of five elements they finish in a fraction of a second and there is nothing to choose between them. The chapter opened with a warning that this comfort does not last: what is invisible at five elements can decide everything at ten lakh.

This section supplies the tool for saying so precisely. It is a way of describing how the work an algorithm does grows as the amount of data grows — and it is the reason computer scientists design new sorting techniques even though the old ones are perfectly correct.

5.5.1 Why the Same Task Has Faster and Slower Solutions

You have met this idea before. In Class XI, several different algorithms were compared for deciding whether a number is prime: testing every divisor from 2 up to \(n-1\), stopping at \(n/2\), stopping at \(\sqrt{n}\). All of them give the same verdict on every number. They differ only in how much work they do to reach it, and on a large number the difference is glaring.

Sorting is the same story with higher stakes, because sorting is applied to whole collections rather than to single values. A problem can always be solved in more than one way, and the ways are not equally cheap.

The obvious way to compare two solutions would be to run both and time them with a clock. That measurement is almost useless, because it describes the machine as much as the algorithm. Run the same program on a newer computer and every timing improves; run it while other programs are busy and every timing worsens. What is needed is a measure that belongs to the algorithm itself.

The measure used is the number of basic operations performed, counted as a function of the amount of data. For a sorting technique the basic operation is the comparison of two elements, and the amount of data is the number of elements, written \(n\). That count does not change when the computer changes, and it is exactly what was being worked out in Table 5.3 and in section 5.3.5.

5.5.2 What Time Complexity Measures

The amount of time an algorithm takes to process the data given to it is called its time complexity, and it is expressed not as a number of seconds but as an expression in \(n\).

NoteDefinition

The time complexity of an algorithm is a description of how the number of operations it performs grows with the size of the input, expressed in terms of that size.

Read the definition carefully, because it is easy to want more from it than it offers. Time complexity does not tell you how long a program will take. It tells you what happens to the running time when the input grows — whether doubling the data doubles the work, or quadruples it, or leaves it untouched. That is a coarse statement, and it is deliberately coarse, because the coarse statement is the one that survives a change of machine.

For small collections the distinction hardly registers. Every technique in this chapter sorted five elements instantly, and so would a considerably worse one. It is when a program built for ten records meets ten lakh records that the shape of the growth decides whether an answer arrives in a moment, arrives after lunch, or never arrives at all.

Working out complexity exactly involves mathematics beyond this book. What can be done here — and what is genuinely useful — is to estimate it by looking at the structure of the loops in the code.

5.5.3 Estimating Complexity by Reading the Loops

The estimate rests on a simple observation: statements that are executed once contribute almost nothing, while statements inside a loop contribute once for every repetition of that loop. So the complexity of a piece of code is decided by its loops, and by how they are arranged.

Four guidelines cover everything in this chapter.

  1. Code with no loop at all performs a fixed number of steps, whatever the size of the data. Its time complexity is constant.
  2. A single loop that runs from beginning to end of the data performs its body \(n\) times, so its complexity is \(n\).
  3. A loop inside another loop performs its body up to \(n\) times for each of \(n\) repetitions of the outer loop, so its complexity is \(n^2\).
  4. When code contains a nested loop and also a separate single loop, the nested loop decides the estimate. For large \(n\) the term \(n^2\) dwarfs the term \(n\), so adding the smaller term changes nothing worth recording.

Table 5.9 – loop structure and the time complexity it produces | Structure of the code | Number of times the inner work is done | Time complexity | Name given to such algorithms | |—|—|—|—| | no loop | fixed, independent of n | constant | constant time algorithm | | one loop over the data | n | \(n\) | linear time algorithm | | a loop within a loop | about \(n \times n\) | \(n^2\) | quadratic time algorithm | | a nested loop plus a separate single loop | about \(n^2 + n\) | \(n^2\) | quadratic time algorithm |

adding the loops instead of multiplying them What students write:

for i in range(n):
    for j in range(n):
        total = total + data[i] * data[j]

What happens: asked for the complexity, students often answer \(2n\), reasoning that there are two loops of \(n\) each. The inner loop is not run once — it is run once for every repetition of the outer loop, so the assignment is executed about \(n \times n\) times. The fix: loops written one after the other are added; loops written one inside the other are multiplied. Nesting is the signal to multiply.

5.5.4 Constant, Linear and Quadratic Time

Three names cover the algorithms met in this chapter, and each deserves a concrete example.

Constant time
def midpoint_of_range(data):
    return (data[0] + data[-1]) / 2

This function does the same three or four operations whether data holds five elements or five lakh, because it never looks at anything except the two ends. Its running time does not grow with \(n\) at all, and such an algorithm is called a constant time algorithm.

Linear time
def count_above(data, limit):
    count = 0
    for value in data:
        if value > limit:
            count = count + 1
    return count

Here the body of the loop runs once per element, so the work is proportional to \(n\). Doubling the number of elements doubles the work. This is a linear time algorithm, and finding the largest value, adding up a list and searching an unordered list for a value are all of this kind.

Quadratic time
def count_pairs(data, total):
    n = len(data)
    pairs = 0
    for i in range(n):
        for j in range(i + 1, n):
            if data[i] + data[j] == total:
                pairs = pairs + 1
    return pairs

This function counts the pairs of elements that add up to a given total — on the list [10, 20, 30, 40] with a total of 50 it returns 2, since 10 with 40 and 20 with 30 both qualify. Every element is combined with every later element, so the comparison runs about \(\frac{n^2}{2}\) times. Doubling the data multiplies the work by roughly four, and such an algorithm is called a quadratic time algorithm.

Fig. 5.8 shows what these three growth patterns look like when drawn side by side.

Fig. 5.8

Fig. 5.8 – Growth rates of constant, linear, and quadratic time algorithms.

The dashed line in the figure carries the practical lesson. Near the left edge the three curves are almost on top of one another, which is exactly why a badly chosen algorithm behaves perfectly well in testing on ten records. Move right and they separate without limit.

Q. A linear time algorithm and a quadratic time algorithm are given the same task. Is the linear one always faster?

A. Not always — only eventually, and “eventually” can be beyond the sizes you care about. Time complexity describes the shape of the growth, not the exact number of seconds. A quadratic algorithm with very cheap steps can beat a linear one with expensive steps on small inputs. What complexity guarantees is that beyond some size the quadratic one falls behind and keeps falling behind, and no faster computer can rescue it.

5.5.5 Complexity of the Three Sorting Techniques

Now look back at the three programs written in this chapter. In bubble_sort, a for loop over j sits inside a for loop over i. In selection_sort, a for loop over j sits inside a for loop over i. In insertion_sort, a while loop over j sits inside a for loop over i. All three have a loop within a loop, so by the third guideline all three are quadratic time algorithms, with a time complexity of \(n^2\).

This agrees exactly with the counts worked out earlier by hand. Both bubble sort and selection sort make \(\frac{n(n-1)}{2}\) comparisons, and multiplying that out gives \(\frac{n^2 - n}{2}\) — an expression whose largest term is \(n^2\).

The estimate is deliberately blunt. It ignores the division by 2, and it ignores the \(-n\), because neither changes what happens as \(n\) grows large. Two algorithms that both take about \(n^2\) steps may still differ by a constant factor, and that factor is worth having; but it is a smaller matter than the shape of the curve, and it is the shape that the complexity records.

Table 5.10 makes the consequence concrete for \(\frac{n(n-1)}{2}\) comparisons. The last column assumes a machine that performs \(10^8\) comparisons per second, which is the right order of magnitude for an ordinary computer.

Table 5.10 – comparisons required by a quadratic sorting technique, and the approximate time taken at 10⁸ comparisons per second | Number of elements (n) | Comparisons, \(\frac{n(n-1)}{2}\) | Approximate time | |—|—|—| | 10 | 45 | not measurable | | 100 | 4,950 | not measurable | | 1,000 | 499,500 | about 0.005 second | | 10,000 | 49,995,000 | about 0.5 second | | 1,00,000 | about \(5 \times 10^9\) | about 50 seconds | | 10,00,000 | about \(5 \times 10^{11}\) | about 1.4 hours |

Read the table downwards. Each row multiplies the number of elements by ten, and each row multiplies the work by roughly a hundred. That is the signature of quadratic growth, and it is why a program that sorts a class list of forty names in no time at all can be entirely unusable on a list of ten lakh.

Q5. Why does the choice of sorting method hardly matter for ten elements, yet matter enormously for ten lakh?

A. Because the cost of these techniques grows as \(n^2\), not as \(n\). At ten elements a quadratic technique needs about forty-five comparisons and a much cleverer one might need thirty — a difference nobody can perceive. At ten lakh elements the quadratic technique needs roughly \(5 \times 10^{11}\) comparisons, which takes over an hour on an ordinary machine, while the growth patterns that better techniques achieve would finish the same work in a few seconds. The methods have not changed; the size of the data has, and quadratic growth turns a negligible difference into a decisive one.

One qualification belongs here. Insertion sort’s inner while loop does not always run to completion — on a list already in order it stops after a single comparison, giving \(n-1\) comparisons in all, which is linear. Its quadratic label describes its worst case, not every case. Selection sort has no such escape: its inner loop is a for over a fixed range and runs fully every time, whatever the data.

5.5.6 Choosing a Technique for a Given Situation

Since all three techniques are quadratic, complexity alone cannot separate them. The separation comes from how each behaves on particular kinds of data, and from whether comparing or moving is the expensive operation in the situation at hand.

Table 5.11 – the three sorting techniques of this chapter compared | Feature | Bubble sort | Selection sort | Insertion sort | |—|—|—|—| | What one pass achieves | largest unsorted element reaches the right end | smallest unsorted element is placed at the left boundary | one more element is placed correctly among those already in order | | How elements move | swapping neighbours | one exchange per pass, over any distance | shifting elements one position right | | Comparisons in the best case | \(n-1\), if the early stop is used | \(\frac{n(n-1)}{2}\), always | \(n-1\) | | Comparisons in the worst case | \(\frac{n(n-1)}{2}\) | \(\frac{n(n-1)}{2}\) | \(\frac{n(n-1)}{2}\) | | Data movement in the worst case | up to \(\frac{n(n-1)}{2}\) swaps | at most \(n-1\) exchanges | up to \(\frac{n(n-1)}{2}\) shifts | | Time complexity | \(n^2\) | \(n^2\) | \(n^2\) | | Can it sort data that is still arriving? | no | no | yes, one insertion per arrival | | Suits which situation | small lists, and lists that may already be in order | small lists where moving an element is costly | small or nearly sorted lists, and lists built up over time |

Three readings of this table are worth carrying away. If the data is likely to be already ordered or nearly so, insertion sort — or bubble sort with the early stop — does very little work, while selection sort does all of it regardless. If each element is a bulky record and copying it is expensive, selection sort’s promise of at most \(n-1\) exchanges is valuable. And if the elements arrive one at a time rather than all at once, insertion sort is the only one of the three that fits at all.

The final reading is the widest one. All three are quadratic, so for genuinely large collections none of them is the right answer, and Python’s own sorting facilities — built on techniques beyond this book — should be used instead. What these three teach is not a toolkit for large data. It is the habit of asking, of any program you write, how its work grows when its input does.

Add a counter for comparisons to each of the three sorting functions written in this chapter and have each return its count. Run all three on the same list of eight numbers arranged in three ways — already ascending, already descending, and shuffled — and tabulate the nine counts. Which technique’s count changed the most across the three arrangements, and which changed not at all?

5.6 Summary

Sorting and Why It Is Done
  1. Sorting is the process of rearranging the elements of a collection so that they appear in a specified order. No element is added, removed or altered; only positions change.

  2. Numbers may be arranged in ascending order, running from the smallest value to the largest, or in descending order, running from the largest down to the smallest. Strings may be ordered alphabetically or by length.

  3. The value on which the ordering is decided is called the sort key. The same collection produces entirely different arrangements under different keys, so an instruction to sort is incomplete until the key and the direction are stated.

  4. Ordered data makes later work cheaper: searching becomes faster, the smallest and largest elements sit at the two ends, duplicates come to lie next to each other, and two ordered lists can be merged in a single sweep.

  5. Sorting itself costs time, and time spent making later work cheaper is called overhead. It is worth paying when the ordered collection will be used repeatedly, and wasteful when a single fact is wanted once.

  6. Because a list is mutable, all three techniques in this chapter rearrange the original list in place rather than building a second one. Exchanging two elements is done in a single statement so that neither value is overwritten before it is read.

Bubble Sort
  1. Bubble sort compares adjacent elements only, and swaps a pair whenever the left element is greater than the right one.

  2. One complete traversal of the region under consideration is called a pass. A pass guarantees that the largest element of that region finishes at its right end, because the sweep keeps pushing that element rightwards.

  3. Each pass settles one element, so the region examined shrinks by one after every pass, and \(n-1\) passes are enough for a list of \(n\) elements.

  4. A small element moves left by only one position per pass, which is why many passes are needed even though a large element can travel a long way in one.

  5. The technique makes \(\frac{n(n-1)}{2}\) comparisons. The number of swaps depends on the data: none in the best case, when the list is already ordered, and one at every comparison in the worst case, when the list is exactly reversed.

  6. A pass that performs no swap proves that the list is already sorted. Recording that fact in a flag variable, reset at the start of every pass, allows the remaining passes to be abandoned.

  7. Reversing the comparison inside the decision reverses the sorted order. The structure of the loops and the number of comparisons do not change.

Selection Sort
  1. Selection sort keeps the list divided by a boundary: everything to the left is in its final position, everything to the right is still unexamined.

  2. Each pass scans the unsorted region for the smallest element, remembering its position rather than its value, and then exchanges that element with the leftmost element of the region. The boundary advances by one position.

  3. The decision is taken first and the data is moved once, at the end of the pass, so an element travels any distance in a single exchange.

  4. The number of comparisons is \(\frac{n(n-1)}{2}\) regardless of the data, so the technique cannot recognise a list that is already ordered and cannot stop early.

  5. At most \(n-1\) exchanges are performed, which makes selection sort attractive when moving an element is expensive.

Insertion Sort
  1. Insertion sort treats the first element as an ordered region of one, then takes each remaining element in turn and inserts it into that region at its correct place.

  2. The elements in the ordered region are in order among themselves, but not necessarily in their final positions; an element already placed may be pushed rightwards later.

  3. Space is made by shifting rather than swapping: each element greater than the value being inserted is copied one position right, and the value is then written into the vacated position.

  4. The value being inserted must be copied into a temporary variable before any shifting begins, because the first shift overwrites the position it occupied.

  5. The inner scan stops for either of two reasons — reaching the left end of the list, or meeting an element that is not greater than the value being inserted. The index must be checked before the element at that index is read.

  6. On an already ordered list the technique makes only \(n-1\) comparisons and shifts nothing; on a reversed list it makes \(\frac{n(n-1)}{2}\) comparisons and shifts at every one.

  7. Because each pass is itself an insertion into an ordered list, the technique can keep a collection in order while data is still arriving, one insertion per new element. The other two techniques require the whole collection to be present before they begin.

Time Complexity of Algorithms
  1. The time complexity of an algorithm describes how the number of operations it performs grows with the size of the input, expressed in terms of that size. It is not a measurement in seconds, and it does not change when the machine changes.

  2. Complexity is estimated from the structure of the loops. Code with no loop is constant time; a single loop over the data is linear time, written \(n\); a loop within a loop is quadratic time, written \(n^2\).

  3. Where a nested loop and a separate single loop both occur, the nested loop decides the estimate, since for large inputs the larger term dominates.

  4. All three sorting techniques in this chapter contain a loop within a loop, so all three are quadratic time algorithms with a time complexity of \(n^2\).

  5. Quadratic growth means that multiplying the number of elements by ten multiplies the work by about a hundred. This is why the choice of technique is invisible on a few dozen records and decisive on several lakh.

  6. The quadratic label describes the worst case. Insertion sort behaves linearly on data that is already nearly in order, whereas selection sort performs the same comparisons whatever it is given.

  7. Since all three grow quadratically, none of them is the right choice for very large collections. What they teach is the habit of asking how a program’s work grows when its input does.

5.7 Key Terms

  1. Adjacent elements — two elements of a list occupying consecutive positions, such as the elements at index j and index j+1.

  2. Ascending order — an arrangement in which each element is smaller than or equal to the one after it, so the collection runs from the smallest value to the largest.

  3. Best case — the arrangement of input data on which an algorithm does the least work; for bubble sort and insertion sort, a list already in the required order.

  4. Bubble sort — a sorting technique that repeatedly compares adjacent elements and swaps them when they are out of order, so that the largest element of the unsorted region reaches its right end in each pass.

  5. Constant time — the behaviour of an algorithm whose number of operations does not grow at all as the input grows, because it contains no loop over the data.

  6. Descending order — an arrangement in which each element is greater than or equal to the one after it, so the collection runs from the largest value to the smallest.

  7. Flag — a variable whose value records a yes-or-no fact about what has happened, such as whether any swap occurred during a pass.

  8. Insertion sort — a sorting technique that takes each element in turn and inserts it into its correct place among the elements already in order, shifting larger elements one position right to make room.

  9. Linear time — the behaviour of an algorithm whose number of operations grows in proportion to the size of the input, produced by a single loop over the data.

  10. Mutable — the property of a list that allows an element to be replaced by assigning to its index, so that the list changes without a new list being created.

  11. Overhead — time spent on work that is not the final answer but that makes the final answer cheaper to obtain, such as the time spent sorting before searching.

  12. Pass — one complete traversal of the region of a list under consideration, during which every required pair of elements is examined once.

  13. Quadratic time — the behaviour of an algorithm whose number of operations grows as the square of the input size, produced by a loop nested within another loop.

  14. Selection sort — a sorting technique that scans the unsorted region for its smallest element and exchanges that element with the leftmost element of the region, advancing the boundary by one position each pass.

  15. Shifting — copying an element one position to the right within the same list so as to vacate a position for another element; it moves one element, unlike a swap, which exchanges two.

  16. Sort key — the value, drawn from each element, that is used to decide that element’s position in the sorted order.

  17. Sorting — the process of rearranging the elements of a collection so that they appear in a specified order, without adding, removing or altering any element.

  18. Sorting algorithm — a definite sequence of steps that arranges the elements of a collection into the required order.

  19. Swap — to exchange two elements so that each takes the other’s position, done in Python in a single assignment statement.

  20. Time complexity — a description of how the number of operations an algorithm performs grows with the size of its input, expressed in terms of that size rather than in seconds.

  21. Worst case — the arrangement of input data on which an algorithm does the most work; for all three techniques of this chapter, a list arranged in exactly the reverse of the required order.

5.8 Points to Ponder

  1. Bubble sort with a flag can discover in a single pass that a list is already in order, while selection sort scans every element every time and can never make that discovery. Yet selection sort is the one recommended when each element is a bulky record. What does this suggest about calling one algorithm “more efficient” than another without saying what is being counted?

  2. Bubble sort and selection sort make exactly the same number of comparisons for a list of \(n\) elements. If the comparison count is identical, what did the second technique actually contribute, and would you call it a genuinely different idea or the same idea rearranged?

  3. In selection sort an element placed in the sorted region never moves again; in insertion sort an element already placed may be pushed rightwards many times before the work ends. Insertion sort nevertheless finishes with the same correct list. Which of the two guarantees is worth more, and to whom — the person writing the program, or the machine running it?

  4. Time complexity deliberately discards the division by 2 and the \(-n\) in \(\frac{n(n-1)}{2}\), keeping only \(n^2\). Something true is being thrown away on purpose. What is gained by the discarding, and can you describe a situation in which the discarded detail would have mattered?

  5. A quadratic technique sorting ten lakh records takes over an hour on an ordinary machine. Suppose a computer ten times faster were available. Would that be a fair solution to the problem, and what would happen the following year when the data grew to a crore?

  6. Sorting a list is expensive; searching a sorted list is cheap. For a collection that will be searched some number of times, there must be a point at which sorting first begins to pay for itself. How would you go about estimating where that point lies, and what would you need to know about the data to do it?

5.9 Objective Questions

  1. A list of 12 elements is to be sorted using bubble sort, written without any early termination. How many passes will be made over the list?

    1. 10
    2. 11
    3. 12
    4. 13 [1 mark]
  2. Which of the following techniques performs at most n - 1 exchanges on a list of n elements, whatever the starting arrangement?

    1. Bubble sort
    2. Selection sort
    3. Insertion sort
    4. All three perform at most n - 1 exchanges [1 mark]
  3. An algorithm contains a loop written inside another loop, each running over the whole data. Its time complexity is estimated as

    1. constant
    2. \(n\)
    3. \(2n\)
    4. \(n^2\) [1 mark]
  4. In the following question, an Assertion (A) is followed by a Reason (R). Choose the correct option. Assertion (A): In insertion sort, the element being inserted is copied into a temporary variable before any shifting begins. Reason (R): The first shift writes into the position that the element was occupying.

    1. Both A and R are true, and R is the correct explanation of A.
    2. Both A and R are true, but R is not the correct explanation of A.
    3. A is true but R is false.
    4. A is false but R is true. [1 mark]
  5. In the following question, an Assertion (A) is followed by a Reason (R). Choose the correct option. Assertion (A): Selection sort makes fewer comparisons when the list handed to it is already in ascending order. Reason (R): The inner loop of selection sort runs over a fixed range and is completed in every pass.

    1. Both A and R are true, and R is the correct explanation of A.
    2. Both A and R are true, but R is not the correct explanation of A.
    3. A is true but R is false.
    4. A is false but R is true. [1 mark]
  6. Fill in the blank: In bubble sort, one complete traversal of the region of the list under consideration is called a ____________. [1 mark]

  7. State whether true or false: In insertion sort, an element that has been placed in the ordered region never changes its index again. [1 mark]

  8. Match each item in Column A with the correct item in Column B.

Column A Column B
a. Code containing no loop over the data i. Linear time
b. A single loop running over the whole data ii. Selection sort
c. A loop written within another loop iii. Constant time
d. An element moving any distance in one exchange iv. Quadratic time

[1 mark]

5.10 Check Your Concepts

  1. Define sorting. State one reason why a collection that will be searched many times is sorted first. [2 marks]

  2. Differentiate between shifting and swapping as used in this chapter, in two points. [2 marks]

  3. Using the list [30, 10, 20], show what one complete pass of bubble sort produces, and explain why the largest element of a region must finish at the right end of that region. [3 marks]

  4. For a list of 6 elements, state the number of comparisons made by each of the following, and justify each answer in one line:

    1. selection sort, on any arrangement of the data
    2. insertion sort, on a list already in ascending order
    3. bubble sort written without early termination [3 marks]
  5. Write a user-defined function that arranges a list of integers in descending order using bubble sort, abandoning the remaining passes as soon as a pass performs no swap. Explain in two lines why the flag must be set to False at the start of every pass rather than once before the outer loop. [4 marks]

  6. Readings from an instrument arrive one at a time through the day, and the list of readings must be in ascending order after every arrival.

    1. Name the sorting technique this situation corresponds to, and justify the choice in one line.
    2. Write a user-defined function that inserts one new reading into an already ascending list.
    3. Give the reasoning that shows your function leaves the list in ascending order.
    4. State one kind of input on which this technique performs badly. [5 marks]

5.11 Output & Debugging

  1. Predict the output of the following code, and state how many swaps take place.
data = [5, 2, 9, 1]
n = len(data)
for j in range(n - 1):
    if data[j] > data[j + 1]:
        data[j], data[j + 1] = data[j + 1], data[j]
print(data)

[2 marks]

  1. The following program is meant to sort the list in ascending order using selection sort, but it does not.
def sort_list(data):
    n = len(data)
    for i in range(n - 1):
        min_pos = i
        for j in range(i + 1, n):
            if data[j] < data[min_pos]:
                min_pos = j
                data[i], data[min_pos] = data[min_pos], data[i]

values = [4, 1, 3]
sort_list(values)
print(values)

State the output the program actually produces, identify the single error, and write the corrected program. [3 marks]

  1. State the value printed by the following code, and say in one line what the printed value represents for a list of six elements.
data = [12, 7, 25, 3, 18, 6]
count = 0
n = len(data)
for i in range(n - 1):
    for j in range(n - 1 - i):
        count = count + 1
print(count)

[2 marks]

  1. Give the output of the following code. Also state the value held by j at the moment the while loop stops during the last repetition of the outer loop.
data = [20, 35, 15, 40, 10]
for i in range(1, 3):
    temp = data[i]
    j = i - 1
    while j >= 0 and data[j] > temp:
        data[j + 1] = data[j]
        j = j - 1
    data[j + 1] = temp
print(data)

[3 marks]

5.12 Case-Based Questions

  1. A school library records how many times each of six books was issued during a month. The counts are held in a list, in the order in which the books appear on the shelf:
issues = [14, 3, 27, 9, 21, 6]

The librarian wants the list rearranged in descending order, so that the most frequently issued books can be identified at a glance, and has decided to use bubble sort. a. State the single change that must be made to an ascending bubble sort function to obtain descending order. [1 mark] b. Show the contents of the list at the end of the first complete pass, and mark the element that has reached its final position. [2 marks] c. State how many comparisons the second pass will make on this list. [1 mark] [4 marks]

  1. At a bus depot, departure times are recorded as the number of minutes past midnight. The times are entered one at a time as each service is scheduled, in this order: 420, 375, 500, 400. The clerk needs the list to be in ascending order after every entry, not only after the last one.
    1. Identify the sorting technique that this situation is really using, and state in one line why the other two techniques of this chapter do not fit. [2 marks]
    2. Write a user-defined function add_time(times, new_time) that inserts new_time into the already ascending list times, keeps the list ascending, and returns the number of shifts it performed. [3 marks]
    3. State what your function does when times is empty. [1 mark] [5 marks]

5.13 Programming Practice

  1. Write a complete program that reads a count n from the user, reads n integer marks into a list, and passes the list to a user-defined function that arranges it in ascending order using selection sort. The function must also count the exchanges it performs and return that count. The program should display the list before sorting, the list after sorting, and the number of exchanges. Submit: the complete program and an identifier description table for the function. Sample input: 5 followed by 48 12 35 7 26. The output should show the sorted list and a single exchange count. [4 marks]

  2. A shop keeps the prices of its items in a list and needs to display the k cheapest items. Write a program that accepts a list of prices and a value k, sorts the list in ascending order using a user-defined insertion sort function, and displays the first k prices of the sorted list. The program must reject a value of k that is larger than the number of prices, with a suitable message. Submit: the user-defined function, the complete program, and one line stating what your program prints for the input [45, 12, 78, 30, 25] with k equal to 3. [5 marks]

  3. Write a user-defined function count_comparisons(data) that sorts the list data in ascending order using bubble sort with early termination, and returns the number of comparisons it made. Write a program that calls the function twice — once on [10, 20, 30, 40, 50] and once on [50, 40, 30, 20, 10] — and prints both counts. Submit: the function, the complete program, and a two-line note explaining why the two counts differ. [3 marks]