Strings · String Fundamentals

Run-Length Generation

Building the next term of a self-describing sequence by scanning the current term once and emitting each run's length immediately followed by its character.

  • 5 min read
  • 8 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The approach
  3. A second flavor: compression with multi-digit counts
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

Describing a string by its runs, so "aaabcc" becomes "three a's, one b, two c's", is the heart of run-length compression, of the count-and-say sequence, and of every "longest streak" question. The scan is short: find where each run ends, then emit its length and value. The bugs are in the last run, in counts of 10 or more, and in data that has no runs worth compressing.

We'll go from simple to more technical:

  1. The pattern: an outer loop over runs and an inner loop that finds where each one ends.
  2. A second flavor: compression with counts that may have several digits.
  3. Where run-length encoding shows up in production (images, columnar storage, fax).
  4. What breaks it: the last run, multi-digit counts, data without runs.

The approach

Java

String nextTerm(String s) {                        // count and say: "1211" -> "111221"
    StringBuilder sb = new StringBuilder();
    int i = 0;
    while (i < s.length()) {
        char c = s.charAt(i);
        int run = 0;
        while (i < s.length() && s.charAt(i) == c) { run++; i++; }   // consume the whole run
        sb.append(run).append(c);
    }
    return sb.toString();
}

Python

def next_term(s: str) -> str:                        # count and say: "1211" -> "111221"
    parts = []
    i = 0
    while i < len(s):
        c = s[i]
        run = 0
        while i < len(s) and s[i] == c:              # consume the whole run
            run += 1
            i += 1
        parts.append(f"{run}{c}")
    return "".join(parts)

# itertools.groupby finds the runs for you: "".join(f"{len(list(g))}{k}" for k, g in groupby(s))

The inner loop consumes an entire run, so the outer loop always starts exactly at the next run - and the last run is handled like every other one. The one-loop alternative ("if this character differs from the previous one, flush the run") has to remember to flush once more after the loop, and forgetting drops the last run.

