Algorithms and Algorithmic Thinking
I. Defining algorithms in depth
An algorithm is simply a set of instructions. We use instructions in everyday life, sometimes consciously, sometimes unconsciously. Think about the routines you follow in the morning, for example. The alarm clock sounds. What do you do next? Do you go prepare coffee? Shower? Brush your teeth first?
Most of us follow the same steps every single morning. You could say we’ve programmed ourselves to follow those steps. Now think of a time your schedule changed and your routine was different. I know I’ve had to stop and regroup many times because my program no longer works. I can’t wake up at 6 a.m. for a 5 a.m. flight, for example.
Algorithms for computers are similar in that we need to reprogram the set of instructions if a set of conditions has changed. The programs can only go as far as we have stated parameters for them. Most programs cannot adjust or adapt to any new information that is not previously coded into it. That said, machine learning and artificial learning are evolving. We’re not talking about those kinds of programs, but even in those instances, we’d still need to adjust those programs to do what we need them to.
To design algorithms, we need to make sure that they meet some specific characteristics:
- They are clear and unambiguous.
- They have inputs that are well defined.
- They have outputs that are well defined.
- They have finiteness.
- They are feasible.
- They are language-independent. </b>
Let’s look at each of the characteristics in the preceding list and define them.
1. Algorithms should be clear and unambiguous
An algorithm is clear and unambiguous when every one of the steps can easily be understood, is easily defined, and has inputs and outputs that are also clear and well defined. There should also be only one meaning for each component of the algorithm.
2. Algorithms should have inputs and outputs that are well defined
The inputs for an algorithm can be user-provided, meaning that the user of the program enters the data. Input can also mean something that is defined within the program. This means that I may include a variable with a set value already provided.
For example, if I need a user to tell me the number of tickets they are purchasing, I can write the algorithm to ask for that input. I can also give that input as a defined variable with a given value already. An algorithm does not always require an input – zero-input algorithms do exist – but when the algorithm requires input, defining that input is important. An example of an input is asking for a user’s name in a program. Think about modern video games. Many of them will prompt the user for a name with phrases such as, “Hello traveler. What is your name?”
As a user, I’d enter Sofia when given that prompt, which gives me the following:
1
"Hello Sofia. Welcome to the adventure!"
As you can see, the game will then produce an output and uses my name in that output.
This final line is the output of the program. I can write a simple program to ask that question in Python as well:
1
2
3
name = input("Hello traveler. What is your name? ")
salutation = "Hello %s. Welcome to the adventure!" % name
print(salutation)
When run, the program looks like this:
1
2
Hello traveler. What is your name? Sofia
Hello Sofia. Welcome to the adventure!
This simple algorithm allowed us to save the name as a variable. That variable was used only once in the output of this simple code. However, in a game, that name variable may be used in multiple instances, such as during conversations with characters within the game, and so on.
The output of a program is the information that leaves a system, that is, the product of your program. Given some information or code, the output is what is produced from the instructions in the program
3. Algorithms should have finiteness
An algorithm has to have finiteness . This means that an algorithm must end. Let’s look at a situation where an algorithm would not end. I don’t recommend writing this or running it! Nonetheless, let’s look at the steps we would take to create this algorithm:
- Define a variable, i, and set it as equal to 0:
1
i = 0
- Increase the value by 1. There are a few different ways we can do that:
1
2
i = i + 1
i += 1
Both of the preceding lines of code will increase the value of i by 1
- Add an error! We’re about to create an error in finiteness. Again, I’m only doing this to prove a point, but this is an error you want to avoid:
1
2
3
4
i = 0
while i >= 0:
i += 1
print(i)
In this algorithm, I’m telling the program to continue to increase i by 1 so as long as it is greater than 0, then the computer is supposed to print the value. This will just continue to go on forever and ever, without stopping, because the condition will always hold true as given. So, the output for the program will begin at 1, but will continue printing the next item in the sequence as 2, 3, 4, 5, and so on. The program simply has no way to end.
Now, a similar program may be done given a few different conditions. Let’s say we want to print all the values of our addition, but only so long as i is less than 15:
1
2
3
4
i = 0
while i < 15:
i += 1
print(i)
I know I said this program did not include 15. It doesn’t. Since this happens while i is less than 15, the last value it will evaluate for is 14. However, it says that while the value is less than 15, we increase it by 1 (i += 1). So, when i is 14, the printed value is 14 + 1, or 15. Finiteness allows the program to terminate.
4. Algorithms have to be feasible
An algorithm also has to be feasible . To be feasible, an algorithm needs to be possible with the available content and resources. When writing algorithms, we have constraints, or conditions we may write into the steps. If there is no way to meet all the constraints, then the algorithm isn’t feasible. Think of the two conditions, given as follows:
- It is 3:00 p.m.
- It is 5:00 p.m.
If we set both of these constraints on a variable, for example, it would not be possible. It cannot be both 3:00 p.m. and 5:00 p.m. at the same time. This is what we call infeasible . While the algorithm can continue, we’re still creating a problem by making these two things true at the same time. Some constraints will never be met, so the algorithm is considered infeasible. There has to be a way for the algorithm to meet all constraints in order to be feasible. In addition, if an algorithm is written to depend on future technology, for example, it is also considered infeasible.
5. Algorithms are language-independent
Finally, an algorithm must be language-independent . The set of instructions in an algorithm should be written as simply as possible. A good algorithm will be such that it can be written in any language easily and produce the same output.
II. Designing algorithms
When designing algorithms, order matters. There are hierarchies that matter when we are working with programming languages. That includes when we are working with Python. Think about this as the order of operations in mathematics. If you recall, we use the mnemonic PEMDAS to remember the order of operations in mathematics. PEMDAS stands for </b> Parentheses, Exponents, Multiplication/Division, </b> and Addition/Subtraction .
I write Multiplication/Division together like this because multiplication and division hold the same weight. That is, multiplication does not necessarily need to happen before division. If I have a division first and then a multiplication from left to right, then the division happens first. The same is true for addition and subtraction. Neither has more weight than the other, so we perform them in order of appearance from left to right.
1. Problem 1 – An office lunch
An office is ordering catering for employees. Employees were given two lunch options: sandwiches or salads. Each sandwich meal costs $8.50, while each salad meal costs $7.95.
a. Office lunch mathematical algorithm
The number of employees who choose each option is unknown. Let’s use some variables to help us in designing the mathematical algorithm. Let’s use s for the number of sandwiches and b for the number of salad bowls. And I know what you’re thinking, those two variables aren’t very helpful if you come back to this problem a while from now. But we’ll talk about that in a second. For now, let’s just write what our total cost, c, will look like:
\[c = 8.5s + 7.95b\]This is a simple mathematical problem that requires two unknown variable inputs, s and b, in order to get our total, c. Now let’s look at a different version of the same lunch scenario.
b. Office lunch Python algorithm
Now let’s think about a few more considerations when writing the program. As we design a Python algorithm for this problem, we’ll need to think about two perspectives: the programmer and the user.
Sometimes we’re both the programmer/developer and the end user for our programs, but many times, we’ll write or develop content for someone else to use. It is important that we keep those considerations in mind because it may affect how we write our program and define our variables. In addition, if we’re writing a program as part of a company, others may need to go and edit our programs at some point.
That means we need to write the program in a way that others will be able to understand. Our variables should be easily understood, so writing a simple one-letter variable may make it harder for another programmer or user to understand. Let’s look at a program for Problem 1. Recall that in that problem, we’re trying to determine the final cost for an office lunch for employees given two possible options:
- $8.50 for a sandwich meal
- $7.95 for a salad mea
Let’s create the program for this problem using Python. Let’s clarify some variables first. We’ll want to use full words or a series of words separated by _ to define these variables. Before we start, you may want to recall that for Python variables, some rules need to be followed so as not to cause an error:
- Variables must start with a letter or an underscore (_).
- Variables can only contain letters, numbers, and underscores.
- Variables cannot start with a number.
- Variables are case sensitive (alpha is not the same variable as Alpha or ALPHA).
For Problem 1, we need three variables:
- The total cost of the lunch
- The number of sandwich meal lunches
- The number of salad meal lunches
Now we need to name them:
- total_cost = the total cost for all lunches
- number_of_sandwiches = the total number of sandwich meals ordered
- number_of_salads = the total number of salad meals ordered
The important thing here is that those variables are easily read and easily understood. I should make a note that I am partial to lowercase variables when programming. I do have some exceptions for when I like to use capital letters, but you’ll see many examples with only lowercase letters and underscores. I found a long time ago that even when capital letters made sense to me at the time I was writing a program, I’d later forget which letters were capitalized, which was just an added headache that could be avoided if I just used lowercase letters instead.
In addition, some programmers eliminate the underscores and use variables such as numberofsandwiches or simply sandwiches, for example. Both of those are acceptable, of course, and the simple sandwiches will make it easier to write some of the code. There are both pros and cons to doing this, however. If someone else is looking at the program, readability will be important. Like I said, I am partial to clear, lowercase variables and the use of underscores, but it is up to every programmer to make that choice themselves.
Now that I have defined my variables, I can begin to write my program. What will I need to ask the user for? I need inputs from the user for both the number of sandwiches and the number of salads. What I want as an output, or what the user will want as an output, is the total cost of the lunch. To ask for input from the user in Python, we need to use the input command. However, we also need to remember that since we are using this number in an algorithm that uses a float number (decimals are float characters), we need to convert the number provided to integer or float. Employees will not be able to order half a salad, so we can safely save them as integers, or int.
1
2
3
4
5
6
7
8
# Ask the user for the number of sandwich meals ordered and save as variable.
number_of_sandwiches = int(input("How many sandwich lunches were ordered? "))
# Ask the user for the number of salad meals ordered and save as variable.
number_of_salads = int(input("How many salad lunches were ordered? "))
# Create total_cost variable and save the algorithm for total the new variables.
total_cost = 8.50 * number_of_sandwiches + 7.95 * number_of_ salads
#Print the total cost. Don't forget to convert the total_cost to string.
print("The total cost for the employee lunch is $" + str(total_cost) + ".")
When running the code, the user can enter the number of each of the options for the office lunch. The code first asks the user for the number of sandwiches like so:
1
How many sandwich lunches were ordered?
The code will then ask for the number of salad lunches and provide a total cost. The following sample takes an input of 12 sandwich lunches and 23 salad lunches, which would be a total cost of $284.85:
1
2
3
How many sandwich lunches were ordered? 12
How many salad lunches were ordered? 23
The total cost for the employee lunch is $284.85.
Now let’s take a look at a similar problem, but from a different perspective.
2. Problem 2 – A catering company
Let’s say you start a simple catering company. You begin only selling two options, a sandwich meal for $8.50 and a salad meal for $7.95. You can create a program that stores these options using a Python dictionary.
1
2
3
4
5
catering_menu = {
"sandwiches": 8.50,
"salads": 7.95
}
print(catering_menu)
Now, dictionaries are common and very useful for various reasons: primarily, that they are easy to read and they provide a way to change data as required. When printed, the dictionary code looks like this:
1
{'salads': 7.95, 'sandwiches': 8.5}
Now that you have a dictionary, let’s talk about its usefulness to your catering company. Let’s say that there is a cost increase for your salad ingredients that you want to account for by changing the price of the salads. You can do so in a few different ways. You can change it in the original program, since it is so short, or you can just tell the program what you want to change based on the key. This is important because you may have two items for sale now, but what happens when your menu options become much wider? Would you want to search for each item every time you change a price? Python makes it easy to identify what you want to change and then change it.
To do so, you can use the following code:
1
2
3
4
5
6
catering_menu = {
"sandwiches": 8.50,
"salads": 7.95
}
catering_menu["salads"] = 9.50
print(catering_menu)
But, what happens if you want to add a menu item? Say you want to add a soup option for $3.75. In this case, you can add the menu option to your dictionary by using a simple line of code, as follows:
1
catering_menu["soup"] = 3.75
When you put it all together, the initial code and the changes would look like the following code block. Notice that you have the initial dictionary, then the two changes below that. When you print the dictionary, it will include all changes along with the addition of the soup option:
1
2
3
4
5
6
7
catering_menu = {
"sandwiches": 8.50,
"salads": 7.95
}
catering_menu["salads"] = 9.50
catering_menu["soup"] = 3.75
print(catering_menu)
We can use the information within the dictionary to create more robust programs, such as an online menu, an ordering menu option, and much more. In this section, we learned about designing an algorithm with the help of two problems.
III. Analyzing algorithms
As mentioned previously in this chapter, when we design algorithms, they should meet the following characteristics:
- They are clear and unambiguous.
- They have inputs that are well defined.
- They have outputs that are well defined.
- They have finiteness.
- They are feasible.
- They are language-independent.
In addition to those characteristics, when we are looking at algorithms and analyzing them, we want to make sure we ask ourselves some questions:
Does the algorithm do what we want?
Does the output make sense?
Is there another way to get the same information in a clearer way? </i>
There are many more questions we can ask ourselves when analyzing algorithms, but for now, let’s take a look at some algorithmic solutions and analyze them based on the aforementioned characteristics and questions
1. Algorithm analysis 1 – States and capitals
A student has created an algorithm that includes a list of US states and the capitals for each of those states, but only those states she has already studied are included. Her algorithm is shown as follows:
1
2
3
4
Ohio = "Columbus"
Alabama = "Montgomery"
Arkansas = "Little Rock"
print(Ohio)
The program is simple, yet not easy to use, nor helpful when run. Does it contain the information needed? Yes. Can we organize it in a different way so we can call the information in other ways? Yes.
Think about states and capitals as key pairs. We can use a dictionary to store the information. You may recall from earlier in this chapter that a dictionary can be adjusted and adapted easily, adding a new key with a simple line of code. Let’s first convert the information in the previous code into a dictionary:
1
2
3
4
5
6
state_capitals = {
"Ohio" : "Columbus",
"Alabama" : "Montgomery",
"Arkansas" : "Little Rock"
}
print(state_capitals["Ohio"])
Notice that we can now access the information for the state capital by simply giving the state name. The output for this code is simply Columbus. But what if you just want to run the program and ask for the user to input a state of their choosing? We can also write that in a line of code with the existing dictionary. Take a look at the following code:
1
2
3
4
5
6
7
8
state_capitals = {
"Ohio" : "Columbus",
"Alabama" : "Montgomery",
"Arkansas" : "Little Rock"
}
state = input("What state's capital are you looking for today?")
capital = state_capitals[state]
print("The capital of " + state + " is " + capital + ".")
In this code, the user enters the state for which they want to find the capital.
Now let’s look at the need for the algorithm in the first place. The student wants to continue to add states to the program. With this program, since it is dictionary-based, she can simply add a line of code when she needs to add another state. For example, if she wanted to add the state of Iowa , whose capital is Des Moines, she’d need to use the following code:
1
state_capitals["Iowa"] = "Des Moines"
Take a look at the following code block. Note the placement of the code within the program. It is important that we place that new code before the new variables, otherwise, if you try to run the program and input Iowa, the code will return an error rather than providing the capital of Iowa.
In algorithms, logic is extremely important. We cannot use a value we have not defined in variables that have already been used. That is, if the variables state and capital are used before identifying the new value for Iowa, then the code ends with an error when the input is Iowa. However, if we add the key pair values before we run those two variables, the code runs as expected:
1
2
3
4
5
6
7
8
9
state_capitals = {
"Ohio" : "Columbus",
"Alabama" : "Montgomery",
"Arkansas" : "Little Rock"
}
state_capitals["Iowa"] = "Des Moines"
state = input("What state's capital are you looking for today?")
capital = state_capitals[state]
print("The capital of " + state + " is " + capital + ".")
As you can see, we can adapt and adjust the code to better suit our needs. Now let’s take a look at a few algorithms to determine whether they would run; that is, whether they would produce an error or run appropriately.
2. Algorithm analysis 2 – Terminating or not terminating?
As we discussed earlier in this chapter, algorithms should be terminating. That is, they must have a way to end, or they can cause many errors. Let’s look at an algorithm and analyze it to determine whether it will terminate or not:
1
2
3
4
x = 0
while x >= 3:
x += 1
print(x)
First, let’s take a look at the value of the x variable. The x variable starts the program with a value of 0. The while loop, which states the conditions under which the value of x will change, states that when the x value is greater than 3, it is incremented by a value of 1.
This algorithm terminates because it will print the original value for the variable, 0. However, this algorithm doesn’t really perform any actions, as the condition will never be met. Also, notice that the print command is not indented. If it were indented, no output would be given for this algorithm, as the print command would never be called since the variable will never meet the conditions of the while loop.
Now let’s take a look at the following algorithm:
1
2
3
4
j = 0
while j >= 0:
j -= 1
print(j)
In this case, the variable condition is met because j has to be greater than or equal to 0 for the program to run. Once the condition is met, the value of the variable is decremented by 1, so the print command will produce an output of -1. The code will not run a second time because the value of the variable is no longer greater than or equal to 0. This algorithm is terminating, produces an output, and is feasible.
Finally, let’s take a look at the following algorithm with a changed condition:
1
2
3
4
j = 0
while j <= 0:
j -= 1
print(j)
In this case, the algorithm isn’t terminating. Because we changed the while loop to be less than or equal to 0, this algorithm will now continue to run forever.
Analyzing algorithms can be very complex. We have only started to touch on some of the components of algorithms. As we delve deeper into other computational thinking problems throughout this book, we will need to keep in mind the characteristics of a good algorithm in order to analyze our own code effectively. It is also important that we continue to take into consideration the elements of the computational thinking process: decomposition, pattern recognition, pattern generalization , and algorithm design .
When we are designing the algorithm and testing it, using the characteristics of good algorithms will allow us to observe errors, adjust our algorithm for ease of use, provide better inputs and outputs, and ensure that we are not creating infeasible and non-terminating algorithms.