Discussion 4: Recursive Methods
The goal of today’s discussion is to practice thinking recursively by developing some recursive methods involving String
s. You’ll also get some practice visualizing the call structure of a recursive method and using this to analyze its time and space complexity. As we will soon see, recursion is a powerful tool for developing sorting algorithms such as Merge Sort and Quicksort. It’s also useful when working with data structures such as trees and graphs, for which many natural traversal algorithms are most elegantly expressed recursively.
Learning Outcomes
- Draw memory diagrams that visualize the state of execution of a program involving multiple method calls and/or recursion.
- Develop recursive methods in Java given their specifications.
- Determine the number of recursive calls and the maximum depth of the call stack of a recursive method and use these to compute its time and space complexities.
Reminder: Discussion Guidelines
The work that you complete in discussion serves as a formative assessment tool, meaning it offers the opportunity to assess your understanding of the material and for our course staff to get a “pulse” on how things are going so we can make adjustments in future classes. Your grade in discussion is based entirely on attendance and participation; if you show up and you are actively engaged with the activity (working on the activity on your computer, discussing the activity with your group, asking and answering questions, etc.) for the entire 50-minute section period, you will earn full credit. If you complete the activity early, helping other students is a great way to further your own understanding. You do not need to submit any of the work that you complete during discussion.
Since discussion activities are not graded for correctness, we do not place any restrictions on resources that you may use to complete them, which include notes, books, unrestricted conversations with other students, internet searches, and the use of large language models or other generative AI. We advise you to be pragmatic about your use of these resources and think critically about whether they are enhancing your learning. Discussion activities are intended to serve as “strength training” for programming tasks we will expect on assignments and exams (and that you will encounter in future courses and careers), and their main benefit comes from critically thinking to “puzzle” them out.
The isPalindrome()
Method
A String
is a palindrome if it reads the same forwards and backwards. For the purpose of this discussion, we’ll ignore case sensitivity (and, as a challenge extension, remove all non-alphanumeric characters).
Some examples of a Palindrome include:
"racecar"
is a palindrome."Madam"
is a palindrome (ignoring case)."hello"
is not a palindrome."a"
is a palindrome (start thinking about base cases!).""
(empty string) is also a palindrome.
The goal of this discussion is to gain exposure to recursive thinking using a classic problem that can be defined in terms of smaller, structurally-identical problems.
PalindromeChecker.java
which contains a single static method isPalindrome()
and some unit tests in PalindromeCheckerTest.java
. Your task is to complete the implementation of the isPalindrome(String s)
method using recursion. Some helpful String
methods are listed at the bottom of this handout.
Input Formatting:
It will be helpful to first format the input String s
before we start our recursive implementation. Think carefully on how to format the String
by looking at the examples above.
Identify the Base Cases:
Inside the isPalindrome()
method, first check for the simplest possible inputs. When can we directly know that a String
is a palindrome without needing to do any work?
Implement the Recursive Step:
If the input is not a base case, you must shrink the problem and call the method again on a smaller piece. What smaller String
should be the argument to this recursive call, and how do we obtain this smaller String
? How should we use the result of the recursive call to finish our definition of the method?
Run Unit Tests:
PalindromeCheckerTest.java
file contains a set of 8 unit tests. Initially, 2 cases will pass due to our default implementation always returning false. Run the unit tests to verify the correctness of your implementation.
(Bonus Challenge): Handling Punctuation and Spaces
- Uncomment the last test case in
PalindromeCheckerTest.java
- The objective is to enhance your
isPalindrome()
method to make the test case: “A man, a plan, a canal: Panama” pass (example from leetcode) - Make sure to remove all characters that are non-alphanumeric(not letters or numbers).
- Hint: one way to implement this is to utilize the replaceAll() method. It uses a regular expression to match characters of interest.
String
of length \(N\), let's think about the time and space complexity of the isPalindrome()
method.
isPalindrome()
method on a String
of length \(k\). By summing this over all of the recursive calls in the stack, what is the overall time complexity of isPalindrome()
in terms of the original input length \(N\)?
isPalindrome()
on a String
of length \(k\)? By summing this over all of the recursive calls in the stack, what is the overall space complexity of isPalindrome()
?
isPalindrome()
by adopting a similar strategy to the "array views" that we saw in lecture. Define a recursive helper method rangeIsPalindrome(String s, int i, int j)
that returns whether s.substring(i,j)
is a palindrome, and use this to re-implement isPalindrome()
(which should only handle the pre-processing and call this helper method), and analyze the time and space complexities of this alternate definition.
Useful String
Methods
Here, we summarize some methods from Java’s String
class that may be useful for the problems that follow.
1. int length()
[Documentation] This method returns the number of characters comprising a String
and runs in \(O(1)\) time (constant, not depending on the length of the String
).
|
|
|
|
2. char charAt(int index)
[Documentation] This method returns the character located at the specified index within the String
(where the first, leftmost index is 0). It runs in \(O(1)\) time (constant, not depending on the length of the String
).
|
|
|
|
3. String substring(int beginIndex, int endIndex)
[Documentation] This method returns a new string that is a portion of the original string. The substring begins at beginIndex
and extends to the character at endIndex - 1
. The endIndex
is exclusive. It runs in time \(O(\texttt{endIndex} - \texttt{beginIndex})\), the length of the substring that is constructed and returned.
|
|
|
|
4. String toLowerCase()
[Documentation] This method returns a new string in which all characters have been converted to lowercase. It runs in \(O(N)\) time, where \(N\) denotes the length of the String
.
|
|
|
|
5. boolean isEmpty()
[Documentation] This method is a convenient shortcut that returns true
if, and only if, length()
is 0. It runs in \(O(1)\) time (constant, not depending on the length of the String
).
|
|
|
|