ENGI E1006: Introduction to Computing for Engineers and Applied Scientists


Lecture 2: Control; algorithms

Reading: Punch and Enbody Chapter 2-3

The if statement

if (boolean expression): indented stuff here more indented stuff here this stuff is only executed if the boolean expression evaluates True

In [1]:
a=5
b=1
if(a<10):
    b=12
b
Out[1]:
12

Remember a boolean expression is just any expression that evaluates to True or False. The colon after the if condition indicates that the indented block of code that follows is only to be executed when the boolean expression evaluates to True.

else is an optional accessory for use with if statements. You use an else when you have a block of code that you wish to execute if and only if the boolean if condition evaluates to False.

In [2]:
a=20
if(a<10):
    b=12
else:
    b=3
b
Out[2]:
3

Example

roshambo.py

In [3]:
#**********************************
# File: roshambo.py
# This program plays a game
# Written by Cannon
#*********************************
import random


# now let's make some constants to keep track of the move choice and winner
HUMANWINS=1
COMPUTERWINS=2
TIE=0
ROCK=1
PAPER=2
SCISSORS=3
        
#winner = 0 is a tie (we can use the constant TIE for this)
#winner =1 means human wins (we can use the constant HUMANWINS for this)
#winner =2 means computer wins (we can use the constant COMPUTERWINS for this)

winner=HUMANWINS #let's initialize the winner to be the human player


#Print greeting

print("Welcome to the true test of personhood")
print("Welcome to roshambo death match")

playerMove = int(input("pick rock/paper/scissors 1/2/3: "))

computerMove = int(random.random()*3+1)
if computerMove==ROCK:
    computerChoice='Rock'
if computerMove==PAPER:
    computerChoice='Paper'
if computerMove==SCISSORS:
    computerChoice='Scissors'
    
print("Player 2 chooses " + computerChoice +" !") 
    

# Determine who the winner is,
# Remember we set the default to the human player
# so we only need to check for ties or computer vicory

if playerMove==ROCK and computerMove==PAPER:
  winner=COMPUTERWINS
if playerMove==PAPER and computerMove==SCISSORS:
  winner=COMPUTERWINS
if playerMove==SCISSORS and computerMove==ROCK:
  winner=COMPUTERWINS
if playerMove==computerMove:
  winner=TIE

# Print who the winner is

if winner==HUMANWINS:
  print("you win!")
if winner==COMPUTERWINS:
  print("Player 2 wins!")
if winner==TIE:
  print("It's a tie!")


 

    # print goodbye

print( "so long sucker")
Welcome to the true test of personhood
Welcome to roshambo death match
pick rock/paper/scissors 1/2/3: 1
Player 2 chooses Scissors !
you win!
so long sucker

What just happened?

import random: This statement is necessary because we will be importing the random module. We will see more on this below.

constants: A constant is simply an ordinary varialbe in Python where we do not want the value to change once it's set. There's nothing really special about them. We use all caps when naming constants. This convention lets us know that these variables should not change values. We use these constants instead of the literals they represent because it makes the code below more readable and easier to adjust. You can think of them as a kind of mnemonic.

random.random(): This function in the random module also has the name random, hence the random dot random() call. It returns a float between 0 and 1, not including 1. When we multiply that number by three, add 1 to it, and then convert it to an int, we get a random number that is equally likely to be a 1,2, or 3. There are other functions for doing this without all the arithmetic and you are free to use them; all that is really required though, is random.random().

==: Remember how a single = is the assignment operator in Python. We use == as a relational operator that compares for equality. See your textbook or the Python documentation for a complete list of relational operators.

The while loop

while (boolean expression): indented stuff here more indented stuff here this stuff is only executed if the boolean expression evaluates True the expression is checked again after each execution of the loop body if it's still true, we repeat execution of the loop body

A while loop is similar to an if statement except that it incorporates repition. When you want to perform some task until something happens, a while loop is the way to go.

In [4]:
a=10
while (a>0):
    print(a)
    a=a-1
10
9
8
7
6
5
4
3
2
1

Example

guess_a.py

In [ ]:
# ***************************************
# Hi I'm a happy comment
# In this program the computer guesses
# a number between 1 and 3
# ***************************************

import random


print("welcome to guessing game death match")
again = 'y'
while again =='y':
    print("I am picking a number between 1 and 3")

    number = int(random.random()*3+1)
    guess=-1
    
    while not(guess == number):
         guess = int(input("what did I pick?"))
         if guess == number:
             print("awesome dude")
         if guess > number:
             print("you guessed too high, sucker")
         if guess < number:
             print("you guessed too low, freak")

    again = input("wanna play again? (y/n)")

