Introduction

Removing duplicate characters from a string is a common string manipulation problem in Java. It frequently appears in technical interviews, coding challenges, and real-world applications such as data cleaning, text normalization, and user input processing.

For example:

  • "hello""helo"
  • "banana""ban"
  • "programming""progamin"

In most scenarios, duplicate removal preserves the first occurrence of each character while maintaining the original order.

Advertisement

This guide explores four different approaches, from collection-based solutions to modern Java Streams, along with their advantages, limitations, and performance characteristics.


A LinkedHashSet automatically removes duplicates while preserving insertion order, making it one of the cleanest and most commonly recommended solutions.

 
import java.util.LinkedHashSet;
import java.util.Set;

public class RemoveDuplicatesLinkedHashSet {

    public static String removeDuplicates(String str) {

        if (str == null || str.isEmpty()) {
            return "";
        }

        Set<Character> uniqueCharacters =
                new LinkedHashSet<>();

        // Store unique characters
        for (char ch : str.toCharArray()) {
            uniqueCharacters.add(ch);
        }

        StringBuilder result =
                new StringBuilder();

        // Build final string
        for (char ch : uniqueCharacters) {
            result.append(ch);
        }

        return result.toString();
    }

    public static void main(String[] args) {

        System.out.println(removeDuplicates("hello"));
        System.out.println(removeDuplicates("programming"));
        System.out.println(removeDuplicates("banana"));
    }
}
 

Output

 
helo
progamin
ban
 

How It Works

  1. Traverse every character.
  2. Insert it into a LinkedHashSet.
  3. Duplicate insertions are ignored automatically.
  4. Iterate over the set to reconstruct the string.

Advantages

  • Preserves original order.
  • Very easy to understand.
  • Handles Unicode characters.
  • No manual duplicate checking.

Disadvantages

  • Requires additional memory.
  • Slight overhead from collection operations.

Time Complexity

O(n)

Space Complexity

O(n)


Method 2: Using Stream API (Java 8+)

Java Streams provide a concise functional approach using the distinct() operation.

 
public class RemoveDuplicatesStream {

    public static String removeDuplicates(String str) {

        if (str == null || str.isEmpty()) {
            return "";
        }

        return str.chars()
                  .distinct()
                  .collect(
                      StringBuilder::new,
                      (builder, ch) ->
                              builder.append((char) ch),
                      StringBuilder::append)
                  .toString();
    }

    public static void main(String[] args) {

        System.out.println(removeDuplicates("banana"));
        System.out.println(removeDuplicates("hello"));
    }
}
 

Output

 
ban
helo
 

How It Works

  • chars() converts the string into an IntStream.
  • distinct() removes duplicate character values.
  • The remaining characters are collected into a StringBuilder.

Advantages

  • Modern Java style.
  • Very concise.
  • Easy to integrate into stream pipelines.

Disadvantages

  • Less intuitive for beginners.
  • Slightly slower due to Stream overhead.

Time Complexity

O(n)

Space Complexity

O(n)


Method 3: Using StringBuilder with HashSet

This approach combines a HashSet for duplicate detection with a StringBuilder for efficient string construction.

 
import java.util.HashSet;
import java.util.Set;

public class RemoveDuplicatesHashSet {

    public static String removeDuplicates(String str) {

        if (str == null || str.isEmpty()) {
            return "";
        }

        Set<Character> seen =
                new HashSet<>();

        StringBuilder result =
                new StringBuilder();

        for (char ch : str.toCharArray()) {

            // add() returns true only for new characters
            if (seen.add(ch)) {
                result.append(ch);
            }
        }

        return result.toString();
    }

    public static void main(String[] args) {

        System.out.println(removeDuplicates("aabbcc"));
        System.out.println(removeDuplicates("mississippi"));
    }
}
 

Output

 
abc
misp
 

How It Works

The HashSet stores every character encountered.

If add() returns:

  • true → character is new → append it.
  • false → duplicate → skip it.

Advantages

  • Fast duplicate detection.
  • Preserves first occurrence.
  • Efficient for large strings.

Disadvantages

  • Requires two data structures.
  • Slightly more code than the LinkedHashSet approach.

Time Complexity

O(n)

Space Complexity

O(n)


Method 4: Using Regular Expression

Regular expressions can remove duplicate characters using a backreference.

 
public class RemoveDuplicatesRegex {

    public static String removeDuplicates(String str) {

        if (str == null || str.isEmpty()) {
            return "";
        }

        return str.replaceAll("(.)(?=.*\\1)", "");
    }

