Problem Set 4
Due by 11:59 p.m. Eastern time on July 28, 2026.
See below for a summary of the policies regarding
late submissions.
Preliminaries
Homework is due by 11:59 p.m. Eastern time on the stated due date. If it is submitted more than 10 minutes after that time, it will be considered a full day late. There will be a 10% deduction for submissions that are up to one day late, and a 20% deduction for submissions that are 2 or 3 days late. We will not accept any homework that is more than 3 days late. Plan your time carefully, and don’t wait until the last minute to begin an assignment. Starting early will give you ample time to ask questions and obtain assistance.
In your work on this assignment, make sure to abide by our policies on academic conduct.
If you have questions while working on this assignment, please
come to office hours, post them on Ed Discussion, or email cscie22-staff@lists.fas.harvard.edu
Part I
50 points total
Creating the necessary folder
Create a subfolder called ps4 within
your s22 folder, and put all of the
files for this assignment in that folder.
Creating the necessary file
The problems from Part I will all be completed in a single PDF file. To create it, you should do the following:
-
Access the template that we have created by clicking on this link and signing into your Google account as needed.
-
When asked, click on the Make a copy button, which will save a copy of the template file to your Google Drive.
-
Select File->Rename, and change the name of the file to
ps4_partI. -
Add your work for the problems from Part I to this file.
-
Once you have completed all of these problems, choose File->Download->PDF document, and save the PDF file on your machine. The resulting PDF file (
ps4_partI.pdf) is the one that you will submit. See the submission guidelines at the end of Part I.
Problem 1 Tree traversal puzzles
10 points total; 5 points each part
-
When a binary tree of characters (which is not a binary search tree) is listed in postorder, the result is IKHAFLMBCQ. Inorder traversal gives IAKHQCFLBM. Construct the tree by editing the diagram that we have provided in section 1-1 of
ps4_partI:-
Click on the diagram and then click the Edit link that appears below the diagram.
-
We have provided the necessary nodes and edges, but you will need to move them into the appropriate positions and connect them.
-
When you have completed your edits, click the Save & Close button.
-
-
When a binary tree of characters (which is not a binary search tree) is listed in preorder, the result is BAFCIDGEHJ. Inorder traversal gives FCAIBGHEJD. Construct the tree by editing the diagram that we have provided in section 1-2 of
ps4_partI.
Problem 2 Huffman encoding
8 points total
Consider the following table of character frequencies:
| Character | Frequency |
| l | 8 |
| f | 10 |
| e | 17 |
| d | 20 |
| i | 40 |
-
(6 points) Show the Huffman tree that would be constructed from these character frequencies by editing the diagram that we have provided in section 2-1 of
ps4_partI. It includes some of the necessary nodes and edges, but you should create more of them as needed, move them into the appropriate positions, and connect them. -
(2 points) Using the Huffman tree from part 1, what will be the encoding of the string field?
Problem 3 Binary search trees
10 points; 2 points each part
Consider the following binary search tree, in which the nodes have the specified integers as keys:
-
If a postorder traversal were used to print the keys, what would the output be?
-
What would be the output of a preorder traversal?
We will cover the material needed for the remaining parts of this problem in the July 20 lecture.
-
Show the tree as it will appear if 33 is inserted, followed by 60. Edit the diagram that we have provided in section 3-3 of
ps4_partI. -
Suppose we have the original tree and that 42 is deleted and then 26 is deleted, using the algorithm from the lecture notes. Show the final tree by editing the diagram that we have provided in section 3-4 of
ps4_partI. -
Is the original tree balanced? Explain briefly why or why not.
Problem 4 Checking for keys below a value
12 points total
The code below represents one algorithm for determining if there
are any keys smaller than a given value v in an instance of our
LinkedTree class. The anySmallerInTree() method returns
true if there are any keys smaller than v in the tree/subtree
whose root node is specified by the first parameter of the
method, and it returns false if there are no such keys. The
anySmaller() method returns true if there are any keys
smaller than v in the entire tree represented by the
LinkedTree object on which the method is invoked, and false
otherwise.
public boolean anySmaller(int v) {
// make the first call to the recursive method,
// passing in the root of the tree as a whole
return anySmallerInTree(root, v);
}
private static boolean anySmallerInTree(Node root, int v) {
if (root == null) {
return false;
} else {
boolean anySmallerInLeft = anySmallerInTree(root.left, v);
boolean anySmallerInRight = anySmallerInTree(root.right, v);
return (root.key < v || anySmallerInLeft || anySmallerInRight);
}
}
-
For a binary tree with n nodes, what is the time efficiency of this algorithm as a function of n? Use big-O notation, and explain your answer briefly.
If the time efficiency depends on the keys in the tree or on the tree’s shape, you should explain why and give three big-O expressions: one for the best case, one for the worst case if the tree is balanced, and one for the worst case if the tree is not balanced. If the time efficiency does not depend on the keys or the shape of the tree, you should explain why and give one big-O expression.
-
If the tree is a binary search tree, we can revise the algorithm to take advantage of the ways in which the keys are arranged in the tree. Write a revised version of
anySmallerInTreethat does so. Your new method should avoid visiting nodes unnecessarily. In the same way that the search for a key doesn’t consider every node in the tree, your method should avoid considering subtrees that aren’t needed to determine the correct return value. Like the original version of the method above, your revised method should also be recursive.Note: In the files that we’ve given you for Part II, the
LinkedTreeclass includes the methods shown above. Feel free to replace the originalanySmallerInTree()method with your new version so that you can test its correctness. However, your new version of the method should ultimately be included in your copy ofps4_partI. -
For a binary search tree with n nodes, what is the time efficiency of your revised algorithm as a function of n?
Here again, if the time efficiency depends on the keys in the tree or on the tree’s shape, you should explain why and give three big-O expressions: one for the best case, one for the worst case if the tree is balanced, and one for the worst case if the tree is not balanced. If the time efficiency does not depend on the keys or the shape of the tree, you should explain why and give one big-O expression.
Problem 5 2-3 Trees and B-trees
10 points; 5 points each part
Let’s say that you want to insert items with the following sequence of keys:
F, C, H, I, E, J, A, D, P, M
-
Insert this sequence into an initially empty 2-3 tree by editing the diagram that we have provided in section 5-1 of
ps4_partI. Show the tree after each insertion that causes a split of one or more nodes, and the final tree.We have given you a sample diagram that includes nodes of different sizes. Make copies of the diagram so that you can use separate diagrams for the results of each insertion that causes a split, and for the final tree. Note that you do not need to keep the shape of the tree that we have given you. Rather, you should edit it as needed: deleting or adding nodes and edges, replacing the Xs with keys, adding or removing keys, and making whatever other changes are needed.
-
Insert this sequence into an initially empty B-tree of order 2 by editing the diagram that we have provided in section 5-2 of
ps4_partI. Show the tree after each insertion that causes a split of one or more nodes, and the final tree. Here again, you should make copies of the diagram that we have given you and edit them as needed.
Submitting your work for Part I
Submit your ps4_partI.pdf file by taking the following steps:
-
If you still need to create a PDF file, open your file on Google Drive, choose File->Download->PDF document, and save the PDF file on your machine.
-
Click on the name of the assignment in the list of assignments on Gradescope. You should see a pop-up window labeled Submit Assignment. (If you don’t see it, click the Submit or Resubmit button at the bottom of the page.)
-
Choose the Submit PDF option, and then click the Select PDF button and find the PDF file that you created. Then click the Upload PDF button.
-
You should see a question outline along with thumbnails of the pages from your uploaded PDF. For each question in the outline:
- Click the title of the question.
- Click the page(s) on which your work for that question can be found.
As you do so, click on the magnifying glass icon for each page and doublecheck that the pages that you see contain the work that you want us to grade.
-
Once you have assigned pages to all of the problems in the question outline, click the Submit button in the lower-right corner of the window. You should see a box saying that your submission was successful.
Important
-
It is your responsibility to ensure that the correct version of every file is on Gradescope before the final deadline. We will not accept any file after the submission window for a given assignment has closed, so please check your submissions carefully using the steps outlined above.
-
If you are unable to access Gradescope and there is enough time to do so, wait an hour or two and then try again. If you are unable to submit and it is close to the deadline, email your homework before the deadline to
cscie22-staff@lists.fas.harvard.edu
Part II
50-60 points total
Preparing for Part II
-
You should begin by downloading the following zip file:
ps4_partII.zip -
Unzip/extract the contents of the file.
-
Depending on your system, after extracting the contents you will either have:
-
a folder named
ps4_partIIthat contains all of the files that you need for the problems in Part II -
an outer folder called
ps4_partIIthat contains an inner folder namedps4_partIIthat contains all of the Java files that you need.
Take the
ps4_partIIfolder that actually contains the necessary files and drag it into yourps4folder so that you can easily find and open it from within VSCodium. -
-
Launch VSCodium on your laptop.
-
In VSCodium, select the File->Open Folder menu option, and use the resulting dialog box to find and open the
ps4_partIIfolder that you created above – the one that contains the provided files. (Note: You must open the folder; it is not sufficient to simply open one of the Java files in the folder.)The name of the folder should appear in the Explorer pane on the left-hand side of the VSCodium window, along with a list of all of its contents.
We will cover material relevant to Part II in lecture on July 20.
Problem 6 Adding methods to the LinkedTree class
25 points
Make sure to begin by following the instructions given above in the Preparing for Part II section.
In the file LinkedTree.java, add code to the LinkedTree class
that completes the following tasks:
-
Write a non-static
sumKeysTo(int key)method that takes takes an integer key as its only parameter and that uses uses iteration to determine and return the sum of the keys on the path from the root node to the node with the specified key, including the key itself. Your method should take advantage of the fact that the tree is a binary search tree, and it should avoid considering subtrees that couldn’t contain the specified key. It should return 0 if the specified key is not found in the tree.Note: There are two methods in the
LinkedTreeclass that can facilitate your testing of this method and the other methods that you’ll write:-
The
insertKeys()method takes an array of integer keys, and it processes the array from left to right, adding a node for each key to the tree using theinsert()method. (The data associated with each key is a string based on the key, although our tests will focus on just the keys.) -
The
levelOrderPrint()method performs a level-order traversal of the tree and prints the nodes as they are visited; each level is printed on a separate line. This method doesn’t show you the precise shape of the tree or the edges between nodes, but it gives you some sense of where the nodes are found.
For example, below are some examples of
depthIter(). To help you visualize the tree, it’s worth noting that we’re using an array of keys that produces the following binary tree:
If we run the following test code:
LinkedTree tree = new LinkedTree(); System.out.println("sum 1 = " + tree.sumKeysTo(13)); int[] keys = {37, 26, 42, 13, 35, 56, 30, 47, 70}; tree.insertKeys(keys); System.out.println("sum 2 = " + tree.sumKeysTo(13)); System.out.println("sum 3 = " + tree.sumKeysTo(56)); System.out.println("sum 4 = " + tree.sumKeysTo(37)); System.out.println("sum 5 = " + tree.sumKeysTo(50));we should see:
sum 1 = 0 sum 2 = 76 sum 3 = 135 sum 4 = 37 sum 5 = 0 -
-
Write two methods that together allow a client to determine the number of leaf nodes in the tree:
-
a private static method called
numLeafNodesInTree()that takes a reference to aNodeobject as its only parameter; it should use recursion to find and return the number of leaf nodes in the binary search tree or subtree whose root node is specified by the parameter. Make sure that your method correctly handles empty trees/subtrees – i.e., cases in which the value of the parameterrootisnull. -
a public non-static method called
numLeafNodes()that takes no parameters and that returns the number of leaf nodes in the entire tree. This method should serve as a “wrapper” method fornumLeafNodesInTree(). It should make the initial call to that method – passing in the root of the tree as a whole – and it should return whatever value that method returns.
For example, if we run the following tests, which use the same
keysarray as the one given above:LinkedTree tree2 = new LinkedTree(); System.out.println("count 1 = " + tree2.numLeafNodes()); int[] keys = {37, 26, 42, 13, 35, 56, 30, 47, 70}; tree2.insertKeys(keys); System.out.println("count 2 = " + tree2.numLeafNodes());we should see:
count 1 = 0 count 2 = 4 -
-
Write a non-static method
deleteSmallest()that takes no parameters and that uses iteration to find and delete the node containing the smallest key in the tree; it should also return the value of the key whose node was deleted. If the tree is empty when the method is called, the method should return -1. Your method should take advantage of the fact that the tree is a binary search tree.Important: Your
deleteSmallest()method may not call any of the otherLinkedTreemethods (including thedelete()method), and it may not use any helper methods. Rather, this method must take all of the necessary steps on its own – including correctly handling any child that the smallest node may have.For example, if we run the following tests using the same
keysarray as above:LinkedTree tree3 = new LinkedTree(); System.out.println("empty tree: " + tree3.deleteSmallest()); int[] keys = {37, 26, 42, 13, 35, 56, 30, 47, 70}; tree3.insertKeys(keys); System.out.println("levels of original tree:"); tree3.levelOrderPrint(); System.out.println("\ndeleteSmallest: " + tree3.deleteSmallest()); tree3.levelOrderPrint(); System.out.println("\ndeleteSmallest: " + tree3.deleteSmallest()); tree3.levelOrderPrint();we should see:
empty tree: -1 levels of original tree: 37 26 42 13 35 56 30 47 70 deleteSmallest: 13 37 26 42 35 56 30 47 70 deleteSmallest: 26 37 35 42 30 56 47 70Note that first we delete 13, because it is the smallest key in the original tree. Next we delete 26, because it is the smallest remaining key. As a result of these deletions, 35 and 30 move up a level in the tree.
-
Writing well-formatted units tests is an extremely important part of a programmer’s work. In the
main()method ofLinkedTree.java, we’ve given you an example of what such a unit test should look like.Update the
main()method to include at least two unit tests for each of your new methods. Your unit tests must follow the same format as our example test. In particular, the output of each of your unit tests should include:- a header that specifies the test number and a description of what is being tested
- the actual return value that you get from that test
- the expected return value
- whether the actual return value matches the expected return value.
Put each test in the context of a
try-catchblock so that you can handle any exceptions that are thrown. Leave a blank line between tests.Additional notes:
-
For part 2 (
numLeafNodesInTree()/numLeafNodes()), your unit tests only need to callnumLeafNodes(), since doing so will also callnumLeafNodesInTree(). -
Our model unit test can be used to test the
anySmaller()/anySmallerInTree()methods from Problem 4.
Problem 7 Binary tree iterator
25 points
We will cover material that will be helpful for this problem in section.
The traversal methods that are part of the LinkedTree class are
limited in two significant ways: (1) they always traverse the
entire tree; and (2) the only functionality that they support is
printing the keys in the nodes. Ideally, we would like to allow
the users of the class to traverse only a portion of the tree,
and to perform different types of functionality during the
traversal. For example, users might want to compute the sum of
all of the keys in the tree. In this problem, you will add
support for more flexible tree traversals by implementing an
iterator for our LinkedTree class.
You should use an inner class to implement the iterator, and it should implement the following interface:
public interface LinkedTreeIterator {
// Are there other nodes to see in this traversal?
boolean hasNext();
// Return the value of the key in the next node in the
// traversal, and advance the position of the iterator.
int next();
}
There are a number of types of binary-tree iterators that we
could implement. We have given you the implementation of a
preorder iterator (the inner class PreorderIterator), and you
will implement an postorder iterator for this problem.
Your postorder iterator class should implement the hasNext()
and next() methods so that, given a reference named tree to
an arbitrary LinkedTree object, the following code will perform
a complete postorder traversal of the corresponding tree:
LinkedTreeIterator iter = tree.postorderIterator();
while (iter.hasNext()) {
int key = iter.next();
// do something with key
}
Important guidelines
-
In theory, one approach to implementing a tree iterator would be to perform a full recursive traversal of the tree when the iterator is first created and to insert the visited nodes in an auxiliary data structure (e.g., a list). The iterator would then iterate over that data structure to perform the traversal. You should not use this approach. One problem with using an auxiliary data structure is that it gives your iterator a space complexity of O(n), where n is the number of nodes in the tree. Your iterator class should have a space complexity of O(1).
-
Your iterator’s
hasNext()method should have a time efficiency of O(1). -
Your iterator’s constructor and
next()methods should be as efficient as possible, given the time efficiency requirement forhasNext()and the requirement that you use no more than O(1) space. -
We encourage you to consult our implementation of the
PreorderIteratorclass when designing your class. It can also help to draw diagrams of example trees and use them to figure out what you need to do to go from one node to the next.
Here are the tasks that you should perform:
-
In order for an iterator to work, it’s necessary for each node to maintain a reference to its parent in the tree. These parent references will allow the iterator to work its way back up the tree.
The version of
LinkedTreethat we discussed in lecture did not include parent references, but we’ve included them in the copy ofLinkedTree.javathat we’ve given you for this assignment, and it is important that you start by reviewing the code that we’ve added for this purpose:-
First, note that we have added a field called
parentto the innerNodeclass:private class Node { private int key; private LLList data; private Node left; private Node right; private Node parent; // added for PS 4, Problem 7 ...This new
parentfield is assigned a value ofnullby theNodeconstructor. It doesn’t actually point to the node’s parent until theNodeobject is added to the tree by theinsertmethod. -
As discussed in lecture, the
insertmethod makes use of a local variable namedparentthat serves as a trailing reference during the search for the key that is being inserted. At the end of that search, the local variableparentholds a reference to theNodeobject that is about to become the parent of the new node. As a result, we are able to set the value of the new node’sparentfield by using the following line of code at the very end of theinsertmethod:newNode.parent = parent;Note: When we insert an item into an empty tree, the local variable
parentwill benullat the end of theinsertmethod, and thus we will end up assigningnulltonewNode.parent. This makes sense, because when we add a node to an empty tree, it becomes the root of the entire tree, and thus itsparentfield should have a value ofnull! -
When a node that has one child is deleted, that child’s parent changes. This change is handled by the following lines, which have been added to the middle of the
deleteNodemethod:if (toDeleteChild != null) { toDeleteChild.parent = parent; }
-
-
The
deleteMaxmethod that you implemented for Problem 6 can also change the parent of a node in some cases. Update yourdeleteMaxmethod so that it correctly updates theparentfield of the affectedNodeobject in such cases. -
Review the code that we’ve given you in the
PreorderIteratorclass and thepreorderIterator()method, and understand how that iterator works. We will review this iterator in section, and we have also provided an overview of it here. -
Next, add a skeleton for your iterator class, which you should name
PostorderIterator(note that only thePandIare capitalized). It should be a private inner class of theLinkedTreeclass, and it should implement theLinkedTreeIteratorinterface. Include whatever private fields will be needed to keep track of the location of the iterator. Use ourPreorderIteratorclass as a model. -
Implement the constructor for your iterator class. Make sure that it performs whatever initialization is necessary to prepare for the initial calls to
hasNext()andnext().In the
PreorderIteratorconstructor that we’ve given you, this initialization is easy, because the first node that a preorder iterator visits is the root of the tree as a whole. For an postorder iterator, however, the first node visited is not necessarily the root of the tree as a whole, and thus you will need to perform whatever steps are needed to find the first node that the postorder iterator should visit, and initialize the iterator’s field(s) accordingly. -
Implement the
hasNext()method in your iterator class. Remember that it should execute in O(1) time. -
Implement the
next()method in your iterator class. Make sure that it includes support for situations in which it is necessary to follow one or moreparentlinks back up the tree, as well as situations in which there are no additional nodes to visit. If the user calls thenext()method when there are no remaining nodes to visit, the method should throw aNoSuchElementException. -
Add an
postorderIterator()method to the outerLinkedTreeclass. It should take no parameters, and it should have a return type ofLinkedTreeIterator. It should create and return an instance of your new class. -
Test everything! At a minimum, you must do the following: In the
main()method, add a unit test that uses thewhile-loop template shown near the start of this problem to perform a full postorder traversal of a sample tree.
Problem 8 Finding keys between two values
10 points; required for grad credit; partial extra credit for others
In your LinkedTree class, add two methods that together allow a
client to find and print all keys in a binary search tree that
are between two boundary values:
-
a method with the following header:
private static void findBetweenInTree(Node root, int k1, int k2)It should process the tree or subtree whose root is specified by the parameter
root, finding and printing all keys in that tree/subtree that are betweenk1andk2inclusive (i.e., all keys greater than or equal tok1and less than or equal tok2). The relevant keys should be printed in increasing order, separated by spaces. See the important notes below for some additional guidelines. -
a public non-static method called
findBetween()that serves as a “wrapper” method for your private static method. It should take two integersk1andk2, and it should begin by checking those parameters, throwing anIllegalArgumentExceptionifk1is greater thank2. Otherwise, it should make the initial call tofindBetweenInTree– passing in the root of the tree as a whole as the first parameter.
For example, if we run the following test:
LinkedTree tree = new LinkedTree();
int[] keys = {37, 26, 42, 13, 35, 56, 30, 47, 70};
tree.insertKeys(keys);
tree.findBetween(30, 50);
we should see:
30 35 37 42 47
Important notes
-
For full credit, your static method must take full advantage of the fact that the tree is a binary search tree, and it should avoid considering subtrees that could not contain any keys between
k1andk2. -
Make sure that your static method correctly handles empty trees/subtrees – i.e., cases in which the value of the parameter
rootisnull. For such cases, it should simply return without printing anything.
Submitting your work for Part II
You should only submit your LinkedTree.java file.
Here are the steps:
-
Click on the name of the assignment in the list of assignments. You should see a pop-up window with a box labeled DRAG & DROP. (If you don’t see it, click the Submit or Resubmit button at the bottom of the page.)
-
Add the file to the box labeled DRAG & DROP. You can either drag and drop the file from its folder into the box, or you can click on the box itself and browse for the file.
-
Click the Upload button.
-
You should see a box saying that your submission was successful. Click the
(x)button to close that box. -
The Autograder will perform some tests on your file. Once it is done, check the results to ensure that the tests were passed. If one or more of the tests did not pass, the name of that test will be in red, and there should be a message describing the failure. Based on those messages, make any necessary changes. Feel free to ask a staff member for help.
Note: You will not see a complete Autograder score when you submit. That is because additional tests will be run later, after the final deadline for the submission has passed. For such problems, it is important to realize that passing all of the initial tests does not necessarily mean that you will ultimately get full credit on the problem. You should always run your own tests to convince yourself that the logic of your solutions is correct.
-
If needed, use the Resubmit button at the bottom of the page to resubmit your work. ** Important: Every time that you make a submission, you should submit all of the files for that Gradescope assignment, even if some of them have not changed since your last submission.**
-
Near the top of the page, click on the box labeled Code. Then click on the name of each file to view its contents. Check to make sure that you see the code that you want us to grade.
Important
-
It is your responsibility to ensure that the correct version of every file is on Gradescope before the final deadline. We will not accept any file after the submission window for a given assignment has closed, so please check your submissions carefully using the steps outlined above.
-
If you are unable to access Gradescope and there is enough time to do so, wait an hour or two and then try again. If you are unable to submit and it is close to the deadline, email your homework before the deadline to
cscie22-staff@lists.fas.harvard.edu