print("thanks for playing, you're my hero")
welcome to guessing game death match
I am picking a number between 1 and 3
what did I pick?2
awesome dude
wanna play again? (y/n)n
thanks for playing, you're my hero

What just happened?

What's new here is only the use of the while loop. Notice the indented block and the repetition until the while condition is false. Also notice the nesting of two while loops. No big deal, right?

Let's look at one more example that puts all of this together.

Example

piggybank.py

In [ ]:
# *************************************
# Your name
# date
# file: piggybank.py
#
# This program determines the value of set of coins
#***************************************

# display greeting

name = input("what's your name? ")

if name == 'Adam':
    print ("oh, it's you again. word.")
else:
    print ("hi, nice to meet you, {}".format(name))

again = 'yes'

while again == "yes":
    # get coin numbers from user
    pennies = int(input('How many pennies you got?: '))
    nickles = int(input('and how many nickes?: '))
    dimes = int(input('and what about dimes?: '))
    quarters = int(input('you got any quarters too? How many?: '))
    if quarters > 1000:
        print ('wow, that\'s a lot quarters!')

    # calclulate total        
    total = .01 * pennies + .05 * nickles 
    total = total + .1 * dimes + .25*quarters

    # print results
    print ( name + ', your total is worth ${:.2f} dollars'.format(total))
    again = input( 'You wanna do this again? yes/no ')

# display goodbye

print ('so long sucker!')

What just happened?

There is nothing we haven't already seen in the above example. Notice the inconsistent use of single versus double quotes. This is to illustrate their interchangeability and should be avoided. You can use either single or double quotes to denote strings but be consistent. Notice the use of the escape sequence \'.

The for loop

for item in list: repeat some set of statements where with each iteration, the variable item is the next element in the list

In fact, in Python we can replace the list above with any collection type. A collectibon is a more general type than a list. It is a single object used to maintain multiple elements. The list type and the str type are examples of collections. If we want to be even more precise, we should consider what an iterator is in Python. I will leave that to the reading for now. We will disucss it in lecture later in the semester.

In [ ]:
stuff = [4,6,'happy', -45, True]
for element in stuff:
    print(element)
In [ ]:
stuff = 'stuff'
for element in stuff:
    print(element)

Summary so far

So far we learned how to control the flow of execution in a program. We saw how to articulate branching via if statements and we saw how to articulate repition using while and for loops. Why are there multiple ways to represent repition? Simply as a matter of convenience. The truth is, we could use while loops for everything (or for loops for that matter) but it's just more convenient to use for loops sometimes and while loops other times.

Algorithms

Recall we defined Algorithm: A well-ordered sequence of operations that produce a result and halt in a finite amount of time.

A more formal definition: A well-ordered collection of unambiguous and effectively computable operations that, when executed, produces a result and halts in a finite amount of time.

There are three components to an algorithm:

  1. A set of inputs, where each input is a finite sequence of items.
  2. A set of outputs, where each output is a finite sequence of items.
  3. A method consisting of a finite sequence of instructions, each one of which can be mechanically executed in a fixed length of time with a fixed amount of resources. The method must produce an output for each input in the set of possible inputs in a finite amount of time.

Pseudocode

  • A programming language is a notation for specifying instructions that a computer can execute
  • Every (programming) language has a syntax and a semantics
    • Syntax specifies how something is said (grammar)
    • Semantics specifies what it means
  • Pseudocode is an informal programming language with English-like constructs modeled to look like statements in a Python-like language
  • Anyone able to program in any computer language should understand how to read pseudocode instructions.
  • When in doubt just write it out in English.

A first algorithm