Count-and-say starts from "1" and applies nextTerm repeatedly: 1, 11, 21, 1211, 111221, 312211, ... Only the digits 1, 2 and 3 ever appear and no run is longer than 3, so every count is a single digit. The length grows by about 30% per term (Conway's constant, about 1.3036): the 30th term already has 4,462 characters.

A second flavor: compression with multi-digit counts

Run-length encoding writes each character followed by its run length - here only when the run is longer than 1:

Java

String compress(String s) {                        // "aabcccccaaa" -> "a2bc5a3"
    StringBuilder sb = new StringBuilder();
    int i = 0;
    while (i < s.length()) {
        char c = s.charAt(i);
        int run = 0;
        while (i < s.length() && s.charAt(i) == c) { run++; i++; }
        sb.append(c);
        if (run > 1) sb.append(run);              // append(int) writes every digit: 12 -> "12"
    }
    return sb.toString();
}

Python

def compress(s: str) -> str:                         # "aabcccccaaa" -> "a2bc5a3"
    parts = []
    i = 0
    while i < len(s):
        c = s[i]
        run = 0
        while i < len(s) and s[i] == c:
            run += 1
            i += 1
        parts.append(c)
        if run > 1:
            parts.append(str(run))                   # str() writes every digit: 12 -> "12"
    return "".join(parts)

Runs can be longer than 9, so the count must be written as a number: sb.append((char) ('0' + run)) turns 10 into ':' and 12 into '<' (so does Python's chr(ord("0") + run) - write str(run)). And RLE only helps when runs are long - "abc" as "a1b1c1" doubles in size, which is why this version omits counts of 1. If the input itself can contain digits, "a12" becomes ambiguous (12 a's, or "a" then "1" twice?), so real formats restrict the alphabet or escape.

Where you'll see it

Fax machines encode each scan line as lengths of alternating white and black runs; BMP, TIFF (PackBits) and PCX images use RLE; columnar formats such as Parquet mix run-length and bit-packed encodings for repetitive columns; and compressed bitmaps (Roaring) store long stretches of set bits as runs.

Which tool for which job?

You needUse
Describe a string by its runsan outer loop per run, with an inner loop to its end
Compress repeated charactersRLE with counts written as numbers
Never expand incompressible dataa packet scheme (PackBits: runs or literal blocks)
Columns in a database or filesort first so equal values form runs, then RLE
General compressionDEFLATE or zstd, which handle runs and repeated substrings

Common mistakes

MistakeWhat happensDo this instead
No bounds check in the inner loopreads past the endi < n && s[i] == c
One-loop version without a final flushthe last run is droppedemit it after the loop
(char) ('0' + run) for the count10 becomes ':'append(run) / str(run)
Char-plus-count RLE on random dataabout double the sizePackBits-style literal blocks
Digits in the input"a12" is ambiguousrestrict the alphabet, or escape

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Why does the inner-loop scan need no special handling for the last run?

Show answer

The inner loop consumes each run completely and appends its description before the outer loop checks for more input. When the string ends, the last run has already been written. A one-loop version writes a run only when the next character differs, so it must flush once more after the loop.

2. What does sb.append((char) ('0' + run)) produce for a run of 12, and what's the fix?

Show answer

'<' - '0' + 12 is the character code 60, not the text "12". Use sb.append(run), which writes the number's digits.

3. When does run-length encoding make data bigger?

Show answer

When runs are short: every run costs a character plus a count, so "abc" becomes "a1b1c1". Omitting counts of 1 (or using literal packets) limits the damage.

Key takeaways

  • Consume each run with an inner loop; the last run then needs no special case.
  • Count-and-say only ever uses digits 1-3 and grows about 30% per term.
  • Write counts with append(int): runs can be 10 or longer.
  • RLE only pays off with long runs, and needs a digit-free alphabet or escaping to decode.

Quick revise

The whole skill on one page, for the night before an interview.

This page puts the lesson and the deep dive together. Skim it before practice or an interview, and go back to the full lesson for anything you don't recognize.

Pick the tool

You needUse
Runs of a stringouter/inner loop scan
Compress repeatsRLE with numeric counts
Never expandPackBits packets
Columnssort, then RLE
General dataDEFLATE / zstd

Snippets worth knowing by heart

Java

int i = 0;
while (i < s.length()) {
    char c = s.charAt(i);
    int run = 0;
    while (i < s.length() && s.charAt(i) == c) { run++; i++; }
    sb.append(c);
    if (run > 1) sb.append(run);                      // every digit of the count
}

Python

from itertools import groupby
parts = []
for ch, group in groupby(s):             # one (character, run) pair per run
    run = len(list(group))
    parts.append(ch + (str(run) if run > 1 else ""))
encoded = "".join(parts)

The traps

  • Inner loop bounds: the run scan reads past the end. Check i < n before s[i] == c.
  • The last run: a one-loop version drops it. Emit it after the loop.
  • Single-char counts: (char) ('0' + run) turns 10 into ':'. Append the number with append(run) or str(run).
  • Data without runs: char-plus-count roughly doubles random data. Use PackBits-style literal blocks.
  • Digits in the input: "a12" becomes ambiguous. Restrict the alphabet, or escape digits.

Staff-level points, one line each

  • 10,000 random bytes: char+count RLE gave 19,908 and PackBits 10,098. On run-heavy data, 620 against 614.
  • Sorting a 10-value column cut its runs from 90,046 to 10.
  • DEFLATE: 10,000 equal bytes became 34, repeated text 97, random data 10,011.
  • Count-and-say term 50 has 894,810 digits; the growth ratio is about 1.303, and only the digits 1-3 appear.

Say it in an interview

  • "Each run is found by an inner loop, so it's one pass and the last run is never lost."
  • "Counts can be multi-digit, so I append numbers, not characters."
  • "RLE expands data without runs; PackBits-style literals fix that."
  • "Sorting a column first is what makes RLE pay off in columnar storage."

Practice

This skill comes with 8 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Run-Length Generation free

Count and Say

MediumProCoding problemJava · Python

Build a sequence of digit strings by reading each term out loud. Start with 1. Each next term describes the one before by its runs of equal digits: 1 is "one 1", so the second term is 11; that's "two 1s", giving 21; then "one 2, one 1" gives 1211. Return the n-th term.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Run-Length Generation free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime