Idsa Notes
Idsa Notes
Dr Richard Klein
Course Outline 7
2 C++ Fundamentals 21
2.1 Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.2 Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.3 Setting up your environment . . . . . . . . . . . . . . . . . . . . 25
4 Linked Lists 39
4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.2 When arrays aren’t good enough . . . . . . . . . . . . . . . . . . 40
4.3 Linked Lists – forward_list . . . . . . . . . . . . . . . . . . . . 42
4.4 Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4.5 Iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
3
4 CONTENTS
5 Stacks 57
5.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
5.2 Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
5.3 Implementations . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
5.4 std::stack<T, C> . . . . . . . . . . . . . . . . . . . . . . . . . . 60
5.5 Applications . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
5.6 Lab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
5.7 Project: Solving Sudokus . . . . . . . . . . . . . . . . . . . . . . 64
5.8 Additional Reading . . . . . . . . . . . . . . . . . . . . . . . . . . 64
6 Queues 65
6.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
6.2 Operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
6.3 Implementations . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
6.4 std::queue<T, C> . . . . . . . . . . . . . . . . . . . . . . . . . . 68
6.5 Project – The Shortest Path in a Maze (BFS) . . . . . . . . . . . 68
6.6 Additional Reading . . . . . . . . . . . . . . . . . . . . . . . . . . 74
8 Balanced Trees 87
8.1 AVL Trees . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
9 Binary Heaps 89
9.1 Lab: Binary Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . 89
10 Sorting 93
5
6 CONTENTS
Course Outline
https://www.youtube.com/watch?v=kcaMqEbhAcg
Lecturer
Lecturer: Dr Richard Klein
Office: UG12, TWK Mathematical Sciences Building
Email: richard.klein@wits.ac.za
COVID-19
Due to COVID-19, this course will be presented entirely online. This online book
will be updated regularly with new text, videos, and other resources. There will
be several quizzes (usually weekly). There will also be labs and projects. These
will be made available via Moodle and all submissions should be done on Moodle.
I believe that a very important aspect of learning is interacting with
peers/tutors/lecturers. Keeping this social element alive during online teaching
is difficult, but I encourage you to interact regularly on Discord and Moodle.
Academic integrity is of absolute importance. Communication during quizzes,
sharing of answers or practicals, and all forms of plagiarism are taken very
seriously by the university and will result in failing the course (FCM) and/or
being reported to the legal office. In most cases, I encourage you to help each
other with problems but be aware of the line between helping someone and
giving them the answer. A good rule of thumb: if I am struggling, I can show
someone a section of my code for assistance, but I cannot look at theirs. We
will be monitoring Moodle logs closely and will be checking all submissions for
plagiarism.
Timetable
In the timetable, there is a double lecture and triple lab for IDSA each week:
• Tuesday: 10:15-12:00
7
8 CONTENTS
• Thursday: 14:15-17:00
You will be assigned a tut group and each week you must meet your tutor for
a live audio/video call. The tutors will be able to answer any theory/practical
questions that you may have during this session. These tutorial sessions are
compulsory and will contribute to Satisfactory Performance. They will take
place on Thursday afternoons during the lab session.
Communication
All official communication will be posted to the announcements forum on
Moodle. Moodle will email these announcements, but it is still your responsi-
bility to ensure that you’re receiving these announcements.
For live chat, please use the discord server which you can subscribe to here:
https://discord.gg/yWRjJmgsGa. I suggest that you use the discord server
in place of student WhatsApp groups which are There are desktop, web and
mobile apps available for the various platforms. The tutors and I will be online
as often as possible.
In general, please try to use public channels to speak with me and the tutors
unless it is about code that you shouldn’t be posting publicly - it is useful for
others to be able to see the answers to your questions. Please do not email
general questions to me unless it only relates to you personally or is sensitive in
some way. I am always happy to help where I can, but avoiding duplication is
important in large classes like this one. I will always respond to discord queries
before emails.
CONTENTS 9
Consultations
Due to the number of students in this course, it is really important to book time
with me if you would like a live one-on-one consultation. You can book time
slots directly into my calendar using https://calendly.com/kleinric/idsa. You
can book up to 7 days in advance and calendly will send you a calendar invite
with a Google Meet or Teams link in it. Please, test that your chosen platform
is working before the meeting. You can invite others to the meeting as well if
your friends have similar questions.
If you have bandwidth or data issues, you can tell Google Meet not to receive
video. If you don’t have any of these issues, feel free to keep your video on, it’s
nice to chat face-to-face and still get to see you, even if we’re not on campus!
Textbook
The structures presented in this course are fairly common and you will find many
resources that cover this content online. I try to avoid prescribing a textbook
for this course because of the associated costs. However, if you can buy a book
or are funded, then I strongly recommend trying to get a copy of:
• Mark Allen Weiss, Data Structures and Algorithm Analysis in
C++ (Fourth Edition), Pearson, 2014 (Weiss, 2014)
I also recommend the following books:
• M Goodrich, R Tamassia, D Mount, Data Structures & Algo-
rithms (Second Edition), Wiley & Sons, 2011 (Goodrich et al.,
2011)
• N Dale, C Weems, T Richards, C++ Plus Data Structures (Sixth Edition),
Jones & Bartlett Learning, 2018 (Dale et al., 2018)
The best, most comprehensive book about algorithm analysis is Cormen et al.
(2009) and is prescribed by almost every major Computer Science course worth
attending. This book is advanced and covered every major algorithm. It is also
prescribed in honours, so might be worth getting if you are planning to stick
around:
• TH Cormen, CE Leiserson, RL Rivest, and C Stein. Introduc-
tion to Algorithms. MIT press. 2009. (Cormen et al., 2009)
For a book on introductory programming in C++ see:
• Bjarne Stroustrup, Programming – Principles and Practice Using C++
(2nd Edition), Addison-Wesley, 2014 (Stroustrup, 2014)
Other Resources
Where relevant I will post links to other resources as well. I encourage you
to use Google, StackOverflow and YouTube generally. More specifically MIT
10 CONTENTS
Grading
The entire course will take place online. There will be no in-person assessments
in 2021. The mark breakdown is as follows:
Item Weight
Participation* 4%
Projects 20%
Labs 13%
Quizzes 13%
Exam (Lab) 25%
Exam (Written) 25%
Tentative Schedule
Block 3
Week Topics
1 C++ Revision: abstraction, functions, recursion, static/dynamic memory allocation, pointers an
2 Classes
3 Arrays & Vectors
4 Linked Lists
5 Linked Lists
6 Stacks & Queues
7 Stacks & Queues
CONTENTS 11
Block 4
Week Topics
1 Complexity Analysis
2 Binary Search Trees
3 Balanced Trees (AVL Trees)
4 Priority Queues & Binary Heaps
5 Sorting Algorithms (Bubble, Insertion, Selection, AVL and Heap Sorts)
6 Sorting Algorithms (Mergesort & Quicksort)
Satisfactory Performance
This outline extends the general undergraduate computer science course out-
line. At least 80% of labs and quizzes must be completed. There are both lab
and theory components to this course, you must achieve at least 35% for both
components to pass the course, regardless of your average.
12 CONTENTS
Chapter 1
13
14 CHAPTER 1. DATA STRUCTURES & ALGORITHMS
that the runtime is quadratic because the function that describes it is quadratic
in the size of the input. So the time complexity is quadratic.
In the second case, we would try to do the same thing, but rather than modelling
the runtime, we would need to model the amount of RAM that is needed for
the algorithm to finish. For example, if an algorithm requires 𝑔(𝑛) = 42𝑛 + 3
bytes of memory, then we would say that the memory requirements are linear
because as the size of the input grows, the amount of memory we need grows
linearly. The space complexity is linear.
https://www.youtube.com/watch?v=b_Tr25QDnpY
Well… it depends on the values given to the algorithm. If the input list is
[1, 2, 3, 4, 5, 6, 7, 8, 9] and the search value is 42, then we will do 𝑛 comparisons
(where 𝑛 is the length of the list). This is called the worst-case because it
causes us to do the most work for inputs of size 𝑛. On the other hand, if the
search value was 1 when we would only do a single comparison and then the
function would return. This is called the best-case because it causes us to do
the least work for inputs of size 𝑛.
We can also consider something called the average case. Here, we use the
statistical definition of expectation and we calculate the total amount of work
we expect to do by weighting it by the probability of us actually having to
do that amount of work. For example, a QuickSort might make 𝑎𝑛2 + 𝑏𝑛 + 𝑐
comparisons to sort a list in the worst case, but if we look really carefully, this
can only actually happen in one or two of the 𝑛! permutations of the list. So
even though the worst case is quadratic, it is so rare that the average case is still
really really good! We will look at QuickSort in detail later on in this course.
We leave average case analysis to second and third years, and we will only focus
on the best and worst-case analysis of the algorithms considered in this course.
Note: The best and worst cases have NOTHING to do with the size
of the input, only the configuration. An input of length 0 is NOT
1.3. WHEN DO WE ACTUALLY CARE? 15
This also means that we shouldn’t get too bogged down in counting every single
thing. Consider this code below:
def pow(x, n):
out = 1.0
i = 0
while i < n:
out = out * x
i = i + 1
return out
So in the first two lines we assign values to out and i, after that our loop runs.
We do 𝑛 comparisons where we enter the loop and on comparison 𝑛 + 1, 𝑖 ≮ 𝑛
and the loop stops. In side the loop, we perform the calculation and then assign
it to the variable. This happens each of the 𝑛 times that we enter the loop. So
in total:
𝑡𝑖𝑚𝑒 = 2𝑎 + 𝑏(𝑛 + 1) + (𝑎 + 𝑐)𝑛 + (𝑎 + 𝑑)𝑛 (1.1)
= 2𝑎 + 𝑏𝑛 + 𝑏 + 𝑎𝑛 + 𝑐𝑛 + 𝑎𝑛 + 𝑑𝑛 (1.2)
= (𝑏 + 2𝑎 + 𝑐 + 𝑑)𝑛 + (2𝑎 + 𝑏) (1.3)
= 𝑒𝑛 + 𝑓, (1.4)
where 𝑒 and 𝑓 are some constants that are very specific to my hardware and
operating system and are not useful to anyone else. Ever. The important
thing here is that this is a linear function and that it will be linear on every
machine, everywhere, forever. So we try to pick a dominant operation and count
how many times it is performed. We’ll see that this is usually a comparison,
assignment or pointer operation.
So when we do this type of analysis, we want to compare the shape of the graphs
and ignore the constants that are specific to my machine. We are simply asking:
as 𝑛 becomes very very large, how do the runtime and memory requirements
of the algorithm grow? We like to think of the complexity belonging to a class
such as constant, linear, quadratic, cubic, logarithmic, exponential, linearithmic,
factorial etc.
We write these as:
1.3. WHEN DO WE ACTUALLY CARE? 17
Class Notation
Constant 𝑂(1)
Linear 𝑂(𝑛)
Quadratic 𝑂(𝑛2 )
Cubic 𝑂(𝑛3 )
Logarithmic 𝑂(𝑙𝑜𝑔(𝑛))
Exponential 𝑂(2𝑛 )
Linearithmic 𝑂(𝑛𝑙𝑜𝑔(𝑛))
Factorial 𝑂(𝑛!)
This is called Big-Oh notation. If 𝑓(𝑛) ∈ 𝑂(𝑔(𝑛)) this means that 𝑔 multiplied
by a constant is an upper bound of 𝑓, when 𝑛 is big enough. Don’t panic. We
will look at the maths formally later on once you have done a bit more work
in DCS. For now just remember: if we say that an algorithm takes 𝑂(𝑛) time,
then the runtime is bounded above by a linear function.
We also have Big-Ω (Big Omega), which means it is bounded below: if we say
the algorithm takes Ω(𝑛) time, then the runtime is bounded below by a linear
function. This means we do at least a linear amount of work. Finally, we have
Big-Θ (Big Theta), which means that it is bounded above and below. So Θ(𝑛)
is exactly linear function.
Don’t panic about these definitions yet. We’ll look at them carefully later on
and we’ll talk about the maths when we need it. For now, we will only think
about Big-Oh.
In the pow function above, we saw that the amount of work that we did (number
of operations) is a linear function of the size of the input, 𝑛. We can write this
in Big-Oh notation as 𝑂(𝑛).
In the search function above, we had a best and a worst-case. In the best case,
the number we’re looking for is at the front. So in the best case, we only ever
do 1 comparison. This means that regardless of how big 𝑛 is, we always do
a constant amount of work. So in the best case, search takes 𝑂(1) time. In
the worst case, when the number wasn’t there, we had to compare the search
value to every number in the list. This means that we need to do 𝑛 comparisons
because there were 𝑛 numbers. So the amount of work is linear and we can
write 𝑂(𝑛).
Have a look at the graphs on Big-Oh Cheat Sheet. See how the different classes
grow at different speeds as 𝑛 increases. Using the graphs, try to order the classes
in the table above from best to worst.
18 CHAPTER 1. DATA STRUCTURES & ALGORITHMS
1.6 Conclusion
By studying and analysing various structures, and the algorithms that manage
them, we can learn to write code that is not just correct, but also efficient
and scalable. This is a core aspect of Computer Science and you will study
Algorithmic Analysis and Complexity Theory in the first, second and third years,
as well as honours. In IDSA we will study, analyse and implement the data
structures. We’ll consider time and space complexity and we’ll see how using
20 CHAPTER 1. DATA STRUCTURES & ALGORITHMS
these structures can change the efficiency of the programs that we implement.
In DCS you will learn a number of techniques from discrete mathematics, and
you will learn a number of different proof techniques. Next year, in Analysis
of Algorithms (AA), you will put the two together and you will learn to prove
mathematically that algorithms are correct and that they are efficient. In third
year you will start to look at Advanced Analysis of Algorithms where you learn
about different problem classes themselves and you will learn to prove that an
algorithm is optimal by finding lower bounds on the amount of work that needs
to be done. In Honours, you’ll learn more advanced algorithm and analysis
techniques and you’ll be able to analyse recursive and stochastic algorithms.
This is a core aspect of what it means to be a computer scientist and I suggest
that you invest in IDSA and DCS as much as possible so that you have a strong
foundation to build on in future years.
1.7 TODO
Sorry, this introduction was very text-heavy, as we move forward there will be
significantly more demo videos and explanations throughout the text.
1. Make sure that you registered in the Moodle course for IDSA.
2. Install Discord, join the server and say hi!
3. Download the “C++ Revision Notes” pdf from Moodle and ensure that
you’re comfortable with the concepts discussed there. If you’re unsure
about anything, please post on Discord and we can discuss things. You
will also be able to discuss these ideas with your tutors during the first
online tutorial session.
4. There will be a lab on Thursday. Make sure that you have set up your
local C++ programming environment.
Chapter 2
C++ Fundamentals
2.1 Classes
In C++ (and all programming languages that support Object Orientation) we
are able to create our own types by connecting many of the primitive types
together. The normal primitive types are the ones that come with C++ and
are built into the language – such as int, float, char, double and pointers.
Note that a vector and all the other types that you #include into your files are
usually classes and not primitive types. They have been added to C++ through
the Standard Template Library.
A vector is a good example of why we might like a class. We will study them in
more detail later on, but fundamentally, vectors store an integer that tracks the
number of items being stored, it tracks the amount of memory that has been
reserved and it keeps a pointer to that memory. It is useful to bundle these 3
variables into a single object that we can think of as a vector. We can then
perform operations on that vector, such as push_back() or size(). When we
group a number of types together into a single new type, this is called a Class.
A class is a new type of variable – for example, Car is a Class, but a specific
instance of a Car is an object.
class Car{
public:
int num_wheels, num_doors;
string colour;
int coolness;
Car(){
// This is a constructor, which runs when the car is created
// It should allocate memory and setup variables.
num_wheels = 4;
21
22 CHAPTER 2. C++ FUNDAMENTALS
num_doors = 4;
coolness = 10;
}
~Car(){
// This is a destructor, which runs when the car is destroyed
// It should free memory and clean up any mess that it made (opened any files etc
}
float price(){
// This is a member function
if(colour == "Pink"){
return 100*coolness + 10*num_wheels;
}else{
return 10*coolness + 5*num_wheels;
}
}
};
void main(){
Car richardsCar;
richardsCar.num_wheels = 5; // We have a spare wheel
richardsCar.colour = "Pink"; // Because I can
richardsCar.coolness = 1000000; // Obviously
Car stevesCar;
stevesCar.num_wheels = 3; // We've lost a wheel or two
stevesCar.colour = "Blue";
stevesCar.coolness = 1;
In the example above, Car is a class, it tells the compiler that we can have
variables of type Car. richardsCar on the other hand, is an instance of a Car
and we would say that richardsCar is an object. We see that the Car type has
some members called num_wheels, num_doors, colour and coolness. These
are values that are associated with each car that we create. In the example
above we see that richardsCar and stevesCar are both instances of Cars and
therefore each one has its own version of num_wheels, coolness etc.
Classes can contain more than just data. They can also contain member func-
tions. These functions belong to the class and usually operate on the data stored
inside the class. For example, we have a function called price. This function
only makes sense in the context of an actual instance of a Car. So we could ask
“How much would you pay for richardsCar or stevesCar?” but you wouldn’t
ever ask “How much would you pay for Car?”
2.1. CLASSES 23
In C++, classes are just like normal variable types. We can pass an instance of
a car into a function by value or by reference. We can also get the address of
that object as well.
class Car{...};
void main(){
Car richardsCar;
richardsCar.num_wheels = 5; // We have a spare wheel
richardsCar.colour = "Pink"; // Because I can
richardsCar.coolness = 1000000; // Obviously
Car stevesCar;
stevesCar.num_wheels = 3; // We've lost a wheel or two
stevesCar.colour = "Blue";
stevesCar.coolness = 1;
// Paint takes pointers to the object and we need to pass the address.
paint(&richardsCar, "Neon Pink"); // Richard's car is now Neon Pink.
paint(&stevesCar, "Jungle Grey"); // Steve's car is now Jungle Grey.
There are many finer details to objects and Object Orientation as a program-
ming paradigm. We can do fancy things like abstract/virtual classes, interfaces,
inheritance and polymorphism which allow us to create modular, reusable code.
This is the main standard for programming in industry and you should learn
as much about this paradigm as you can. You will learn about these concepts
more formally in second and third-year courses.
For now, the important thing to remember about classes is that they allow us
to build abstractions – you don’t know how a vector or a car works, but you
know what member functions you can call and the documentation should tell
you what those functions do. We don’t need to know how the class achieves
those goals, just that the class will make sure it works. When you used a vector
in IAP, you knew that you could use myVector.push_back(thing) even though
you didn’t know how that function manipulated the underlying memory. The
class abstracts away, or hides, the implementation details from you and you can
focus on solving the actual problem that you care about.
However, we can’t completely ignore what the class is doing. Just because
we’re making a single function call, doesn’t mean that the object is not doing
a lot of work. These functions may contain loops, they may perform various
memory allocations and could do any amount of work that we don’t know about.
It is important to know that the way we are using classes is efficient. This is
why we will look at a number of different data structures (like vectors) and
algorithms (like push_back) to understand their computational complexity.
This will allow you to use the right structure for the job and you’ll know that
your code will still be efficient and scalable.
Here is another detailed description of classes and how they work:
https://www.youtube.com/watch?v=-re_xRIonSs
There are also some more explanations if you are still not comfortable with
classes:
1. https://www.youtube.com/watch?v=2BP8NhxjrO0
2. https://www.youtube.com/watch?v=ABRP_5RYhqU&t=71s
3. https://www.youtube.com/watch?v=-IrueTrxNHA&t=202s
Additional Reading:
1. Chapter 1.5 (the basics covered here) – Goodrich et al. (2011)
2. Chapter 2 (more advanced) – Goodrich et al. (2011)
3. Chapter 1.4 – Weiss (2014)
4. Chapter 2.3, 2.4 – Dale et al. (2018)
Goodrich et al. (2011) cover this topic particularly nicely.
2.2. POINTERS 25
2.2 Pointers
Pointers are discussed in some detail in Section 10 of the “C++ Revision Notes”
on Moodle. Please ensure that you’ve gone through those notes in detail. We
will revisit the idea of pointers and dynamic memory allocation multiple times
in this course.
I strongly recommend that you read through these sites and examples as well:
• https://www.geeksforgeeks.org/pointers-in-c-and-c-set-1-introduction-
arithmetic-and-array/
• https://www.geeksforgeeks.org/pointers-c-examples/
Additional Reading:
1. Chapter 1.5 – Weiss (2014)
2.3.1 Linux
While connected to the internet, open a terminal and run:
sudo apt install build-essential git qgit git-gui meld qt5-default qtcreator libsdl2-* libclang-c
You have all the software you need to complete this course.
2.3.2 Windows/MacOS
MinGW is a version of g++ for Windows. You can download MinGW-64 di-
rectly from http://mingw-w64.org/doku.php. This will give you a terminal and
technically everything that you need to compile. MinGW can also be installed
as part of QtCreator (see the steps below).
If you would prefer to use CLion there is a tutorial here: https://www.jetbra
ins.com/help/clion/quick-tutorial-on-configuring-clion-on-windows.html. Wits
has licences for all the JetBrains products, just sign up for an account with your
Wits email address.
Installing Qt:
• Download the online installer: https://www.qt.io/download-thank-you?o
s=windows or https://www.qt.io/download-thank-you?os=macos or the
offline installer from https://courses.ms.wits.ac.za/software/Qt.
26 CHAPTER 2. C++ FUNDAMENTALS
• You need to sign up for an account, unfortunately. This is new and I don’t
like it, but there isn’t really anything else to do.
• When selecting components, select:
– Qt 5.15.0 → MinGW 8.1.0 64-bit (Qt that was compiled with
MinGW). If you’re using the offline installer then the version is
5.12.x. The exact compiler version will likely change, but as long as
it supports C++11 it will be fine.
– Developer and Designer Tools → MinGW 8.1.0 64-bit
(MinGW Compiler - g++ for Windows) + the two defaults.
– Watch out that this could be a pretty large download. Unfortunately,
I’m not sure how big it is though. Alternative below…
• You can open the .pro files that I supply with the labs.
https://www.youtube.com/watch?v=uMjYgNC1s2c
27
28 CHAPTER 3. ARRAYS & VECTORS
Remember
• There are two spaces where we can allocate memory: The stack and the
heap.
• Automatic variables are allocated on the stack.
• Dynamic memory is allocated on the heap.
• Frames are like Pancakes: You add to the top of the stack, you remove
from the top of the stack.
• The call stack holds a frame for every function that gets called. The
frame contains space for all the automatic variables inside the function.
• When the function finishes, the frame gets popped off of the call stack. We
always remove it from the top of the stack. All the automatic variables
are automatically destroyed.
• Dynamically allocated memory is controlled by us and we are in charge of
returning it to the operating system when we’re finished.
• Dangling points point to memory that we no longer own. Don’t derefer-
ence them!
1 We do something like 𝑎𝑛 + 𝑏 steps, 𝑛 times. We’ll look at this case in more detail later.
3.3. CONTIGUOUS MEMORY & POINTER ARITHMETIC 29
// Dynamic Allocation
int* myListC = (int*) malloc(n*sizeof(int)); // C [Not typesafe]
int* myListCpp = new int[n]; // C++
Figure 3.1 shows the myList pointer which contains the address of the first
object stored in the array. In this case, the first object is stored at 0x04 and
each integer takes up 32 bits (4 bytes).
Because every object/block in the memory is the same size, we can calculate
the address of an item at index 𝑖 by taking the address of the first item in
the array and adding 𝑖 ∗ datatype size to it. For example, to get the address of
myList[4] we would say myList + i*sizeof(int). However, as the compiler
knows what type of pointer we are dealing with, it already knows to multiply
the index by the size of the data type. It does this automatically, so we rather
just say myList + i and the compiler will do the extra work for us. To get the
value we simply dereference this new pointer.
This process is called pointer arithmetic.
Here are some examples of how to increase the size of an array and how to
analyse the amount of time taken to do so.
https://www.youtube.com/watch?v=ssWdVH2wodo
You’ll notice in the code and examples above that all the memory used is dy-
namically allocated – we always used the new keyword. Unfortunately, when you
call a function, the stack frame size for that function’s automatic memory needs
to be known upfront. This means that it is impossible to resize an array that
was allocated on the call stack – doing so would require that we resize the stack
frame for the function. The compiler needs to know the size of these frames
at compile-time. So when working with arrays allocated on the stack, you are
stuck with the size that was used when the code was compiled.2
Similarly, there is no way to manually free the memory that was allocated on the
stack, and the runtime system will only reclaim that memory once the function
is finished.
2 In C++ dynamically sized arrays are not allowed on the stack. However, the g++ compiler
does allow this by changing the size of the stack frame, but it is not part of the C++ standard
and this only works in very specific circumstances. This is a feature of C99 (not C++) called
Variable Length Arrays that g++ decided to implement. You should avoid using it because
other compilers won’t necessarily support it. Also, the stack is usually not big enough for
large arrays and these can cause stack overflows – large arrays should never be allocated on
the call stack. Every time I compile your code on Moodle, I explicitly tell g++ to disable the
VLA extensions.
32 CHAPTER 3. ARRAYS & VECTORS
Both of these classes provide size to fetch the number of items, operator[](i)
to fetch the value stored at index i without bounds checking, at(i) to fetch the
value stored at index i with bounds checking and many other functions. Both
containers use contiguous memory and pointer arithmetic for their underlying
storage.
Because std::array allocates its memory on the stack, you need to know the
size at compile-time. On the other hand, std::vector represents a dynamically
allocated array where the storage is on the heap. This means that it can real-
locate, expanding or shrinking its memory as necessary. A vector<int> would
look similar to the following declaration:3
class vector{
int* data; // Pointer to storage buffer
size_t n_items; // Number of items stored
size_t n_allocated; // Amount of memory allocated
}
The resulting memory layout would be as shown in 3.2. Note that the vector’s
member variables (data, n_items, n_allocated) are allocated on the stack,
but the actual storage is dynamically allocated on the heap.
implementation-dependent.$
3.7. QUESTIONS 35
Pushing a new item to the back of the vector has two cases. In the best case,
there is already space available to perform the insert and the value is just copied
into the memory buffer. This takes constant time – 𝑂(1). In the worst case,
there is no extra space in the buffer and the vector needs to reallocate memory,
copy all the items across, and then free the old memory. In this case, we have
𝑛 items and each item gets copied once. Therefore we do 𝑂(𝑛) operations.
Similarly, when popping from the back of the vector we have two cases again.
In the best case, we aren’t wasting too much space and so we don’t have to
reallocate. So the vector simply removes the item from the buffer in constant
time – 𝑂(1).5 In the worst case, we decide that we are starting to waste too
much memory and we reallocate, which makes 𝑛 copies again taking us linear
time – 𝑂(𝑛).
These worst cases seem bad, but by choosing those reallocation thresholds very
carefully, we can mathematically prove that over 𝑛 insertions and/or deletions
we still only do a linear amount of work. This is called Amortised Analysis and
we say that these functions take Amortised Constant Time. You will learn to
do this type of analysis in fourth-year and it shows that even with reallocations,
the vector is very efficient.
https://www.youtube.com/watch?v=dUbrk-RHr_0
Note that the time complexity of C++ STL containers and algorithms is part
of the ISO C++ Standard. This means that for a compiler to be standards
compliant, its STL implementation needs to be at least as efficient as men-
tioned by the standard. Have a look at the different functions offered by the
std::vector as well as the associate complexity of each. A reference can be
found here: http://www.cplusplus.com/reference/vector/vector/.
3.7 Questions
1. How would you implement a push_front(int value) function for a
std::vector?
5 In a real vector, the STL uses ‘placement new’ to construct objects, so when it manually
destructs the objects when necessary. This is the only time you should ever manually call a
destructor. This is more sophisticated C++ than we’ll cover in this course, so for our purposes,
we’ll consider that the objects are simple and can just be overwritten – like an int.
36 CHAPTER 3. ARRAYS & VECTORS
1. Are there best and worst cases, or does it always have the same
asymptotic complexity?
2. What is the complexity?
2. How would you implement a push_at(int value, int idx) function for
a std::vector?
1. Are there best and worst cases, or does it always have the same
asymptotic complexity?
2. What is the complexity?
3. You would like to create a custom vector object. You don’t ever need the
push_back function but you do need a push_front function. How would
you implement it and what would the complexity be?
4. What is the purpose of the begin and end functions of the std::vector
and std::array?
5. Why is the return type of vector<int>::operator[] and vector<int>::at
an int& rather than an int?
6. What does the reserve function do?
7. What does the resize function do?
8. When would you prefer a std::array over a std::vector?
9. Exactly how many items are in the vector after the following code com-
pletes? [Try to do this without running the code.]
vector<int> v;
for(int i = 0; i < n; ++i){
v.push_back(i);
}
10. Exactly how many items are in the vector after the following code com-
pletes? [Try to do this without running the code.]
vector<int> v;
for(int i = 0; i < n*n; ++i){
v.push_back(i);
}
11. Exactly how many items are in the vector after the following code com-
pletes? [Try to do this without running the code.]
vector<int> v;
for(int i = 0; i < n; ++i){
for(int j = 0; j < n; ++j){
v.push_back(i);
}
}
3.8. ADDITIONAL READING 37
12. Exactly how many items are in the vector after the following code com-
pletes? [Try to do this without running the code.]
vector<int> v;
for(int i = 0; i < n; ++i){
for(int j = 0; j < i; ++j){
v.push_back(i);
}
}
13. Exactly how many items are in the vector after the following code com-
pletes? [Try to do this without running the code.]
vector<int> v;
int i = n;
while(i > 0){
v.push_back(i);
i /= 2; // Hint: Integer division
}
Linked Lists
4.1 Introduction
4.1.1 Arrays
https://www.youtube.com/watch?v=HFJIXfTbxxU
So far we’ve used arrays to implement lists of objects. For example, if we wanted
a list of 6 integers, we would use the following code:
int myList[6]; // Allocated on the Stack
int *myList = new int[6]; // Dynamically Allocated on the Heap
On the first line, space for the array will be reserved on the call stack when the
frame for this function is created. Because the array will be inside the frame,
we need to know the size of the array when the frame is allocated. This means
that you can’t have a function that reads in a number and then uses the stack to
allocate an array of that size, because the stack frame has already been created.1
On line 2, new[] will ask the operating system for 6×sizeof(int) bytes from
the Heap or Freestore. The operating system will find a convenient place in
RAM to reserve this memory and will return the address of the first object in
the array. We store the address inside the pointer myList. We may be left with
the following memory layout:
Once the operating system has allocated the block of memory, the size of the
block cannot be changed. If we suddenly decided that the array isn’t big enough,
we would need to make a new, bigger array and then copy all the items that we
need across. After copying the 𝑛 items to the new memory, we would have to
free the old one. For example:
39
40 CHAPTER 4. LINKED LISTS
Note: Even though the realloc function from C99 changes the size of a mem-
ory block/array, in practice, it can only make the array longer if there is free
space after the original one. If there is no free space, it will allocate new memory
and copy the contents across – just as shown above.
4.1.2 Questions
1. If you have an array of size 𝑛 that needs to be increased to size 2𝑛, how
many copies must your program make?
2. If it takes 3 time steps to copy a single number, how long does the copy
take?
3. If the list of 2𝑛 items fills up and we now need to increase the size to 3𝑛,
how many copies will the program have done in total?
Let’s say that we have a program that must read numbers into an array until
it reads −1. We have no idea how much memory to allocate at the start of
our program. If we try to allocate too much it might fail because the operating
system can’t find enough contiguous space.2 If we allocate too little, we might
waste a lot of time copying items to new arrays every time it fills up. If we had
to reallocate 4 times, then it means the first item added to the list needs to be
moved 4 times as well. It would be useful if we could come up with some data
structure that allows us to change the size of the list without any major cost
in both time and memory requirements (space).
Suppose you have a program where there is an array of 100 integers. I need you
to insert a number at the beginning of the list and each number shifts over
by one. The last number in the list is removed. To do this, you first need to
move all the numbers one item to the right - list[98] moves to list[99] etc.
When you have moved all the numbers in the list, you are now free to add the
new item to list[0].
Suppose you have a program where you need to delete the 𝑖𝑡ℎ number in an
array. While we can fetch the number in constant time3 all the numbers in the
array need to be consecutive in memory. If we just removed the number and
left everything where it was, we’d leave our array with a hole in the middle –
this would break the array because the objects are no longer contiguous. To fix
this, we need to shift every item from 𝑖 onwards, one block to the left.
4.2.1 Questions
1. If you have an array of 𝑛 items, how many ‘move/copy’ operations must
you do to insert a number at the front of the list?
2. If you have an array of 𝑛 items and you want to delete the item at the
front, how many ‘move/copy’ operations must you do?
3. If you want to insert 𝑛 items into a list that is already 𝑛 items long, exactly
how many ‘move/copy’ operations should you do?
The idea behind this type of structure is to allocate separate pieces of memory
on the heap and then link them together like a chain, using pointers.
struct Link{
int value;
Link *next;
}
The idea is that we simply store a pointer to the first link (sometimes called a
node), and then each node stores the address of the next node. So as long as we
remember where the first item is, we can follow its ‘next pointer’ to get to the
next link in the chain. The last link’s next pointer should contain the nullptr
because there is no next link to point to.
To create the list in Figure 4.3 we might use the following code:
class Link{
public:
4.3. LINKED LISTS – FORWARD_LIST 43
int value;
Link *next;
Link(int v) : value(v), next(nullptr){
/* Initialise value with v and set next to the nullptr */
}
};
class LinkedList{
public:
Link* head;
LinkedList() : head(nullptr){
/* The line above initialises head to be the nullptr */
}
~LinkedList() {
/* Traverse the list and deallocate each link */
}
};
int main(){
LinkedList myList;
44 CHAPTER 4. LINKED LISTS
Notes:
4.4 Operations
https://www.youtube.com/watch?v=4WzVV0BgqEs
We’d like to consider the following operations. Where T is the type of object
stored in the list.
1. void push_front(T value)
2. void pop_front()
3. Link* get_link(int i)
4. T& at(int i)
5. void push_back(T value)
6. void pop_back()
7. void insert(int index, T value)
8. void erase(int index)
9. size_t size()
10. iterator begin()
11. iterator end()
4.4.1 push_front
https://www.youtube.com/watch?v=aAPrQ8ptyuU
To insert an item at the front of the list, we simply create a new link with the
relevant value and set its next pointer to the first item in the list. We then
point the head at the new item.
Show/Hide Code
// Create a new link
Link *tmp = new Link(42);
Analysis: Regardless of how many items there are in the list (𝑛), it will always
take us 1 memory allocation, 3 pointer assignments and 1 pointer dereference to
insert at the front of the list. We don’t care about these constants, we just care
that it will always take the same amount of time, so we say that this operation
takes constant time – 𝑂(1).
46 CHAPTER 4. LINKED LISTS
4.4.2 pop_front
https://www.youtube.com/watch?v=dBDfRd4HfV0
To remove the item from the front of the list, we need to update the head to
point to the next item and then delete the link that was originally at the front.
Note:
1. We need to make sure we still have a pointer to the item, otherwise, we
would have a memory leak.
2. We can’t delete the item and then update head because after deleting the
first item, we wouldn’t be able to access the next pointer. Again we would
have a memory leak.
Show/Hide Code
// Remember the first item
Link *tmp = head;
// Increment the head
head = head->next;
// Free the item
delete tmp
Analysis: Regardless of how many items there are in the list (𝑛), it will always
take us 1 memory deallocation, 2 pointer assignments and 1 pointer dereference
to delete at the front of the list. We don’t care about these constants, we just
care that it will always take the same amount of time, so we say that this
operation takes constant time – 𝑂(1).
4.4.3 get_link
https://www.youtube.com/watch?v=Qo1SlH4Zp_E
To get the link at index i, we need to traverse4 to the correct link in the chain.
This is like when you pick up a chain and move from link to link to link until
you get to the one you want.
Show/Hide Code
// Start at the front of the list
Link *curr = head;
// Until we're at the ith link, go to the next one
for(int c = 0; c < i; ++c){
curr = curr->next;
}
// curr now points to the ith link, return it etc.
return curr;
Analysis: In the worst case we traverse to the end of the list, in which case,
we do 𝑛 − 1 traversal steps (curr = curr->next). This is a linear function, so
we say that we do 𝑂(𝑛) operations.
Beware… get_link is a linear function, so the code below is not linear. What
is the complexity of the following code segment?
LinkedList myList;
//... fill the list with items ...
for(int i = 0; i < n; ++i){
Link *curr = myList.get_link(i);
cout << curr->value << endl;
}
4.4.4 at
https://www.youtube.com/watch?v=S3prNWIvqWQ
To get the value at index i, we need to traverse to the correct link and then
return the value stored inside it. This is easy to implement because we already
have a get_link function.
Show/Hide Code
// Get the link
Link* curr = get_link(i);
// Return a reference to the value
return curr->value;
Analysis: The call to get_link is 𝑂(𝑛) and then there is another 1 dereference
operation. This results in a linear function plus a constant, which is still a linear
function. So the complexity is still 𝑂(𝑛) in the worst case.
4.4.5 push_back
https://www.youtube.com/watch?v=wWjxwD4wz80
To add to the back of the list, you need to traverse to the last link in the
chain. You can do this with a while loop. Traverse until the curr->next ==
nullptr, which means that curr is pointing at the last link. Once you have a
pointer to the last link, it is straightforward to allocate a new link and add it
to the chain.
Show/Hide Code
// Left as an exercise for the reader.
Analysis: For a list with 𝑛 items, push_back will take O(n) operations. How
could you alter the linked list data structure to achieve 𝑂(1) performance for
this function?
48 CHAPTER 4. LINKED LISTS
4.4.6 pop_back
https://www.youtube.com/watch?v=zQQ9tXaqBJw
To remove the last item in the list, we need to update the next pointer in the
link before the last one. This means that we should traverse to the second last
link in the list, use its next pointer to deallocate the relevant memory, and then
set the next link to the nullptr.
Show/Hide Code
// Left as an exercise for the reader.
Analysis: For a list with 𝑛 items, how many operations does it take to insert
at the end of the list? Would your strategy to improve the performance of
push_back work to improve the performance of pop_back? Why or why not?
4.4.7 insert
https://www.youtube.com/watch?v=dU5nJTRzqr4
To insert at an arbitrary position, 𝑖, in the list, we need to traverse to the link
before 𝑖, i.e. 𝑖 − 1. We then create the new link and set its next pointer to point
to the item currently at position 𝑖. The next pointer at 𝑖 − 1 should now point
to the new link.
Show/Hide Code
// Create the new link
Link *nl = new Link(value);
// Get the link at i-1
Link *curr = get_link(i-1);
// Point to the old link i
nl->next = curr->next;
// Point to the new link i
curr->next = nl;
4.4.8 erase
https://www.youtube.com/watch?v=D7h9eQ2pkTI
To delete the value at an arbitrary position, 𝑖, in the list, we need to traverse to
the link before 𝑖, i.e. 𝑖 − 1. The next pointer at 𝑖 − 1 needs to point to the link
at 𝑖 + 1. This removes the link from the list after that we free the memory for
link 𝑖. We need to be careful to store the address of the link that we’re deleting
4.5. ITERATORS 49
4.4.9 Size
https://www.youtube.com/watch?v=XkJvvm-1e7w
To get the size of the list we simply traverse from the front to the back counting
the number of links until we reach the nullptr.
Analysis: In Big-Oh notation, how many operations will this function take?
How can we alter the structure so that this happens in 𝑂(1)?
4.5 Iterators
4.5.1 What?
An iterator is an object that allows the programmer to traverse a container.
Think of an iterator like a more sophisticated pointer. A C++ iterator5 must
provide two functions:
1. The dereference operator – T& operator*() – which returns a reference
to the current object.
2. The increment operator – operator++() – which advances the iterator to
the next object in the container.
In C++, all containers should have begin and end functions which return it-
erators. The begin function should return an iterator that references the first
item in the container, while the end function should return a ‘one past the end’
iterator – this is an iterator that we would get if we called the increment oper-
ator one last time after hitting the last object. You should never dereference a
one past the end iterator.
For vectors/arrays, pointers satisfy all the requirements of an iterator.
Thing* MyVector::begin(){
return data;
}
Thing* MyVector::end(){
return data+n_items;
}
The dereference (*) operator follows the pointer to the relevant object and
because the objects are all contiguous, the increment (++) operator advances
5 http://www.cplusplus.com/reference/iterator/
50 CHAPTER 4. LINKED LISTS
the pointer from object 𝑖 to 𝑖 + 1. Links in a linked list are not contiguous so
this kind of pointer arithmetic will not work here. We need to build an iterator
that acts in the same way, but knows that when incremented it should follow
the next pointer in the link.
The iterator should store a pointer to a link in the list. The dereference operator
should return a reference to the value in the link. The increment operator
should advance our link pointer to the following link using next. For example:
class LinkedListIterator{
private:
Link* pointer;
public:
// Default constructor: The iterator contains a nullptr
// This represents a one-past-the-end iterator
LinkedListIterator() : pointer(nullptr) {}
// The iterator should store a link to the pointer provided
LinkedListIterator(Link* p) : pointer(p) {}
Thing& operator*(){
return pointer->value; // Return the relevant value
}
LinkedListIterator& operator++{
pointer = pointer->next; // Traverse to the next link in the chain
return *this; // We need to return a reference to this LinkedListIterator
}
};
We can then write the begin() and end() functions for our LinkedList as
follows:
LinkedListIterator LinkedList::begin(){
return LinkedListIterator(head); // Pointer to the first link
}
LinkedListIterator LinkedList::end(){
return LinkedListIterator(); // Pointer one past the last link (nullptr)
}
4.5.2 Why?
The obvious question is then: why do we care about iterators when we know how
to access the items already? The answer is abstraction. By using iterators to
traverse the container, it doesn’t matter what the underlying container is, the
Standard Template Library (STL) is written so that many of the algorithms
only need an iterator to function. This means that as long as your container
provides the correct iterators, then you can use all the built-in algorithms that
come with C++.
4.6. DOUBLY LINKED LISTS 51
Both code segments above are correct and are equally efficient. However, the
code for each looks really different. This is undesirable as it makes the code
harder to read and harder to write. It means that we need to know the type
of container before we can print the values. If we rewrite the loop in terms of
iterators, then the same loop works for both types of container:
// auto tells the compiler to figure out what type the curr and end variables should be
// Thing* for MyVector / LinkedListIterator for LinkedList
for(auto curr = myContainer.begin(), end = myContainer.end();
curr != end;
++curr){
Thing& val = *curr; // Thing&
We now have one for loop that can handle both types of container – if we
decided to swap between Vectors or Linked Lists or any other containers we
wouldn’t need to adjust our printing code. This pattern was so common that it
was officially added into the language as range-based for loops6 which were
introduced in C++11 and extended in C++20. The compiler will translate the
range-based for loop below into the code above before compiling.
for(Thing& val : myContainer){
cout << val << endl;
}
If we would like to be able to access the last link in 𝑂(1) time, then we could
also keep track of a tail pointer which we update whenever necessary. Consider
how this affects the implementation and complexity of all the functions above.
There is no container in STL that corresponds to a forward_list with a tail
pointer.
Consider how all of the functions and analysis in the previous sections would
need to be adjusted for a doubly-linked list. Doubly linked lists will be exam-
inable.
7 http://www.cplusplus.com/reference/forward_list/forward_list/
8 http://www.cplusplus.com/reference/list/list
9 Interestingly, storing the size was only required in C++11. In C++98 the complexity
requirement of the size function was ‘up to linear’. This means that the compilers could
implement it using either approach. When the complexity requirements became 𝑂(1) in
C++11, the compilers were forced to keep track of the size.
4.8. DRAWBACKS OF LINKED STRUCTURES 53
4.8.3 Be Careful
In general, you should be aware that linked structures can be more inefficient
than we expect, with traversals and caching/locality both playing a role. This
translates into other languages as well, such as Lists in Java/Python/JavaScript
etc. These structures have their place, but ensure that the actual behaviour of
the structure on your hardware is as you expect.
Here is a great talk by the creator of C++, Bjarne Stroustrup, where he
compares the run times of Vectors and Linked Lists with surprising results:
https://www.youtube.com/watch?v=YQs6IC-vgmo
10 Remember that we noted earlier that 𝑂(⋅) hides the constants associated with the different
operations, but also assumes that the operations themselves take the same amount of time –
e.g. assignment always takes the same amount of time. Caching breaks this assumption and
in some cases, the memory access patterns may be more important than we’d otherwise think.
54 CHAPTER 4. LINKED LISTS
Here is a more recent video by Computerphile, where they run several experi-
ments to investigate the efficiency of the different structures. https://www.yo
utube.com/watch?v=DyG9S9nAlUM
4.9 Lab
https://www.youtube.com/watch?v=pMcYt5fHvdw
In the next lab, you need to implement your version of a forward_list along
with additional copy and reverse functions. Relevant unit tests and function
descriptions will be provided.
class Thing;
class Link{
public:
Link *next = nullptr;
Thing value;
};
class LinkedListIterator{
public:
Link* ptr = nullptr;
Thing& operator*();
LinkedListIterator& operator++();
};
class LinkedList{
public:
Link* head = nullptr;
LinkedList();
~LinkedList();
size_t size();
Thing& front();
Thing& back();
4.10. ADDITIONAL READING 55
LinkedListIterator begin();
LinkedListIterator end();
LinkedList* copy();
void reverse();
};
Stacks
5.1 Introduction
https://www.youtube.com/watch?v=0hHsu_llV-0
Often we would like to keep track of objects or events in a way that allows
us to access the newest/most recent one first. For example, we have seen the
Call Stack which keeps track of functions and makes sure that the functions are
always ordered from the most recent call (i.e. the function that we’re currently
in) to the oldest call (i.e. the main function). When we call a new function,
its stack frame (memory for automatic variables, program counter/line number
etc.) gets added to this structure and when the function returns, then that
frame is removed from the structure. We are always working on the same end of
the structure: adding and removing items that are the most recent. This type of
structure follows a Last In First Out (LIFO) structure,1 because the last thing
that we added, is the first one that can be removed. This type of structure is
called a Stack and is illustrated in Figure 5.1.
When a new pancake has been cooked we always place the new pancake on
top of the stack. When we remove a pancake,2 we always remove the pancake
which is on top – this means that the pancake we remove is always the one that
was cooked most recently. The last pancake placed onto the stack is the first
pancake we remove from the stack and devour. Beyond this, we can add and
remove pancakes arbitrarily: you’re cooking, I’m eating… You place p1 onto the
stack [p1], then you place p2 onto the stack [p1, p2], then you turn around
and I remove p2 and eat it [p1], you come back and place p3 onto the stack
[p1, p3], then you place p4 onto the stack [p1, p3, p4], then you place p5
onto the stack [p1, p3, p4, p5], then I eat p5 [p1, p3, p4], then p4 [p1,
p3], then p3 [p1]… Then you eat p1, which is probably cold by now anyway.
1 Or equivalently, First In Last Out (FILO)
2 And remember that we only remove 1 pancake at a time because we’re not a glutton.
57
58 CHAPTER 5. STACKS
https://www.youtube.com/watch?v=ya-E2aYI3WQ
Stacks are very useful structures whenever we want to be able to retrace our
last steps. Think of the undo button in any computer program. As you perform
different operations, the program stores each action on a stack. The most recent
thing that you did is on top of the stack and the most recent thing after that is
underneath it. If you hit undo, it will remove the last action that you did from
the top of the stack and do the opposite action. If you hit undo again, it will
remove the second last action from the stack and reverse that action. As you
perform an action it is added to the top of the stack, as you hit undo, the action
on top of the stack is removed and then reversed in the program. Go to your
favourite word processor and play around with the undo button. Think about
what the stack would look like at each point.
https://www.youtube.com/watch?v=NzIfJW2WeGM
There are plenty of other examples of stacks that you use every day. Think
about how the examples below form a stack and how objects would be added
or removed.
1. The back button in your browser.
2. The forward button in your browser. When do pages get added to this
stack?
3. Views/activities/pages in a mobile application (imagine navigating for-
ward and backwards through the settings page of an app).
4. Recursive function calls in a C++ program.
5. Retracing your steps after hitting a dead-end in a maze.
5.2. OPERATIONS 59
You can see in the examples above, we often use stacks to keep track of decisions
that we have made, and then use the stack to retrace our steps if we decide that
we have made a mistake. This is how we can implement a class of algorithms
called backtracking algorithms. You came across this idea previously in IAP
when you discussed how to solve a Sudoku by using a recursive function. That
algorithm used the call stack to keep track of your actions when you realised
you’d made a mistake, the function returned false and the previous function
resumed after undoing the changes you’d made – Figure 5.2. Later on in this
chapter, we’re doing to talk about how to implement a backtracking algorithm
without recursion, by managing the stack ourselves.
There are lots of other problems that can be solved with backtracking algorithms.
Some of these are discussed further here https://www.geeksforgeeks.org/back
tracking-algorithms/.
Before we move on to how we can use stacks, let’s first talk about the interface
of the Stack ADT (Section 5.2) followed by some implementations (Section 5.3).
5.2 Operations
A Stack is a fairly simple ADT. It has only 4 important functions. Note that
there is no operator[] or at function because we are only ever allowed to
access the item on top of the stack. The only way to access other items would
be to pop off items until the one we wanted was at the top. In general, if you
need “random access” to access arbitrary items in the container, then a stack
is probably the wrong data structure and you should consider using something
else.
Assume that T is the type of object stored in the list, e.g. int.
1. void push(T value)
• Add value to the top of the stack.
2. void pop()
• Remove the item from the top of the stack.
3. T& top() or sometimes T& peek()
• Return a reference to the top item.
4. size_t size()
• Return the number of items in the stack.
https://www.youtube.com/watch?v=x9y0I_Horjk
5.3 Implementations
There are two main implementations for a stack, one is based on contiguous
memory, the other is based on a linked list.
https://www.youtube.com/watch?v=WWP5AoBAR60
60 CHAPTER 5. STACKS
using std::stack;
class Thing;
5.5 Applications
5.5.1 Postfix Notation
When we’re doing arithmetic, we use infix notation to represent the operators
and operands of a formula. This means that the operator (+ - x ÷) acts on the
operands which are on either side of it – the operator is in the middle, e.g. a +
b.
In the 1920s a Polish mathematician, Jan Łukasiewicz, invented Polish Notation.
He showed that by writing the operators in front of the operands you no longer
5.5. APPLICATIONS 61
RPN was used in the instruction language of the English Electric computers
in the 1960s. Hewlett-Packard engineers found that by requiring the user to
enter instructions in RPN, the electronics inside their early calculators could be
dramatically simplified. This means that users needed to learn the new notation,
but that the number of components in and therefore the cost of the calculators
could be reduced. In 1968, the HP9100A became the first calculator to use RPN
and is widely considered to be the first desktop computer.
By using RPN, the machine does not need to parse the formula first and in-
termediate values do not need to be written down as brackets are completely
unnecessary. These days, machines are powerful enough that parsing or under-
standing the infix formulae does not cost much computational time. Similarly,
transistors are so small and cheap, the extra hardware required to do this does
not affect the price or size.
Several modern calculators like the HP12C still use this notation and if you take
the Wits Actuarial Science course you are likely using one!
Solving Sudoku
THe kind of search explained above does not always have to happen in a maze
or other ‘physical space’. We can think of each position in a search space cor-
responding to a different state of the world as well. In the maze above, each
decision was one of four directions. When solving sudoku, each decision involves
setting the value of a cell to one of nine numbers. In the maze, we followed a
path until we hit a dead end and couldn’t make any valid turns. When solving
a sudoku, we can make a decision (i.e. place a number in an empty block) and
then check if the state of the board is valid. If so, then we can make that move
and try all boards that lead on from that state. If none of those boards are
solvable, then we must have made an incorrect move and we need to change
that decision (i.e. that number needs to be updated to a different number).
At each decision, we push our action onto the stack. If we later realise that
there are no valid boards from this decision onwards (i.e. we made a decision
that leads us to a dead-end), then we can pop that action off the stack, try the
next action, and then check those boards.
We will formalise this algorithm below, where we consider your first project.
In IAP you saw this code when learning about recursion:
This code is doing the same thing! This code is a recursive implementation of
a Depth First Search. It uses the call stack to keep track of decisions and, as
each function returns, the previous state of the sudoku can be restored.
3 The alternative is to keep track of many paths at the same time where we go wide and
we call this a Breadth-First Search, which we will cover in the next chapter.
5.6. LAB 63
5.6 Lab
You need to implement both types of stack: based on a vector and based on a
linked list.
class Stack{
public:
// Add t to the top of the stack
void push(Thing &t);
// Remove the top item from the stack
void pop();
// Return a reference to the top item in the stack
Thing& top();
// Return the number of items in the stack
size_t size() const;
// Return true/false based on whether the stack is empty
bool empty() const;
public:
vector<Thing> data; // list<Thing> data;
};
You should make use of the relevant functions from the vector (vector<Thing>)
64 CHAPTER 5. STACKS
and linked list (list<Thing>) structures from the Standard Template Library
(STL). Both implementations should look remarkably similar.
Description Grades
Auxiliary Functions. 30%
Solves a 9 × 9 sudoku correctly. 30%
Solves a 16 × 16 sudoku correctly. 10%
Solves a 25 × 25 sudoku correctly. 5%
Time taken to solve a 9 × 9 sudoku. 10% [based on speed ranking]
Time taken to solve a 16 × 16 sudoku. 10% [based on speed ranking]
Time taken to solve a 25 × 25 sudoku. 5% [based on speed ranking]
Queues
6.1 Introduction
In the previous chapter, we discussed the idea of a stack that allowed us to access
the most recent item. This enables us to implement a class of algorithms called
backtracking algorithms because to undo the most recent action, we could check
the top of the stack. In many algorithms, we need to do a similar operation,
but we need to access the first item that was added, i.e. the one that arrived
first. This type of structure forms a Queue. Just like every queue you’ve ever
stood in, we add to the back and remove from the front. A queue is known as
a First In First Out (FIFO) structure.
We often use queues when we need to process objects in the order that they ar-
rive. For example, when you are typing on the keyboard the hardware generates
a keypress event which stores which key was pressed. The operating system
gets notified of the event through an interrupt and it adds it to an event queue
for our program to process. When our program is running, we can ask the OS
to give us each event to process. The events must be given to us in the same
order that they were generated, otherwise, we’d receive mixed up data.
https://www.youtube.com/watch?v=p5ByAjY0NZM
65
66 CHAPTER 6. QUEUES
This is such a common process that when writing games we often create an
event loop that looks something like the code below.1 In the code below, the
operating system will add SDL_Event objects to the event queue. When we
call SDL_PollEvent it will check whether there is an event in the queue, if there
is, it will remove it from the front and place it into e. We can then check what
type of event it is and update the state of our game (e.g. change the position
of the character). Once we have handled the event, our loop repeats and we
check whether there is anything else in the event queue. if there is nothing in
the queue, then we update the state of our game (e.g. did the character collect
treasure in this move?) and redraw the updated image to the screen. Then our
main loop repeats and we check for more input events from the user.
while(!quit){
// This object contains the event information
SDL_Event e;
In the example above, the operating system added events to the back of the
queue and our program removed events from the front. With a stack, we could
only ever access the item at the top. With a queue, we can only ever access
the item at the front. If you need to access another item in the container, then
you’re using the wrong structure.
1 This is based on the SDL framework. If you’re using a high-level framework like Unity or
a language like Java, then the event loop and dispatch will usually be done automatically for
you.
6.2. OPERATIONS 67
Whenever you want to process data in the order that it arrives, then a queue is
the correct data structure to use. Some examples of queues include:
1. Event Queues.
2. Print Queues.
3. Wits in-person registration queues.
4. Asking Richard questions during tutorials.
5. A Breadth-First Search – Section 6.5
Again, just as with the stack, we can implement it using linear data structures
that we have already built: vectors and lists. Although we’ll see that using
vectors directly leads us to an inefficient implementation and in this case if
we want to use contiguous memory, then we should implement something new
called a Circular Array.
6.2 Operations
A Queue is a very simple ADT. It has only 4 important functions. Note that
there is no operator[] or at function because we are only ever allowed to access
the item at the front of the queue. The only way to access other items would
be to dequeue/pop off items until the one we wanted was at the front – except
then we wouldn’t be able to replace those items at the front as we can only add
at the back. In general, if you need “random access” to access arbitrary items
in the container, then a queue is probably the wrong data structure and you
should consider using something else.
Assume that T is the type of object stored in the queue, e.g. int.
1. void push(T value) or void enqueue(T value)
• Add value to the back of the queue.
2. void pop() or void dequeue()
• Remove the item from the front of the queue.
3. T& peek() or T& front()
• Return a reference to the front item.
4. size_t size()
• Return the number of items in the queue.
https://www.youtube.com/watch?v=xt34AwFIi-g
6.3 Implementations
We will consider two implementations of a queue, one is based on contiguous
memory, the other is based on a linked list.
using std::queue;
class Thing;
//Good:
// Queue of Things, underlying container: deque<Thing>
queue<Thing> q0;
// Queue of Things, underlying container: deque<Thing>
queue<Thing, deque<Thing>> q1;
// Queue of Things, underlying container: list<Thing>
queue<Thing, list<Thing>> q2;
//Bad:
// Queue of Things, underlying container: vector<Thing>
// This is a compile error because vector does not have a pop_front
queue<Thing, vector<Thing>> q3;
// Queue of Things, underlying container: forward_list<Thing>
// This is a compile error because forward_list does not have a push_back
queue<Thing, forward_list<Thing>> q4;
These systems work on a structure called a graph, which you’ll learn more
about later in COMS2.
In this project, you will need to calculate a path through a maze using a Breadth-
First Search. This will be useful on the snake game for example, where you may
need to find the shortest path from the head of a snake to the apple so that
you can get there first. Another example is if you were implementing Artificial
Intelligence for the ghosts in Pacman. In this case, the source may be the ghost’s
current position, and the goal would be Pacman’s current position.2 You will
implement the BFS algorithm in a new program that will be submitted to
Moodle for automatic marking.
https://www.youtube.com/watch?v=3QYk3vKuXVY
6.5.2 Algorithm
Because each step in the path towards the goal costs the same (uses the same
amount of energy/takes the same amount of time/etc), we can use a standard
breadth-first search to find the shortest path. The first path found that starts
at the source and ends at the goal will be the shortest path.3 Starting at the
source, the algorithm proceeds as follows:
1. Starting at the source, find all new cells that are reachable at distance
1, i.e. all paths that are just 1 unit in length, and mark them with that
length.
2. Using the distance 1 cells, find all new cells which are reachable at distance
2.
3. Using all cells at distance 2 from the source, find all cells with distance 3.
4. Repeat until the target is found. This expansion creates a wavefront of
paths that search broadly from the source cell until the target cell is hit.
5. From the target cell, select the shortest path back to the source and mark
the cells along the path.
This ‘wavefront’ is called the fringe – the edge of what we’ve seen so far. At each
iteration, we take a cell from the fringe and look at its undiscovered neighbours.
Note that if it takes 𝑛 steps to get to an item in the fringe, it then takes
𝑛 + 1 steps to get to any of its undiscovered neighbours. By checking all paths
of length 𝑛 first, we can be sure that there is no quicker way to get to an
undiscovered neighbour.4 The fringe can be represented using a queue, this
2 Technically, in Pacman the ghosts (Blinky, Pinky, Inky and Clyde) have different goals.
Blinky aims directly at Pacman, Pinky aims two blocks ahead of Pacman, Inky aims 2 blocks
ahead of Pacman but with the distance doubled based on the distance between the ghost and
Pacman. Finally, Clyde will chase Pacman just like Blinky, unless he is fewer than 8 tiles
away, in which case Clyde will retreat to the bottom left corner of the map. In each case, a
BFS could be used to find the shortest path to the goal.
3 This would not be the case if different steps cost different amounts. In that case, you’d
need to use a priority queue in the BFS, which leads you to an implementation of Dijkstra’s
algorithm. You will be doing this next year.
4 See if you can prove this, try a proof by induction and/or contradiction.
70 CHAPTER 6. QUEUES
means that in iteration 𝑖, dequeue a cell from the fringe and enqueue all of its
unvisited neighbours, which will have a path length of 𝑖 + 1. Because the fringe
is FIFO, we will dequeue all cells at level 𝑖 before we dequeue any cells at level
𝑖 + 1. It turns out that this is a simplified, special case of Dijkstra’s algorithm
for finding the shortest path in a graph. You’ll learn more about this next year.
6.5.3 Examples
The figures below show how the wavefront propagates through the maze and
around obstacles. The red blocks indicate the cells in the fringe (wavefront).
The green blocks indicate the start and endpoints. The numbers show how
many steps we must make to get from the start point to the relevant block.
6.5.4 Pseudocode
When running on a grid where each step costs the same, the shortest path
algorithm reduces to a normal breadth-first search. In the algorithm that follows,
we make use of a queue to keep track of the fringe. We use a parent array to
keep track of the row/column indices of the cell through which we travelled on
the way to the current cell. The distance array keeps track of how long the path
is to get from the source to the current cell. When we have found a path to
the goal, we follow the parent array back to the source and reverse it to get the
shortest path from the source to the goal.
Note: While it doesn’t affect the correctness of your algorithm in general, for
the marker it is important to visit neighbours of the current cell in the correct
order: down, left, up, right.
1. The first line will contain 2 integers, m and n, separated by a space. These
numbers represent the number of rows and columns in the grid respec-
tively.
2. There will then be m lines each with n columns that represent the maze.
• A space represents an open block in the world. You may travel
through this block.
• An x represents an obstacle. You may not travel through this block.
• An S represents the start.
• A G represents the goal.
3. The output should be the maze with * characters along the shortest path
from the start to the goal. 1 If there is no path between the start and
goal, then the output should say No Path.
You may use any built-in C++11 STL types that you think are useful.
There are many test cases on Moodle that handle various mazes with and with-
out valid paths and that will test several edge cases in your code.
7.1 Trees
https://www.youtube.com/watch?v=AMESParVjOA
Up to this point we have dealt with linear data structures. In some sense, the
items were always stored one after the other. There was always some kind
of linear arrangement to the way that the data was stored and there was the
concept of the next item in the structure. From here onwards, we will focus on
data structures that are non-linear. We will consider various types of trees and
the algorithms associated with them.
Another reason to use a tree is when we have large amounts of data and the ac-
cess and search times associated with lists become too expensive. By organising
data in a tree, we can achieve search times of 𝑂(𝑙𝑜𝑔𝑛) in comparison to 𝑂(𝑛)
that we would usually get when searching through a vector or linked list. This
change might not seem substantial, but note that a running time of 𝑂(𝑙𝑜𝑔𝑛) is
exponentially faster than 𝑂(𝑛).1 If there are 1 million items in a vector, then
a linear search would perform 1 million comparisons in the worst case. With
a balanced binary tree, we could search the entire list for a number under 20
comparisons. In fact, this is how database systems work – they build an index
using some kind of balanced binary tree (usually a B-Tree) which allows for
exceptionally fast lookups even when there is a very large amount of data to
search through.
1 Literally, exponential speedup in the mathematical sense: 2𝑙𝑜𝑔2 𝑛 = 𝑛
75
76 CHAPTER 7. BINARY SEARCH TREES
5. 𝑛𝑖 = (𝑛 − 1)/2
6. 𝑛𝑙 = (𝑛 + 1)/2
7. 𝑛 = 2𝑛𝑙 − 1
8. 𝑛𝑖 = 𝑛𝑙 − 1
https://www.youtube.com/watch?v=d7sxjBXEFFc
7.3.1 Insertion
https://www.youtube.com/watch?v=aBZwcZW3Nl8
Inserting a value into a BST requires that we start at the root and compare
the value to be inserted with the value at the current node. If the value to be
inserted is less than the current node’s value, then we should recursively insert
the value into the left subtree, otherwise, we should recursively insert the value
into the right subtree. When we find an empty space in the tree (e.g. we need
to insert into the left subtree and there is no left child) then we create a node
with the new value and insert that node into the space.
In the worst case, our algorithm needs to traverse the tree along the longest
path from the root to a leaf before inserting. In this case, we traverse ℎ nodes
before we can perform the insert. So in the worst case, the amount of work done
is proportional to the height of the tree.
From the relationships in Section 7.2 above, this means that the amount of work
we need to do will depend on the structure of the tree. In the best case, the
height of the tree will be 𝑙𝑜𝑔2 (𝑛 + 1) − 1 and in the worst case, the height of the
tree will be 𝑛 − 1. In Big-O notation, we are interested in the shape of these
functions and so we say that the height is 𝑂(𝑙𝑜𝑔𝑛) or 𝑂(𝑛) respectively.
When the height of the tree is 𝑂(𝑛) we say that the tree is degenerate, and it
has basically become a linked list. In later chapters we will consider algorithms
that help make sure that the tree is always balanced and therefore has a height
that is 𝑂(𝑙𝑜𝑔𝑛).
78 CHAPTER 7. BINARY SEARCH TREES
7.3.2 Searching
https://www.youtube.com/watch?v=iKYzYwaEW2I
If we would like to search for a value, 𝑥, we follow a similar approach. Again we
start at the root and compare 𝑥 with the value of the current node. If it is equal
then we have found the value, if 𝑥 is less than the current node’s value, then
we should recursively search the left subtree, otherwise, we should recursively
search the right subtree. If the subtree being searched is ever empty, then it
means that 𝑥 is not present in the BST.
In the worst case, the number is not in the tree and the path we follow corre-
sponds to the longest path from the root to a leaf. In this case, we traverse ℎ
times, so the amount of work done is proportional to the height of the tree. Just
as in the case of insertion, this means that we will do either 𝑂(𝑙𝑜𝑔𝑛) or 𝑂(𝑛)
depending on the structure of the tree. In the best case, the search key (𝑥) is
the value stored in the root and we do 𝑂(1) amount of work.
7.3.4 Delete
https://www.youtube.com/watch?v=BFoeIkPAa2s
To remove a value from the tree we need to consider what happens to that
node’s children. There are three different cases to consider.
When the node has no children, then we can simply delete the node, and set
the parent’s child pointer to nullptr. This process resembles deleting the last
link in a linked list.
When the node has one child, then we need to set the parent’s child pointer
to point to the current node’s child. After that is done then we can delete the
current node.
Finally, when the node has two children, then we need to replace the value
inside the current node with the successor. This means we need to find the next
largest value in the tree and move it to the current node. We can then delete
that value from the relevant subtree. The next largest value in the BST will
be the minimum value in the right subtree. So we traverse to the right subtree
7.4. TREE TRAVERSALS 79
and then traverse as far left as possible. This value can simply replace the value
that was being deleted, and then we recursively delete that new value from the
right subtree. We are guaranteed that in that case, then it will have zero or one
child and the deletion can take place using the strategies above.
Using the proof techniques from DCS, how do you think you could prove that
the successor does not have a left child?
in_order_traversal(curr->left);
cout << curr->value << " ";
in_order_traversal(curr->right);
}
We could perform a breadth-first search where we visit the nodes in the tree
level by level. We first consider the root (at level 0), then we consider all nodes
at level 1, then all nodes at level 2 and continue in this way until we have visited
all nodes.
7.5.1 Submission 1
Construct a binary search tree based on the numbers given on stdin. Then print
out the pre-order, in-order, and post-order traversals each on their own lines.
You should print out the numbers separated by spaces and it is acceptable to
have a space at the end of each line.
Sample Input:
50
30
35
61
24
58
62
32
-1
Sample Output:
50 30 24 35 32 61 58 62
24 30 32 35 50 58 61 62
24 32 35 30 58 62 61 50
7.5.2 Submission 2
Construct a binary search tree based on the numbers given on stdin. Then print
out the minimum number and maximum numbers each on their own line.
Sample Input:
7.5. LAB: BINARY SEARCH TREES 81
50
30
35
61
24
58
62
32
-1
Sample Output:
24
62
7.5.3 Submission 3
Construct a binary search tree based on the numbers given on stdin. Then read
another number from stdin and search for it in the tree. You should print out
true or false depending on whether you can find the number or not. Your
program should exit when it encounters another -1.
Sample Input:
50
30
35
61
24
58
62
32
-1
-5
50
64
59
66
99
24
-1
Sample Output:
false
true
false
false
false
82 CHAPTER 7. BINARY SEARCH TREES
false
true
7.5.4 Submission 4
Finally, you should construct a binary search tree based on the numbers given on
stdin. You should then continue to read numbers from stdin until you encounter
a -1 again. After reading each number, you should delete it from the tree, and
then give the pre-order, in-order and post-order traversals (just as in Submission
1) followed by a blank line. For each number, you delete you should print all
three traversals.
Sample Input 1:
50
30
35
61
24
58
62
32
-1
30
24
32
-1
Sample Output 1:
50 32 24 35 61 58 62
24 32 35 50 58 61 62
24 35 32 58 62 61 50
50 32 35 61 58 62
32 35 50 58 61 62
35 32 58 62 61 50
50 35 61 58 62
35 50 58 61 62
35 58 62 61 50
Sample Input 2:
50
30
35
61
24
7.5. LAB: BINARY SEARCH TREES 83
58
62
32
33
-1
30
24
32
-1
Sample Output 2:
50 32 24 35 33 61 58 62
24 32 33 35 50 58 61 62
24 33 35 32 58 62 61 50
50 32 35 33 61 58 62
32 33 35 50 58 61 62
33 35 32 58 62 61 50
50 35 33 61 58 62
33 35 50 58 61 62
33 35 58 62 61 50
class Tree{
private:
TreeNode* root = nullptr;
// These functions do the actual work
84 CHAPTER 7. BINARY SEARCH TREES
public:
// Public interface to the recursive functions above
void insert(int value){
insert(value, root);
}
// Submission 1
void preOrderTraversal(){
preOrderTraversal(root);
cout << endl;
}
void inOrderTraversal(){
inOrderTraversal(root);
cout << endl;
}
void postOrderTraversal(){
postOrderTraversal(root);
cout << endl;
}
// Submission 2
int min(){
return min(root);
}
int max(){
return max(root);
}
7.6. ADDITIONAL READING 85
// Submission 3
bool contains(int value){
return contains(value, root);
}
// Submission 4
void remove(int value){
remove(value, root);
}
~Tree(){
// Here you should recursively delete all the nodes in the tree
// This is not tested by the Moodle marker, but see if you can
// figure out how to implement this.
}
};
Balanced Trees
87
88 CHAPTER 8. BALANCED TREES
Chapter 9
Binary Heaps
https://www.youtube.com/watch?v=rdRRWifomVw
9.1.2 Sample IO
Sample Input 1:
89
90 CHAPTER 9. BINARY HEAPS
6
5
32
10
42
x
Sample Output 1:
6
6 5
32 5 6
32 10 6 5
42 32 6 5 10
Sample Input 2:
8
12
3
5
10
p
9
12
6
x
Sample Output 2:
8
12 8
12 8 3
12 8 3 5
12 10 3 5 8
10 8 3 5
10 9 3 5 8
12 9 10 5 8 3
12 9 10 5 8 3 6
class BinaryHeap{
9.1. LAB: BINARY HEAPS 91
public:
vector<int> data;
void pop(){
void print(){
for(auto &v : data){
cout << v << " ";
}
cout << endl;
}
};
int main()
{
BinaryHeap bh;
string line;
while(getline(cin, line) && line[0] != 'x'){
if(line[0] == 'p'){
bh.pop();
}else{
bh.push(stoi(line));
}
92 CHAPTER 9. BINARY HEAPS
bh.print();
}
return 0;
}
Chapter 10
Sorting
https://www.youtube.com/watch?v=FUzu-qypGuw
93
94 CHAPTER 10. SORTING
Bibliography
Cormen, T. H., Leiserson, C. E., Rivest, R. L., and Stein, C. (2009). Introduction
to algorithms. MIT press.
Dale, N. B., Weems, C., and Richards, T. (2018). C++ plus data structures.
Jones & Bartlett Learning, 6th edition.
Goodrich, M. T., Tamassia, R., and Mount, D. M. (2011). Data structures and
algorithms in C++. John Wiley & Sons, 2nd edition.
Stroustrup, B. (2014). Programming: principles and practice using C++. Pear-
son Education.
Weiss, M. A. (2014). Data structures & algorithm analysis in C++. Pearson
Education, 4th edition.
95