Linear Search (also called sequential search): An algorithm to determine wheather a given item appears on a list.

  • Input: A list of n > 0 items A[1], A[2], ..., A[n] and a target item x
  • Output: The message "Sorry, x is not on the list" if x is not on the list.
    Otherwise, the message: "x occurs at postiong i on the list."
  • Method:
    found = "no"
    i=1
    while (found == "no" and i <=n)
        if (A[i] == x)
            found = "yes"
            location = i
        i=i+1
    if (found =="no")
        print("sorry, ", x, " is not on the list")
    else
        print(x , " occurs at position ", location, " on the list)
    

What just happened? We search the list one item at a time until we either find the item or we reach the end of the list. We use a while loop to articulate the iterative search. This algorithm will terminate after at most n comparisons.

Imagine using linear search to look for a name in a phonebook. It would be pretty awful. We don't do that. Instead we exploit the sorted nature of the names in a phonebook. Binary search is an algorithm that searches sorted lists in a much more efficient way than linear search. Of course, we need a sorted list to do this. How does binary search work?

  • We start in the middle of the list and determine which half of the list our target is in.
  • Repeat this procedure eliminating half of the remaining list elements on each iteration until we find our target or determine that it is not on the list.
  • This allows us to find our target or determine that it is not on the list in at most lg n comparisons, where lg is a shorthand notation for log to the base 2.

Binary Search

  • Input: A list of n > 0 items A[1], A[2], ..., A[n] and a target item x
  • Output: The message "Sorry, x is not on the list" if x is not on the list.
  • Method:
    found = "no"
    begin = 1
    end = n
    while (found == "no" and begin <= end)     
      m = begin + floor((end-begin)/2)
      if(A[m] == x)       
          found = "yes" 
          location = m  
      if(A[m]>x)
          end = m-1
      if(A[m]<x)
          begin = m+1 
    if (found == "no")  
      print("Sorry, " , x , " is not on the list")     
    else 
      print(x , " occurs at position " , location , 
     " on the list")
    

Selection Sort

So Binary Search with its lg n efficiency is exponentially faster at searching than Linear Search. It can accomplish this only because the list is sorted. That is, to use Binary Search we must have a sorted list. This is not the case with linear search. So what if we have a big unsorted list and we want to use Binary Search to search it? Well, first we must sort the list. Selecction Sort is an intuitive algorithm for sorting lists in place.

Selection Sort

  • Input: A list of n > 0 items A[1], A[2], ..., A[n]
  • Output: The list sorted in ascending order.
  • Method:
    for i = 1 to (n-1)
      min_location = i
      for j = i+1 to n
          if (A[min_location] > A[j])
              min_location = j
      excchange(A[min_location], A[i])
    

Insertion Sort

Insertion sort is another algorithm for sorting. It is also somewhat intuitive, you might think of it as the procedure you would use to sort numbered playing cards in your hand. Of course on first glance the pseudocode may not appear to be doing that, but if trace through a couple of examples it will become more obvious.

Insertion Sort

  • Input: A list of n > 0 items A[1], A[2], ..., A[n]
  • Output: The list sorted in ascending order.
  • Method:
    for j = 2 to n
      temp = A[j]
      i = j-1
      while (i . 0 and A[i] > temp)
          A[i+1] = A[i]
          i = i-1
      A[i+1] = temp
    

Analyzing Algorithms

At this point we have seen two different algorithms for sorting a list. Which one is better? Assuming that both algorithms sort correctly, what do we mean by better? We would like to measure the quality of an algorithm so that we can compare algorithms that perform similar tasks. To do this we have three obstacles we must overcome.

  1. We want our meausre to be machine (and implementation) independent. Computer technology changes, some coders are more talented at implementing algorithms than others, we don't want our basis of comparing algorithms to depend on these things. So we will count the number of (arithmatic and/or logical) operations an algorithm must perform to complete its task. This is a mathematical characteristic of the algorithm and indepenent of implementation and technological advances.

  2. For some algorithms the number of operations needed will vary depending on luck. Observe the difference in number of comparisons when Insertion Sort sorts the list [1,2,3,4,5] versus sorting the list [5,4,3,2,1]. To handle this obstacle we will consider the worst-case-performance. Not because we are pessimists but because this way we can provide a guarantee on performance.

  3. Not only will the number of operations depend on luck, it will also depend on the size of the input. Clearly sorting 5 things requires fewer operations than sorting 5 million things. Hence our number of operations won't be a number at all. Instead we end up with a function of the size of the input. We will call this function the time complexity of the algorithm and it is one measure of algoritmic efficiency.

Big-O Notation

So how do we compare these Time-complexity functions? One thing to keep in mind is that these functions will all be nondecreading in n, the size of the input. This makes sense, right? How could the number of steps decrease as the size of the problem increases? We say nondecreasing because some algorithms only require a constant time, i.e: they don't really depend on the size of the problem.

So, if the functions we're interested in are nondecreasing, we can look the way they grow to compare them. Given a function of n we will consider the most dominant term. What we really care about is the order of magnitude of growth. Here are a couple of examples:

  1. $$ T(n) = 25n^3 + \log(n) + 2^n = \mathcal{O}(2^n)$$
  2. $$ T(n) = 3n^2 + 100n = \mathcal{O}{n^2}$$

We'll save the technical defintion for big-O notation for a course in discrete or applied mathematics...

In [ ]: