Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
80 views

Section 4 Further Problem-Solving and Programming Skills

This document provides an introduction and overview of Jupyter Notebooks and Python programming concepts for a computer science course. It discusses what Jupyter Notebooks are and how they allow for mixing code, explanations, and other media types. It also covers installing Jupyter Notebooks, different programming paradigms like object-oriented and declarative programming, and handling exceptions in Python code using try/except blocks. The document aims to supplement textbook materials for a computer science course with additional explanations and examples that can be interactively run in Jupyter Notebooks.

Uploaded by

Trynos
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
80 views

Section 4 Further Problem-Solving and Programming Skills

This document provides an introduction and overview of Jupyter Notebooks and Python programming concepts for a computer science course. It discusses what Jupyter Notebooks are and how they allow for mixing code, explanations, and other media types. It also covers installing Jupyter Notebooks, different programming paradigms like object-oriented and declarative programming, and handling exceptions in Python code using try/except blocks. The document aims to supplement textbook materials for a computer science course with additional explanations and examples that can be interactively run in Jupyter Notebooks.

Uploaded by

Trynos
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 26

Introduction

This is a course/reference for CAIE Computer Science Problem-solving and Programming,


meant to help you understand the content alongside the textbooks. I try to cover most of
the syllabus directly related to Python programming, trying to provide an alternate
explanation where possible. I also talk about some parts (like expressions, and the
range() function) which are sometimes not well explained by textbooks.

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.

4.3: Further programming


4.3.1 Programming paradigms
There are several paradigms (styles) of programming, of which one or more may be
adopted in a project depending on the requirements and the platform(s) used. Often, a
programming language would be tied to a particular paradigm. For example, Python is an
object-oriented programming language, LabVIEW (language: G) is a data-flow language,
and C++ supports multiple paradigms.
1. Low-level programming

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

This involves writing about how a program accomplished something in a rather


detailed manner. This is probably what you have been doing upto now.

3. Object-oriented programming

The program consists objects that can have methods


(actions/functions/procedures) and properties (attributes). This is implemented
using the concept of a class.

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.

4.3.3: Exception handling


The importance of error handling or exception handling cannot be overstated. Every
programmer eventually runs into code that throws an error. It is crucial that the program
should be able handle the error and continue execution; if not, it must have a way to report
it to the user without simply crashing.
In Python 3, the try: keyword is used to attempt execution of a block of code that might
throw an error. We then execute the next block(s) of code depending on whether or not an
error occurred. Anything after the else: keyword is executed only if no errors occur. One
or more except: statements can be specified so that different code can execute depending
upon the exact exception raised. Here is a concise summary:
try:
### code to attempt

except errorTypeOne:
### code to execute if errorTypeOne occurred

except errorTypeTwo:
### code to execute if errorTypeTwo occurred

... # various errors

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 [any] error that comes up (such as divide-by-zero)


except:
print ("You entered an invalid value.")

### Output results if operation was successful


else:
print (dividend, "/", divisor, "=", quotient)

### Execute this regardless of program


finally:
print ("This division program has completed executing.")

### Attempt a division problem


try:
dividend = float(input("Enter a dividend: "))
divisor = float(input("Enter a divisor: "))
quotient = dividend / divisor

### Handle error wherein the entered value cannot be converted to a


