Section 4 Further Problem-Solving and Programming Skills
Section 4 Further Problem-Solving and Programming Skills
This reference is interactive—you can try out the examples as soon as you read the
explanation, without having to launch an IDE. The expetcation is that you use this and you
textbooks to get an overview of the concepts, and then dig in by researching—I'll provide
some links where appropriate.
Note: this lesson expects that you have familarity with programming in any one of these
ways:
• Completed CAIE 9608/2 or 9618/2 syllabus
• A few years of experience programming with Python 3
• Been through my section 2 notebook.
0: Jupyter Notebooks
What you're reading right now is called a Jupyter Notebook. It allows me to write
documentation (explanation), place graphics/diagrams, and write executable Python code
in the same place. You can execute my code, edit it, or just create a new cell and
experiment.
[Over]Simply put, a Jupyter notebook is a file with various cells. A cell is a block of code,
explanation (like this one), or a multimedia element. To execute a cell, click on it to select it,
and press Ctrl + Enter (or Cmd + Return). Go ahead, and try it with the cell below—you
should get a message telling that you succeeded. If you already know Python programming,
you can edit that code too!
print("You successfully executed a cell. Congratulations!")
1: Tags
Each cell has one or more tags. If it's visible, you should see the words Meta and Jupyter
written in two small boxes on the top-left of this cell, and you don't have to do anything. If
not, click View > Cell Toolbar > Tags.
Tags tell you which syllabuses a given cell pertains to, or otherwise describes the content.
For example, this cell has two tags:
• Meta This cell is not about any syllabus-specific content, and just describes
something about the course.
• Jupyter This cell describes how to use Jupyter or its features.
Here is a full list of tags you'll encounter.
Tag Explanation
IGXXXX This represents a CAIE IGCSE syllabus, with
syllabus code XXXX. You must understand
this content.
ALYYYY This represents a CAIE AS & A Level
syllabus, with syllabus code YYYY. You
must understand this content.
General This is something not explicitly mentioned
by the syllabus, but is common knowledge
amongst programmers. Knowing this may
help you get a better grasp of the syllabus.
Extended Content that might be interesting or useful,
but not explicitly covered by the syllabus.
You should avoid using this on exams.
Used May appear with some General or
Extended topics. You can use these in your
exams.
Meta These are not content about programming
itself, instead it may explain something
about my writing style, or help getting
around Jupyter.
Jupyter May appear with some Meta tags. These are
about the Jupyter environment.
2: Installation?
If you don't want to edit my code and save those changes, then you don't have to install
anything—you can keep coming back to this notebook in your browser using Binder.
However, if you would like to experiment, create notebooks, or want more customizability,
install Jupyter. I'll give you a simple method below, but visit the official website for more
detail about Anaconda and other methods.
This method uses a command line. While you can just follow these instructions, it might be
worthwhile understanding what's going on. If any of this is unfamiliar, take a look at my
advice for programming learners.
1. If you don't already have it, download and install Python from the its website. Make
sure you get version 3.3 or newer.
– If you need to check whether you have Python, open a command line, type
python --version, and press Enter.
– If you get a version number, you have Python; if not, you don't.
2. Open up a command line.
– If you're on Windows, press Windows + R to bring up the run dialog box.
Type powershell and hit Enter.
– If you're on MacOS, launch the application called Terminal.
– If you're on a Linux-based OS, I suppose you know what to do.
3. Type in pip3 install notebook (try pip install notebook if that didn't
work) and press Enter. If you're asked whether you want to continue, type y for
'yes' and press Enter. Wait for the installation to complete.
To launch Jupyter, open a command line, and go to the folder you want to save your files in.
Type in jupyter notebook and press enter.
3: Learn more
Now that you've had a basic introduction to Jupyter, we can jump into the lesson.
If you're interested in learning more, I'll leave links to some sources below:
• Anaconda
• Jupyter demo and more detailed overview
• Guide to using Jupyter
• Installing Jupyter Notebook and more
• Jupyter documentation
4: My sources
Most of the content here is about explaining skills, which I've acquired over time through
practice and reverse-engineering others' examples. It would not make sense for me to cite
these. However, for trasparency, I'll do my best to explain where I got information from.
I sometimes refer sources online, for things like definitions, and I'll make sure to leave you
a hyperlink to these (though it may take the form of suggested further reading).
A lot of what I know also comes from the following:
• My computer teacher at school, who taught me Python and a lot of rigour
• My robotics mentor, who introduced me to programming through C++
• Random YouTube vidoes (my recommendations)
• My A Level texboook
• CAIE syllabus specifications: I look up individual topics as per these documents.
This is used to program instructions that the CPU can execute directly, for example
using assembly programming. This has already been covered in papers 1 and 3.
2. Imperative programming
3. Object-oriented programming
4. Declarative programming
Programs are written without paying much attention to the details of their
execution. This is often used to specify facts and rules in order to solve a problem.
except errorTypeOne:
### code to execute if errorTypeOne occurred
except errorTypeTwo:
### code to execute if errorTypeTwo occurred
except:
### code to execute if an exception occurred
# but it was not explicitly handled above
else:
# code to execute if no error occurred
finally:
# code to execute regardless of errors
A list of common built-in exceptions can be found here, from Python's official website.
### Attempt a division problem
try:
dividend = float(input("Enter a dividend: "))
divisor = float(input("Enter a divisor: "))
quotient = dividend / divisor
### Handle error wherein a zero was entered for the divisor, which is
mathematically not defined (aka impossible)
except ZeroDivisionError:
print ("You tried dividing by 0, which is mathematically not
possible.")
It must be noted, however, that this is just an example of some of the recursive functions
you're likely to run into. Like everything else in programming, there are many ways to do it,
and you'll likely run into recursive functions with a different format.
if x == 0:
answer = 1
else:
answer = x * factorial(x - 1)
return answer
return answer
4.1.1: Abstraction
Classes: objects, properties and methods
A class can be thought of as a template, that allows a programmer to define several objects
with the same (or similar) properties and methods without rewriting the definition each
time. This is analogous to using a pre-defined keyword (such as int for INTEGER) to define
multiples identifiers with same data type. Indeed, it can be useful to think of a data type as
a class—each identifier, then, is an instance of the int class. Consider defining a class
human: this would have properties name, height, mother_tounge, favoutite_book and
sex; it could have methods sleep(), eat(food_name), study(), exercise() and
readBook(book_name). Each method can be executed, and each property behaves as a
variable. If a programmer had to define identifiers for a hundred different humans, they
needn't declare each property individually each time—instead, they can use an instance of
the class human by declaring an identifier for each person, and all instances automatically
inherit all the properties and methods defined in the base class.
Each element is accessed using a dot, with the syntax:
identifier = class.property
class.procedure_method()
class.procedure_method(arguments)
identifier = class.function_method()
identifier = class.function_method(arguments)
Setting the properties this way is a rather lengthy process. Most programming languages
allow a constructor to be defined, which lets a value to be passed into the declaration of
the class itself, as a parameter. This involves the __init__() construct.
It is usual to use two identifiers with the same name (but in different scopes) when using a
constructor. Consider a constructor into which a name and an age are passed as
parameters: this would also have the word self to indicate that it is part of a class and
must be able to access its methods and properties.
class animal():
name = ""
age = 0
Consider the statement self.name = name: it must be kept in mind that this is a valid
assignment operation and the identifiers on either side of the equals sign are distinct. The
identifier self.name is a property of the class animal—this is what other methods
defined within animal, and indeed any other function from an instance of the class (such
as cow.age), would access. The identifier name, is simply an argument passed into the
constructor—no method or routine outside the definition of the __init()__ can access it.
### Define a class for animals
class animal():
### define properties for the class
name = ""
height = 0
sex = ""
# initialize
def __init__ (self, ISBN, title, author):
self.ISBN = ISBN
self.title = title
self.author = author
self.validateISBN() # notice how the function is called as
a method
if valid:
checkDigit = 0
multiplyDigits = [1, 3]
currentDigit = False
checkDigit %= 10
if checkDigit != 0:
checkDigit = 10 - checkDigit
if not valid:
self.ISBN = input("The ISBN for " + self.title + " is
incorrect. Please re-enter: ")
Inheritance
Classes can also inherit from other classes, which turns out to be extremely useful.
Continuing with the example of the class human, consider creating classes for other animals
—a class dog for example. The class dog would have some methods and properties that are
the same as for the class human: the methods sleep() and eat(food_name) and
properties name, height and sex. Properties and methods about books, studying and
languages would be of little use for a dog. However, a dog may also have additional
properties is_pet and favourite_bone, and methods scratchEar() and bark(). It is
clear that dog and human cannot be defined identically but it would also be wasteful to
repeatedly define the common elements. A programmer could define both classes
efficiently but still maintain the flexibility if they first define a parent class animal with the
common elements, and then define dog and human as child classes.
### Define a class for humans thats inherits from the animals class
class human(animal): # the parent class is specified in parenthesis
mother_tongue = ""
favourite_book = ""
def exercise(self):
print (self.name, "is exercising.")
Polymorphism
Operators and subroutines can behave in different ways, depending on different condition.
You have already met two examples of this at AS Level P2, but might not have explicitly
noticed. This feature (an operator/procedure can choose a behaviour depending on the
oprands/arguments it is provided) is called overloading and the example below
demonstrates operator overloading.
a = 1
b = 2
addedValue = a + b + 4
print (addedValue)
# In this case, the plus sign (+) is used to add together numeric
values.
if self.velocity < 0:
self.velocity = 0
• Non-composite: these are defined from scratch, without relying on any other data
types. For example, enumerated (enum) is a data type on its own.
Many of the ADTs we will explore below (stack, queue, linked list) are built from arrays and
seem to be different ways of storing items with an index, but it is worth noting that they
have vastly different applications. Stacks are used to maintain program control, and you
can see a call stack (this is internally the backbone of recursion) whenever you run a
program in the debugging mode of an IDE. Queues are often used to "place events on hold",
such as in a printer's buffer. A linked list is often used internally to implement other data
structures (such as arrays); here, rather ironically, linked lists will be implmented using
arrays.
1: Stacks
A stack is a list containing several items, operation on the last in, first out (LIFO) principle.
Items can be pushed (added) onto the stack, and popped (removed) from the stack. It has
a pointer that helps keep track of the current element.
numberOfElements = 10
3: Linked List
A linked list is a list containing several items in which every item in the list points to the
next item in the list. In a linked list, a new item is always added to the start of the list.
numberOfElements = 10
myLinkedList = [0 for index in range(numberOfElements + 1)]
myLinkedListPointers = [index for index in range(numberOfElements +
1)]
startPointer = -1
heapStartPointer = 0
nullPointer = -1
4: Records
It could be useful to think of this ADT with databases in mind: as a record holds data about
a particular entry in a table for an entity, the ADT record holds data about a particular
entry.
### Define a record for students as a class
class student():
name = ""
no_of_subjects = 0
sex = ""
kat = student()
kat.name = "Katherine"
kat.no_of_subjects = 4
kat.sex = "female"
5: Binary trees
A binary tree can also be implemented as a class. The best way to understand one is
probably to visualize it, and the following image from Wikimedia Commons shows a binary
tree. Each circle is a node and this is the part that is an identifier and can store an element
of data. Some nodes also have nodes under them, and these are called branches. In the
diagram below, the blue nodes branch off from the red nodes, and themselves branch
further. The green nodes do not branch further, and are called leaves.
Since a tree always has the same structure, i.e., every node has a value and either branches
or terminates as a leaf, recursion is used to perform many of the operations associated with
trees.
### Define a class for a node of the tree
# Unlink other data structures, it is more useful to write the
related algorithms (insertion, searching et cetera) here, and not
separately
class node():
def printTree(self):
if self.left:
self.left.printTree() # recursion
if self.right:
self.right.printTree() # recursion
else:
self.left.insertNode(data) # recursion
elif data > self.data:
if self.right is None:
self.right = node(data)
else:
self.right.insertNode(data) # recursion
else:
self.data = data # base case
tree = node(None)
for i in range(10):
tree.insertNode(i)
print (tree.find(7))
print (tree.find(34))
tree.printTree()
else:
print ("Stack is full, cannot push")
2: Pop from a stack
To pop an element from the from the stack, we first check whether or not the stack is
already empty. If it is not empty, we access the element item at the position
[topPointer], and then decrement topPointer by 1; else we print out an error message.
def pop():
global topPointer, basePointer
item = None
else:
item = stack[topPointer]
topPointer -= 1
return item
3: Enqueue an element
To enqueue an element item, we first check whether or not a queue is already full. If it is
not full, we add item at the position [rearPointer] and increment queueLength; else
we print out an error message. If rearPointer and the queue is not full, the item is stored
in the first element of the array.
def enQueue(item):
global queueLength, rearPointer
else:
rearPointer = 0
queueLength += 1
queue[rearPointer] = item
else:
print("Queue is full, cannot enqueue")
4: Dequeue an element
To dequeue an element, we first check if the queue is already empty. If it is not empty, we
access the element at position the [frontPointer] and decrement queueLength by 1;
else we print out an error message. If frontPointer points to the last element, it is
updated to point to the first element in the array, rather than the next element in the array.
def deQueue():
global queueLength, frontPointer
item = None
if queueLength == 0:
print("Queue is empty, cannot dequeue")
else:
item = queue[frontPointer]
else:
frontPointer += 1
queueLength -= 1
return item
if myLinkedList[itemPointer] == itemSearch:
found = True
else:
itemPointer = myLinkedListPointers[itemPointer]
return itemPointer
else:
tempPointer = startPointer
startPointer = heapStartPointer
heapStartPointer = myLinkedListPointers[heapStartPointer]
myLinkedList[startPointer] = itemAdd
myLinkedListPointers[startPointer] = tempPointer
if startPointer == nullPointer:
print("Linked list empty")
else:
index = startPointer
if index == nullPointer:
print("Item", itemDelete, "not found")
else:
myLinkedList[index] = None
tempPointer = myLinkedListPointers[index]
myLinkedListPointers[index] = heapStartPointer
heapStartPointer = index
myLinkedListPointers[oldIindex] = tempPointer
arr = []
for i in range(n):
r = randint(0, 10 * n)
while r in arr:
r = randint(0, 10 * n)
arr.append(r)
return arr
1: Linear search
This is the simplest of the searching algorithms. The program traverses an array, going
through every element until a match is found. An element [i] is checked in the iteration i.
If element [i] matches the required element, the programs returns i and halts; else it
goes to [i + 1] until all elements have been searched.
def linearSearch(listIn, element):
index = -1
for i in range(len(listIn)):
if listIn[i] == element:
index = i
break
return index
# Test
arr = generateRandom(10)
x = arr[5]
print ("In list", arr, "element", x, "occurs at position",
linearSearch(arr, x))
2: Bubble sort
This is the simplest sorting algorithm. The program traverses an array, while comparing
the current [i] element to the next element [i + 1]. If the element [i + 1] was greater
than the element [i], they are swapped.
def bubbleSort(listIn):
key = None
# Test
arr = generateRandom(10)
print("Unsorted array:\t", arr)
bubbleSort(arr)
print("Sorted array:\t", arr)
3: Insertion sort
This is more efficient than bubble sort as it requires far lesser passes. The program works
by placing items into the correct position until they array sorted.
This is a comparison sort in which the sorted array (or list) is built one entry at a time. The
program starts at element [i], and compares it with every element ranging from [i - 1]
to [0]. If the element [i] is lesser than any the given current element [j], it is swapped
with [j].
def insertionSort(listIn):
key = None
# Test
arr = generateRandom(10)
print("Unsorted array:\t", arr)
insertionSort(arr)
print("Sorted array:\t", arr)
4: Binary search
In order to use binary search, an array must be sorted. The algorithm works by dividing
and sub-dividing the array into halves until the required element is found.
If there are n elements and the value x is required, the algorithm checks whether x=⌊ 0.5 n ⌋
, x < ⌊ 0.5 n ⌋ , or x > ⌊ 0.5 n ⌋ . If x=⌊ 0.5 n ⌋ , the current index is returned, else the search space
is halved in the required direction.
Binary search is not as easy to code as the other algorithms. So, it may help you to get
different takes on it as you figure it out.
• [Link] Video from Tom Scott that explains binary search in the context of database
indexing.
• [Link] Video from mCoding that has an unusual but quite clearly explained
perspective on binary searching.
### Binary search using recursion
def binarySearch (listIn, lowerBound, upperBound, element):
if upperBound >= lowerBound:
middleIndex = (upperBound + lowerBound) // 2
if listIn[middleIndex] == element:
return middleIndex
else:
return binarySearch(listIn, middleIndex + 1, upperBound,
element)
else:
return -1
# Test
arr = generateRandom(10)
insertionSort(arr) # Since binary search requires an array to be
sorted
x = arr[3]
print ("In list", arr, "element", x, "occurs at position",
binarySearch(arr, 0, len(arr) - 1, x))
if element == listIn[index]:
return index
return index
# Test
arr = generateRandom(10)
insertionSort(arr) # Since binary search requires an array to be
sorted
x = arr[8]
print ("In list", arr, "element", x, "occurs at position",
binarySearch(arr, x))
Algorithm Big O
Bubble sort O (n )
2
Insertion sort O (n )
2
Quicksort O ( n log n )
Bogosort O ( ( n+1 ) ! )
Algorithm Big O
Linear search O ( n)
Binary search O ( log 2 n )
It is important to keep in mind that the Big O formula of an algorithm does not descibe how
well it performs. For example: bubble sort and insertion sort, from the table above, have
the same formula O ( n2 ) but insertion sort is typically faster. Rather, the function n2 only
indicates that the graphs of the performance of both functions (that is, number of
comparisions required O ( f ( n )) against the number of elements n ), have the same basic
parabolic shape. The big O equation only tells us how well the performance of an algorithm
scales with the number of inputs.