Introduction
This problem is essentially equivalent to this HackerRank problem.
The task is to remove all duplicate characters that are adjacent to each other in a given String
of lowercase ASCII characters in range ['a','z']
, until no adjacent characters are the same. If a String
only contains adjacent duplicate characters, then return an empty String
.
Examples:
baab
=>bb
=>""
(return
an emptyString
)aabcd
=>bcd
Implementation
The core implementation logic uses Stack
s to do most of the work.
- Translate the input
String
to aStack
made up ofCharacter
s - Create a new
Stack
that will contain the non-duplicateCharacter
s which will be outputted pop
through the inputStack
- If the output
Stack
is empty, add the character - If the first element of the output
Stack
is the sameCharacter
as the poppedCharacter
thenpop
the outputStack
because this indicates that the two adjacentCharacter
s are duplicate
- Else
push
thepop
ped inputStack
element to the outputStack
- If the output
Feedback
I'm looking for general feedback as I'm still pretty green to Java. So if I have some serious design pattern issues, or if I'm missing something very obvious, I'd love to get feedback around that.
A couple thoughts on my mind:
- Initially I tried implementing logic that iterated through
List
s which worked but isn't as efficient as this implementation. One thing that I do like about this implementation is that it identifies the duplicate characters in one iteration, which you could achieve throughList
s, I think, but the logic is more intuitive when usingStack
s (I think). - Naming
- I think the name
AdjacentDuplicateCharactersRemover
is relatively appropriate, but what are good names for the underlying methods? I struggled with this for a while because they are both doing similar things (removing / filtering characters).
- I think the name
Actual Implementation
public class AdjacentDuplicateCharactersRemover {
public static String removeAdjacentDuplicateCharacters(final String candidate) {
if (candidate.length() < 2) {
return candidate;
}
final Stack<Character> originalCharacters = new Stack<>();
for (char c : candidate.toCharArray()) {
originalCharacters.push(c);
}
final Stack<Character> filteredCharacters = AdjacentDuplicateCharactersRemover.filterAdjacentDuplicateCharacters(originalCharacters);
final StringBuilder stringBuilder = new StringBuilder();
while (!filteredCharacters.empty()) {
stringBuilder.append(filteredCharacters.pop());
}
return stringBuilder.toString();
}
public static Stack<Character> filterAdjacentDuplicateCharacters(final Stack<Character> characters) {
if (characters.size() < 2) {
return characters;
}
final Stack<Character> filteredCharacters = new Stack<>();
while (!characters.empty()) {
final char poppedChar = characters.pop();
if (filteredCharacters.empty() || poppedChar != filteredCharacters.peek()) {
filteredCharacters.push(poppedChar);
} else {
filteredCharacters.pop();
}
}
return filteredCharacters;
}
}
4 Answers 4
The naming of the class is OK, I think. It could be better named AdjacentSameCharacterPairsRemover
. "Same" is shorter than "duplicate". "Pairs" corrects a misunderstanding that I initially had, which was that "aaa"
→ ""
instead of "aaa"
→ "a"
. The name still wouldn't convey the idea that the elimination is applied repeatedly, but there's only so much information that you can fit into a name.
I don't think that the function name needs to be that long, if the class name is already descriptive. The parameter name, candidate
, is peculiar. What office is that string running for, or what validation are you performing on it?
I don't recommend marking parameters and local variables as final
— it just adds visual clutter. Functions should be short enough that you can easily keep track of all the variables in your head simultaneously.
You don't need the special case for if (candidate.length() < 2)
; the algorithm works just fine without it. The added complexity isn't worth the potential performance benefit.
I would work directly with the char[]
and dispense with the Stack
and StringBuilder
. By doing so, you would eliminate the need for extra storage, the boxing of char
s as Character
s, the synchronized
behaviour of java.util.Stack
, and the semi-deprecated status of java.util.Stack
. You also would't need to push all of the original characters onto the stack and then filter it, since the character array is the input. You wouldn't need to translate the Stack
back to a StringBuilder
to make the String
. The code would be simplified so much that you could eliminate the helper function altogether.
public class AdjacentSameCharacterPairsRemover {
// Suppress the default constructor
private AdjacentSameCharacterPairsRemover() {}
public static String process(String s) {
char[] chars = s.toCharArray();
int len = 0; // Length of result (initially empty)
for (int i = 0; i < chars.length; i++) {
if (len == 0 || chars[i] != chars[len - 1]) {
chars[len++] = chars[i]; // Push one char onto "stack"
} else {
len--; // Pop one char from "stack"
}
}
return new String(chars, 0, len);
}
}
-
\$\begingroup\$ What's the reason behind suppressing the default constructor? Is the idea here that if there's nothing to construct then don't allow anybody the ability to do so? \$\endgroup\$Jae Bradley– Jae Bradley2016年07月03日 05:41:08 +00:00Commented Jul 3, 2016 at 5:41
-
\$\begingroup\$ Less clutter in the generated bytecode. Better indication of how the class is meant to be used. Harmless either way. Mainly a matter of taste. \$\endgroup\$200_success– 200_success2016年07月03日 05:42:04 +00:00Commented Jul 3, 2016 at 5:42
Problem Background
Essentially this problem is asking you to remove all palindromes of even length from the string as they are first encountered. So abccbaab
becomes ab
and NOT abcc
even though both abccba
and baab
are even-length palindromes.
Naming
Unfortunately, I cannot think of a succinct name for what operation this function actually does. You could go with removeFirstEncounteredEvenLengthPalindromes
but that is a bit much. I would just stick with removeEvenPalindromes
and document the function with some commented examples.
Method
The good news is that you do have the right solution by using a stack. The bad news is that you are using some unnecessary intermediary data types for processing. Note that you do not need to put the data into an actual stack. Instead you can just use the output string as the stack. By eliminating these intermediary data types and processing steps, the code is much more readable.
Here is a char array solution:
public static String removeEvenPalindromes(final String input)
{
char[] inputArray = input.toCharArray();
int n = inputArray.length;
int prevIndex = -1;
for (int i = 0; i < n; i++)
{
if (prevIndex >= 0 && inputArray[i] == inputArray[prevIndex])
{
// pop a character from the stack
prevIndex--;
}
else
{
// push a character onto the stack
prevIndex++;
inputArray[prevIndex] = inputArray[i];
}
}
return new String(inputArray, 0, prevIndex+1);
}
And here is a StringBuilder solution:
public static String removeEvenPalindromes(final String input)
{
StringBuilder builder = new StringBuilder();
int inputLength = input.length();
for (int i = 0; i < inputLength; i++)
{
char inputChar = input.charAt(i);
int prevIndex = builder.length()-1;
if (prevIndex >= 0 && inputChar == builder.charAt(prevIndex))
{
// pop a character from the stack
builder.deleteCharAt(prevIndex);
}
else
{
// push a character onto the stack
builder.append(inputChar);
}
}
return builder.toString();
}
This may be just me, but your function names seem a bit long. Generally long names are fine as long as they are absolutely needed to describe their functionality, but in this case you can probably knock off "Character" from all of those names and reduce "Adjacent" to just "Adj".
public class AdjDuplicatesRemover
public static String removeAdjDuplicates(final String candidate)
public static Stack<Character> filterAdjDuplicates(final Stack<Character> characters)
-
1\$\begingroup\$ maybe I'm not reading this correctly but how does this deal with the case like
baab
which should return an emptyString
becausebaab
becomesbb
which becomes""
. \$\endgroup\$Jae Bradley– Jae Bradley2016年07月03日 04:48:24 +00:00Commented Jul 3, 2016 at 4:48
Multiple return statements
Avoid multiple return statements in a method. It hinders you to apply refactorings like "extract method".
Parameter pass-through
Do not pass an object reference in and return it again if you in other cases create a new object to return.
The problem here is that the caller is not aware of this assumption. This can be a pitfall if the caller want to work with the result AND the parameter because in one case they will differ in object identity (size>= 2) on the other case it's the same object (==) (size <2).
Do not work on parameters
This belongs to the category of defensive programming. The caller may not be aware of that you change the passed in complex parameter "Stack" AND return an alternate object. A method should have only one expected type of outcome. The "final" key word does not hinder you to change contents of a stack.
If your parameter is a String then you have no side effects because of immutablitiy of String objects. But if you use "final" (as you do) you cannot reuse the reference.
Move to object scope
Although you have no object "state" you should consider this for future extensions.
Object scope enables you to test selective parts of your algorithm. With static methods you are only able to test the whole call stack.
Of course, if you want to test static methods you may mock descending static methods with PowerMockito.
Extract methods
To adress the SRP try to identify the responsibilities and extract them in separate methods.
Hard borders (pedantic)
This something I call "Using hard borders". This is because you need more assumptions than necessary.
Use "x <= 1" instead of "x < 2" because you introduced a hidden assumption: This work great with the Integer-Type.
The Code
public class AdjacentDuplicateCharactersRemover {
public String removeAdjacentDuplicateCharacters(final String candidate) {
String result = null;
final Stack<Character> originalCharacters = createStackFrom(candidate);
if (candidate.length() <= 1) {
result = candidate;
} else {
final Stack<Character> filteredCharacters = filterAdjacentDuplicateCharacters(originalCharacters);
result = toString(filteredCharacters);
}
return result;
}
private String toString(final Stack<Character> filteredCharacters) {
final StringBuilder stringBuilder = new StringBuilder();
while (!filteredCharacters.empty()) {
stringBuilder.append(filteredCharacters.pop());
}
return stringBuilder.toString();
}
private Stack<Character> createStackFrom(final String candidate) {
final Stack<Character> originalCharacters = new Stack<>();
for (char c : candidate.toCharArray()) {
originalCharacters.push(c);
}
return originalCharacters;
}
public Stack<Character> filterAdjacentDuplicateCharacters(final Stack<Character> characters) {
final Stack<Character> filteredCharacters = new Stack<>();
if (characters.size() <= 1) {
filteredCharacters.addAll(characters);
} else {
filteredCharacters.addAll(filterDuplicates(characters));
}
return filteredCharacters;
}
private Stack<Character> filterDuplicates(final Stack<Character> characters) {
final Stack<Character> workingCopy = createWorkingCopy(characters);
Stack<Character> filteredCharacters = new Stack<>();
while (!workingCopy.empty()) {
final char poppedChar = workingCopy.pop();
if (filteredCharacters.empty() || poppedChar != filteredCharacters.peek()) {
filteredCharacters.push(poppedChar);
} else {
filteredCharacters.pop();
}
}
return filteredCharacters;
}
private Stack<Character> createWorkingCopy(final Stack<Character> characters) {
final Stack<Character> workingCopy = new Stack<>();
workingCopy.addAll(characters);
return workingCopy;
}
}