Deques
Debugging, implementing, and testing double-ended queues.1
Learning Goals
Identify flaws in a program using the debugging cycle.
- Develop a hypothesis based on what we know about the problem.
- Gather information about the program to identify the root cause.
- Make productive changes based on the root cause.
- If the bug is not fixed, return to step 1.
Learning how to apply this process to identify flaws in a program will save a lot of our time in the future. When a TA is assisting a student in office hours, they typically use exactly this process but they bring to bear more experience about where and how flaws tend to develop in a program.
Understand the difference between implementing an ADT and using an ADT.
The first part of the assignment involves fixing and developing two different data structures that both implement the Deque ADT. The second part of the assignment involves using these ADT implementations to solve the problem of identifying palindromes.
Apply your understanding of invariants towards testing programs.
Unit tests are code. Writing code for unit tests is a skill, just as writing code for programs is a skill. In several of the following homework assignments, you will be expected to verify the correctness of the programs you write, and the most convenient way to do this in an automated manner is to write tests to verify it for us.
Table of contents
Getting Started
To get the skeleton (starter) code for the homework, open IntelliJ. IntelliJ should automatically open your most recently used project containing the contents of your personal repository. In the menu bar, tap on the VCS item, then hover over the Git drilldown and tap the Pull… menu item when it’s revealed.
In the window that appears, change the “Remote” to skeleton and select skeleton/master in the “Branches to merge”. If there are no branches to merge, try tapping the refresh icon next to the remote to update the list of branches. Then, tap the blue Pull button to get the homework skeleton code.
Deque API
Deque (usually pronounced like “deck”) is an irregular acronym of double-ended queue. Double-ended queues are sequence containers with dynamic sizes that can be expanded or contracted on both ends (either its front or its back).2
Deques can do everything that both stacks and queues can do. Specifically, any deque implementation must have exactly the following operations.
public void addFirst(T item)
: Adds an item of typeT
to the front of the deque.public void addLast(T item)
: Adds an item of typeT
to the back of the deque.public boolean isEmpty()
: Returns true if deque is empty, false otherwise.public int size()
: Returns the number of items in the deque.public T removeFirst()
: Removes and returns the item at the front of the deque. If no such item exists, returns null.public T removeLast()
: Removes and returns the item at the back of the deque. If no such item exists, returns null.public T get(int index)
: Gets the item at the given index, where 0 is the front, 1 is the next item, and so forth. If no such item exists, returns null. Must not alter the deque!
Unlike lists, we do not allow users to add items to the middle of a deque.
Debugging ArrayDeque
Explore the Circular ArrayDeque Demo3 below to see how the ArrayDeque
works: it uses the same invariants as the ArrayQueue
class presented in lecture.
We’ve provided a nearly-working implementation in ArrayDeque.java
, along with a failing test in ArrayDequeTest.java
. Your task is to fix the flaw in the implementation. To the best of our understanding, there is only one problem with this class and it involves changing no more than 5 lines of code.
Follow the debugging cycle, and use the IntelliJ debugger and the Java Visualizer to aid in gathering information.
- Develop a hypothesis based on what we know about the problem.
- Gather information about the program to identify the root cause.
- Make productive changes based on the root cause.
- If the bug is not fixed, return to step 1.
As we learned in lecture, we can also gather information from the test itself. We recommend reading the code by first skimming all of ArrayDeque.java
to learn the layout of methods and variables and then analyzing ArrayDequeTest.testTricky
.
Implementing LinkedDeque
There are many ways to implement LinkedDeque
. Part of this section is to give you an opportunity to explore the Implementer’s Design Decision Hierarchy. We’ll present two recommended approaches, each with their own set of invariants, to aid you in developing the data structure, but you’re welcome to solve the problem however you prefer.
- The first approach is to use two sentinel nodes, one at the front of the deque and the other at the back.4 A sentinel node is a node that is always present in a linked data structure, even when the data structure is empty. The
item
is not used and doesn’t actually represent an item in the data structure. Using a sentinel node differs from what we saw in lecture but allows us to simplify the way that we represent an empty deque. - The second approach is to use a single sentinel node with links wrapping around to create a circular, linked data structure.4
Your implementation is subject to a few additional requirements not specified by the Deque
interface.
add
andremove
operations must not involve any looping or recursion. A single such operation must take constant time, i.e. execution time should not depend on the size of the deque.get
must use iteration, not recursion.size
must take constant time. This is already done for you.
In addition, the amount of memory that your program uses at any given time must be proportional to the number of items. For example, if you add 10,000 items to the deque, and then remove 9,999 items, the resulting size should be more like a deque with 1 item than 10,000. Do not maintain references to items that are no longer in the deque.
A template for the LinkedDeque
class is provided for you in LinkedDeque.java
. We’ve provided two sanity tests in the LinkedDequeTest
class, addIsEmptySizeTest
and addRemoveTest
. While LinkedDequeTest
is not graded, it is to your benefit to write more comprehensive tests. One place to start is to adapt ArrayDequeTest.testTricky
for LinkedDeque
by copying the unit test into the LinkedDequeTest
class and making a few changes. But it’s also important to write simpler unit tests that check to make sure that inserting one item works, for example.
Tips
- If you’re not sure how to get started, look back at LinkedIntList. You’re also welcome to refer to
LinkedStack.java
andLinkedQueue.java
from the textbook as case studies. We don’t recommend copying any code from these examples since our approaches aren’t compatible, but the ideas contained within them can be helpful to study. - Take things a little at a time. Writing tons of code all at once is going to lead to misery and only misery. If you wrote too much stuff and feel overwhelmed, comment out whatever is unnecessary.
- If your first try goes badly, don’t be afraid to scrap your code and start over. The amount of code for this part isn’t actually that much. (Our solution adds about 40 lines of code to the skeleton.)
- Work out what your
LinkedDeque
will look like on paper before you try implementing them in code! If you can find a willing friend, have them give commands, while you attempt to draw everything out. Try to come up with operations that might reveal problems with your implementation. - Make sure you think carefully about what happens if the data structure goes from empty, to some non-zero size (e.g. 4 items) back down to zero again, and then back to some non-zero size. This is a common oversight.
- If you’ve been stuck debugging for more than an hour, take a break and work on the next part first which only requires one of either
ArrayDeque
orLinkedDeque
working. - Use the Java Visualizer to aid in debugging!
Palindromes
In this part of the homework, we will write a few methods using our deque implementations and write tests for them. The tests you write for this part are graded.
- Warning
- The autograder output for this part is going to be terse and unhelpful. This is because we want you to use your tests to build confidence in your code. You will not be able to rely on the autograder for correctness.
Implementing wordToDeque
Implement Palindrome.wordToDeque(String word)
. Given a string word
, wordToDeque
should return a Deque<Character>
where the characters appear in the same order as in the word. For example, if the word is “persiflage”, then the returned deque should have ‘p’ at the front, followed by ‘e’, and so forth.
Don’t implement wordToDeque yet! Instead, run the unit test provided in PalindromeTest
first which should fail since we haven’t implemented the method. Your goal is to now pass this test by correctly implementing wordToDeque
, running the test and using the debugger to gather more information about the state of the deque that you’re creating.
Testing isPalindrome
The Palindrome.isPalindrome(String word)
method should return true if the given word is a palindrome, and false otherwise.
A palindrome is defined as a word that is the same whether it is read forwards or backwards.
For example “a”, “racecar”, and “noon” are all palindromes. “horse”, “rancor”, and “aaaaab” are not palindromes.
Any word of length 1 or 0 is a palindrome.
‘A’ and ‘a’ should not be considered equal; you don’t need to do anything special for capital letters to work properly. In fact, if you forget that capital letters exist, your code will work fine.
However, because there are no tests for this method, we have no way of telling if our implementation will work. Before we implement isPalindrome
, let’s write some tests. Add at least one test to the PalindromeTest
class that tests the isPalindrome
method.
You’ll probably find the assertTrue
and assertFalse
methods to be useful. You’re also welcome to use any other methods in the JUnit documentation. Ideally, you should write several tests, and not just one, but it’s up to you how to divide the methods as long as PalindromeTest
provides good overall coverage of the different possible inputs to isPalindrome
. It’s OK to have multiple asserts in one test, though don’t go too wild. IntelliJ can help you create test methods.
When you run PalindromeTest
, your code should fail all of the tests because we have not yet implemented isPalindrome
.
Implementing isPalindrome
Now that you have a failing test, implement the Palindrome.isPalindrome(String word)
method. Use your wordToDeque
method to simplify the implementation.
While you can technically not use a deque at all, we strongly encourage you to do so. It’s a good exercise in understanding how your choice of abstract data types and data representation (in this case, deque) will have a profound effect on how you write your code. However, don’t use the get
method of your deque. Abstract data types likes deques are useful as a way of organizing data and access patterns by restricting the ways in which we access items. The restriction of only being able to add or remove items from the front and the back of a deque affects the way we design algorithms for solving problems like isPalindrome
.
Once you’ve passed your own tests, you’re ready to move on. Keep in mind that our autograder is going to be very quiet, so you’ll want to make sure your tests are thorough so that you feel good about your code. At the very least, you should have at least one test that checks that some word is a palindrome, and one that checks that some word is not a palindrome, as well as two interesting corner cases.
- Tip
- Consider recursion. There’s a really elegant solution that uses recursion. You’ll need to create a private static helper method for this to work.
- Tip
- If you’re failing your own test and can’t figure out why, remember that you have a debugger and the Java Visualizer. Use it! Don’t just stare at your code looking for the bug. That’s too slow and tedious.
Once you have a functioning implementation of isPalindrome
, you can run the PalindromeFinder
to get a list of all English palindromes of length 4 or more.
Generalizing isPalindrome
Did you notice we included a second Palindrome.isPalindrome
method that accepts a CharacterComparator
in addition to a word? Our goal for this part is to implement this method while exercising the full test-driven development cycle.
For the purposes of palindrome detection, CharacterComparator
allows us to define what it means for two characters to be considered equal to each other by implementing the equalChars
method. OffByOne
is a class that implements CharactorComparator
such that equalChars
returns true for characters that are different by exactly one, i.e. characters that are exactly one (letter) away from each other, rather than the exact same character.
- First, write tests in
OffByOneTest
forOffByOne.equalChars
. - Second, write tests in
PalindromeTest
for the secondPalindrome.isPalindrome
method. Usenew OffByOne()
to instantiate aCharacterComparator
. - Third, implement the
isPalindrome
method and verify that it works.
OffByOne obo = new OffByOne();
obo.equalChars('a', 'b'); // true
obo.equalChars('r', 'q'); // true
obo.equalChars('a', 'e'); // false
obo.equalChars('z', 'a'); // false
obo.equalChars('a', 'a'); // false
- Note
- To allow for odd length palindromes, we do not check the middle character for equality with itself. So “flake” is an off-by-one palindrome, even though ‘a’ is not one character off from itself.
As with our earlier isPalindrome
method, any zero or one character word is considered a palindrome.
- Tip
- Even though a good solution for
Palindrome
andOffByOne
should not explicitly worry about non-alphabetical characters or uppercase letters, your tests could hypothetically be run on a poor solution, and thus in that case should try to cause errors that only apply to non-alphabetical characters.
Once you have a functioning implementation of isPalindrome
, you can modify PalindromeFinder
to use your new isPalindrome
method and OffByOne
class to list all English off-by-one palindromes of length 4 or more.
Submission
Commit and push your changes to GitLab before submitting your homework to Gradescope.
Josh Hug. 2019. Project 1A: Data Structures. In CS 61B: Data Structures, Spring 2019. https://sp19.datastructur.es/materials/proj/proj1a/proj1a
Josh Hug. 2019. Project 1B: Applying and Testing Data Structures. In CS 61B: Data Structures, Spring 2019. https://sp19.datastructur.es/materials/proj/proj1b/proj1b ↩
cplusplus.com. deque - C++ Reference. http://www.cplusplus.com/reference/deque/deque/ ↩
Josh Hug. 2019. cs61b sp19 proj1 slides. In CS 61B: Data Structures, Spring 2019. https://docs.google.com/presentation/d/1XBJOht0xWz1tEvLuvOL4lOIaY0NSfArXAvqgkrx0zpc/edit ↩
Josh Hug. 2019. cs61b lec5 2019 lists3, dllists and arrays. In CS 61B: Data Structures, Spring 2019. https://docs.google.com/presentation/d/1nRGXdApMS7yVqs04MRGZ62dZ9SoZLzrxqvX462G2UbA/edit ↩ ↩2