    public static void main(String[] args) {

        System.out.println(removeDuplicates("hello"));
        System.out.println(removeDuplicates("banana"));
    }
}
 

Output

 
helo
ban
 

Understanding the Regex

 
(.)
 

Captures one character.

 
(?=.*\1)
 

Checks whether the same character appears later in the string.

If it does, the current occurrence is removed.


Advantages

  • Very compact implementation.
  • Good demonstration of regex features.

Disadvantages

  • Hard to understand.
  • Slower than loop-based approaches.
  • Not recommended for beginners or performance-critical applications.

Time Complexity

Approximately O(n²) in many practical cases because of regex backtracking.

Space Complexity

Depends on regex engine implementation.


Removing Duplicates While Ignoring Case

Sometimes "A" and "a" should be treated as the same character.

 
public static String removeDuplicatesIgnoreCase(String str) {

    if (str == null || str.isEmpty()) {
        return "";
    }

    Set<Character> seen =
            new HashSet<>();

    StringBuilder result =
            new StringBuilder();

    for (char ch : str.toCharArray()) {

        char lower =
                Character.toLowerCase(ch);

        if (seen.add(lower)) {
            result.append(ch);
        }
    }

    return result.toString();
}
 

Example:

 
Input:
JavaJAVA

Output:
Jav
 

Removing Duplicates While Ignoring Spaces

 
for (char ch : str.toCharArray()) {

    if (Character.isWhitespace(ch)) {
        continue;
    }

    if (seen.add(ch)) {
        result.append(ch);
    }
}
 

Keeping the Last Occurrence Instead of the First

Traverse the string from right to left.

 
StringBuilder result =
        new StringBuilder();

Set<Character> seen =
        new HashSet<>();

for (int i = str.length() - 1; i >= 0; i--) {

    char ch = str.charAt(i);

    if (seen.add(ch)) {
        result.append(ch);
    }
}

System.out.println(result.reverse());
 

Example:

 
Input:
banana

Output:
bna
 

Performance Comparison

Method Time Complexity Space Complexity Preserves Order Readability
LinkedHashSet O(n) O(n) Excellent
Streams O(n) O(n) Good
HashSet + StringBuilder O(n) O(n) Excellent
Regular Expression O(n²) (practical) Depends Moderate

Best Practices

Validate Input

 
if (str == null || str.isEmpty()) {
    return "";
}
 

Preserve Order When Needed

Use LinkedHashSet instead of HashSet.

 
Set<Character> seen =
        new LinkedHashSet<>();
 

Ignore Case if Required

 
str = str.toLowerCase();
 

Avoid Regex for Large Strings

Regex is elegant but considerably slower than loop-based approaches.


Prefer StringBuilder

Avoid repeatedly concatenating strings.

 
StringBuilder builder =
        new StringBuilder();
 

Frequently Asked Questions

Q1: Does order matter?

Answer: Usually yes. Most interview questions expect the original order to be preserved. LinkedHashSet is the simplest solution for this requirement.


Q2: Which method is fastest?

Answer: LinkedHashSet and HashSet with StringBuilder both run in O(n) time and are generally the fastest practical solutions.


Q3: How do I ignore case?

Answer: Convert the string to lowercase (or uppercase) before processing.

 
str = str.toLowerCase();
 

Q4: What about Unicode characters?

Answer: LinkedHashSet, HashSet, and Streams work correctly with Unicode characters because they operate on Java char values.


Q5: Can I remove duplicates but keep the last occurrence?

Answer: Yes. Traverse the string from the end, store unseen characters, then reverse the result.


Q6: How do I remove only consecutive duplicates?

Answer: Compare each character with the previous one.

 
if (i == 0 || str.charAt(i) != str.charAt(i - 1)) {
    result.append(str.charAt(i));
}
 

Example:

 
Input:
aaabbbcccaaa

Output:
abca
 

Q7: How do these methods perform with large strings?

Answer: Loop-based methods scale linearly (O(n)). Regex-based solutions become noticeably slower on large inputs.


Q8: Can I exclude digits or spaces?

Answer: Yes. Add a filtering condition before processing.

 
if (!Character.isLetter(ch)) {
    continue;
}
 

Q9: How do I count removed duplicates?

Answer: Maintain a separate counter whenever a duplicate is detected.

 
if (!seen.add(ch)) {
    duplicateCount++;
}
 

Q10: Do these methods work with special characters?

Answer: Yes. All methods treat punctuation and symbols as normal characters unless you explicitly filter them out.