Week 8 Discussion
Written by Josh Beto
Lesson Plan
This week we’ll be covering linked list and an introduction to recursion
Note: If you can’t compile with nullptr
in c++, that’s because you’re forgetting the -std=c++11
flag when you compile i.e. g++ main.cpp -std=c++11
.
Linked List
A linked list is an alternative to a vector
. Both fundamentally represent a collection of items or data, but their implementations are different. Linked lists represent their data as a series of linked nodes. A node is an object that has both data and a pointer that points to the next node.
struct IntNode {
int value; // the actual data
IntNode* next; // a pointer to the next node
}
Typically, linked list have two node pointers, head
and tail
, which point to the first and last node in the list repsectively. To iterate through a linked list you can do a simple for
loop insead of a while
loop.
// for loop option
for (IntNode* curr = head; curr != nullptr; curr = curr->next) {}
// while loop option
IntNode* curr = head;
while (curr != nullptr) {
curr = curr->next;
}
Cases
There are 3 main cases to worry about in any linked list implementation:
head == nullptr
- The linked list is emptyhead == tail
- The linked list has one nodehead != tail
- The linked list has more than one node
NOTE: Keep in mind that the order you should check these cases should be exactly as presented in the steps. When the linked list is empty, head == tail
is true
since they both point to nullptr
.p Check them in the order of steps provided!
Case 1: head == nullptr:
- Insertion: You need to set both the
head
andtail
pointer to point to the single node being inserted. - Deletion: You can’t delete from an empty linked list! Throw an error.
Case 2: head == tail:
- Insertion: You need to set either the
head
ortail
to the node being inserted depending on if you are pre-pending or appending the node. Set the node connections as usual. - Deletion: Set
head
andtail
tonullptr
. Don’t forget to delete the node!
Case 3: head != tail:
- Insertion: Set the node connections as usual with the inserted node’s neighbors. Depending on if you are inserting at the
head
ortail
, set their respective pointers. - Deletion: Set the node connections as usual. If you delete the
head
ortail
, be sure to adjust thehead
ortail
to point to the next or previous element respectively. Don’t forget to delete the node!
Selection Sort
Selection sort is a sorting algorithm that sorts a collection by scanning for minimum values. You can find out more about selection sort here: https://www.tutorialspoint.com/data_structures_algorithms/selection_sort_algorithm.htm
A key component of selection sort is finding the minimum value of a node to the end of the list.
Given a linked list:
- 2 -> 1 -> 5 -> 4 -> 3
- Find the minimum from the node with value 5 until the end of the list: {5 -> 4 -> 3}
To solve this problem, we need another function called min(IntNode* h)
that finds the minimum value from the IntNode* h
onwards. This function should keep going until the end of the list and keep track of a int &
reference to the minimum value found in the node. We need to keep track of a reference to be able to swap out the value with the current node. We swap values instead of shifting the node pointers themselves since it is more efficient for the int
data type.
Exercises
/*
Implement the following functions
You have access to the following private variables:
IntNode* head
IntNode* tail
*/
// Removes duplicate values from the linked list
void removeDuplicates();
// Deletes that specific node from the list. Assume the node always exist in the list
void deleteNode(IntNode* toDelete);
// Deletes the first node found with the same value as the argument given
void deleteNode(int value);
// Detects if the linked list is circular i.e. the last node points back to the first node
bool isCircular();
Recursion vs. Iteration
Until now, you have been implementing your looping functions with iterative loop structures such as for
and while
.
Recursion is an alternative to using a for
or while
loop by having a function call itself.
There are two main components to recursion:
- Base case (when the recursion ends)
- Using sub-problems to answer the main problem
int fibonacci(int n) { // Base case if (n <= 1) { return n; } // Recursive step // fibonacci(n-1) and fibonacci(n-2) are sub-problems return fibonacci(n-1) + fibonacci(n-2); // we use these subproblems to answer the main problem }
Recursion uses sub-problems, which are smaller versions of the same problem to come up with the answer to the main problem. As an example, if we are interested in
fibonacci(5)
, the answer tofibonacci(5)
is the sum of the sub-problemsfibonacci(3)
andfibonacci(4)
. When we program recursively, we are only concerned with how to use these sub-problems and when the recursion ends, the base case.
Recursion is Magic
One key tip to start programming recursively is to stop tracing the function calls. You will wrap yourself in a loop trying to trace each recursive call. Instead, follow the following steps:
- Define what your recursive function does. How it does it is magic that you shouldn’t worry about.
- Define your base cases, the simplest examples you can think of
- Build up from your base case. Think about the next simplest case after the base case and use the base case to come up with your answer. This will help you realize how to use sub-problems to answer the main problem.
Note: You can have multiple base cases, be sure to think of all the simplest cases!
Let’s go over a simple example of how we can try coding recursively with the following function:
bool isPalindrome(const string &s)
-
Define what the function does:
bool isPalindrome(const string &s)
- This function returnstrue
if the passed inconst string &s
is a palindrome. - Define the base cases:
Our base cases are the emptystring
and astring
with 1 character. In both base cases, they are considered palindromes.bool isPalindrome(const string &s) { // base case if (s.size() <= 1) { return true; } }
- Build up from the base case:
Our next simplest cases are strings of length 2 and 3 respectively. For astring
of lengh 2, you can think of it as such:string: a {} b
, wherea
andb
represent two characters appended to the emptystring
. We can think of astring
of length 3 similarly:string: a {c} b
, where c is our base case of a single character. In each case, we are adding on a characters to both sides of the base case. If the inner string, our sub-problem, is a palindrome and our outer two charactersa
andb
are the same, we have a palindrome!
bool isPalindrome(const string &s) {
// base case
if (s.size() <= 1) {
return true;
}
/*
if the inner string is a palindrome and the outer two characters
are the same, we have a palindrome!
*/
return s.front() == s.back() && isPalindrome(s.substr(1, s.size() - 2));
}
Binary Search
As a quick aside, we will be lightly covering binary search. To understand what binary search is, let’s give you an example:
vector<int> v = {1, 2, 3, 4, 5}
.
I want you to tell me if the value 5 exists in our vector<int> v
. You could just scan through all the elements and test if any of the elements equal 5. However, you could do this much faster by using binary search. A key property of this vector is that it is already sorted. If you test the midpoint of v
, you can reduce your search space by half each time. Let’s run through an example.
- Test the midpoint of our vector - it’s 3.
- Since our value 5 is higher than 3 and the vector is already sorted, we only need to search the right half of the vector.
- Now we test the midpoint of the right half, 4. 4 is less than 5, so we test the right side of 4.
- Since there is only 1 element on the right side of 4, we check it and it’s 5. Our algorithm returns true.
Notice how in our example, we performed our search recursively. After we evaluated the midpoint, we recursed on the left or right half of that mid-point depending on the comparison. Our base cases are when the mid-point is equal to our value or we’ve run out of elements to check - the value doesn’t exist in our vector.
Practice
Recursion is an incredibly hard topic to understand without practice, but is essential for later data structure courses such as CS014
and CS141
. The only way to get better with recursion is to practice! Below are practice problems categorized by difficulty.
Easy
// returns the minimum value inside the vector<int> v
int findMin(const vector<int> &v);
// calculates the factorial of a given n, factorial(3) = 3 * 2 * 1
int factorial(int n);
// reverses the contents of a vector, v = {1, 2, 3} - after reverse: v = {3, 2, 1}
void reverseVector(vector<int> &v);
Medium
// Returns true if you can find the value inside the vector. The vector is SORTED
// Your algorithm MUST NOT scan through all the elements.
bool binarySearch(const vector<int> &v, unsigned start, unsigned end, int value);
// sorts the linked list recursively using selection sort
void selectionSort(IntNode* head)
Hard
/*
returns all subsets of a vector v
Example: getSubsets(v) // v = {1, 2, 3}
getSubsets should return a vector of sets:
{1}
{2}
{3}
{1, 2}
{1, 3}
{2, 3}
{1, 2, 3}
*/
vector<vector<int>> getSubsets(const vector<int> &v);
Conceptual Questions
- What’s the difference between the following code snippets?
void print(IntNode* head) { if (head == nullptr) { return; } print(head->next); cout << head->value << endl; }
void print(IntNode* head) { if (head == nullptr) { return; } cout << head->value << endl; print(head->next); }
void print(IntNode* head) { cout << head->value << endl; print(head->next); }
- Previous
- Next