number by the float() function
except ValueError:
print ("One of the values you entered is not a number, so it
cannot be used for division.")

### 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.")

### Handle other generic error


except:
print ("Congratulations! You made an error the author of this
notebook could not think of!")
### Output results if operation was successful
else:
print (dividend, "/", divisor, "=", quotient)

### Execute this regardless of program


finally:
print ("This division program has completed executing.")

4.3.2: Open a binary file


Binary/Random files are accessed using a module called pickle. Unlike the case for text
file, the methods will not inherit from the file object; rather, the file object will be passed
as a parameter.
# General syntax of using a text file
fileObject = open("path.txt", "mode")
fileObject.write("text to write")
identifier = fileObject.read()
fileObject.close()

# General syntax of using a binary file


fileObject = open("path.dat", "mode")
pickle.dump(contentsIdetifier, fileObject)
identifier = pickle.load(fileObject)
fileObject.close()

### Import the pickle module


import pickle

### Open the file


fileObject = open("binaryFile.dat", "wb")

### Write a list to the file


pickle.dump([1, 2, 4, 7], fileObject)

### Close the file


fileObject.close()

### Import the pickle module


import pickle

### Open the file


fileObject = open("binaryFile.dat", "rb")

### Print out the contents of the file


print (pickle.load(fileObject))

### Close the file


fileObject.close()
4.1.4: Recursion
Recursion occurs when a procedure or function is defined in terms of itself, and calls itself
repeatedly. It is one of the most elegant tools in a programmer's arsenal. Recursion is
useful when an action has to performed on something, and then repeated on a subsection
of the same thing. Searching for an element in a binary tree is a good example, and this will
come up later in this notebook. We define recursive functions by using base case(s) and
general case(s). The general case is the recursive element that calls itself repeatedly; the
base case is a pre-defined condition that is not recursive. The general case recursively calls
itself, until the base case is reached.
It is useful to know that many operations that are performed recursively can be done just
as well with a loop.

1: The general structure of recursion


def recursiveRoutine(parameters):

if condition(parameters): # general case


return recursiveRoutine(someOperation(parameters))

else: # base case


return parameter

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.

2: Example of calculating factorials


A commonly used mathematical function, called a factorial, is an example of recursion. The
factorial (denoted as x ! ) of any positive integer x , is the product of every integer between x
and 1 inclusive. For example, 5 !=5× 4 × 3 ×2 ×1 so 5 !=120.
Here, we define the general case as x !=x × ( x − 1 ) ! and the base case as 0 !=1.
def factorial(x):

if x == 0:
answer = 1

else:
answer = x * factorial(x - 1)

return answer

print("0! =", factorial(0))


print("5! =", factorial(5))

### Demonstrate the same code using a loop


def factorial(x):
answer = 1

for counter in range(x, 1, -1):


answer = answer * counter

return answer

print("0! =", factorial(0))


print("5! =", factorial(5))

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)

### Define a class for animals


class animal():

### define properties for the class


name = ""
height = 0 # in metres :D
sex = ""

### define the methods—this requires the use of a keyword self


def sleep(self):
print (self.name, "is sleeping now.")
# a property of a class can be accessed using
# the self keyword

def eat(self, food_name):


print(self.name, "is eating", food_name, "now.")

### Set an instance of the class


elephant = animal()
elephant.name = "Dumbo" # each property can be accessed
individually (written to here)
elephant.height = 3
elephant.sex = "male"
print(elephant.name, "is", elephant.height, "metres tall.")
# (read from here)
elephant.eat("papaya")
elephant.sleep()

### Set another instance of the class


cow = animal()
cow.name = "Moo" # each property can be accessed individually
cow.height = 1.8
cow.sex = "female"
cow.sleep()
cow.eat("grass")

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

def __init__ (self, name, age):


self.name = name
self.age = age

cow = animal("Moo", 10)

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 = ""

### define the constructor


def __init__(self, name, height, sex):
self.name = name # don't confuse the identical names!
self.height = height
self.sex = sex

### define the other methods—this requires the use of a keyword


self
def sleep(self):
print (self.name, "is sleeping now.")
# a property of a class can be accessed using
# the self keyword

def eat(self, food_name):


print(self.name, "is eating", food_name, "now.")

### Set an instance of the class


elephant = animal("Dumbo", 3, "male") # the initialization is much
shorter indeed
print(elephant.name, "is", elephant.height, "metres tall.")
elephant.eat("papaya")
elephant.sleep()

### Set another instance of the class


cow = animal("Moo", 1.8, "female")
cow.sleep()
cow.eat("grass")

### Define a class for books


class book():
ISBN = ""
title = ""
author = ""

# 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

# define a validation routine, using ISBN 13


# https://isbn-information.com/the-13-digit-isbn.html
def validateISBN(self):
valid = False
while not valid:

valid = (len(self.ISBN) == 13)

if valid:
checkDigit = 0
multiplyDigits = [1, 3]
currentDigit = False

for digit in self.ISBN[0:12]:


checkDigit += int(digit) *
multiplyDigits[currentDigit]
currentDigit = not currentDigit

checkDigit %= 10

if checkDigit != 0:
checkDigit = 10 - checkDigit

valid = (int(self.ISBN[12]) == checkDigit)

if not valid:
self.ISBN = input("The ISBN for " + self.title + " is
incorrect. Please re-enter: ")

engineering = book("9789056995010", "The Art of Doing Science and


Engineering", "Richard Hamming")
intelligence = book("9781493402697", "Mechanical Intelligence", "Alan
Turing") # the actual ISBN is 9781493302697

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 __init__(self, name, height, sex, mother_tongue,


favourite_book): # notice how all parameters are passed
animal.__init__(self, name, height, sex) # set the inherited
properties
self.mother_tongue = mother_tongue
self.favourite_book = favourite_book

### define the other methods—these apply only to humans


def study(self):
print (self.name, "is studying right now.")

def exercise(self):
print (self.name, "is exercising.")

def readBook(self, book_name):


print (self.name, "is reading", ('\"' + book_name + '\"'),
"now" + (" and it is his favoutite book." if book_name ==
self.favourite_book else "."))

### Set an instance of the class


mark = human("Mark", 1.8, "male", "English", "The Art of Doing Science
and Engineering, Richard Hamming")
mark.eat("cake") # access method from parent
mark.sleep()
mark.readBook("Humble Pi, Matt Parker") # access method from child
mark.readBook("The Art of Doing Science and Engineering, Richard
Hamming")
print (mark.name, "speaks", mark.mother_tongue, "at home.") #
access properties from both classes

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.

concatenatedString = "Hello" + ' ' + "World" + '!'


print (concatenatedString)
# In this case, the plus sign (+) is used to concatenate two strings.
What was the other polymorphic operator?
It is possible to define methods to do this, and demonstrate inheritance at the same time.
Consider writing a program about school buses. All of them would require methods to take
in fuel and get the amount of it remaining, so it might be a good idea to define those in the
parent class and then let child classes inherit them. However, different busses might use
different types of fuel (such as diesel, petrol or natural gas) and might require different
methods—this would lead to different identifiers and make the program difficult to use. A
programmer can use the efficiency benefits of inheritance, but still use same identifiers
for buses with different systems using the concept of polymorphism.
The table below summarises the classes that would be produced this way.

Class Parent class Child classes Methods Properties


bus dieselBased accelerate() route
petrolBased decelerate() driverCode
CNGBased
dieselBased bus getFuelLevel() fuelLevel
refuel()
petrolBased bus getFuelLevel() fuelLevel
refuel()
CNGBased bus getFuelLevel() fuelLevel
refuel()
### Define the parent bus class
class bus():

def __init__ (self, route, driverCode):


self.route = route
self.driverCode = driverCode
self.velocity = 0

def accelerate(self, acceleration, time):


self.velocity += acceleration * time

if self.velocity > 60: # speed limits!


self.velocity = 60

self.fuelLevel -= 0.05 * self.velocity * time

def decelerate(self, deceleration, time):


self.velocity -= deceleration * time

if self.velocity < 0:
self.velocity = 0

self.fuelLevel -= 0.05 * self.velocity * time


### Define the child bus classes
class dieselBased (bus):

def __init__ (self, route, driverCode, fuelLevel):


bus.__init__ (self, route, driverCode)
self.fuelLevel = fuelLevel

def getFuelLevel (self):


print ("There are", self.fuelLevel, "litres of diesel left.")

def refuel (self, value):


self.fuelLevel += value
print ("There are now", self.fuelLevel, "litres of diesel in
the tank of.")

class petrolBased (bus):

def __init__ (self, route, driverCode, fuelLevel):


bus.__init__ (self, route, driverCode)
self.fuelLevel = fuelLevel

def getFuelLevel (self):


print ("There are", self.fuelLevel, "litres of petrol left.")

def refuel (self, value):


self.fuelLevel += value
print ("There are now", self.fuelLevel, "litres of petrol in
the tank.")

class CNGBased (bus):

def __init__ (self, route, driverCode, fuelLevel):


bus.__init__ (self, route, driverCode)
self.fuelLevel = fuelLevel

def getFuelLevel (self):


print ("There are", self.fuelLevel, "litres of CNG left.")

def refuel (self, value):


self.fuelLevel += value
print ("There are now", self.fuelLevel, "litres of CNG in the
tank.")

### Demonstrate the classes


busAA002 = dieselBased("AA", "AA001", 0)
busAA002.refuel(10)
busAA002.accelerate(3, 2)
busAA003 = petrolBased("AA", "AA003", 0)
busAA003.refuel(20)
busAA003.accelerate(3, 5)

print (busAA002.driverCode, "is driving at", busAA002.velocity,


"kmph.", end = ' ')
busAA002.getFuelLevel()
print (busAA003.driverCode, "is driving at", busAA003.velocity,
"kmph.", end= ' ')
busAA003.getFuelLevel()

4.1: Computational thinking and problem-solving


4.1.3: Abstract Data Types (ADTs)
An ADT is a collection of data and a set of operations on that data.
ADTs come in two flavours:
• Composite: these are ADTs defined using other built-in data types. For example,
queues are built using arrays of some atomic datatype, and records store
information as various other datatypes.

• 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

stack = [None for index in range (numberOfElements)]


basePointer = 0
topPointer = -1
stackFull = 10
2: Queue
A queue is a list containing several items operating on the first in, first out (FIFO)
principle. Items can be enqueued (added) to the queue and dequeued (removed) from the
queue. It has a pointer that helps keep track of the current element.
numberOfElements = 10

queue = [None for index in range(numberOfElements)]


frontPointer = 0
rearPointer = -1
queueFull = 10
queueLength = 0

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 = ""

### Declare instances


alex = student()
alex.name = "Alex"
alex.no_of_subjects = 2
alex.sex = "male"

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 __init__ (self, data):


self.left = None
self.right = None
self.data = data

def printTree(self):
if self.left:
self.left.printTree() # recursion

print(self.data) # base case

if self.right:
self.right.printTree() # recursion

def insertNode(self, data):


if self.data:

if data < self.data:


if self.left is None:
self.left = node(data)

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

def find(self, value):


if value < self.data:
if self.left is None:
return False
return self.left.find(value) # recursion

elif value > self.data:


if self.right is None:
return False
return self.right.find(value) # recursion

else: # base case


return True
print('i')

tree = node(None)

for i in range(10):
tree.insertNode(i)

print (tree.find(7))
print (tree.find(34))

tree.printTree()

4.1.2 Algorithms for manipulating or reading ADTs


Here we will look at the algorithms used to read from, or write to, ADTs. We will use
global to access global variables within a function definition.

1: Push onto a stack


To push an element onto a stack, we first check whether or not the stack is already full. If it
is not full, we write the new element item to the position [topPointer], and then
increment topPointer by 1; else we print out an error message.
def push(item):
global topPointer

if topPointer < (stackFull - 1):


topPointer += 1
stack[topPointer] = item

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

if topPointer == (basePointer - 1):


print("Stack is empty, cannot pop")

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

if queueLength < queueFull:

if rearPointer < (len(queue) - 1):


rearPointer += 1

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]

if frontPointer == (len(queue) - 1):


frontPointer = 0

else:
frontPointer += 1

queueLength -= 1

return item

5: Finding an item in a linked list


To find an item itemSearch in a linked list, we step through every element while following
the pointers. If itemSearch is found (i.e., itemSearch equals the element at
[itemPointer]), then the search loop terminates; else it searches till it reaches
nullPointer.
def find(itemSearch):
found = False
itemPointer = startPointer

while (itemPointer != nullPointer) and (not found):

if myLinkedList[itemPointer] == itemSearch:
found = True

else:
itemPointer = myLinkedListPointers[itemPointer]

return itemPointer

6: Insert an item in a linked list


To insert itemAdd into a linked list, we first check whether or not the linked list is full. If it
is not full, we insert item at the location heapStartPointer points to. Then make the
element at previously at startPointer point to item, and make item and male
heapStartPointer point to the next free location.
def insert(itemAdd):
global startPointer
if heapStartPointer == nullPointer:
print("Link list full, cannot insert")

else:
tempPointer = startPointer
startPointer = heapStartPointer
heapStartPointer = myLinkedListPointers[heapStartPointer]
myLinkedList[startPointer] = itemAdd
myLinkedListPointers[startPointer] = tempPointer

7: Delete an item from a linked list


To delete an item itemDelete from a linked list, we first check to see if the list is empty,
i.e., if startPointer = nullPointer. If it is not empty, we traverse the linked list to find
the itemDelete. If itemDelete exists, we "delete" it and set the pointers so as to skip
itemDelete. The heapStartPointer is set to point to itemDelete, marking it an empty
element.
def delete(itemDelete):
global startPointer, heapStartPointer

if startPointer == nullPointer:
print("Linked list empty")

else:
index = startPointer

while (myLinkedList[index] != itemDelete) and (index !=


nullPointer):
oldIndex = index
index = myLinkedListPointers[index]

if index == nullPointer:
print("Item", itemDelete, "not found")

else:
myLinkedList[index] = None
tempPointer = myLinkedListPointers[index]
myLinkedListPointers[index] = heapStartPointer
heapStartPointer = index
myLinkedListPointers[oldIindex] = tempPointer

4.1.2: Algorithms for sorting and searching


Computers often have to search vast amounts of data for various purposes. It is also often
critical for them to sort data (in either ascending or descinding order). A search engine
such as Google provides an excellent example of both jobs, though its actual implentation is
understandably much more complex. Suppose, a search query "quickSort" has been typed
into the Google search bar: within fraction of a second, their servers must find
approximately 10 (yes just ten) results out of the trillions of webpages indexed on Google
search, and show them on the first page. Not only does Google search find these needles in
a digital haystack spanning several continents, but it also displays the reults in a very
speficic sequence. This should give you an appreciation of the importance and ubiquity
searching and sorting algorithms.
Here we will meet [simple] common algorithms for sorting and searching.
References (YouTube videos):
• [Link] A fun and intuitive introduction to different sortingm ethods from TED-ed.
• [Link] Sorting and Big O notion from Tom Scott.

0: Generate random numbers


We would need to generate lists of random numbers to test our code. Here is the function
to do that.
from random import randint # Import the function to generate random
integers

## Subroutine to generate random list of unique integers


def generateRandom(n):

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

for i in range (len(listIn)):

for j in range (len(listIn) - 1):

if listIn[j] > listIn[j + 1]:


key = listIn[j]
listIn[j] = listIn[j + 1]
listIn[j + 1] = key

# 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

for i in range(1, len(listIn)):


key = listIn[i]
j = i - 1
while (j >= 0 and key < listIn[j]):
listIn[j + 1] = listIn[j]
j = j - 1
listIn[j + 1] = key

# 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

elif x < listIn[middleIndex]:


return binarySearch(listIn, lowerBound, middleIndex - 1,
element)

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))

### Binary search using a loop


def binarySearch(listIn, element):
upperBound = len(listIn)
lowerBound = 0
found = False
index = -1

while (not found) and (lowerBound != upperBound):


index = int((upperBound + lowerBound) / 2)

found = (element == listIn[index])

if element == listIn[index]:
return index

if element > listIn[index]:


lowerBound = index + 1

if element < listIn[index]:


upperBound = 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))

Efficiency of sorting and searching algorithms


The efficiency of a sorting algorithm may be roughly encapsulated by its Big O Notation,
written as O ( f ( n )) for an algorithm where f is a function of the number of elements n . The
algorithm then takes f ( n ) passes for n elements on average.

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.

You might also like