Escolar Documentos
Profissional Documentos
Cultura Documentos
8:42 AM
Inheritance
- This will mainly show up in the multiple choice and in the finding errors part of the test
- You'll have to know how public, private, and protected work, and something about data slicing as
well
Overview
- So what is inheritance?
Inheritance - the ability to derive a new class by extending existing ones
Derived Class/Subclass - an extension of its parent class
Base Class/Superclass - a class which is extended
- Throughout the rest of this, I'm going to explain things in terms of parents, children, and siblings
and I'll refer to classes as subclasses and superclasses, simply because that's how I was taught in
Java
The parent, child, and sibling hierarchy works the same way as you'd expect it to work in a
family
Each child has a parent, where the parent is the superclass and the child is a subclass
Ex: suppose we have a Person class and a Student class. Then the Student class is a CHILD of
the Person class
The Person class is the PARENT of the Student class
Suppose we also extend Person using an Employee class
The Person is the PARENT of the Employee
The Employee is the CHILD of the Person
The Employee is the SIBLING of a Student because both of them have the same
parent, Person
- Now that that's out of the way, let's proceed
- The general idea behind inheritance is that subclasses inherit EVERYTHING from a parents
This includes all member variables and member functions
Let's say we have a class Parent with a member variable x = 5, and a subclass Child that
extends Parent
class Parent {
public:
int x = 5;
};
class Child : public Parent {
// Literally nothing here
};
Then:
int main() {
Parent p;
cout << p.x << endl; // Will print out 5
Child c;
cout << c.x << endl; // Will also print out 5
return 0;
}
The cout << p.x << endl makes sense; this is just PIC 10A material
However, notice that the class Child has NO member declarations inside
We didn't write public: int x = 5; in there anywhere
So how come we're able to print out c.x?
Study Guide Page 1
If something is private, then NO ONE can access it EXCEPT for the Example class
NO. One. This means no children, no outside functions, NO one
Any member functions of the Example class WILL be able to access it, but no one else
will
This means the following will break:
int main() {
Example e;
cout << e.private_int; // Error: private_int is inaccessible
return 0;
}
This is because private_int is inaccessible to main, since main is an outsider
Remember how subclasses inherit everything from parents?
They technically inherit private variables, but they become hidden to the child
This means that they aren't able to access them in their methods
For example:
class Parent {
public:
int getVar();
private:
int var = 5;
};
class Child : public Parent {
public:
int childGetVar();
};
int Parent::getVar() {
return var; // Works just fine because member of Parent
}
int Child::childGetVar() {
return var; // Will throw error because var is inaccessible to Child
}
The childGetVar() function above will fail because it tries to access var
However, var is private for Parent, which means it is hidden to Child
So what did I mean when I said that it still exists?
Even though Child can't directly access var in its member functions, it can
access getVar, which is a PUBLIC function of Parent
Remember, since it is a subclass of Parent, it inherits EVERYTHING from
Parent
This means it inherits both var and getVar
Even though Child is calling getVar, getVar is still a function of Parent
This means that it still has full access to var
Therefore, the following is perfectly valid:
Child c;
cout << c.getVar(); // Will print out 5
The last type of access is protected
This means that the variable will ONLY be accessible to itself and children
This is mainly used for getters and setters
In the example above, we created a public getVar function for the Parent class
However, this means that anyone can access it
What if we only wanted Child and other children to access it?
If we made getVar() protected instead of public, then this would do it!
Study Guide Page 3
Subclasses
- To create a subclass of a parent class, we use a modified version of a class declaration
- Basically:
class Subclass : public Superclass { };
- Notice how we wrote public Superclass. This means that we're extending the Superclass as a
public class as opposed to a protected or private class.
Why does this matter?
If we extend the class as a public class, then all of the Parent's members will be inherited at
the same level of access that they are defined with
Public members of Parent will be inherited as public
Protected members of Parent will be inherited as protected
Private members will be inherited as private
- What happens if we extend as protected or private?
If we extend as private, then everything is private
This is terrible.
This means that all public and protected functions, functions which the subclass
should have access to, are now inaccessible because they are now private
If we extend as protected, then all public and protected members become protected
- Here's a table to help illustrate!
Public Inheritance Protected Inheritance Private Inheritance
Public Members
public
protected
private
protected
private
Private Members
private
private
private
Summary
- Classes can borrow code from other classes by extending them, meaning they will inherit
everything
- Inheritance follows an is a structure, where a subclass is a superclass
- Three types of access control to restrict what is inherited: public, protected, private
Private: everyone can access
Protected: only subclasses can access
Private: only this specific class can access
Item 1
- Suppose we have the following class and function:
class Parent {
private:
int variable = 1;
void print( Parent p );
Study Guide Page 4
Item 2
- Suppose we have the following:
class Parent {
protected:
int x = 5;
};
class Child : public Parent {
public:
void print(int y);
};
void Child::print(int y) {
cout << y;
}
int main() {
Child c;
c.print(c.x);
return 0;
}
Print is a public function of Child, which means it should be valid in main
x is a protected variable of Parent, but Child should have access to it since it's protected
What will the following code do?
If you guessed error, you're right! Why?
When we try to pass c.x as an argument to the print function, we're doing so inside the main
function
Remember that the main function is an outsider; it doesn't have access to protected items
Study Guide Page 5
- Remember that the main function is an outsider; it doesn't have access to protected items
Polymorphism
- This will show up in both finding errors and reading code
You will have a snippet of code which you will have to read and write output
Overview
- Polymorphism - the ability to treat different objects related by a common base class uniformly
- Basically, we're taking advantage of the inheritance thing we just covered
Think of this as creating different permutations of a certain method
- For example, let's say we have an Employee parent class and an Engineer, Janitor, and Physicist
subclasses
- Let's say the Employee class has a doWork method
- Obviously, an engineer's do work is going to be different from that of a janitor, whose job is not
going to be THAT different from a theoretical physicist (<-- jab at my theoretical physicist friend),
but still somewhat different
- Given an arbitrary engineer, janitor, or physicist, we want to be able to tell them to doWork and
have them do their special job on their own
THIS is the guiding principle behind polymorphism
- There are three ways to do this:
Using a virtual function
Creating a polymorphic collection of objects
Using dynamic method binding
Function Overloading
- When an Engineer object is created, it automatically inherits the doWork method from the
Employee
- However, this is going to be a generic doWork method
- We want to specialize the doWork method for the Engineer
- To do this, we just define the doWork method again
void Engineer::doWork() {
// Whatever an engineer does
}
- This will overwrite the original doWork command so when you construct a new Engineer object
and tell him to do something, he'll do the new method
Engineer e;
e.doWork(); // will do what the overwritten method says to do
Data Slicing
- Data Slicing - the loss of data when converting from the object's type from a Parent to Child
- Let's consider the following code:
vector<Employee> v;
v.push_back( Engineer() );
- Engineers are technically employees, so we are allowed to push it into the vector
- However, when we created the vector, we created it specifically to hold Engineer objects, not
subclasses of Engineers
This means that any new variables or functions you add to the Engineer class will NOT be
preserved
- Data slicing sucks and will probably show up as an error on the exam
- To fix it, use a vector of pointers instead
vector<Employee*> v;
v.push_back( new Engineer() );This works because we use pointers, not the actual object
itself
Study Guide Page 6
itself
An employee pointer stores no data itself; merely that it points to objects of Employee type
When we create the object and have the pointer point to it, there is no data slicing because
we aren't trying to force the Engineer object into a container which it isn't designed for
- When we mentioned using a polymorphic collection of objects earlier, this is what we meant
Virtual Methods
- However, this isn't enough :(
- From the previous example, let's say we constructed a vector of Employee pointers and added a
new Engineer
- What happens when we do v[0]->doWork()?
- This will use the Employee version of the function, NOT the Engineer version
- To fix this, the method that is to be overwritten must be virtual
This is dynamic method binding
- If a method is not virtual, then the class will choose whatever version its type says it should have
For example, if we do the following:
Employee a; // will use the Employee version of doWork
Engineer b; // will use the Engineer version of doWork
Employee c = b; // will use the Employee version of doWork
Employee* d = new Engineer(); // will use the Employee version of doWork
Employee* e = &b; // will use the Employee version of doWork
Notice the problem we have here? Unless we create the object as an Engineer, our method
won't be overwritten
- Note that this is an error on the exam and something you will have to be able to read; ALWAYS
check if the method is virtual
If it is virtual, use the overloaded method
If it is not virtual, then check what the object was when it was created
UNLESS it was explicitly created as the subclass, then use the parent version of the
method
- To fix this, make the function virtual in the Employee class
- This means that the compiler will look for an overloaded version; if found, it will use the
overloaded version instead of the original version
- Note: once a function is declared virtual, it remains virtual in all derived classes, even if it is
overwritten by a non-virtual method
Abstract Classes
Abstract Class - a class with at least one pure virtual function
Pure Virtual Function - a virtual function that is set equal to 0 in the class definition
Note that abstract classes CANNOT be instantiated; this was one of the errors in the midterm
How can we tell whether a class is abstract or not?
If in the class definition, you see something like:
virtual void function() = 0;
- If it's set to 0, then it is a pure virtual function, and that class will NOT be able to be created
directly
- For example, let's pretend we have a Parent class that contains a pure virtual function and we
have a Child class that extends it
We can do:
Child c;
Parent p = c;
We CANNOT do:
-
We CANNOT do:
Parent p;
This is an error on the exam; you CANNOT create objects of abstract classes
You are allowed to create pointers for abstract classes though
Why have pure virtual functions at all? Why have abstract classes?
Abstract classes are useful for organizing your code; it's not something you HAVE to have
For instance, let's say we're creating a geometry program
We might want to have a Shape abstract class to hold member variables and functions that
all shapes might have
However, it's impossible to say, getArea() of a shape since you don't know what type of
shape it even is
Friends
- Friend - a function or class that has direct access to a class's protected and private members
- This is a one-way thing; just because you designate another function/class as your friend does not
mean that you automatically become their friend
This is a very cruel metaphor for real life :(
- This will become more useful in later sections of this course, you don't really have to worry about
that right now
- The following is an example on how to designate a function and a class as a friend:
class Example {
friend Person;
friend void printX();
private:
int x;
};
Streams
- I find this topic really stupid and I don't see why he's teaching it
- Like seriously, this is all just google-able stuff
- If you're preparing for your final like I am right now :D Just write this stuff down on your formula
card and you're good to go
I'm so sorry for you if you're doing your first midterm :P
Don't worry, the questions aren't that difficult :D
- Nonetheless, on your first midterm, TWO, that's right, TWO of your short answer questions will be
about streams
Since there are only two short answer questions anyways, that means all of them are going
to be about streams
I've also been told that they will return for the final, so whatever
Overview
- You've worked with streams before in the form of cout and cin
- These are special input streams for the console (that's right, the c in cout and cin stands for
console!)
- These streams fall in the #include<iostream> library
- You're probably already familiar with some basic syntax as well
Study Guide Page 8
File Streams
- This is basically just reading to/from a file instead of to/from the console
Study Guide Page 9
This is basically just reading to/from a file instead of to/from the console
To use a filestream, you have to #include<fstream>
Suppose you're given a string containing the filename called filename
To open the file for input or output, use the following syntax:
out.close(); // you will lose points if you don't put this here
}
- And tada! Yeah, I forgot to put in out.close() in the code above; oops, -2 for me :P
- Before we finish with filestreams, here's a question straight from the test :D
- Define a function that reads in input from the console, ignoring whitespace, and prints it one
character at a time to the file with the given filename
Repeat this process until you reach a '?'
Remember to close the output file stream
- Here's my code:
void type_to_file( string filename ) {
ofstream out( filename.c_str() );
char c;
cin >> c;
while( c != '?' ) {
out << c;
cin >> c;
}
out.close(); // remember to close the filestream
}
String Streams
- The other type of stream you will be working with is a string stream
- Basically, given a string, you're going to convert it into a stream object that you can >> and << to at
your leisure
- To do this, you must #include<sstream>
- Once done, you can convert a string as such:
string source = "Hello World";
istringstream in( source );
ostringstream out;
// Insert some code where you << stuff to it
string output = out.str();
That's all there is to it!
In terms of actually working with the string stream, you will still use << and >> as normal
For example, here's another one of his test questions:
Define a function that creates an istringstream object to convert the given money string into its
double value; Assume that the string always begins with the dollar sign '$'
- Andd
-
Operator Overloading
- So he really hyped this topic up for the first midterm and then didn't test us on it at all
- Nonetheless, I'm sure it might show up for your midterm, and I was told that this is a big part of
the free response for the final, so why not
Overview
-
The basic gist of this topic is, you know all those operators you've been using? +, -, *, ==, !=, etc etc
You can override what they normally do!
This is useful when you are creating a custom class
There are three ways to overload them
As a member function of the class (so basically, what you would normally do)
Operator[]
Operator+=
Operator-=
Operator*=
Operator/=
Operator++
As a friend function of a class
Operator>>
Operator<<
As a nonmember function
Operator+
Operator Operator*
Operator/
Study Guide Page 12
Operator/
Operator==
Operator!=
Operator>
Operator>=
Operator<
Operator<=
The general rule is, if it is a stream operator, make it a friend
If it needs direct access to the object itself (such as += or []), then it should be a member
function
If it doesn't fit either of the first two rules, and all of the comparison operators fall here,
then make it nonmember
What about operator=, you may be asking? We'll get to that in the next section
There are two types of operators
Unary and binary
Unary operators only take a single argument; for instance, operator+= only needs one
argument, the input that you want to add to the given object
Binary operators take two; all comparison operators fall here, because you need two things
to compare
When overloading a binary operator, note that the left argument is the item on the left, and the
right argument is the item on the right
For example, consider the Set class (from your homework!)
Set operator+ (const Set& a, const Set& r);
Member Operators
- This section encompasses all operators that should be member functions
- Namely, the operator+=, operator-=, etc etc
- Let's consider a ComplexNumber class
A ComplexNumber will have a real component and an imaginary component
Let's call the real component x and the imaginary y
Study Guide Page 13
Nonmember Operators
- What about for nonmember operators?
- Since they are nonmember, they won't show up inside the class definition (well, duh)
- Rather, they'll show up in some arbitrary .cpp or .h file
You know, like the functions you did in PIC 10A before you even learned classes existed
- When overloading nonmember functions, first check whether you created any member functions
that you can re-use
- For instance, consider operator+
- What differentiates operator+ from operator+= is that operator+ returns a new value, whereas
operator+= modifies the calling object
- Well, we can work that into our code
ComplexNumber operator+ (const ComplexNumber& z1, const ComplexNumber& z2) {
ComplexNumber answer = z1; // Create a new ComplexNumber equal to the first param
answer += z2; // Add it to z2 by shamelessly reusing operator+=
return answer; // return the temp answer
}
- You'll notice some differences
- First of all, it doesn't return by reference; unlike operator+= which returned the calling object,
operator+ (and all nonmember functions) will return a new object
- You'll also notice the lack of a scope; this is because it is nonmember
Whereas we had ComplexNumber::operator+=, we only need operator+ because this is not
part of the ComplexNumber class
- Finally, we have two parameters instead of just one
When we add two complex numbers, the one to the left of the + sign will be z1, and the one
Study Guide Page 14
Comparison Operators
- These can be either member or nonmember functions
- An important thing to note about these functions is they all have to be const
- Consider operator==
If it's a member function
bool ComplexNumber::operator==(const ComplexNumber& z) const
And if it's not
bool operator==(const ComplexNumber& z1, const ComplexNumber& z2) const
- Two differences abound!
The member function only has one argument; this is because the calling object will be the
left side by default; for nonmember functions, you have to accept two parameters
You can tell the first is a member function because of the scope parameter (the
ComplexNumber::)
- The actual code for the operator overloading isn't very interesting
- Really, just write customized code to check if the two are equal, so basically just check if their x's
and y's are equal to each other
- If you had to then overload operator!=, save yourself some trouble and just write:
bool operator==(const ComplexNumber& z1, const ComplexNumber& z2) const {
return !(z1 == z2);
}
- Or for the member function
bool ComplexNumber::operator==(const ComplexNumber& z) const {
return !(this == z);
}
- ALWAYS re-use your code. No shame.
- The second argument, on the other hand, can be anything you want, but you probably want it to
be the class you're working with
- For example, for the ComplexNumber class, we'd want our second argument to be that of a
ComplexNumber
- Suppose we want to print out the ComplexNumber in the format x + yi (or x - yi if y is negative)
ostream& operator<<( ostream& out, const ComplexNumber& z ) {
cout << z.x;
if( y > 0 ) // switch based on whether y is positive or negative
cout << " + ";
else if
cout << " - ";
cout << z.y << "i";
return out; // VERY IMPORTANT, DO NOT FORGET
}
Yeah, I know in his lecture notes he used an ostringstream, but screw it, this works just as
well
You can't directly concatenate integers or floats to strings, but I'm not doing that in my
above code
Rather, I print them out separately, which is perfectly valid
And
suppose
we want to read it in from that same format, x + yi / x - yi (with those spaces as well)
istream& operator>>( istream& in, ComplexNumber& z ) {
in >> z.x;
char c;
in >> c; // ignore whitespace by using >>
double y;
in >> y;
if( c == '+' )
z.y = y;
else
z.y = -y;
in.ignore(); // get rid of that i at the end
return in; // VERY IMPORTANT, DO NOT FORGET
}
- Do note that the ComplexNumber argument for operator>> cannot be const because we need to
directly modify that variable to read in the value
- While stream functions are more complex, there are only two of them yay :D Just memorize these
and you're golden
Unary Operators
-
To be honest, he probably won't test you on postfix or prefix or anything in this section
I'm only including it because it's in the lecture slides
For unary operators such as - or !, (as in !x or -x), they should be friend functions
They take a single parameter, the class type by reference
friend ComplexNumber operator-(const ComplexNumber& z);
Implicit Conversion
- So notice how when we overloaded all of these operators, we set our type to the class?
ComplexNumber?
- What if we want to be able to add doubles to ComplexNumbers?
- Will we have to copy all of our operators and change the left argument to double?
- Nope! Implicit conversion!
Implicit Conversion - the process of wrapping another variable type in a constructor to
convert it to the calling type
That's a very confusing definition, sorry :(
- Also, Ouellette really likes implicit conversion; my TA really doesn't like implicit conversion, so, use
it at your own risk
- Basically what this means is, let's say we wanted to add doubles to our ComplexNumbers
- Let's say we had the following constructor
ComplexNumber::ComplexNumber(double d);
- And let's say we tried to do the following
ComplexNumber z(3, 5);
double a = 5.0;
z += a;
- Normally, operator+= takes in the argument (const ComplexNumber& z), and a clearly is not a
ComplexNumber
- However, the compiler will try its hardest to make this work
It will look for a constructor with a double as its argument and use that to convert a into a
ComplexNumber
Therefore, behind the scenes, it'll look like this:
z += ComplexNumber(a);
Which will work, because a is now wrapped as a ComplexNumber
- However, do note that the compiler still can't do this for things on the left of operators
a += z; // will throw an error because still not overloaded
You would have to overload += as a friend function as such:
double& operator+=( const ComplexNumber& z );
Which I'm not even sure will work because I don't know if we can do this for doubles
Nevertheless, you will not be asked to do this on the test; just be able to identify this as an
error
Errors
- These have shown up in error questions both on purpose and on accident
- Something to note: none of the operators we just discussed here are overloaded by default
ONLY operator= is overloaded by default
In the second midterm, Ouellette accidentally assumed operator== was overloaded when it
wasn't
Therefore, bonus points! This is good to know
- For questions that have shown up on the test intentionally
Using operator> but only overloading operator< in the class definition
In the class, he overloads operator< for the given class
However, in the actual code, he uses h > k
Study Guide Page 17
Code Memory
- Really all you have to take away from this section is function pointers
- When you compile your C++ program, all of the functions you create will be put into a section of
memory called "code memory"
- You can create pointers to point to functions here and make them do stuff!
- The syntax for this is ugly and scary, but it's not that bad
- What's great about these pointers is you can store them in arrays and pass them to other
functions
- But I digress; let's look at what a function pointer actually looks like
Static Data
- Static data memory holds both global and static variables
- Global Variable - a variable declared and defined outside the scope of any function or class
- Consider the following:
int variable = 5;
void foo( int x ) {
cout << x + variable;
}
- Notice how we created variable outside of the foo function
- Also notice how it isn't a member of any class
- Yet, we're STILL able to access it inside the foo function, because it's considered a global variable
We'll be able to access it anywhere within this program!
This is probably something you did in PIC 10A without even realizing what you were doing
- Static variables are a bit different
Static Variable - a local or member variable declared with the keyword static that persists in
all instances of its creator
- It will exist for the entire duration of the program and is only initialized once
- For example, consider the following:
int foo() {
static int x = 0;
return ++x;
}
Study Guide Page 19
int main() {
cout << foo() << endl; // Prints out 1
cout << foo() << endl; // Prints out 2
cout << foo() << endl; // Prints out 3
}
We initialize x in the first call to foo() and return ++x, which means we return 1
The second time we call it, x is already initialized and has value 1; when we call return ++x, we
now return 2 because we increment it from 1
The same for the third iteration
Is this cool? Sure
Are you going to use this? Probably not; if you ever face a point where you're going to need to use
it, either make that variable a member variable or make it a global variable
I mean, I guess this is more efficient, but you could still do all the problems without ever
having learned this
Stack Memory
- Stack - a type of memory where ordinary nonstatic local variables are stored
- Also function calls. Function calls are stored here too.
- This is the stack which the eponymous Stack Overflow comes from
- Whenever you create variables in the main method, they get pushed to the stack
- Whenever you make a function call in the main method or something, it gets pushed to the stack
- Just know that this exists; who knows, it might even show up as a single MC question someday
Operator=
Destructor
You will have to be able to write code for these, plus the default constructor
If you don't explicitly create these, C++ will automatically create a default constructor, copy
constructor, operator=, and destructor for you
That's why we're able to use operator= for custom classes when we neglect to explicitly
define them
So why are these the big 3?
These are things you should always overwrite when performing operations on the heap
For starters, we need a destructor to deallocate things from the heap
We also need to overwrite operator= to perform a deep copy
Deep Copy - creating a copy of the values instead of just creating a reference
Consider the following code:
int x = 5;
int y = x;
x = 3;
cout << y; // prints out 5
This is a deep copy. When we do y = x, we set y equal to the VALUE of x, not equal to x
So what? Well, pretend this performed a shallow copy, that is, it set y equal to x
Then, when we did x = 3, y still equals x and would equal 3
Then cout << y would print out 3 instead of 5
Wouldn't that be irritating?
To avoid that kind of stuff, we have to create deep copies using operator=
The final item is the copy constructor
Basically, given a parameter equal to our current type, we create a new object equal to that
Consider the following
ComplexNumber::ComplexNumber( const ComplexNumber& z );
Given a ComplexNumber, it will create a copy of this
In order to avoid creating a shallow copy (the same problem that operator= faces), we
should define our own copy constructor
What's great about the big 3 is it should really just be the big one, operator=
If you play your cards right, the only thing you actually have to write code for is operator=
Let's consider the following example, which coincidentally is the big free response question from
the first midterm
Given the following class, define all the functions. Store the member variable p on the heap:
using namespace std;
class Character {
public:
Character(char value);
Character(const Character& c);
~Character();
Character& operator= (const Character& c);
private:
char* p;
};
- Basically, the big three plus an additional constructor
- To approach this problem, let's start with the constructor
This should just create a new Character object and set p equal to the value; easy!
Character::Character(char value) {
p = new char(value);
}
Yeah, in order to create primitives on the heap, you have to say new type(value)
If you didn't know this syntax before, learn it now :P
That's all there is!
Study Guide Page 21
Overview
- Backtracking is just a super large recursive problem solving technique
- It has the following structure
Base case: if you're at the end, then you're done; return true;
If you can legally do what you're trying to do in your current position, then do it
Recursively call the command on the next available position
If that command returned true, then return true
Else, undo what you did and return false
Return false;
- There are two formats to this problem; the first is making the function a bool, and the second is to
make it a void
- In the homework, you make it a void, which I find ridiculous but oh well
- He will either make it a bool or void on your midterm; mine was a bool
Bool Version
-
else {
moves--;
maze[x][y] = 1; // reset the spot we just visited
return false;
}
}
return false;
}
Okay, let's break this down
The first step is to write the base case. Even if you have absolutely NO idea what you're doing, you
can write down the base case
if( x == (N - 1) && y == (N - 1)) {
maze[x][y]++;
return true;
}
Basically, check if you've made it to the end; if yes, return true
Also, I completely forgot you could use the N from the arguments for array size here; that might
be important information for you in the future :P
NEVER, ever ever EVER forget the base case or else you'll be stuck in infinite recursion
- All you have to do for the base case is check if you are done; if yes, return true
- Okay, let's break down the next part
if( canVisitSquare( maze, x, y )) {
// some stuff
}
- He's going to give you a method to check whether you can perform your task here
All you have to do is use that method in an if statement here
- For the stuff inside the if statement, there are three parts
Perform your move here
Recursively call the command for the next step and check if it is valid
If it is not valid, undo what you did
Study Guide Page 25
Void Version
For your homework, he'll make you do a void version as well
I find this version to be much less intuitive than the boolean version
Basically, you'll pass a bool by reference as one of the arguments in the function
Here's a sample problem from his other midterm
Suppose you have a 1D array of numbers from 1 to 9 in random order
Some of the numbers will be 0 while others will already be placed there
Ex: 8 0 0 4 0 6 0 0 7
Write a function to fill in all the 0s with numbers from 1 to 9, without any duplicates
- Here's the class provided:
class Sudoku {
public:
bool number_on_square(int column) const;
void fill_square(int column, int number);
void erase_square(int column);
void next_square(int column, bool& success);
bool valid_square(int column, int number) const;
private:
static const int BOARD_SIZE = 9;
int num_squares_filled;
};
- Our mission is to define the next_square function, so let's do it
-
}
- This problem is a bit different than before, but it more closely resembles the homework, so there's
that!
- Still, the format's pretty similar
- Base case
if( num_squares_filled == BOARD_SIZE ) {
success = true;
return;
}
- If we're at the end condition; that is, we've reached the end of the board, we set success equal to
true and return
- Since it's a void, we can't just return true; we have to set the argument bool to true, and then
return to quit
- Otherwise, we iterate through all of the possible values to place onto the board
Yeah, I know we didn't do this for the previous problem, but we didn't have to
You have to do something similar to this for your homework
- For every iteration, we check whether we can place the number on the given spot
By the way, there are two checker methods for this, valid_square and number_on_square
I didn't provide the documentation for them; they're provided on the test, but here's what
they do
valid_square checks whether the current square is non-zero AND whether the number
you're trying to place has already been placed on the board
number_on_square only checks whether the current square is non-zero
Anyways, for every iteration, we use valid_square to check if the current spot is nonzero and
to make sure this number doesn't already exist on the board
If so, then:
fill_square( column, x );
next_square( column + 1, success );
Study Guide Page 27
Overview
- There are two types of searching you will be responsible for knowing: linear search and binary
search
- That's about it
Linear Search
- Linear search works by beginning at the start, iterating through until you either find the item
you're looking for or you reach the end of the list
- Here's the code:
int linear_search( const int A[], const int SIZE, int target ) {
for( int x = 0; x < SIZE; x++ ) {
if( A[x] == target )
return x;
}
return -1;
}
- Basically, just uses a for loop to find the index of the target
- If the target cannot be found, it returns -1
- This is a pretty simple algorithm, and there isn't much to say about this except for the fact that it's
O(N) (which will make no sense to you now, but will make more sense as you go into the next few
sections)
- Linear search's advantage over binary search is that it has the same level of efficiency regardless
of whether the list you're searching through is already sorted or not
Binary search can't work on unsorted lists
Speaking of which
Binary Search
- Binary search works by dividing and conquering
- You ever play that game where you ask someone to think of a number between 1 and 100 and you
try to guess it?
- Let's play a variant of that game where the person who's guessing the number will tell you if your
guess is higher or lower than the number they chose
- To start, a good guess will be 50
This will divide the numbers neatly in half; either you guessed it right, or you will be told
which 50 numbers you no longer have to consider (1 to 50 or 51 to 100)
Let's say I was told it was higher
I know the number falls between 51 and 100 then
My next guess will divide that range in half; I'd guess 75
Depending on what I'm told, I will then be able to eliminate another 25 numbers
Study Guide Page 29
Depending on what I'm told, I will then be able to eliminate another 25 numbers
Notice how we keep dividing in half
At worst, you will have to make 6 (or is it 7) guesses!
That's pretty good!
Meanwhile with linear search, if my number were say, 99, then you'd have to make 99 guesses
Clearly binary search is better if you're given a sorted list
Let's examine the code:
int binary_search( const int A[], int first, int last, int target ) {
if( first > last ) return -1;
int middle = (first + last)/2;
if( A[middle] == target ){ return middle; }
if( A[middle] > target )
return binary_search( A, first, middle - 1, target );
Else
return binary_search( A, middle + 1, last, target );
}
Note that this is a recursive function
The base case is that we reach a point where either we've found the target or it is no longer
possible to search in the range we're given
Otherwise, we find the value that's in the middle of our range
We compare to our target; if our middle is currently greater than the target, then we need
to search the target's left (basically, all the values less than the middle)
Otherwise, search all the values greater than the middle
We perform this task recursively, by calling ourselves with a revised boundary
For instance, let's say we're given the following:
int A[] = {2, 5, 7, 8, 9, 11, 12, 14, 20};
binary_search(A, 0, 8, 14);
Well, we first check if first > last; nope, 0 is not greater than 8
Alright, let's calculate the middle; this is (0 + 8)/2 = 4
A[4] is equal to 9
However, we're looking for 14; since our target is greater than the middle, we want to
search the larger values
Thus, we will return binary_search( A, 5, 8, 14 );
Okay, let's see what that does for us
First is not greater than last because 5 is not greater than 8
We calculate the middle -> (5 + 8)/2 = 6
A[6] is equal to 12
12 is still smaller than our target, so we return binary_search( A, 7, 8, 14 );
Let's do this again
7 is not greater than 8
Our middle is (7 + 8)/2 = 7
A[7] is equal to 14, so we're done! We return middle, which is 7
Remember all the other binary_searches we did before? Well, they form a function call stack that
looks like this:
binary_search( A, 7, 8, 14 );
binary_search( A, 5, 8, 14 );
binary_search( A, 0, 8, 14 );
The first binary search with args A, 0, 8, 14 is still waiting for a response from A, 5, 8, 14, which is
waiting for a response from A, 7, 8, 14
Since A, 7, 8, 14 returns 7, it passes that response to A, 5, 8, 14
We told A, 5, 8, 14 to return whatever binary_search( A, 7, 8, 14 ) returned, so it will also return 7
Finally, we get to the bottom call: A, 0, 8, 14, which will return what A, 5, 8, 14 returns (which is 7)
Therefore, our last function will also return 7 as well
You'll be told to construct a stack exactly like this on the midterm, so be sure you understand this
Study Guide Page 30
- You'll be told to construct a stack exactly like this on the midterm, so be sure you understand this
Algorithm Analysis
- Ehh, see the Big-O section. I feel that this should be merged with that and will make a lot more
sense after you read that
Sorting
- I happen to like sorting. After reading this section and his lectures, you will probably end up not
liking sorting. Then you'll end up hating everything I like because I have very poor taste in liking
things :P
- I'll take that risk. ANYWAYS, for the exam, you will be asked:
1 MC question where you're given code and need to identify what sorting algorithm it is
1 free response question where you're given a string of numbers and need to sort it
- Really not all that bad, but it's important to understand the sorting algorithms conceptually
- For the midterm, know how to do bubble sort, selection sort, and insertion sort; he is most likely
going to make you perform selection sort or insertion sort, since bubble sort takes too much time
and merge sort, shell sort, and radix sort are too complicated to write out
Overview
- Before we jump into the different sorting algorithms, I like to divide them into two categories (this
will help later on in the Big-O section)
Bad sorting algorithms
Bubble Sort
Selection Sort
Insertion Sort
Good sorting algorithms
Shell Sort
Merge Sort
Quick Sort
Radix Sort
- You do not have to know quick sort for this class at all
- However, he covers it briefly in his lecture for some reason, and this is the ultimate job interview
question; when they ask you to sort something, be all, "yo I got this" and then describe how quick
sort works
Basically what my roommate tells me
- Anyways, the difference between bad sorting algorithms and good sorting algorithms is that good
sorting algorithms are optimized for really large data sets
- You'll see what I mean in the later sections. For now, this is just an introduction to each sorting
algorithm and an example of how they work. Onwards!
Bubble Sort
- Bubble sort is named this way because the largest elements "bubble" to the top
What the heck does that mean?
- Bubble sort works by iterating through the entire list (well almost, iterating until the second last
element) and swapping the current index with the next element if the current element is bigger
than the next element
Let's say you have a list: 9 1 3 5 4
You start at 9 and check whether it's bigger than the next element, 1
Of course it is; swap them
Now we're at 1 9 3 5 4
We're now at index 2, which is 9 and compare to index 3, which is 3
Swap!
Now we're at 1 3 9 5 4
Study Guide Page 31
Now we're at 1 3 9 5 4
Notice how the 9 is moving up in the world list
It's "bubbling" up to the top!
- Once it finishes an iteration, it will start over again and do the swapping thing until it runs through
an entire cycle without swapping anything
This is unique about bubble sort; it is the ONLY sorting algorithm with a built-in system to
check whether the list is already sorted
- Note: Every new iteration iterates for one less item
The first iteration runs from index 0 to index N - 1
The second iteration runs from index 0 to index N - 2
The third runs from index 0 to index N - 3
The reason for this is because the largest element in the list will tend to bubble to the front
of the list
The second time we iterate, we're looking for the second largest element because we
know the largest element is at the end of the list
Therefore, we will bubble the second largest element to the second last index
Then, we will repeat again for the third largest element to get that to the third last index
- Hopefully that makes sense. Time for an example!
Sample Run
- We begin with 0 5 3 2 1 4
First iteration:
0 5 3 2 1 4 // index 0: 0, index 1: 5, don't swap because 0 < 5
0 3 5 2 1 4 // (this is after swap) index 1: 5, index 2: 3, swap because 5 > 3
032514
032154
0 3 2 1 4 5 // Finished first cycle; notice we iterated 5 times and there are 6 elements
Second iteration:
0 3 2 1 4 5 // Dont swap because 0 < 3
0 2 3 1 4 5 // We're at index 1 now, which is 2 after the swap (3 before the swap)
0 2 1 3 4 5 // We're at index 2 now, which is 1 after the swap
0 2 1 3 4 5 // Compare index 3 to index 4; 3 < 4 so no swap
// Finished second cycle; notice how we iterated 4 times, 1 less than last
Third iteration:
0 2 1 3 4 5 // No swaps!
0 1 2 3 4 5 // Swap!
0 1 2 3 4 5 // 3rd iteration, 1 less than last time; notice how it's sorted, but since we
made swaps this time, we still have to go through another cycle
Fourth iteration:
0 1 2 3 4 5 // No swaps; it's already sorted, but we still have to iterate
0 1 2 3 4 5 // And we're done
Since there were no swaps during that cycle, we quit!
- Notice how each cycle we perform one fewer iteration than the previous cycle
We have 6 elements, so our first cycle is 5 iterations
Then we do 4, then 3, then 2
- On the test, he will make sure you write the right number of iterations; even if there are no swaps,
you still have to write each iteration
You don't have to write out every swap though, just how the array looks after every
iteration
So you'd write:
0 5 3 2 1 4 // before any calls
0 3 2 1 4 5 // after first iteration
0 2 1 3 4 5 // after second iteration
0 1 2 3 4 5 // after third iteration
Study Guide Page 32
Selection Sort
- The next sort is Selection Sort
- Conceptually, it works by iterating through the list from the beginning and finding the smallest
element
Then it will swap the first element with the smallest element
- This process will repeat, but instead of starting from the beginning of the list, it will start from the
second spot
- This means it will find the second smallest item in the list and swap it with whatever the second
item in the list is
Sample Run
- We begin with 5 3 2 0 1 4
We iterate from index 1 to N to find the smallest element; once found, we'll swap it with the
first element
0 3 2 5 1 4 // 0 was the smallest, swap with 5
Now that we know the smallest element is at index 1, we repeat the process but iterate
from 2 to N
0 1 2 5 3 4 // 1 was the smallest, swap with 3
Now repeat from 3 to N
0 1 2 5 3 4 // 2 was the smallest; didn't find any smaller, so no swaps
Now repeat from 4 to N
012354
And again, for the last time; remember, Selection Sort performs N-1 iterations
012345
- Without the catch to check if it's sorted, even if you're given a sorted list, selection sort will still
perform N-1 iterations
- On the test, you'd write it in the format:
5 3 2 0 1 4 // before any calls
0 3 2 5 1 4 // after first iteration
0 1 2 5 3 4 // after second iteration
0 1 2 5 3 4 // after third iteration
0 1 2 3 5 4 // after fourth iteration
0 1 2 3 4 5 // after fifth iteration
- Include the comments; they will clear up any ambiguity and help you argue for points back if they
dock you
Insertion Sort
- Insertion sort divides the array into two sections, a sorted side and an unsorted side
- The sorted side is generally the left side
- It begins with 1 element in the sorted side (since a list of size 1 is always sorted) and N - 1 in the
unsorted side
- It then takes the first element from the unsorted side and sorts it into the sorted side
Basically, it will keep shifting it one step over to the left until the value on the left is smaller
than it
- What this means is that if you have an array that's already sorted, it won't perform any swaps or
anything
It will still iterate through the array, but it won't actually perform any actions
Study Guide Page 33
It will still iterate through the array, but it won't actually perform any actions
This makes it second best or equal to BubbleSort in that it won't do anything when given a
sorted array
Of course, when you're given an unsorted array, insertion sort is superior to bubble sort in
many ways
Sample Run
- We begin with 8 3 6 4 0 (this is a problem directly from my midterm by the way)
The 8 forms the sorted part of the array and the 3 6 4 0 is the unsorted part
We get the first element from the unsorted part, 3, and we want to sort it into the sorted
part
Basically, we keep swapping it with the element to its left until it encounters something
smaller than it or it is the smallest
This will yield:
38640
Our sorted array now encompasses 3 8, while the unsorted is 6 4 0
We then sort the first element of the unsorted, 6
36840
And then 4 0 were left
34680
03468
- On the test you'd write:
8 3 6 4 0 // before any calls
3 8 6 4 0 // after first iteration
3 6 8 4 0 // after second iteration
3 4 6 8 0 // after third iteration
0 3 4 6 8 // after fourth iteration
- Remember that insertion sort will make N - 1 iterations
Shell Sort
- Shell sort is an improvement on Insertion Sort made largely because Insertion Sort makes too
many swaps
- The idea is that we create a sequence of h values and we divide the array according to that
For instance, if we have an h value of 3, then our first sub-array consists of index 0, 2, 5, 8,
etc etc
Our second sub-array would be 1, 3, 6, 9, etc
We then perform insertion sort on each of these subarrays
- Once done, we then decrease our h value, repeating this process until we reach an h-value of 1
- Once we're at an h-value of 1, then our "subarray" is just our regular array
Insertion sort on that will then result in a fully sorted array!
- But wait, how is this more efficient, you might be wondering; we're performing insertion sort like
a hundred times inside shell sort
That may be true, but shell sort's real strength lies in the fact that we don't perform as many
swaps in each iteration
Sure, we may have more iterations, but we perform far fewer swaps
This is very noticeable on large lists of values
- Shell sort will provide performance almost on par with merge sort (which we'll get to next), while
insertion sort will get nowhere near mergesort on large lists of values
- On SMALL lists of values, however, shell sort is LESS efficient than insertion sort
Sample Run
- Suppose we have the following array: 8 4 1 3 7 9 2 5 0
Notice that this is larger than stuff we've done in the past
- Our h-values are going to be 3, 2, 1
Study Guide Page 34
Merge Sort
- This is my favorite sorting algorithm, and I use this to sort papers after grading :D
- It's a recursive sorting algorithm that divides and conquers
- Here's how I sort papers:
Divide the stack into a lot of smaller stacks, usually 2 papers each
Sort the 2-paper stacks, which takes like no time at all
Now, pick two-paper stacks at random, and "merge" them
By that, I mean I look at the top of both of the two paper stacks and I take the paper
that comes first in alphabetical order
I repeat this process until both stacks are empty
Then I'll end up with a bunch of 4-paper stacks
Repeat the process to merge the 4 paper stacks
Repeat until I finish sorting
This sounds like a convoluted way to sort, but trust me, sorting papers is a LOT faster than
Study Guide Page 36
This sounds like a convoluted way to sort, but trust me, sorting papers is a LOT faster than
manually sorting
When you manually sort papers, you basically perform insertion sort, and it takes forever to
find the right place to put in the paper
- I'm not crazy! This actually works well! :(
- Also, this is a pretty good sorting algorithm; this is the "worst" acceptable sorting algorithm to talk
about at a job interview
If they ask you to sort something, you should generally either use heapsort or quicksort, but
if you can't use either one, then mergesort is acceptable
- Anyways, here's merge sort with numbers
Sample Run
- Let's sort 8 3 4 0 1 5 7 2, shall we?
Let's divide this into two piles
8340
Let's divide this into two piles
83
Let's divide this into two piles
8
3
40
Let's divide this into two piles
4
0
1572
Let's divide this into two piles
15
Let's divide this into two piles
1
5
72
Let's divide this into two piles
7
2
At this point, we have 8 sorted piles
Since each pile only has one item, they have to be sorted
Now, let's merge:
Let's merge the two piles 8 and 3
Compare the top elements of both
3 is smaller, so we add 3 to our new list
Now there's only one pile, so we add 8 to our list
We now have one pile that's 3 8
Let's merge the two piles 4 and 0
Compare the top elements of both and push them to the new list in the
order of the smallest
This yields 0 4
Let's merge the two piles 1 5
This yields 1 5
Let's merge the two piles 7 2
This yields 2 7
Okay, now we have 4 sorted piles
38
04
15
27
Study Guide Page 37
27
Let's merge 3 8 and 0 4
Compare the top elements: 3 and 0
We take 0; our piles are now 3 8 and 4
We take 3; our piles are now 8 and 4
We take 4; our only pile is now 8
We take 8;
Our new list is 0 3 4 8
Let's merge 1 5 and 2 7
Compare the top elements: 1 and 2
We take 1; our piles are now 5 and 2 7
We take 2; our piles are now 5 and 7
We take 5; our pile is now just 7
We take 7
Our new list is 1 2 5 7
We now have 2 sorted piles, 0 3 4 8 and 1 2 5 7
Merge!
Compare top elements: 0 and 1
We take 0, our piles are now 3 4 8 and 1 2 5 7
We take 1, our piles are now 3 4 8 and 2 5 7
We take 2, our piles are now 3 4 8 and 5 7
We take 3, our piles are now 4 8 and 5 7
We take 4, our piles are now 8 and 5 7
We take 5, our piles are now 8 and 7
We take 7, our pile is just 8 now
We take 8
And we're done; we've produced the sorted pile 0 1 2 3 4 5 7 8
- Wasn't that fun? I sure think so
- Here's a more concise version of what I was doing:
Radix Sort
Fortunately, you don't know have to know quicksort for this test
Unfortunately, you have to know radix sort!
Radix sort is incredibly efficient; unfortunately, it only works for numbers
Basically, you sort numbers by their ones place, then by their tens place, then hundreds, etc
To do this, you construct 10 vectors, one for each possible digit (0 through 9)
You then read each number into a vector based on the current digit; for example, if we are sorting
through the ones place, we'll put 48 into the vector for 8
- Once all of the numbers have been put into vectors, you empty the vectors out in order, from 0 to
9
- Then you repeat the process for the next digit (so start with ones place, then move onto the tens
-
- Then you repeat the process for the next digit (so start with ones place, then move onto the tens
place, etc)
- The advantage of radix sort is that it's incredibly fast; the disadvantage is that it takes up a lot of
memory space to store all of these vectors
Sample Run
- Suppose we want to sort 48 63 33 24 11 1 31 88
- Here's what it looks like after we put them all into vectors
0:
1: 11 1 31
2:
3: 63 33
4: 24
5:
6:
7:
8: 48 88
9:
- Then we read them out in order from the beginning of each vector, in the order of 0 -> 9
We get 11 1 31 63 33 24 48 88
- Okay, repeat for 10s place
0: 1
1: 11
2: 24
3: 31 33
4: 48
5:
6: 63
7:
8: 88
9:
- And read them out in order again
1 11 24 31 33 48 63 88
- And ta-da, they're sorted! Wasn't that quick and easy?
Last Thing
- On the exam, he will give you the code for a sorting algorithm and ask you to identify what it is
- Here's what to look for:
Characteristic
Sorting Algorithm
Bubble Sort
A swap function; also the only one that iterates from x = 0 to SIZE - 1 Selection Sort
Insertion Sort
Shell Sort
Merge Sort
Radix Sort
Quick Sort
Big-O
- This is the hardest topic you will do (unless you count recursion)!
Study Guide Page 39
This is the hardest topic you will do (unless you count recursion)!
Fortunately, this will probably only show up as one or two MC questions worth like 1 point each
Unfortunately, this is one of the most important concepts in programming
Fortunately, I will try my best to make it as simple as possible :P
He likes to write a lot of crap about actually counting the number of statements and
determining Big O from that. That's why his slides are a smorgasbord of mathematical
expressions While you COULD do it that way, understanding how Big-O works conceptually
is so much faster
Overview
- The point of Big-O analysis is to determine how efficient an algorithm is and how long it takes
given small or large inputs
- For instance, let's start with something simple
Remember linear search? The search method where you look through a list one by one until
you find the right element?
Let's say we have a list of 100 elements, and let's say the thing we're looking for is the 47th
If we use linear search, how many comparisons will we have to make to find it?
Well, we start from the first one and compare. Since our thing is the 47th and not the
first, we move on to the second
Again, ours is the 47th, not the second. Move on.
Please don't make me type this 47 times. Basically, we will have to make 47
comparisons before we actually find what we're looking for.
What if our item is the 99th? We'll have to make 99 comparisons.
What if our item is the 3rd? We'll only have to make 3 comparisons.
I think you're beginning to see a trend here. Basically, to find the n-th element, we will have
to make n comparisons until we find what we're looking for.
This is called O(N)
This means that the algorithm is directly related to the size of the array
- In our example above, we made exactly n-comparisons to find the n-th element
- What if we did something where we would make exactly 2n comparisons to find the n-th
element? Is this O(2N)?
Technically yes, but this will reduce to O(N).
Big-O notation is NOT meant to be an exact count of how many statements it takes, despite
what Ouellette might write in the slides
Rather, it is a conceptual understanding of how the algorithm operates
We can think of this linear search as a single for loop
Basically, that's how we'd write it; we'd write a for loop and compare things one by
one
- In general, if there is only a single for loop, it is O(N), because it will generally run about N times
As we increase N by 1, we execute the inner code one more time, so it grows linearly
It's okay if this confuses you right now; it'll make more sense once we look at the
alternatives
- What if we had a for loop INSIDE of a for loop?
Let's say the inner for loop runs M times and the outer for loop runs N times
This means we'd be running the inner for loop N times
Since the inner for loop runs M times, we'd do a total of M * N runs
As we increase N by 1, we're going to increase the number of runs by M
Before with O(N), we'd only increase the number of commands by 1
Now, we're increasing them by M
This will grow exponentially; this is called O(N2)
Why is it called O(N2) and not O(M*N)?
Again, Big-O notation is a simplification of the situation; even though both for loops
are not running N times, we say O(N2) to say that it grows exponentially
- In general, if there is a nested for loop, it is O(N2)
Study Guide Page 40
Case Study
- To help conceptualize Big-O operations, imagine a phone book.
O(1): given a page that a person's name is on and their name, find their phone number
This is easy for you to do; you will know exactly where to look and how to find the
phone number
O(log N): given a person's name but not the page, find the phone number
Assuming you use binary search and not linear search, you'll open the book to some
page that isn't the first page
Then you'll check whether the person's name comes before or after the page you're
on
If it comes after, then flip randomly forward a bit; if it comes before, then flip
randomly before a bit
Repeat and keep flipping forward/backwards in smaller and smaller increments until
you find the page with the name on it
Then look up the phone number
Yes, I know this isn't actually binary search since binary search will divide in half and
such, and flipping randomly forwards and backwards will probably take a bit longer,
but it's still log N in terms of complexity
It isn't mathematically log N, but it follows the same methodology as an
algorithm that is log N; therefore, we can consider it log N
O(N): find all people whose phone numbers contain the digit "5"
Yeah, I don't envy you if you ever have to do this
You're going to have to check every single phone number to see if it contains 5 in it
This means that you're going to have to iterate through the entire book
This is similar to doing a single for loop
This will therefore be O(N)
O(N): alternatively, given a phone number, find the person's name
Again, you're going to have to check every single person and see if their phone
number matches the one you have
Again, similar to a single for loop
- Now, pretend that we're in a printer's office printing phone books for customers!
O(N log N): personalize each phone book for each customer by opening the phone book for
that customer, finding their name, and putting a sticker next to it
Doing this for one person is just O(log N), as we discussed earlier
However, you have to do this O(log N) operation N times, for the number of
customers you have
N log N is just doing log N operations N times
O(N2): an extra 0 was added to every phone number by accident; open every phone book
and white out the extra 0 for every phone number in every phone book
This is the equivalent of doing a nested for-loop
For each phone book
For each number in the phone book
Study Guide Page 42
Bubble Sort
- Bubble sort is a fun sort; see the sorting algorithms section for a refresher on it
- Let's consider the best and worst case scenarios
Best Case:
Remember how bubble sort has a catch to tell if the list is already sorted?
You probably didn't :D Well, REMEMBER IT.
Anyways, the catch works by checking whether any swaps were made in that pass
If no swaps were made in that pass, then quit
This means that best case, it needs to make a single pass through all the stuff
How fast is a single pass?
Did you guess O(N)?
Well you should have. It's O(N) because it's directly proportional to the length of
the items
Therefore, our best case scenario is O(N)
Worst Case:
Worst case is that our catch never kicks in, and that we make swaps every single time
That means we're going to have to iterate through the entire list roughly N times
Okay, technically the amount we iterate through decreases each time, from N 1 to N - 2 to N - 3, etc etc
For simplicity's sake, let's just say it's about the same, okay? Let's not
complicate things too much
Conceptually, we have to iterate through each list (which is already O(N)) N times,
because our catch never kicks in
Since we're performing an O(N) algorithm N times, we will have O(N2) complexity
- Again, the important thing is not an accurate count of the mathematical statements, as Ouellette
seems to emphasize, but a rough understanding
- Therefore, Bubble Sort is:
Best Case: O(N)
Worst Case: O(N2)
- What about average case? On average, the catch will probably kick in somewhere, so we don't do
the O(N) operation N times
However, on average, we will still have to perform the O(N) operation some number of
times, and if we increase the size of the list, we will have to perform the O(N) operation
more times, meaning it scales with the size of the list
Therefore, we can still conclude that Bubble Sort is O(N2) in average case
Selection Sort
Study Guide Page 43
Selection Sort
- While selection sort is better than bubble sort, it's still a pretty terrible sorting algorithm
- On the bright side, it has the same efficiency for worst case, best case, and average case
The reason for this is because it has no checks for whether the array is already sorted
It will perform the same number of iterations every time
It will iterate through the entire unsorted part of the array and compare elements to
find the smallest element
Then it will do this N - 1 times
- We're performing an O(N) action about N times; what does this mean?
Yup,
- No need to do any of that math stuff he puts into his lecture slides
Insertion Sort
- What's great about insertion sort is it won't actually do anything if the array is already sorted
- This makes it ideal for almost sorted or fully sorted arrays
- The best case is if it's already sorted
In this case, you will still iterate through the entire array
Every iteration, your sorted section grows larger by one and your unsorted grows smaller by
one
You take the first element from the unsorted and try to sort it into the sorted region
However, since the array is already sorted, that element will already be where it
should be
Therefore, it will continue along in its loop
Since you're basically just going to iterate through the loop, the best case scenario is
- Let's consider the worst and average cases
Chances are, you won't be using a sorting algorithm if your array is already sorted
Let's look at an unsorted array
The worst case would be if it were in reverse order
This means that as we move to the next element in the unsorted array, we have to move it
all the way to the front of the sorted array
This is an
operation
Not to add, there are N elements, so performing an
task N times is
On average, it'll also be
Merge Sort
- What's great about merge sort is it has no best case, worst case; they're all the same, just like
selection sort
- Unlike bubble sort and insertion sort, merge sort doesn't discriminate
No matter what, it will always break everything into the smallest piles and sort them the
exact same way
- If you want a comprehensive breakdown on why this is O(N log N) for all three cases, read his
lecture notes
He calculates it using math and stuff
- I can't really give you a really good answer for this, except for the fact that
Since it's a divide and conquer algorithm, the dividing part is O(log N)
However, the number of times we have to divide will scale based on the size of the array, so
it has O(N) growth
- Therefore, combining the two, we get O(N log N)
- This is a grossly oversimplified explanation; there's no way he's going to ask you to prove it on the
test
- At worst, you'll be asked a MC question on merge sort's efficiency
- In that case, just say O(N log N)
- Yes, this was very unsatisfying, but you can just read his lecture notes if you'd like to delve into the
math :D
Study Guide Page 44
math :D
Conclusion
- In general, use insertion sort on small arrays and arrays that are almost sorted
- Shell sort is almost as good as insertion sort on small arrays and almost as good as merge sort and
quick sort on large arrays
- Merge sort and quick sort are fastest on large arrays
Merge sort is a stable sort though, and is more reliable; quick sort is less reliable but faster
on average
- Radix sort is the fastest, but requires memory to hold all of those darn vectors
- If you can understand the following/find it funny, you should be very well prepared :P
Algorithm Analysis
- A really irritating MC problem he puts on the test is that he asks you to find the Big O of something
given T(2N)/T(N)
- What does this mean? Let's say we have a linear function, that is O(N)
- Let's say it takes 10 seconds to run 100 operations
How long do you think it will take to run 200 operations?
Well, since this is linear, you'd probably expect it to take 20 seconds to run
- What about a function that's O(1)? Let's say it takes 10 seconds to run 100 operations and you
know it's O(1)
How long will it take to run 200 operations?
Since it's constant time (that means the time will never change), it'll also take 10 seconds
Study Guide Page 45
Since it's constant time (that means the time will never change), it'll also take 10 seconds
- Now, given times, he's going to ask you to determine what a function's big O is
- Let's consider the O(N) example first
He's going to give you a table like the following:
N
T(2N)/T(N)
100
1.999
200
2.001
Basically, what T(2N)/T(N) means is you take the time it takes to run N operations, you take
the time it takes to run 2N operations, and you divide them
Let's say our N is 100; it takes 10 seconds to run 100 so T(N) = 10
It takes 20 seconds to run 200 so T(2N) = 20
T(2N)/T(N) = 20/10 = 2
But wait, you might complain, my table has 1.999 instead of 2
Yeah, he adds in some stupid "random variation" bs to make it more "realistic"
- Anyways, instead of brute forcing this, I'm going to put a table here with values and answers
T(2N)/T(N) Big-O
1
O(1)
O(N)
2->1
O(log N)
4 -> 2
O(N log N)
O(N2)
O(N3)
- For O(log N) and O(N log N), he's probably going to start you at N 100
That means you'll see numbers more like 2.3 for O(N log N) and 1.15 for O(log N)
- If you don't see any of the following, chances are it's O(2 N)
There's no predictable number for this one, so if it doesn't follow a pattern, it's this
- Just memorize this table and you'll ace that question
Recurrence Relations
- You're probably only going to be asked a single MC question on this, if at all
- Unfortunately, this topic makes its return from Math 61
- Fortunately, it is highly unlikely that he is going to test you on it (yay!)
Then repeat!
Yeah, don't worry if you don't get this; just try to brute force your way through things on the
test only if you're truly desperate
- The following should be enough for the test:
If you see
, then it is going to be
If you see
or N divided by any number, it's going to be
If you aren't given any variables, then it's going to be
If you're told to add N, then it's going to be N times whatever the inside operation is
You'll see what I mean when you look at the selection sort
and merge sort
relations
- Also, memorize the following:
This is binary search
LinearList
- There will absolutely be a question on the test where you will have to make use of his LinearList
class
- The class API will be provided for you, but it's good to know it ahead of time so you don't have to
look at it
- You will have to know how three classes work
Node
Iterator
LinearList
Overview
- So what is a "linear list"? It's essentially a single-y linked linked list
- Like that helps at all. What's a single-y linked linked list?
- Think of one of those metal chain things
- This thing! When you think about it, all a chain really is is just a bunch of metal rings connected to
each other
- A linked list is just like this
Linked List - a data structure consisting of a group of nodes which together represent a
sequence
Make c point to b
Make a point to c instead of b
Ta-da! We're done
No matter how many Nodes are in our linked list, insertion will always involve a constant
number of steps
Therefore, insertion in a linked list is an O(1) operation
Consider deletion as well
Let's say we have a pointing to c pointing to b
We want to end up with a pointing to b
To do this, we just
Make a point to b
Delete c
Again, notice how the number of steps does not depend on the number of elements
in the list
Therefore, deletion in a linked list is an O(1) operation
- TL;DR, the above text is summarized in the table below
Insertion Deletion Accessing/Indexing
Arrays/Vectors O(N)
O(N)
O(1)
Linked Lists
O(1)
O(N)
O(1)
- These advantages will probably show up in a multiple choice question somewhere, so know
these :P
Node
- The Node class has two attributes: an int storing data called 'data' and a pointer to the next node
called 'next'
- It also has two friend classes: Iterator and LinearList
Basically, what this means is that both the Iterator and LinearList classes have full access to
it
If you're writing a method in LinearList or Iterator, you can directly manipulate the Node
pointer because they're friends
- If you have to visualize it, draw a circle and write a number in it
That number is the data
Now, draw another circle and write another number in it
That would be another Node
Draw an arrow from the first circle to the second circle
This is the pointer to the next Node
In the picture above, the green '5' is the data for the first node, and the blue arrow is the
pointer to the next Node
- Every Node HAS to have a pointer. If there is no other Node to point to, then it will point to NULL
If it points to NULL, then it is the very last pointer
In the picture above, the two black dashed lines represent NULL, and the red arrow
represents this Node pointing towards NULL
Iterator
- For the professor's classes, iterator is kind of a misnomer
- The Iterator is a kind of container for a Node. It contains methods to manipulate the Node for you,
so you don't have to directly mess with pointers. It'll help to know how to do it with Iterators and
without Iterators.
DON'T assume that its only purpose in life is to iterate through the LinearList; this is false
Just treat it as a container for a Node
- An iterator contains two attributes, a Node pointer 'position', and a LinearList pointer 'container'
The 'position' attribute is a pointer to the Node that it is currently at
In the picture above, pretend the orange arrow is pointing from an Iterator
Then, the orange arrow is the Iterator's Node pointer to the first Node
- An iterator can access the data and next variables of its Node pointer by doing the following:
Iterator t = l.begin(); // l.begin will return an iterator at the beginning of the list, I'll talk more
about this later
t.position->next; // This will access the Node's next pointer
t.position->data; // This will access the Node's data
- Let's break down the syntax for the commands above
When we create a new Iterator t, we create a new object with a member variable position
The member variable position is a member of t, so we access it using the dot operator
HOWEVER, it is a pointer, which means it is NOT actually the Node object, but merely a
POINTER to it
To access the data from a pointer, we have to dereference it using the * operator
Therefore, *(t.position) will return the actual Node object itself
Now, we can access the next and data by using the dot operator
*(t.position).next will be the next pointer and *(t.position).data will be the data variable
Remember that -> is shorthand for *(something)., however
Therefore, writing t.position->next and t.position->data is quicker and is easier to read
- The Iterator class contains three functions
next()
get()
operator==
- The next() function will "iterate" the Iterator to the next Node by going wherever the current
Node's next pointer points
In the picture far above, calling next() would cause the orange arrow to point to the circle
with the red '6' inside of it
It works by looking at the object it is pointing to right now (the circle with the green '5')
Then it looks at where that object's next pointer is pointing to
The blue arrow from that circle points towards the circle with the red '6'
Therefore, it will switch its position pointer to point at the new circle
The code for this is below:
void Iterator::next(){
assert(position != NULL);
position = position->next;
}
Note that there is an assert(position != NULL)
This ensures that the Iterator doesn't push past NULL
Let's say the Iterator is pointing at the circle with the red '6' already
What happens when we try to call the next() function?
Well, we look at where the red arrow is pointing (NULL), and set our position to that
Does this throw an error?
NO, it DOESN'T (gotcha if you said yes :P)
This is perfectly fine, and this is how l.end() works actually
l.end() (I'll talk more about this under the LinearList section) returns an Iterator
Study Guide Page 51
l.end() (I'll talk more about this under the LinearList section) returns an Iterator
where the position is NULL
So why'd I bring this up?
Consider what happens if we try to call next() AGAIN
Recall that NULL doesn't actually mean anything; in CS, it's basically an object
representation of nothing
Therefore, it doesn't have a position pointer
Notice the lack of arrows coming from the dashed black lines
If we tried to follow one (which doesn't exist), we'd almost certainly get an error
Fortunately, we have a line of code saying assert(position != NULL)!
If our position IS NULL, which it is at this point, then assert will return false
The assert function will then crash our program and return an error
Why'd we go through this convoluted process if we're going to get an error anyways?
Apparently the professor really likes the assert function for some reason, and I guess
having an intentional error is better than an unintentional error?
I guess it's more, "it's the thought that counts"; by adding that line of code, we make
sure it's working as intended and that we aren't trying to call next() on NULL
TL;DR - if the Iterator is NOT currently pointing at NULL, then it will try to point at whatever
the pointer is pointing to
Otherwise, it will crash and burn because of the assert function
- The get() function will return the data at the current position
This is a pretty simple function; it simply returns the data at the given location
The code for this is below:
int Iterator::get()const{
assert(position != NULL);
return position->data;
}
Pretty simple stuff; it accesses the data at the current position pointer and returns it
Remember that position is a POINTER, not an object with actual data
Therefore, we have to dereference it first by using the * operator
*position.data, basically
Again, -> is shorthand for that, so that's why we write position->data
Also, we must check that the Iterator currently points to a Node before attempting to get its
data, hence the assert function
- Finally, we overload the operator== to compare an iterator's position
Remember that operator== is NOT overloaded by default
The code for this is below:
bool Iterator::operator==(const Iterator& it)const{
return position == it.position;
}
We compare the current Iterator's position pointer and check whether it's pointing to the
same place as the parameter's position pointer
If yes, return true; if no, return false; that's all there is to it
LinearList
- Finally, we get to the meat of the entire thing
- Thing is, on the midterm, he doesn't actually ask you about the LinearList
You're only responsible for knowing how to use his Node and Iterator classes
- Still, we'll cover his LinearList
- In his lecture notes, his LinearList class is pretty straightforward
class LinearList {
public:
LinearList();
Study Guide Page 52
LinearList();
private:
Node* head;
};
LinearList::LinearList() {
head = NULL;
}
Remember that the LinearList class is a friend of both the Iterator and the Node classes, so it has
direct access to their methods and member variables
So what's this whole LinearList business all about?
The LinearList is basically a container for all of the Node objects
Think of each Node as a link in a chain; then the LinearList is the entire chain
It stores a Node pointer called head to indicate where the list begins
After that, it's up for each individual Node to indicate where the rest of the chain is
Again, returning to our chain link example, if you know where the beginning of the chain is, you
can find everything in the chain because you can see what each individual link is chained to
Let's cover some operations!
Operations
Insertion of the First Node
- Let's say the list is empty right now and we want to insert our first node
- When we first create a LinearList, the head Node pointer points to NULL because there's nothing
there
- When we insert our first node, we want the head pointer to point to our new Node because this
will be our new head
- Therefore, if we wanted to insert a new Node with value 5:
head = new Node(5);
head->next = NULL;
- The second line isn't required because I believe the Node constructor already initializes it to NULL
by default
Nonetheless, he has it in his lecture notes, so why not; let's be redundant for the sake of
being redundant
- Here's a pictorial representation to make it easier to visualize:
temp->next = head; // the new Node will now point towards the current head
head = temp; // the head pointer now points towards our Node!
- More pretty pictures
delete temp;
- This is what the list looks like right before deleting temp
- Again, note that we always have a backup pointer! After we delete temp, the temp pointer will be
useless because it won't be pointing to anything anymore
Iterator Operations
- On the test, doing everything the way we did it above is FINE.
- You will NOT have to do it using an iterator if you don't want to
- However, it's useful to at least learn how iterators work because they'll be used a lot in the last
third of the course
- I know we already covered the iterator class, but a quick refresher
Iterators are a container for a Node object
All it does is contain a pointer to the Node we want to access
This allows us to get data and iterate through a Linked List without directly touching any of
the pointers
This is a safer approach! Directly manipulating pointers is dangerous
Of course, the only reason to actually USE C++ is pointers Go use a safe language like
Java if you want to be safe
- Anyways, before we continue with iterators, let's add two more functions to the LinearList class
that I omitted earlier
begin()
This function returns an iterator to the head Node
Okay, what it ACTUALLY does is it creates a new Iterator with a pointer to the head pointer
Since we overloaded operator== in the Iterator class, it's pretty much the same thing though
I'll demonstrate how to use this in a sec
end()
The opposite of begin(); it returns an iterator to the Node AFTER the last Node in the list
Basically, it creates an iterator that points to NULL
Consider the last Node in the LinearList; every Node has to point to something, but what
does the last Node point to?
Well, there's nothing for it to point to, so it points to NULL
This is why end() returns an iterator at NULL; when we iterate through, we check if our
iterator equals end
Why not point to the last node?
Consider a for loop; our loop condition is going to be iterator != end()
If end were a pointer to the last Node, then once our iterator reaches the last Node, it
will quit out of the loop before executing the code in the for loop
This means we wouldn't actually perform any operations on the last Node
This is why we have to point to NULL, because after we perform operations on the last
Node, we visit the last Node's pointer
Guess where that points? NULL!
- Now that we have these two functions, let's see how they're used
Study Guide Page 56
- Now that we have these two functions, let's see how they're used
- Here's a for loop using his LinearList class to iterate through the entire LinearList
LinearList l;
for( Iterator itr = l.begin(); !(itr == l.end()); itr.next())
- You'll notice some strange things in this for loop, probably (or maybe not)
- The first part looks fine; we create an iterator and set it equal to the beginning of the list; no
problem here
- You may be wondering why we have to use !(itr == l.end()) instead of itr != l.end()
The reason is because Ouellette never overloaded the != operator
We only overloaded operator==, not operator!=, so saying itr != l.end() doesn't actually
mean anything
This will throw an error because != is not overloaded by default; the function doesn't exist
We did overload operator==, however, and we can negate the whole thing by wrapping it in
parentheses and prefixing it with a not
- Finally, we use itr.next() to advance the iterator
- The syntax is stupid, but I don't think he'll ask you to do something like this on the test; looking at
his past midterms, he's only asked you to use Nodes and Iterators (if you so desire) to swap Nodes
around
- Also note, if you want to get the data from an Iterator, use the get() function
- Therefore, a loop to print out all the values in a LinearList would be:
for( Iterator itr = l.begin(); !(itr == l.end()); itr.next())
cout << itr.get() << endl;
Unlike his homebrew LinearList class, Ouellette will NOT give you an API for the STL classes
Fortunately, for the final you can just write these on your notecard
Unfortunately, this stuff's on the midterm too, so you'd better know this stuff :D
There will be one short answer question on the midterm asking you to write some function to do
something using one of the STL classes
Linked Lists
- So we went over what a linked list is conceptually above
- Fortunately, some very wonderful people have created a standard library version of the LinkedList
for us to work with
- Note that the STL linked list is a doubly-linked linked list; this means that unlike Ouellette's
LinearList, every Node contains a pointer to its predecessor
This means that iterators can move BACKWARDS in addition to moving FORWARDS
We'll get to the iterators in a sec
- To use a STL linked list, simply #include<list>
- You can then create a linked list of ints with the following notation: list<int> l;
- What's great about the STL linked list is all of the methods we want are already implemented for
us!
push_front( element );
Similar to push_back, but it pushes it to the front of the list!
Yeah, probably not going to be using this one
-
Before I can give you the rest of the commands, I have to teach you how the STL iterator works
You're not going to like this syntax
To create an iterator for a list, it would be type list<int>::iterator
Remember the begin() and end() functions for the LinearList? They make a reappearance
Let's say we want to iterate through a Linked List; we would use the following for loop
for( list<int>::iterator itr = l.begin(); itr != l.end(); ++itr )
- Yeah, I know that looks complicated, but you'll get used to it! Let's break it down
The first part is the type; where we would normally say int i, we instead have an iterator itr
We have to specify the type of the iterator; a list<int>::iterator will not work for vectors
Incidentally, you can also use vector<int>::iterator to iterate through vectors if you so
desire!
Anyways, once we create our iterator itr, we need to set it to a starting value
It's good to set this to l.begin(), since that returns an iterator to the first element in
the list
Note: l.end() does NOT point to the last element in the list; it points to the spot that
the last element in the list points to
We then continue to iterate until our iterator == l.end(); basically, it has finished iterating
through every element in the linked list and is now pointing to a point after the last element
- See, not so bad :D
- What if we want to go backwards? He might ask you to print out a linked list backwards
Well, we can't just do
for( list<int>::iterator itr = l.end(); itr != l.begin(); --itr )
The problem with this is that l.end() does not point to the last element; rather, it points to a
point after the last element
Also, l.begin() points to the first element; however, we need it to point to a place BEFORE
the first element to know when to stop
- Seriously, it's a hassle to do this. Fortunately, reverse_iterators exist :D
for( list<int>::reverse_iterator itr = l.rbegin(); itr != r.end(); ++itr )
This will iterate through the list in reverse order. Remember this syntax and notation,
because it will be useful in the future as well
- Anyways, now that we got the for loop structure down, how do we actually access the data at
each point?
- Simple, just dereference the iterator
- The following code will print out every single item in a Linked List
for( list<int>::iterator itr = l.begin(); itr != l.end(); ++itr )
cout << *itr << endl;
-
int count = 0;
for( list<int>::iterator itr = l.begin(); itr != l.end(); ++itr, ++count )
if( count % 2 == 0 )
cout << *itr << endl;
- Basically, iterate through the entire loop and only print it out if count % 2 is even
- I told you it was lazy :P
- Here's the way he probably intends you to do it
Also note, while we can use ++itr and itr++, we CANNOT use itr += 2 to advance forward two
spaces
Unless you import the algorithm library, which you aren't going to, the ONLY way you can
advance an iterator is using ++ and -- With that in mind, here's another solution:
{
list<int>::iterator itr = l.begin();
int max = *itr;
for( ++itr; itr != l.end(); ++itr )
{
if( *itr > max )
max = *itr;
}
return max;
}
- Basically, we know our list is never empty, so there must be at least one element in the list
- Since we need to return something and that something has to come from our list, it makes sense
to make that the first element in the linked list
- Why can't we just do int max = 0?
Well, what if the list only consisted of negative integers?
Then nothing in the list would be greater than 0, which means the function would return 0
which was the default starting value
However, 0 isn't in the list, so our output would be incorrect
- To resolve this, we set our max value to the first element in the linked list
We know that there has to be at least one
If there's only one value, then great; this is our max value
If there's more than one value, then we iterate through the rest
In our for loop, you'll notice it looks pretty standard except for the first part
I used ++itr as the initialization of the counter variable
Since the prefix operator returns the variable after performing the increment, ++itr returns
an iterator that points to the spot directly after l.begin()
Basically, the second element
We already got the first element by setting max to it, so we don't need to iterate with
that again
- Now that we have iterators out of the way, here's the rest of those useful list functions!
insert( iterator, value );
This will insert a new node with the given value BEFORE the given iterator
Example:
list<int> l;
l.push_back(3);
l.push_back(5); // list consists of 3 5
list<int>::iterator itr = l.begin(); // itr points to 3
++itr; // itr now points to 5
l.insert( itr, 4 ); // list now consists of 3 4 5
To insert to the end of the list, just pass l.end() as the argument
Example:
list<int> l;
l.push_back(3); // list consists of just 3
l.insert( l.end(), 4 ); // list now consists of 3 4
Of course, I don't see why you'd do that when you could just use push_back but okayyyyy
erase( iterator );
This will delete the node at the given iterator
Example:
list<int> l;
l.push_back(3);
l.push_back(4);
l.push_back(5); // list consists of 3 4 5
Study Guide Page 60
reverse();
Reverses the order of the elements in the list
empty();
Returns whether the list is currently empty
size();
Returns the number of elements in the list right now
- One final test question to make sure you understand linked lists
Given a list of numbers (for example, 1 3 5), convert that into the following list (1 3 3 3 5 5 5
5 5) where each element is duplicated a number of times equal to its value
Also, you should delete any 0s that you find in the list
- This is good practice to try on your own!
- Here's the solution:
void convertList(list<int>& l) {
for (list<int>::iterator itr = l.begin(); itr != l.end(); ++itr)
{
if (*itr == 0)
{
list<int>::iterator temp = itr;
++itr;
l.erase(temp);
if (itr == l.end())
break;
}
else {
for (int i = 1; i < *itr; ++i)
{
l.insert(itr, *itr);
}
}
}
}
- This is not the most elegant solution, but it is currently 2:40 AM and I don't really care too much
anymore
- Again, when you manually advance an iterator inside a for loop, remember to check for the end
Study Guide Page 61
- Again, when you manually advance an iterator inside a for loop, remember to check for the end
condition and to break if you reach it
Why do we manually advance, you might ask?
Well, if we didn't, then after we erase the iterator pointing at 0, our iterator now isn't
pointing at anything
When the for loop tries to advance it by visiting the current position's next pointer, it will
throw an error
This is because the current position has no next pointer because we deleted the Node
at the current position
Queue
- Queue - a data structure where data is organized in a linear fashion and items are added at the
end of the line and items are removed from the front of the line
- This is a FIFO data structure (first in, first out); think of it the same way you think of FIFO
inventory :D
- Probably doesn't help at all. Let's think of any arbitrary line for a bank or something
When you stand in line, you join at the end of the line
People are removed from the front of the line when they are called to the window or
whatever
You move up in the line as people before you are removed
Eventually you reach the front of the line and are called upon as well
- Queues work in exactly the same way in that items are removed in the exact order that they are
put in
- Fortunately, he doesn't have his own homemade Stack and Queue class (although you will have to
implement your own for your homework)
- You can just use the STL Stack and Queue!
- To use a STL Queue, #include<queue>
- Create a queue in the same manner that you create a vector or a list
queue<char> q;
Queue Functions
push( element );
Adds an element to the end of the queue. Note that this is push, not push_back like a
vector. This is because for vectors, you can use push_back to push to the end of a vector and
push_front to push to the front of a vector. With queues, there is only ONE data entry point,
the end of the queue.
Example:
queue<int> q;
q.push(5); // q contains 5
q.push(7); // q contains 7
q.push(9); // q contains 9
front()
Returns the element from the front of the queue WITHOUT removing it.
Example:
queue<int> q;
q.push(5); // q contains 5
q.push(7); // q contains 5 7
q.push(9); // q contains 5 7 9
cout << q.front(); // will print out 5
size()
Returns the number of elements currently in the queue.
Study Guide Page 62
Stacks
- Stack - a data structure where data is organized in a pile of sorts, where new items are added to
the top of the pile and old items are removed from the top of the pile
- This is a LIFO data structure (last in, first out)
- Think of one of those stacks of plates at a buffet
You always take off plates from the top
When the restaurant waiter guy comes to add more plates, they push them onto the top of
the stack
- Again, we're given a STL stack class yay, just #include<stack>
- To create a stack: stack<char> s;
Stack Functions
push( element );
Similar to the queue's push in that there is no push_back or push_front. However, the push
command will push things to the FRONT of the stack.
Example:
stack<int> s;
s.push(5); // s contains 5
s.push(3); // s contains 3 5
s.push(1); // s contains 1 3 5
top()
Returns the element at the top of the stack, but does not remove it from the stack.
Example:
stack<int> s;
s.push(5); // s contains 5
s.push(3); // s contains 3 5
cout << s.top(); // will print out 3
size()
Returns the number of elements currently in the stack.
pop()
Removes the top element from the stack without returning it.
Example:
stack<int> s;
s.push(5); // s contains 5
s.push(3); // s contains 3 5
s.pop(); // s just contains 5 now
253 is
For other bases, we do something similar
Consider binary; there are only two digits, 0 and 1
This means we multiply by some power of 2
For instance, 110101 in binary is equivalent to:
This equals 53
Our pseudocode for this algorithm will be to divide the number by the base; the remainder
will be our ones place digit
Repeat again, the next digit will be the tens place, the next the hundreds, and so on
We then have to multiply by the base itself to the current power
Let's begin! I'm going to use the STL stack, whereas you have to use the homemade stack for
the homework so it's not exactly the same
unsigned int changeBase(unsigned int n, unsigned int base) {
stack<int> s;
while (n > 0) {
s.push(n % base);
n /= base;
}
int result = 0;
while (!s.empty()) {
result *= 10;
result += s.top();
s.pop();
}
return result;
}
So basically, I do what I said I'd do above
My first while loop is continuously dividing the number by the base and storing the
remainders
Remember that since n is an int, dividing will truncate any remainders
If it weren't an int, then we would never quit the loop :(
Once all the remainders are stored in the stack
Then I pop them off one by one, multiplying each one by 10 to move it forwards
Alternatively, I could have used a string and concatenated them to the end
However, it's easier to just multiply by 10 here
- Another problem he has you do is to check whether a string is a palindrome
For instance, racecar is a palindrome because it reads the same forwards and backwards
The pseudocode for this is rather simple as well; we want to read the string from the front
and the back at the same time
And I just realized that this could be accomplished much quicker using a single for loop and
a vector
Oh well, let's do this with stacks and queues!
We're basically going to push every char into a stack and a queue at the same time
Then we loop through the stack and queue, using the stack's top method and queue's
front method
If at any point the two don't equal each other, then we've got a problem and we can
return false
Otherwise, we'll return true if we can iterate through the entire string with no
problems
The code is below
Templates
- The only reason you care about this for the midterm is so you can find errors
- He might ask you to template things on the final though as a free response question
Let's hope he doesn't :(
Overview
- Ever write a piece of code for integers and think to yourself, man, I really wish this worked for
doubles as well?
- Probably not.
- For the sake of argument, let's say you did
- ^obligatory XKCD
- Anyways, the purpose of templating is to make functions work in general cases
- For instance, suppose we have the following function
int sum( int a, int b ) {
int result = a + b;
return result;
}
Study Guide Page 66
}
- Not a very useful function since we could just do a + b, but roll with it
- Anyways, our function only works for ints. What if we wanted to do the exact same thing using
doubles instead?
- Well, we'd have to write a whole new function with doubles
double sum( double a, double b ) {
double result = a + b;
return result;
}
- How irritating, right? I found that irritating and I copied and pasted that snippet too :|
- Well, with templating, we can generalize code for an arbitrary variable type!
Function Templates
- Yes, there are other types of templating too which we'll get to later
- To make a function a templated function:
Use the keyword template followed by a list enclosed in <> of type parameters
Replace every occurrence of a type to parametrize with the appropriate type parameter
- For our function above:
template<typename T>
T sum( T a, T b ) {
T result = a + b;
return result;
}
- And that's all there is to it!
- Note: older versions of C++ allow saying template<class T> instead of template<typename T>,
even if T isn't a class
This is important because he will 100% put this on the test in an attempt to trick you
template<class T> is NOT an error
- In general, templated function declarations should go in the .h file, not the .cpp file
Not really sure why
Just memorize that; he'll have you do that for your homework
- Also in general, the compiler is smart enough to know what argument type you are trying to use
int m = 2; int n = 3;
cout << sum(m, n); // will properly templatize this for ints
double x = 2.0; double y = 3.2;
cout << sum(x, y); // will properly templatize this for doubles
- What if we try to mix variable types?
- The compiler is also smart enough to throw an error!
- For example:
cout << sum(m, x);
- This will fail because m is type int and x is type double
However, when we templatized the function, we said the args were (T a, T b)
This means that both a and b are of type T
Since a is type int when we call sum(m, x), then b must also be type int
However, this is not the case, because x is type double
Therefore, the compiler will get confused and throw an error
- Another thing that may confuse the compiler is inheritance
- Suppose we have an Animal class which is a parent of the Cat class
Animal a; Cat c;
sum( a, c );
- Let's put aside how ridiculous it is that we're trying to add an animal with a cat and pretend the
operator+ was overloaded for them
However, the compiler will be confused because it won't be sure which type to use
Since c is both an Animal and a Cat, both would work
Study Guide Page 67
Specialization
- What if we had a templatized function and decided to overload it for a specific type anyways?
template<typename T>
void sayHello( T param ) {
cout << "Hello World";
}
Class Templates
- Well, we can templatize functions
- Why not take it one step further and templatize classes while we're at it?
- I mean, classes are just collections of variables and functions; we can generalize the types of these
variables and functions for the entire class
- A good example of this is the vector class
Remember how we can create a vector for any type by doing vector<type>?
Basically the same principle for templatizing your own class
- To templatize a class
Prefix the class with template<typename T>
Prefix all of the friends with template<typename T>
Place all of the function declarations in the same header file as the class
- When defining a templated class, remember that you must also place the template type in the
scope
For example, suppose we have the following:
template<typename T>
class Animal {
public:
Animal();
Study Guide Page 68
Animal();
};
template<typename T>
Animal<T>::Animal() {
// put constructor code here
}
Note that when we define the scope of the function, that is, the thing before the double
colons, we have to put the type in <> notation as well
Common Errors
- Since this will show up as an error problem on the test, I thought I'd highlight the errors he'll use
- Templates don't preserve inheritance
Suppose we have classes Animal and Cat; a Cat is a subclass of Animal
The following code will break:
vector<Animal> v;
Cat kitten;
v.push_back(kitten); // Will throw an error
Just because a vector is of type Animal does not mean that it will accept subclasses
- Templated functions/classes must have the template<typename T> prefix
Look at the top of the class declaration, the friends, and function declarations for this
If it's missing, then that's an error
Especially lookout for a missing template<typename T> for friends; this is the easiest one to
miss
- You must put <> notation for the scope declaration of a templated class
Basically, the Animal<T>::Animal() example above
Instead of that, he'll write something like:
template<typename T>
class Animal {
public:
Animal();
};
template<typename T>
Animal::Animal() {
// put constructor code here
}
This is a good opportunity to see if you can find the error yourself :P If you can't find it,
carefully compare this code to the code above
- Compiler getting confused on what type to use
Cat c;
Animal a = c;
areEqual( a, c ); // Will throw an error because the compiler doesn't know whether to use
type Cat or Animal
- Must also use <> notation when declaring an object of a templated class
Suppose we templatized the Container class
When creating an instance of the Container object, we have to use Container<type> instead
of just Container
Container h(2); // Will throw an error
Container<int> h(2); // Correct
- Not replacing a return type or variable or something with the template type T
For example
template<typename T>
double Quantity<T>::getValue() const {
Study Guide Page 69
Trees
- Thus marks the beginning of finals materials woo
- At this point, I have no idea what's going to be tested cause I haven't really taken the final yet, so
- However, I was told that you'd be given a picture and you'd have to draw some operations and
such
Overview
- Tree - a tree is a data structure with a bunch of nodes and directed edges that connect them
- If you've taken Math 61, then this should be somewhat familiar?
Basically, it'd be the same as a rooted tree, if you remember what that is
- If not
parent is
- For the most part, we're only going to deal with rooted trees in this class, since unrooted trees
aren't interesting
Rooted Tree - a tree with a designated root node that has no parent; all children can be
traced back up to it
- Remember that the height of a tree is the length of the longest path; in the example above, it is 4
- Also for the purposes of this class, we're only going to work with binary trees
Binary Tree - a tree where each child has at most 2 children
TreeNode
- So Ouellette has graciously gone and constructed his own TreeNode class, which I'm sure is going
to show up on the final probably
- This is similar to his Node class
- Each TreeNode has an int data, which is the data at that point, and two pointers to the left and
right children
- If the TreeNode does not have a leftChild or rightChild, those will equal NULL
- Uhh, I think that's all
Insertion
Binary Search Tree operations are very important since these will probably show up on the final
Let's start with insertion!
When inserting, we're obviously going to be given a TreeNode parameter to insert into the tree
If our tree is empty, then we're going to make this TreeNode the root, since there's nothing else in
the tree
- If our tree is NOT empty (which it isn't 99% of the time)
Compare the value of the parameter TreeNode to the value of the root TreeNode
If our node's value is GREATER than the value of the root, we visit the root's right child
If our node's value is SMALLER than the value of the root, then we visit the root's left child
- We will repeat this process until we reach a point where there is no existing Node at that location
Then we will set the previous Node to point to our new Node
- Suppose we have the following tree:
-
Searching
- Well I mean, if the data structure's called a Binary Search Tree, I'd assume there would be some
searching going on here
- Fortunately, the search process with a binary search tree is identical to the binary search that we
went over earlier
- As a refresher, in case you don't remember it, our algorithm was
Start at the center of our data structure
Study Guide Page 73
Suppose we have 100 elements; then, the height of the tree will be expressed in the
following equation
This means, with a complete binary tree with 100 nodes, at most it will take 6 comparisons
Note that we used the log operator in this thing and our algorithm is O(log N) complexity
Coincidence? I think not :P
But anyways, I digress; our algorithm is O(log N) complexity because conceptually, we're just
recursively dividing our search in half as we progress
Let's say we were to expand our tree by 1000 extra nodes
We wouldn't have to make 1000 extra comparisons because we keep subdividing our search
area
Therefore, we will grow at O(log N) speed
Here's the code:
TreeNode* BinarySearchTree::find(int value, TreeNode* subtree){
if(subtree == NULL) return NULL;
else if(value == subtree->data){
return subtree;
}
else if(value < subtree->data){
return find(value, subtree->leftChild);
}
else {
Study Guide Page 74
else {
return find(value, subtree->rightChild);
}
}
Removal
- This one's a bit tricky, unfortunately
- We can't just find the node we want to delete and delete it; then all of that node's children will
become orphans :(
Note: orphan is not a CS term, I'm just making a joke here, please do not use that term on
the test :P
Actually, that's a really great term though; from now on, I'm going to refer to child nodes
with no parents as orphans :D
- Our deletion process will take two steps
Find our node to delete (this is just the stuff discussed above)
Deal with our node and its children
- What does that mean?
- We have to consider three possibilities
The node we want to delete has:
0 children
If it has no children, well, we can just remove the node no problem
That's exactly what we're going to do; just delete the Node
1 child
If it only has one child, then we can fix this easily; we'll just replace the node
we're trying to delete with our child
2 children
- Let's visualize these three cases
No Children
parent->leftChild = NULL;
target = NULL;
- There, now 4 won't remember it has any children
- Therefore, deletion of a no-child node is a two-step process: deleting the node and setting its
parent's child pointer to NULL
- We also have to set the target pointer to NULL to avoid any null pointers
One Child
Two Children
- Here's where things get more complicated, although it isn't that bad still!
Therefore, this node can't be the smallest because it has a left subchild that is smaller
than it
- Once we finally do get down to the no-children or one-children case:
Tree Traversal
- But wait, there's more! We gotta learn how to navigate a tree as well
- This is definitely going to show up on the final (as far as I've heard); you'll be given a tree and
asked to print out what it says using postfix, prefix, and in-order traversal
By the way, if you're curious, all three of the following are depth-first searches
There's one more (level-order traversal, which is breadth-first search) that will come later :P
- What are these fancy terms I'm spewing out?
Let's find out!
I feel that my excitement at this point is far too high, to the point where I'm basically just
Ouellette
It is also like 2 in the morning right now so I'm kinda on a dopamine high off of sleep
deprivation :D
Preorder Traversal
- The point of tree traversal is mainly to iterate through a tree's nodes in a certain order
- In preorder traversal, we follow the following order:
Visit the TreeNode
Recursively visit its left subtree
Recursively visit its right subtree
- Let's say we have the following tree:
- The main reason you use preorder traversal is to print out the order of nodes that, when used to
create a new binary search tree, will create the exact same tree as before
- Even though two BS trees may have the same nodes, they could look completely different
- For example, suppose we tried to construct a tree using the same nodes as above but in the order:
13456789
- Well, our root would then be 1
Since 3 is greater than 1, we would move it to the right of 1
Since 4 is greater than 1 and then greater than 3, we would move it to the right of 3
See the pattern? We'll basically end up with a linked list since every additional item is
greater than the right-most element in the tree
- Therefore, ORDER MATTERS when constructing a new BS tree, and preorder traversal is a way to
preserve that order
- I'm not going to prove it to you since you could probably do that on your own; actually, it'd be
good practice to try to reconstruct a binary search tree using the order we derived just now
In-Order Traversal
- The steps for in-order traversal are:
Recursively visit its left subtree
Visit the TreeNode
Recursively visit its right subtree
- Okay, let's try it again with the same tree
Postorder Traversal
- Also called Reverse Polish Notation, this is kind of the opposite of prefix notation
The comic will make more sense after we finish this :P
If you can understand all the comics I insert into this study guide, then chances are you will
be well prepared for the exam :P
Also for citing XKCD references on the interwebs when conversing with the denizens
of the internet
- Anyways, our ordering for postorder traversal is
Recursively visit its left subtree
Recursively visit its right subtree
Visit the TreeNode
- Notice how all three traversals have the exact same 3 commands, just in a different order
Preorder = visit first
Inorder = visit in the middle
Postorder = visit at the end
- Let's see how this all plays out on the same binary tree as before!
Let's see how this all plays out on the same binary tree as before!
- We begin at the root, but we're told to visit the left subtree before doing anything
- Now we're at 3; left subtree!
- Okay, 1; left subtree!
But wait, it doesn't exist; okay fine, right subtree
That doesn't exist either
We can finally visit 1
Order thus far: 1
- Now we back up to 3, but we have to visit the right subtree first
- We're at 4; visit left? Nothing, so visit right? 5
- Okay, at 5
Visit left, nothing, right, nada
We can finally print out 5
Our order: 1 5
- Back up to 4; since we visited the left subtree and right, we can add that to our order now: 1 5 4
- Back up to 3; since we visited its left and right subtrees, add it! 1 5 4 3
- Okay, now we're at 6
- However, we can't visit 6 yet because we haven't visited its right subtree
- We go to 8 and visit its left, which is 7
Left, nothing, right, nothing; add to order: 1 5 4 3 7
- Okay, back up to 8, now visit 8's right subtree
9 is a leaf as well, so we can print it: 1 5 4 3 7 9
- Back up to 8 again, since we've visited both left and right, we add it to the order: 1 5 4 3 7 9 8
- Finally we're at the root, and since we've done everything, we can add it to the order: 1 5 4 3 7 9 8
6
- And we're done!
- So why did we do all this? What's the point of having postorder traversal and why did I post a
comic of a sausage?
One thing at a time!
Postorder is useful for deleting all the nodes in a binary search tree without causing memory
leaks
Let's say you start at the root node and want to delete all the nodes in the tree
Well, it wouldn't really make much sense to delete the root node and then move on
because after you delete the root node, you can't access its children anymore :(
Study Guide Page 85
because after you delete the root node, you can't access its children anymore :(
Therefore, we probably want to delete from the bottom up
We want to delete the leaves first, which will make their parents leaves; then
we want to delete those new leaves, and recurse all the way until we get back
to the root
Once we have eliminated the root node's entire family, we can finally delete the root
node
Think of it as some really twisted mafia movie or something
Expression Trees
- But what about the sausage? Yeah, I'm pretty sure you don't care that much, but I do need a way
to tie into the next topic somehow
- One use of binary search trees is expression trees
Basically, consider the expression 1 + 2 * 3
- For some reason, some people find it useful to convert this into a tree, where the numbers are
leaves and the parents are nodes
- Like so!
I'm not going to teach you how to read infix notation, by the way; that should be pretty
straightforward
Reading Postfix
- For this, it'll help to imagine a stack because that's what we're going to work with
- The general idea is:
Every time we get to a number, we push it to the stack
Every time we get to an operator, we pop off the top two values of the stack, with the
second top being the left side of the expression, the operator in the middle, and the top
being the right
Repeat until we finish the whole thing!
- Okay, let's do this
- Let's try to read our 1 2 3 * + abomination
We get 1, push to the stack; our stack is 1
We get 2, push to the stack; our stack is 2 1 (I'm putting the top at the left)
We get 3, push to the stack; our stack is 3 2 1
We get *
Since this is an operator, pop off the two values in the stack with the top being the
right and second top being left
This gets us 2 * 3
We push this entire expression to the stack
Our stack is now (2 * 3) 1
Two items, 2*3 is one and 1 is the other item
We get +
We pop off the two items in our stack
Again, the top is the right side of the expression and second top is left
This yields 1 + (2 * 3)
And ta-da! We're done
- Here's another example, let's try with 2 3 + 4 *
We get 2, push to the stack: our stack is 2
We get 3, push to the stack: our stack is 3 2
We get +
This creates the expression 2 + 3, which we push back to our stack
Our stack is now the single item (2 + 3)
We get 4, push to the stack: our stack is now 4 (2 + 3)
We get *
This creates the expression (2 + 3) * 4
- And we're done!
- Hopefully this isn't too bad
- There is an algorithm to convert infix notation into postfix notation called the Shunting Yard
algorithm by Dijkstra
This is not covered in the class, but if he asks on the test to convert between the two
notations, this is certainly a much quicker way to do it or at least check your work
I'm not going to bore you with it here though, but it might be worth reading into
Reading Prefix
- Fun fact, this notation was invented because the inventor didn't want to use parentheses anymore
- Another fun fact, this is how LISP does its math! Which is ironic because LISP is a language that
uses a shitton of parentheses
- But I digress
- For prefix, it's easiest to read right to left; this is more annoying for computers, but what the heck,
we're humans
- Basically, we're going to do the same thing as postfix, just from right to left instead
- Let's try it with * + 2 3 4
Study Guide Page 87
I dunno, I just don't really see this topic being all that important
I heard he might give you a bunch of data and ask the best way to organize it
Sets are the best way to do this if you don't want duplicates, I guess
I think that's it Oh well, I'll cover this topic anyways?
Overview
- Set - a data structure that consists of a collection of distinct elements
Basically, it's just a bunch of things with no duplicates
- IMPORTANT NOTE: In his lecture notes he says that a set is unordered, but the STL set that we
work with is ordered. The STL set works by storing its elements in a binary search tree. Keep this in
mind when writing code involving sets
- Ex: a dictionary
- Fortunately for you, he didn't create his own homebrew Set or Multiset class, so we get to use the
STL sets!
- To use a STL set, include the STL set library
#include<set>
- That's all there is to it
- To create a set, create it the same way you would create a vector
set<int> s;
- Let's run through set functions (the following is a cheat sheet you should put on your final index
card)
Set Functions
insert( element );
Will insert an element into a set if that element does not already exist in the set
Example:
set<char> s;
s.insert('a'); // set now contains 'a' inside of it
s.insert('A'); // set now contains 'a' and 'A'
s.insert('a'); // set already contains 'a'; still just 'a' and 'A'
count( element );
Will count the number of occurrences of an element in a set
Study Guide Page 88
empty();
Returns whether the given set is empty or not
Example:
set<char> s;
cout << s.empty(); // Will print out 1 because this is true
s.insert('a'); // set now contains 'a' inside of it
cout << s.empty(); // Will print out 0 because now this is false
size();
Returns the number of elements in the given set
Example:
set<char> s;
cout << s.size(); // Will print out 0 because the set is empty
s.insert('a'); // set now contains 'a' inside of it
cout << s.size(); // Will print out 1 because there's one element
find( element );
Will return an iterator at the position of that element
If it cannot find the element, it will equal the iterator returned by the end() function
erase( iterator );
Will delete the element at the given iterator
erase( iterator, iterator );
Will delete all elements between the two iterators
Study Guide Page 89
Set Iterator
- Like a vector, sets also have iterators which work in exactly the same way. Nonetheless, here is
the syntax:
set<char> a;
a.insert('a');
a.insert('c');
a.insert('b');
for (set<char>::iterator itr = a.begin(); itr != a.end(); ++itr)
cout << *itr << " ";
This will print out:
abc
Because remember, STL sets are ORDERED.
Oh yeah, the syntax is identical to what you normally do for vectors or other STL objects
You can also use the reverse iterator in the same way:
for (set<char>::reverse_iterator i = a.rbegin(); i != a.rend(); ++i)
cout << *i << endl;
This will print out:
cba
- I guess all you have to remember for iterators is that it's the exact same as iterators for vectors
Basically begin, end, set<type>::iterator, set<type>::reverse_iterator, rbegin, and rend
Also that *itr gets the data from that iterator
Also you can increase an iterator's position by using ++itr but itr += 2 does not work for
reasons
Use Case
Why would we ever want to use a set?
The main thing a set has going for it is that it doesn't contain any duplicates
It also has a binary search tree under the hood, so lookup time is O(log N)
Ouellette's example for this is to use it as a spell checker, to check that every word in a given input
is in the dictionary
- His code is below:
-
if( words.count(word) == 0 )
cout << "Misspelled word " << word << endl;
}
- This takes in two input streams, a dictionary stream and a text stream
We create a set called words and we push every element from the dictionary input stream
into it
This ensures that there are no duplicates in our dictionary
I mean, if we're getting a dictionary stream with every word, why isn't the guy giving
us the dictionary smart enough to not give us duplicates so we could just use a vector?
Beats me It is more efficient to use a set though, because a vector would be O(N)
lookup time
This is because we have to use linear search to find things in a vector, whereas
since sets are built with binary search trees, they have O(log N) lookup time
Okay, so we create our dictionary set called words
While we have words in our dictionary input stream, we put them into the set
Then, we iterate through every word in the text input stream
If that word does not exist in our dictionary set, then print it out and print that it was
misspelled
Again, since words.count(word) returns either 0 or 1, we could've just used:
if( !(bool)words.count(word) )
- And that's all there is to it
Multisets
- As if a set weren't enough, someone deemed it appropriate to create a multiset to, you know,
torment us
- Multiset - a data structure similar to a set, except elements can occur multiple times
- So yeah, take the only thing that makes a set unique and get rid of it
Basically, this is just a collection of things (still sorted though, this still has a binary search
tree under the hood)
- This is also imported by using #include<set>
- To create a multiset, use the following notation:
multiset<int> bag;
- This will create a multiset of ints called bag
- The functions for multisets are the exact same as those for sets, except:
Insert will now insert duplicates
Count is no longer useless (that is, it actually returns an accurate count)
Use Case
- So why a multiset? Ouellette's example is a ballot box where each "vote" is a string of the
candidate's name
- We can quickly count the number of votes each candidate has by using .count("name")
This is probably better done as a map, but we'll get there later
- Here's his code:
void countVotes( istream& votes ) {
set<string> candidates;
multiset<string> ballotbox;
string vote;
while( votes >> vote ) {
candidates.insert(vote);
ballotbox.insert(vote);
}
for( set<string>::iterator itr = candidates.begin(); itr != candidates.end(); ++itr ) {
cout << *itr << ": " << ballotbox.count(*itr) << endl;
Study Guide Page 91
cout << *itr << ": " << ballotbox.count(*itr) << endl;
}
}
- For bonus points, rewrite this using a map once you learn maps :D
- This is a comprehensive exercise in sets and multisets
Basically, we accept an input stream of votes which are just candidate names
We push each vote into both a set and a multiset
Remember that sets don't allow duplicates
This means that the set will only have one of each name, whereas the multiset will
have every single input item
Once we read in everything, we iterate through all of the unique candidates by iterating
through the set
Then we print out the candidate's name as well as the total number of them
We obtain the total number of votes for that candidate by using the .count function
from the multiset
- And yeah, that's it; this is better done using maps
- That's all I have for you for sets and multisets :D They're pretty useless data structures in my
opinion, but I guess we gotta know them anyways
Overview
- In essence, a map is similar to an associative array
You know how arrays/vectors have indices?
If we want to get the first element in array, we'd do something like array[0]
If we wanted to get the fifth, we'd do something like array[4]
Now imagine that we weren't restricted to just numbers to put inside square brackets
What if we could put chars? Or even STRINGS?
Well you CAN
- Map - a data structure that keeps associations between elements of one type called keys and
another type called values
The stuff that goes inside the square bracket is the key, while the actual value is called a
value
For instance, let's say we have a map that is aptly named map
map['a'] == 5;
In this case, our key is 'a' and our value is 5, because when we put 'a' in the square brackets,
we get our value
- But I'm getting ahead of myself
- To use the STL map class, simply #include<map>
- Maps are created with the following syntax: map<char, int> m;
Separate the two types with a comma; the char will be the type of the key and int will be the
type of the value
- Also, keys are going to be stored in a binary search format, so there is no O(1) lookup time; rather,
it's still O(log N)
If you want O(1) lookup time, use a hashtable
This is mostly a note to me and not anything you really have to worry about :D
Use Case
- So what do we do with maps? Well, frequency counting is the best use case I guess
- Suppose you're given a string and you want to count the number of each character (I'm sure
you've done that already using vectors)
With vectors or arrays, you will have to convert the index into the character
Study Guide Page 92
With vectors or arrays, you will have to convert the index into the character
You might do a vector/array of size 26, where 0 corresponds with 'a' and 25 corresponds
with 'z'
Well, why don't we just use 'a' and 'z' as our keys directly!
Consider the following code snippet
map<char, unsigned int> frequency;
frequency['d'] = 1; // frequency['d'] now equals 1
frequency['a'] = 1; // frequency['a'] now equals 1
Note: in his lecture slides, he has the keys pointing to the same value. THIS IS WRONG. Each key
has a separate copy of a value
Like, I'm not nitpicking here or something, this is blatantly wrong.
In his picture, he has both 'd' and 'a' pointing at 1
This implies that if we change frequency['d'] = 2, then frequency['a'] will also equal 2
However it won't; frequency['d'] = 2 and frequency['a'] = 1
Um, yeah, so maps are just a special array that uses keys instead of indices
Pretty much just brush up on vectors
Map Functions
find( key );
Will search the map for the key; if found, it will return an iterator at the position of that pair
If it is NOT found, then it will equal the iterator returned by the end() function
Example:
map<char, int> m; // creates an empty map
if( m.find('a') == m.end() )
cout << "This character could not be found"; // will trigger
erase( key );
Will search the map for the pair with the given key, and then delete it from the map
erase( iterator );
Will erase the pair pointed to by the given iterator
- While these are the only functions he needs you to know for maps, do be advised that all set
functions work for maps as well
- For instance, you can use count(key) to see if the map contains anything for that key; it will still
return 0 or 1
- Not that this function was any help anyways because you could always just get the data at that
point by doing m['a']
Iterators
- Okay, if you're given a choice, do NOT use iterators for maps. Seriously.
Study Guide Page 93
- Okay, if you're given a choice, do NOT use iterators for maps. Seriously.
- The only reason you would want to use an iterator is to iterate through EVERY possible key.
THAT'S it
- If you want to change one value, then use the already overloaded operator[]
- But enough complaining, I guess I shall teach you how to use them
- Iterators for maps are almost identical except for one small difference
You know how we normally use *itr to get the data at an iterator's position? Can't do that
anymore
Instead, you have to use itr->first for the key and itr->second for the second
- The reason for this is because the map iterator actually points towards a pair object, so when you
dereference the iterator, you're actually getting the pair object
The pair object contains two members, first and second
First refers to the key
Second refers to the value
- The following is code to print out every key and value in the map:
map<char, int> m;
m['h'] = 5;
m['a'] = 3;
m['d'] = 4;
for (map<char, int>::iterator itr = m.begin(); itr != m.end(); ++itr)
{
cout << itr->first << ": " << itr->second << endl;
}
- Note that this will print out the following output:
a: 3
d: 4
h: 5
- The reason for this is because keys are stored in a binary search tree, so they will be printed out in
order based on however the key is ordered
Using a reverse iterator will print them out in backwards order, so h d a
Multimaps
- So turns out there are multimaps as well
- Multimap - a data structure that generalizes a map by allowing multiple values to be associated to
the same key
- Conceptually, think of it as a multidimensional array, except you aren't limited to just numbers as
keys
- To create a multimap, make sure you #include<map> and use the following syntax:
multimap<string, string> m;
m.insert( make_pair( "key", "value" ));
- Yes, it's a lot more irritating to insert things, but hey, you can just write this on your formula card!
Multimap Functions/Iterators
insert( make_pair( "key", "value" ));
This is how Ouellette wants you to add things into a multimap
Example:
multimap<string, string> m;
m.insert( make_pair( "joe", "PIC 10A" ));
m.insert( make_pair( "joe", "PIC 10B" ));
m.insert( make_pair( "joe", "PIC 10C" )); // joe is now attached to PIC 10A, 10B, and
10C
Study Guide Page 94
10C
erase( key );
This will delete ALL items attached to that key
Example:
multimap<string, string> m;
m.insert( make_pair( "joe", "PIC 10A" ));
m.insert( make_pair( "joe", "PIC 10B" ));
m.insert( make_pair( "joe", "PIC 10C" )); // joe is now attached to PIC 10A, 10B, and
10C
m.erase( "joe" ); // There will now be NOTHING in the multimap
erase( iterator );
This will delete the element attached to this specific iterator
Example:
multimap<string, string> m;
m.insert( make_pair( "joe", "PIC 10A" ));
m.insert( make_pair( "joe", "PIC 10B" ));
m.insert( make_pair( "joe", "PIC 10C" )); // joe is now attached to PIC 10A, 10B, and
10C
m.erase( m.find( "joe" )); // joe will still be attached to 10B and 10C
lower_bound( key );
This will return an iterator to the FIRST occurrence of the pair with the given key. First is
determined by the order the pairs were added into the multimap
Example:
multimap<string, string> m;
m.insert(make_pair("joe", "PIC 10A"));
m.insert(make_pair("joe", "PIC 10B"));
m.insert(make_pair("joe", "PIC 10C")); // joe is now attached to PIC 10A, 10B, and 10C
m.erase(m.lower_bound("joe")); // m now only contains PIC 10B and 10C
upper_bound( key );
This will return an iterator to a point AFTER the last occurrence of the pair with the given
key. Last is determined by the order the pairs were added into the multimap
Note: you CANNOT delete the last element by using m.upper_bound("joe") since this will
return an iterator to the point AFTER the last pair; can't delete NULL
To delete the last element, you have to delete the point right before the last element
This can be done by using --m.upper_bound("joe") to reach the point before the end
Example:
multimap<string, string> m;
m.insert(make_pair("joe", "PIC 10A"));
m.insert(make_pair("joe", "PIC 10B"));
m.insert(make_pair("joe", "PIC 10C")); // joe is now attached to PIC 10A, 10B, and 10C
m.erase(--m.upper_bound("joe")); // m now only contains PIC 10A and 10B
// m.erase(m.upper_bound("joe")); <-- THIS WILL THROW AN ERROR
To iterate through all the values of a given key, use the following for loop:
for (map<string, string>::iterator itr = m.lower_bound("key"); itr != m.upper_bound("key");
++itr)
cout << itr->first << ": " << itr->second << endl;
Remember that you have to use ->first and ->second to access the key and value at that iterator's
position
You could also use the equal_range function to return two iterators to use as your upper and
lower bounds
Study Guide Page 95
lower bounds
Before I show you the syntax, understand conceptually that it returns a pair of two elements
The first element is the lower_bound iterator
The second element is the upper_bound iterator
Okay, we good? Here it is
equal_range( key );
This will return a pair of two iterators, one with a lower bound for the given key and one
with the upper bound
Example:
pair<map<string, string>::iterator, map<string, string>::iterator> itr_pair =
m.equal_range("joe");
for (map<string, string>::iterator itr = itr_pair.first; itr != itr_pair.second; ++itr)
cout << itr->first << ": " << itr->second << endl;
Let's break down that syntax, shall we
We create a pair of iterators; as such, we must specify the type of the pair
Both are going to be type map<string, string>::iterator; this is something we've
seen before
We then add those two as types for the pair
Once we've set the type of the variable, then equal_range will fill in the rest
In order to access the first and second variables, we use the .first and .second parameters to
access them
Note the for loop; this is how you iterate with equal_range!
Of course, this is much longer than just using lower_bound and upper_bound, so I'd just
recommend that
max_size();
Returns the max number of elements you can store in the map
If you're wondering, it seems to be 268435455 divided by the size of the data type
I highly doubt you'll ever use this function ever. At all.
size();
Returns the number of pairs currently in the multimap; if this is a regular map, it will just be
the number of keys
empty();
Returns whether the map is empty or not
clear();
Erases all elements from the map
count( key );
Returns the number of pairs with the given key. More useful for multimaps than for maps,
but oh well.
And all of the old iterator functions, such as begin, end, rbegin, rend
Priority Queues
Overview
- Priority Queue - a data structure designed to quickly access/remove the element in the collection
with the highest priority
- Not really a queue; it's basically a data structure designed to remove the most important element
first
Study Guide Page 96
first
- As you'd probably expect, the generous gods of the STL have bestowed upon us the honor of
implementing their pre-created priority queue
#include<queue> // yes, it falls under the queue import for some reason
priority_queue<string> vocab_words;
- By default, it will assign the highest priority to the largest number or word based on the order N
<U<L
Numbers smaller than uppercase characters smaller than lowercase characters
- For example:
priority_queue<string> words;
words.push("goodbye");
words.push("latest");
words.push("zebra");
words.push("another");
cout << words.top(); // will print out zebra
- This data structure shares a lot of the same functions that the queue class uses
For instance, pop, top, push, empty, and size are all the same
- In order to modify the priority_queue to work for custom classes, overload the operator<
Priority_queue uses operator< to compare elements
- Another (more convoluted) way to accomplish the same thing is to create a comparison class
Inside this comparison class, we overload the operator() as a const member function
This will return a bool and take two of arguments (const and by reference) of our
element type
Example:
class ReverseComparison {
public:
ReverseComparison();
bool operator() (const int& l, const int& r) const;
};
- Speaking of which, it's time for your topic! Complexity analysis! Yay.
- So, I don't actually know how it works under the hood for the STL priority queue, and at this point
I don't care enough
- However, let's consider how much time it will take THEORETICALLY (since he has these in his
lecture notes)
- Let's say we have an unordered vector
To find the element with the highest priority, we have to use linear search to iterate through
This is O(N)
To remove the largest element, we just swap it with the last element and use pop_back()
We don't have to resort everything because we don't care about order!
This is O(N) for the search for the largest element and O(1) for the actual removal
process
To insert elements, we just use push_back() which is O(1)
- Let's say we have an ordered vector
To find the element with the highest priority, we can just look at the end of the vector,
which is O(1)
To remove the largest element, we can quickly remove it with pop_back() cause it's at the
end, which is O(1)
When we want to insert elements, however, we have to insert it into the vector somehow
Ouellette suggests using push_back() and then performing insertion sort on the last
element, which is O(N)
We aren't sorting the entire vector, just the last element
Heaps
- This will definitely show up on the final (or so I've heard)
- Again, you may have to pictorially represent a heap
- Or convert it to a vector or something I don't know
Overview
- This is a heap
- Note that in the final level, the nodes are as far left as possible
- If a new node were to be inserted, it would be inserted as the left child of the 3 node
The levels will fill from left to right, until all of the parent nodes have children
Then, it will move to the next level, again from left to right
Insertion
- The insertion process is actually not as bad as you might think
- This consists of two steps
Inserting an element into the next available spot
Re-heapifying the heap
Basically, fixing the heap structure to preserve the heap property
Deletion
- Deletion is kinda similar to insertion
- Three steps
Study Guide Page 100
- Three steps
Set the node you want to delete equal to the value of the last node
Delete the last node
Re-heapify to preserve the heap property
- Suppose we want to delete 8 from the following heap
- First things first: set 8 equal to 3; now the root of the tree is 3
- Delete the last element, so now the tree looks like this
Analysis of Complexity
- Of course we have to do this :P
- Let's say we're inserting a new node
There are two steps, adding the new node and performing the re-heapify process
The addition is O(1) because we just append it to the end
- What about the re-heapify process?
Well, let's consider the worst case
Worst case, our new node is the largest, which means that we will have to move it all the
way to the root
However, do we have to compare the new node to every single node in the heap?
No! We only have to compare to its immediate parent
Then, once we swap with its parent, we have to compare to its parent
We're going to repeat this process for however tall the tree is
For the insertion example above, we moved 8 all the way to the top, but we only did 2
comparisons (one with 3 and one with 7) in a tree with 7 nodes
Notice how efficient this is?
This is O(log N) complexity because we only work with half a tree at a time
- What about deletion?
There are three steps this time
Setting the value of the target node equal to the value of the last node; this is O(1)
Removing the last node; this is O(1)
Re-heapifying again
Again, when we re-heapify, we only compare to children
Worst case, we make twice as many comparisons as the bottom-up reheapify because
we have to compare to two children instead of just one parent
This is 2O(log N)
However, remember how we drop coefficients for Big O notation?
Therefore, this is also O(log N)!
- Therefore, heap procedures are very efficient
- Fun fact: heapsort is one of the most efficient sorting algorithms; basically, you construct a heap
and then just print out the max elements
Since the max elements are always the parents, finding the largest is really quick
This sorting algorithm is O(N log N) because we insert N nodes and the insertion process is
O(log N)
Again, this isn't on the final, but if you understand that this kind of process is O(N log N), you
should be well prepared for the final :D
Vector as a Heap
-
- What's great about using a vector is that inserting to the end of the tree is super easy!
Just use push_back to add something to the end
Want to remove something from the end? Pop_back
Level-Order Traversal
-
And here's the last type of tree traversal strategy! This is also known as breadth-first search
Basically, we visit the nodes on each level from left to right, before moving to the next level
This is the same as just iterating through the vector (if we were to use a vector)
For the heap above, it would just be 9 7 4 5 3 1 2
Notice how we read level 1 first (9), then level 2 from left to right (7 4), then level 3 from left
to right (5 3 1 2)
Study Guide Page 103
to right (5 3 1 2)
- You may be asked to code this on the test, given a binary tree instead of a vector
- The following is actually a really interesting algorithm to do this, and perhaps the best way to do it
as far
- Here's the code:
void BinarySearchTree::levelorder(ostream&os, TreeNode* subtree) {
if (subtree != NULL) {
os << subtree->data << " ";
tree_queue.push(subtree->leftChild);
tree_queue.push(subtree->rightChild);
}
tree_queue.pop();
}
void BinarySearchTree::levelorder(ostream& os) {
tree_queue.push(root);
while (!tree_queue.empty())
{
levelorder(os, tree_queue.front());
}
}
Basically,
the algorithm is to create a queue of TreeNode pointers
We push the root node into the queue
Then, while the queue is not empty
We take the first element from the queue
If it has a value (isn't NULL), then we print out its value
Then we push its left child and right child into the queue
Why does this work?
Let's consider the following tree
- We start at the root node, 7, and push a pointer to 7 into the queue
- This is the initialization procedure; now we begin the while loop in earnest
While our queue is not empty (it's not, we have the root node pointer)
We take the first element (the root node pointer)
We print out its value: 7
We then push pointers to its left and right child (in that order) into the queue
Now the queue contains pointers to 5 and 4, in that order
Repeat!
Since 5 is the first element in our queue, we remove it from the queue
We print its value, so now we've printed: 7 5
We push pointers to its left and right child into the queue
Study Guide Page 104
We push pointers to its left and right child into the queue
Now the queue contains 4 2 1
Repeat!
Print out 4, so now we've printed: 7 5 4
Push pointers to its left and right child into the queue (both are NULL, but they get
pushed in anyways)
Now the queue contains 2 1 NULL NULL
Repeat!
Print out 2, push in its left and right children, which are both NULL
Now the queue contains 1 NULL NULL NULL NULL
Repeat!
Print out 1; at this point we've printed out everything, which is: 7 5 4 2 1
Our queue is all nulls now
Fortunately, we have a catch in the code; if it's NULL, just pop it from the queue
- And that's it! This is level-order traversal
- Notice how it follows the same rule as before; we start from the top level, print them in order
from left to right, and then proceed to the next level
- Of course, if you're using a vector as the underlying data structure, you could just, you know, print
them out in order?
Might be a tad easier :P
- Ignore all this other clutter, I'm lazy so I'm stealing Ouellette's pictures
- Anyways, we begin at the root node, which is 1
We look at the second digit in our binary number, which is 1
This means we visit the root node's right child, so now we're at 4
- We look at our last digit, which is 0
This means we visit the left child, so now we're at 1
This is position 6!
- Here's the entire process written in code (note that Ouellette uses a stack for binary conversion
purposes)
TreeNode* Heap::get_node( unsigned_int path ) const {
if( path == 0 || path > size ) return NULL; // invalid input
TreeNode* subtree = root;
Study Guide Page 106