String Compression in Java Using Character Counts (With Examples)

In this tutorial, I will demonstrate how to implement string compression using character counts in Java. This is a common string-based coding interview question that helps you understand string traversal, consecutive character counting, and StringBuilder.

String compression is a technique that represents consecutive repeated characters using the character followed by the number of times it appears continuously.

For example, the string "aaabbcccc" can be compressed into "a3b2c4". Instead of storing every repeated character individually, the program stores each group of consecutive characters along with its count.

This technique is also known as run-length encoding (RLE). It is a simple form of data compression and frequently appears in programming exercises and technical interviews.

In this tutorial, you will learn how to implement string compression using a loop and StringBuilder, how to return the original string when compression does not reduce its length, and how to handle important edge cases.

1. Problem Statement

Write a Java program to compress a string by replacing consecutive repeated characters with the character followed by its count.

Each character must be followed by the number of consecutive times it appears, even when that number is one.

Example 1

Input:
aaabbcccc

Output:
a3b2c4

Explanation:

  • The character 'a' appears three times consecutively.
  • The character 'b' appears twice consecutively.
  • The character 'c' appears four times consecutively.

Therefore, the compressed string is "a3b2c4".

Example 2

Input:
aabcccccaaa

Output:
a2b1c5a3

Notice that the character 'a' appears in two separate groups. The program counts each consecutive group independently.

Example 3

Input:
abcd

Output:
a1b1c1d1

Even though every character appears only once, the basic compression algorithm includes a count of 1 for each character.

2. Understanding String Compression Using Character Counts

The idea is to scan the string from left to right and count how many consecutive times the current character appears.

Whenever the next character differs from the current character, the program appends the current character and its count to the compressed result.

Consider the string:

aaabbcccc

The algorithm identifies three groups:

Group Character Count Compressed Representation
aaa a 3 a3
bb b 2 b2
cccc c 4 c4

Combining these representations produces:

a3b2c4

3. Method 1: String Compression Using StringBuilder

The most practical approach is to use a loop to count consecutive characters and a StringBuilder to construct the compressed string.

StringBuilder is suitable for this problem because Java String objects are immutable. Repeatedly concatenating String objects can create unnecessary intermediate objects.

With StringBuilder, you can append characters and counts efficiently.

Algorithm

  1. Check whether the input string is null or empty.
  2. Create a StringBuilder to store the compressed result.
  3. Initialize a counter to 1.
  4. Start from the second character and compare it with the previous character.
  5. If both characters are equal, increment the counter.
  6. If they differ, append the previous character and its count, then reset the counter.
  7. After the loop, append the final character group.
  8. Return the compressed string.

Java Program Using StringBuilder

public class StringCompression {

    /**
     * Compresses consecutive repeated characters
     * using run-length encoding.
     *
     * Example: aaabbcccc becomes a3b2c4
     *
     * @param str input string
     * @return compressed string
     */
    public static String compressString(String str) {

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

        StringBuilder compressed = new StringBuilder();

        int count = 1;

        for (int i = 1; i < str.length(); i++) {

            // Check if current and previous characters match
            if (str.charAt(i) == str.charAt(i - 1)) {
                count++;
            } else {

                // Append completed character group
                compressed.append(str.charAt(i - 1));
                compressed.append(count);

                // Start counting the new character
                count = 1;
            }
        }

        // Append the final character group
        compressed.append(str.charAt(str.length() - 1));
        compressed.append(count);

        return compressed.toString();
    }

    public static void main(String[] args) {

        String str = "aaabbcccc";

        String result = compressString(str);

        System.out.println("Original String: " + str);
        System.out.println("Compressed String: " + result);
    }
}

Output

Original String: aaabbcccc
Compressed String: a3b2c4

This implementation processes each character once and produces the compressed representation using consecutive character counts.

4. Step-by-Step Explanation of the Java Program

Step 1: Check for Null or Empty Input

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

If the input is null or empty, the method returns it directly.

This prevents the program from accessing an invalid character index.

Step 2: Create a StringBuilder

StringBuilder compressed = new StringBuilder();

The StringBuilder stores the compressed result.

As the program processes the input, it appends each completed character group.

Step 3: Initialize the Counter

int count = 1;

The counter starts at 1 because the first character has already been included in the current group.

Step 4: Traverse the String

for (int i = 1; i < str.length(); i++) {

The loop starts at index 1 so that each character can be compared with the previous character.

Step 5: Count Consecutive Characters

if (str.charAt(i) == str.charAt(i - 1)) {
    count++;
}

If the current character matches the previous character, the program increments the count.

For example, when processing "aaa", the count increases from 1 to 3.

Step 6: Append a Completed Group

else {
    compressed.append(str.charAt(i - 1));
    compressed.append(count);

    count = 1;
}

When the current character differs from the previous character, the current group has ended.

The program appends the previous character followed by its count.

Then it resets the counter to 1 for the new group.

Step 7: Append the Final Group

compressed.append(str.charAt(str.length() - 1));
compressed.append(count);

The loop appends a group only when it encounters a different character.

Therefore, the final group must be appended separately after the loop finishes.

Without these statements, the last group would be missing from the compressed result.

Step 8: Return the Compressed String

return compressed.toString();

The toString() method converts the StringBuilder into the final compressed String.

5. Dry Run: String Compression Using Character Counts

Let's walk through the algorithm using the string:

aaabbcccc

The algorithm tracks consecutive characters and appends completed groups to the result.

Index Current Character Count After Processing Compressed Result So Far
0 a 1 Empty
1 a 2 Empty
2 a 3 Empty
3 b 1 a3
4 b 2 a3
5 c 1 a3b2
6 c 2 a3b2
7 c 3 a3b2
8 c 4 a3b2
After loop Final group c 4 a3b2c4

The final compressed string is:

a3b2c4

6. Method 2: Return the Original String When Compression Is Not Shorter

In some coding interviews, the requirement is slightly different.

The program must return the compressed string only when it is shorter than the original string.

If compression does not reduce the string length, the program should return the original string.

For example:

Input:
abcd

Basic Compression:
a1b1c1d1

Expected Result:
abcd

The compressed representation is longer than the original, so returning the original string is preferable under this requirement.

Java Program

public class StringCompressionOptimized {

    public static String compressString(String str) {

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

        StringBuilder compressed = new StringBuilder();

        int count = 1;

        for (int i = 1; i < str.length(); i++) {

            if (str.charAt(i) == str.charAt(i - 1)) {
                count++;
            } else {
                compressed.append(str.charAt(i - 1));
                compressed.append(count);
                count = 1;
            }
        }

        // Append the final character group
        compressed.append(str.charAt(str.length() - 1));
        compressed.append(count);

        // Return compressed string only if it is shorter
        return compressed.length() < str.length()
                ? compressed.toString()
                : str;
    }

    public static void main(String[] args) {

        String str1 = "aaabbcccc";
        String str2 = "abcd";
        String str3 = "aabb";

        System.out.println(str1 + " -> "
                + compressString(str1));

        System.out.println(str2 + " -> "
                + compressString(str2));

        System.out.println(str3 + " -> "
                + compressString(str3));
    }
}

Output

aaabbcccc -> a3b2c4
abcd -> abcd
aabb -> aabb

How This Approach Works

The compression logic remains the same as in Method 1.

The only difference is the final comparison:

return compressed.length() < str.length()
        ? compressed.toString()
        : str;

If the compressed representation is shorter, the program returns it.

Otherwise, it returns the original string.

This variation is useful for coding interview problems that explicitly require compression to reduce the output length.

7. Method 3: String Compression Using Two Pointers

You can also implement string compression using two pointers.

One pointer identifies the beginning of the current character group, while the other pointer moves forward to locate the end of that group.

Java Program Using Two Pointers

public class StringCompressionTwoPointers {

    public static String compressString(String str) {

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

        StringBuilder compressed = new StringBuilder();

        int left = 0;

        while (left < str.length()) {

            int right = left;

            // Find the end of the current character group
            while (right < str.length()
                    && str.charAt(right) == str.charAt(left)) {

                right++;
            }

            // Calculate consecutive character count
            int count = right - left;

            compressed.append(str.charAt(left));
            compressed.append(count);

            // Move to the next character group
            left = right;
        }

        return compressed.toString();
    }

    public static void main(String[] args) {

        String str = "aabcccccaaa";

        String result = compressString(str);

        System.out.println("Original String: " + str);
        System.out.println("Compressed String: " + result);
    }
}

Output

Original String: aabcccccaaa
Compressed String: a2b1c5a3

How the Two-Pointer Approach Works

The left pointer marks the beginning of a group of identical consecutive characters.

The right pointer moves forward until it reaches a different character or the end of the string.

The number of consecutive characters is:

int count = right - left;

After appending the character and its count, the program moves left to right and processes the next group.

Each character is examined a constant number of times, so the algorithm runs in linear time.

8. Time and Space Complexity

All three implementations process the input string in a single forward traversal.

Approach Time Complexity Space Complexity Behavior
StringBuilder with counter O(n) O(n) Always produces character counts
Return shorter representation O(n) O(n) Returns original if not shorter
Two pointers O(n) O(n) Processes consecutive groups

Here, n represents the length of the input string in UTF-16 code units.

Time Complexity: O(n)

Each character is processed a constant number of times.

Therefore, the time complexity is O(n).

Space Complexity: O(n)

The StringBuilder stores the compressed representation.

In the worst case, the compressed representation can be longer than the original string because every character may require an additional count.

For example:

Original:
abcd

Compressed:
a1b1c1d1

The compressed output still has a length proportional to the input length, so the space complexity is O(n).

9. Handling Important Edge Cases

Case 1: Empty String

Input:
""

Output:
""

The program returns an empty string.

Case 2: Single Character

Input:
"a"

Basic Compression:
a1

Under the shorter-result variation, the program returns "a" because it is shorter than "a1".

Case 3: All Characters Are Identical

Input:
aaaaaa

Output:
a6

Case 4: No Consecutive Repeated Characters

Input:
abcdef

Basic Compression:
a1b1c1d1e1f1

The basic algorithm always appends counts, so the compressed representation can be longer than the original string.

Case 5: Repeated Characters in Separate Groups

Input:
aabbaa

Output:
a2b2a2

The two groups of 'a' characters are counted separately because they are not consecutive.

Case 6: Uppercase and Lowercase Letters

Input:
AAaaBB

Output:
A2a2B2

Java treats uppercase and lowercase characters as different values.

Case 7: More Than Nine Consecutive Characters

Input:
aaaaaaaaaaaa

Output:
a12

The program correctly handles multi-digit counts using StringBuilder.

Case 8: Null Input

The implementations in this tutorial return null when the input string is null.

This is a design choice. In some applications, you may prefer returning an empty string or throwing an exception.

10. Important Limitations of Basic Run-Length Encoding

The character-count format used in this tutorial is intended as a simple coding interview example.

There are two important limitations to understand.

Limitation 1: Characters That Are Digits

If input strings contain digits, a compressed result may become ambiguous when you attempt to decode it.

For example, a string containing the digit '1' can produce a representation that is difficult to distinguish from a character count without additional rules.

If reliable decompression is required, define an encoding format with separators or escaping.

Limitation 2: Unicode Characters

Java strings use UTF-16 encoding, and the charAt() method returns individual UTF-16 code units.

Some Unicode characters, including many emoji, require two UTF-16 code units.

Therefore, the implementations in this tutorial count Java char values rather than complete Unicode code points or user-perceived characters.

For applications requiring full Unicode character handling, adapt the algorithm to work with Unicode code points or grapheme clusters.

11. Common Mistakes to Avoid

Mistake 1: Forgetting the Final Character Group

The most common mistake is forgetting to append the final group after the loop.

The loop appends a group only when the next character differs from the previous character.

Therefore, the final group must be handled separately.

Mistake 2: Counting All Occurrences Instead of Consecutive Occurrences

String compression based on run-length encoding counts consecutive repeated characters.

For example:

Input:
aabbaa

Correct:
a2b2a2

Incorrect:
a4b2

The incorrect result combines separate groups of 'a' characters.

Mistake 3: Forgetting to Reset the Counter

Whenever the character changes, reset the counter to 1.

Otherwise, counts from previous groups may incorrectly carry over.

Mistake 4: Using Repeated String Concatenation

Repeatedly concatenating immutable String objects inside a loop can create unnecessary intermediate objects.

Use StringBuilder for efficient string construction.

Mistake 5: Assuming Compression Always Reduces Length

Run-length encoding does not guarantee a shorter result.

If the input contains few consecutive repeated characters, the encoded representation may be longer.

Use the shorter-result variation when your requirements specify that the original string should be returned if compression provides no benefit.

12. Frequently Asked Questions

Q1. What is string compression in Java?

String compression is the process of representing repeated character sequences in a shorter or more structured form. In run-length encoding, consecutive characters are represented by the character followed by its repetition count.

Q2. What is run-length encoding?

Run-length encoding is a basic compression technique that replaces consecutive repeated values with a representation of the value and its count.

Q3. How do I compress a string using character counts in Java?

Traverse the string, count consecutive occurrences of each character, and append the character and count to a StringBuilder.

Q4. What is the time complexity of string compression?

The time complexity is O(n), where n is the number of characters processed.

Q5. Why is StringBuilder used for string compression?

StringBuilder allows efficient appending of characters and counts without repeatedly creating intermediate String objects.

Q6. Does string compression always reduce the length?

No. If the input contains few repeated characters, the compressed representation may be longer than the original string.

Q7. Can string compression handle multi-digit counts?

Yes. For example, twelve consecutive 'a' characters are represented as "a12".

Q8. Does the algorithm count nonconsecutive repeated characters together?

No. Run-length encoding counts each consecutive group separately.

Q9. Can string compression be implemented using two pointers?

Yes. One pointer marks the start of the current group, while another identifies the end of that group. The group length is the difference between their positions.

Q10. Which approach is best for Java coding interviews?

The StringBuilder approach with a character counter is usually the best starting point. It is simple, efficient, and easy to explain.

13. Conclusion

In this tutorial, you learned how to implement string compression using character counts in Java.

  • StringBuilder with a counter: Compresses consecutive repeated characters in a single traversal.
  • Return-original-if-not-shorter: Avoids returning an encoded string that is not shorter than the input.
  • Two-pointer approach: Processes consecutive character groups using start and end indexes.

For Java coding interviews, I recommend practicing the StringBuilder with character counting approach first. It demonstrates string traversal, condition checking, and efficient string construction.

Once you understand the basic algorithm, practice the shorter-result variation because interviewers sometimes add that requirement.

The same concepts are useful in problems involving consecutive character counting, run-length encoding, string decoding, and compression algorithms.

14. Official Java Documentation

For additional information about the Java APIs used in this tutorial, refer to the official Oracle documentation:

Continue Learning with Java Guides

Want to take your programming skills to the next level? Explore my practical Udemy courses on Java, Spring Boot, Microservices, Full-Stack Development, Docker, and Generative AI. You can also follow my structured learning roadmaps to become a professional developer.

By Ramesh Fadatare - Udemy Instructor Partner

Comments