Solving tips
- This is the read/write two-pointer transducer: a read pointer consumes each run, a write pointer emits the compressed output over the same array.
- State the size argument up front: a run of length L>=2 compresses to 1+digits(L) <= L characters, so write can never overtake read, making in-place safe; target O(n) time, O(1) space.
- Emit the character alone for runs of length 1 (no count), and for multi-digit lengths write each digit separately via str(run), not the number in one cell.
- Pitfall: capture the run's character before advancing read, and return write (the compressed length), ignoring the garbage cells past it.
Problem
You are given a list of characters chars. Compress it in place using run-length encoding: replace each maximal run of a repeated character with the character followed by the runβs length β except that runs of length 1 get no number. Lengths of 10 or more are written as their individual digit characters (e.g. a run of 12 as becomes 'a', '1', '2').
Overwrite the front of chars with the compressed sequence and return its length. Use O(1) extra space β building a separate string is against the rules.
Examples
Example 1
Input: chars = ["a","a","b","b","c","c","c"]
Output: 6, chars = ["a","2","b","2","c","3", ...]
Runs aa, bb, ccc become a2, b2, c3.
Example 2
Input: chars = ["a"]
Output: 1, chars = ["a"]
A single character gets no count.
Example 3
Input: chars = ["a","b","b","b","b","b","b","b","b","b","b","b","b"]
Output: 4, chars = ["a","b","1","2", ...]
a alone, then twelve bs β b followed by digits '1', '2'.
Constraints
1 <= chars.length <= 2000
chars[i] is a lowercase/uppercase letter, digit, or symbol.
- Must run in O(n) time and O(1) extra space β the in-place overwrite is the entire difficulty.
Think about it first
Hint 1
First solve it with a separate output list: scan runs, append char + count. The in-place version is the same scan β the only question is where to put the output.
Hint 2
Use a `read` pointer to find the end of each run and a `write` pointer for the compressed output. Why is it guaranteed that `write` never overtakes `read`?
Hint 3
Because a run of length L (L β₯ 2) compresses to at most 1 + digits(L) β€ L characters, the compressed prefix never outgrows the consumed input. Scan run by run: write the char, and if the run length is > 1, write each digit of the length with `str(length)`.
TL;DR
Read/write two pointers, one pass of run-length encoding in place β O(n) time, O(1) extra space.
Approach 1 β Brute force (build the output separately)
Scan the runs and append to a fresh list, then copy back. This is the right logic with the wrong memory.
from typing import List
class Solution:
def compress(self, chars: List[str]) -> int:
out: List[str] = []
i = 0
n = len(chars)
while i < n:
j = i
while j < n and chars[j] == chars[i]:
j += 1
out.append(chars[i])
run = j - i
if run > 1:
out.extend(str(run))
i = j
chars[: len(out)] = out
return len(out)
Complexity: O(n) time, O(n) space.
Time is fine; the auxiliary list violates the problemβs explicit O(1)-space requirement β that constraint, not speed, is what kills this version.
Approach 2 β In-place two pointers (read runs, write compressed)
The insight: the compressed form of any run never exceeds the run itself β a run of length L β₯ 2 becomes 1 + digits(L) characters, and 1 + digits(L) β€ L for every L β₯ 2 (a run of 2 β 2 chars, 10β99 β 3 chars, etc.), while a run of 1 stays 1 character. Therefore a write pointer can safely overwrite the array behind a read pointer that has already consumed the run: write can never overtake read.
from typing import List
class Solution:
def compress(self, chars: List[str]) -> int:
write = 0
read = 0
n = len(chars)
while read < n:
ch = chars[read]
run_start = read
while read < n and chars[read] == ch:
read += 1
run = read - run_start
chars[write] = ch
write += 1
if run > 1:
for digit in str(run):
chars[write] = digit
write += 1
return write
Walkthrough on chars = ["a","a","b","b","c","c","c"]:
| run found | run length | written | array prefix after | write |
|---|
a at 0β1 | 2 | a, 2 | ["a","2",...] | 2 |
b at 2β3 | 2 | b, 2 | ["a","2","b","2",...] | 4 |
c at 4β6 | 3 | c, 3 | ["a","2","b","2","c","3",...] | 6 |
Returns 6. β
And on chars = ["a","b","b","b","b","b","b","b","b","b","b","b","b"] (one a, twelve bs):
- Run
a, length 1 β write a only (no count). write = 1.
- Run
b, length 12 β write b, then digits '1', '2'. write = 4.
Prefix ["a","b","1","2"], returns 4. β
Complexity: O(n) time β read visits each character once, write trails it. O(1) extra space (the str(run) temporary is at most 4 characters since n β€ 2000).
Common pitfalls
- Writing
"1" for singleton runs: runs of length 1 must emit the character alone; blindly appending the count is the most common wrong answer.
- Multi-digit counts as one token: a run of 12 must become the two characters
'1', '2' β writing the string "12" into a single cell type-checks in Python but is wrong.
- Losing the run boundary: capture the runβs character before advancing
read, or the comparison target shifts mid-run.
- Returning the array or forgetting that cells past
write are garbage: the judge reads exactly chars[:write]; you donβt need to clean up the tail, but you must return write.
Pattern takeaway
This is the read/write two-pointer pattern in its βtransducerβ form: read consumes input tokens (here, whole runs), write emits output tokens, and the whole thing is safe in place because of a one-line size argument β each emitted token is no longer than the input it replaces. Whenever a rewrite provably never expands, you can stream output over input in a single pass; proving that inequality is the first thing to say out loud in an interview.