Brute-force algorithms#

This chapter introduces the notion of brute-force algorithms by implementing two algorithms of this kind: linear search and insertion sort. The historic hero introduced in these notes is Betty Holberton. She was one of the first programmers of the ENIAC and one of the key people for the development of several programming languages and algorithms for sorting objects.

Note

The slides for this chapter are available within the book content.

Historic hero: Betty Holberton#

Frances Elizabeth – known as Betty – Holberton, shown in Fig. 24, was one of the first programmers of the Electronic Numerical Integrator and Computer (ENIAC). The funds of the United States Army permitted the development of this earliest electronic and general-purpose computer between 1943 and 1946. Besides, she contributed to developing several programming languages, such as COBOL and FORTRAN. In addition, she created the first statistical analysis tool used to analyse the United States Census data in 1950.

She dedicated a considerable part of her work to developing algorithms for sorting the elements in a list. The activity of sorting things is a typical human activity. It can be, of course, automatised using computers, and it is a desirable property to have for addressing several tasks.

_images/06-holberton.png

Fig. 24 Picture of Betty Holberton in front of the ENIAC. Source: Wikimedia Commons.#

Of course, sorting things is an expensive task, in particular, if you have to order billions of items. However, having such items sorted is crucial for several activities that we can perform on the list that contains them. For instance, in libraries, books are ordered according to specific guidelines such as the Dewey classification. Such a classification allows one to cluster books according to particular fields, and each cluster contains books ordered according to the authors’ names and the book title. With this kind of order, the librarian can find a requested title, avoiding looking at the billion books available one by one, thus saving a considerable amount of time, after all. Therefore, to sort things in advance is a good practice if one has to search these things several times in the future.

May the (brute) force be with you#

In this chapter, for the very first time, we start to talk about problem-solving methods. In Computer Science, problem-solving is to create a computer-interpretable process (i.e. an algorithm) for solving some given problem, e.g. ordering all the books alphabetically in a library. Computer scientists have proposed several different methods for solving problems, grouped into general categories. Probably, the more uncomplicated class of problem-solving techniques is the brute-force approach.

_images/06-go.png

Fig. 25 The game of Go, which cannot be solved efficiently through a brute-force approach. Picture by Goban1, source: Wikimedia Commons.#

Brute-force algorithms are these processes that reach the perfect solution to a problem by analysing all the possible candidate solutions. There are advantages and disadvantages to adopting such kind of approach. Usually, a brute-force approach is simple to implement, and it will always find a solution to the computational problem by considering iteratively all the possible solutions one by one. However, its computational cost depends strictly on the number of available candidate solutions. Thus, it is often a relatively slow, even if simple, approach for practical problems with a vast solution space. A good suggestion is to use such a brute force approach when the problem size is small.

Abstract strategy board games, such as Chess or Go, belong to that set of computational problems that have a pretty huge solution space. Writing a brute-force algorithm that can play the game appropriately requires considering all the possible legal moves available on the board (shown in Fig. 25). According to John Tromp, the number of all the possible legal moves in Go was determined to be \(10^{170}\) [Tro16]. That makes a brute-force approach intractable, even for an electronic computer.

Python has two alternatives for creating iterative blocks: for-each loops and while loops. The first kind of iteration mechanism is provided in Python through for statement, illustrated in Listing 24. All the instructions within the for block are repeated for each item in a collection (a list, a queue, etc.).

Listing 24 The general structure of a for-each loop in Python.#
1for item in <collection>:
2    # do something using the current item`​
Listing 25 A simple function that takes a list as input and creates a stack with all the list’s values using a for-each loop. The source code of this listing is available as part of the material of the course.#
 1from collections import deque
 2
 3def stack_from_list(input_list):
 4    output_stack = deque()  # the stack to create
 5
 6
 7    # Iterate each element in the input list and add it to the stack
 8    for item in input_list:
 9        output_stack.append(item)
10
11    return output_stack

The for-each loop is handy when we want to iterate on all the elements of a list. For instance, one can apply some operations on each or find a particular value – as discussed in more detail in Section linear-search. Or, we can use a for-each loop for creating a stack with all the elements included in a list, as shown by the simple algorithm in Listing 25.

Listing 26 The general structure of a while loop in Python.#
1while <condition>:
2    # do something until the condition is true

The while loop, instead, works in a slightly different way. Python allows us to create it by using a while statement (as shown in Listing 26). The while statement will repeat all the instructions in such block until the condition specified is true. For instance, it is possible to use a while statement for implementing the run_forever function that maps the Turing machine introduced in Chapter Computability. Listing 27 shows one of its possible implementation in Python.

Listing 27 Another simple algorithm that sums 1 to a starting value indefinitely. The source code of this listing is available as part of the material of the course.#
1def run_forever():
2    value = 0
3    while value >= 0:
4        value = value + 1

In the following sections, we reuse some of these iterations to implement two brute-force algorithms for searching the position of an item in a list and ordering a list. These are known as linear search and insertion sort.

Insertion sort#

As already mentioned in Section Historic hero: Betty Holberton, the task of ordering a sequence of items is an operation we usually have to deal with in everyday life. Recalling the library’s example, having the books sorted will make searching them more efficient. It would allow us to avoid using naive approaches for the search, e.g. the one introduced in Section linear-search. These naive approaches are pretty expensive if we have billions of books to check.

_images/06-insertion-sort.png

Fig. 27 The execution of the insertion sort algorithm using the following list of book titles as input: Coraline, American Gods, The Graveyard Book, Good Omens, Neverwhere. The book highlighted by a bold red border is currently selected in the particular iteration of the algorithm. The red arrow shows the assigned position of the book in the output list. We use a transparent filter on books considered in previous iterations of the process.#

In this section, we propose one particular brute-force algorithm for addressing the following computational problem:

Computational problem

Sort all the items in a given list.

The algorithm that we want to use for addressing the aforementioned computational problem is called insertion sort. It is one of the simplest sorting algorithms to implement[1], and it is pretty efficient for small lists. The idea behind this algorithm is the following. First, it considers the items in the list one by one, according to the order placed. Thus, at each iteration, it removes one item from the input list. Then, it finds the correct location in the output list by looking at the items the output list contains starting from the last one (i.e. the rightmost one). Finally, it inserts it in the location found. The algorithm finishes when there are no more items to add to the output list. Fig. 27 shows an example of the execution of this algorithm.

Listing 31 The insertion sort algorithm described in Python. The source code of this listing is available as part of the material of the course and includes the algorithm’s test case.#
 1def insertion_sort(input_list):
 2    result = list()  # A new empty list where to store the result
 3
 4    # iterate all the items on the input list
 5    for item in input_list:
 6
 7        # initialise the position where to insert the item 
 8        # at the end of the result list
 9        insert_position = len(result)
10
11        # iterate, in reverse order, all the positions of all the
12        # items already included in the result list
13        for prev_position in reversed(range(insert_position)):
14
15            # check if the current item is less than the one in 
16            # prev_position in the result list
17            if item < result[prev_position]:
18                # if it is so, then the position where to insert the 
19                # current item becomes prev_position
20                insert_position = prev_position
21
22        # the item is inserted into the position found
23        result.insert(insert_position, item)
24
25    return result  # the ordered list is returned

To provide a Python implementation of this algorithm, we need to introduce two functions and one additional operation applicable to lists. The first function we need to use is range(<stop_number>). It takes a non-negative number as input. It returns a kind of list (i.e. a range object behaving like a list) of all numbers from 0 to the one preceding stop_number. Thus, for instance, range(4) returns the range([0, 1, 2, 3]), while range(0) returns the empty range object range([]).

The other function is reversed(<input_list>). This function takes a list as input. It returns a kind of list, i.e. an iterator for iterating the items in the list in reversed order. Thus, for instance, reversed(list([0, 1, 2, 3])) returns iterator([3, 2, 1, 0]). We can use these two functions in combination. They allow us to obtain the positions of the items already ordered in past iterations of the algorithm. For instance, reversed(range(2)) returns iterator(range([1, 0])) starting from the position (i.e. 2) of the third item in the input list.

In addition, we need a way for selecting an item in a list and for inserting an item in a specific position in a list. For addressing these tasks, Python makes available the additional list methods <list>[<position>] and <list>.insert(<position>, <item>). In particular, the former returns the item in the list at the specified position – e.g. if we have the list ​my_list = list(["a", "b", "c"]), ​my_list[1] returns “b”. The latter method allows one to put <item> in the position specified, and it shifts all the elements with position greater than or equal to <position> of one position – e.g., by applying my_list.insert(1, "d"), the list in my_list is modified as follows: list(["a", "d", "b", "c"]).

At this point, we have all the ingredients for developing the insertion sort algorithm in Python, shown in Listing 31.

References#

[Tro16]

John Tromp. Counting Legal Positions in Go. 2016. URL: http://tromp.github.io/go/legal.html.

Notes#