01How do you count how often each value appears, in one pass?
A hash map of value to count. For each item, add 1 to its count, reading a missing count as 0.
02How do you reverse an array in place?
Two pointers at the two ends. Swap them, step both inward, stop when they meet.
03Why is 10 not a palindrome number?
Read backwards it is 01, which is 1. Only 0 itself is a palindrome that ends in 0.
A string is an array of characters that you cannot change. So the moves from this chapter carry over: two pointers, a linear scan, a tally.
This lesson's main problem, Valid Anagram, is where most people stop at "sort both strings". Counting is faster, and it is the same tally you built in the last lesson.
Strings: three facts
- Read like an array. You read one character by its index, and the string knows its length.
- You cannot change one in place. Strings are in Java, Python and TypeScript. To edit, copy the characters into an array or list, change that, then join it back.
- Letters map to numbers. Every character has a number code, and "a" is 97. Subtract the code of "a", and the letters a to z become 0 to 25.
| Task | Java | Python | TypeScript |
|---|---|---|---|
| Character at i | s.charAt(i) | s[i] | s[i] |
| Length | s.length() | len(s) | s.length |
| Editable copy | s.toCharArray() | list(s) | [...s] |
| Letter to 0..25 | c - 'a' | ord(c) - ord("a") | s.charCodeAt(i) - 97 |
In Java, a char is already a number, so 'c' - 'a' is 2. For many small edits, Java also has StringBuilder.
┌────────────────────────────────────────────┐ │ │ │ letter a b c ... z │ │ code 97 98 99 ... 122 │ │ code - 97 0 1 2 ... 25 │ │ │ └────────────────────────────────────────────┘
That last fact turns a letter into an index for an array of 26 counts.
Read the problem
t has a c and no t.
- 1 ≤ s.length, t.length ≤ 5 × 10⁴
- lowercase English letters
Which pattern? Decide first, then open.
“same letters, in any order”order does not matter, counts do: a tally
“lowercase English letters”26 possible letters: an array of 26 counts
pattern: Strings
Ask these before you code:
- Different lengths? They cannot be anagrams. Return
falsebefore any other work. - Repeated letters? They count. "aab" and "abb" use the same letters, but not the same number of times.
- Upper case, spaces, emoji? Not here: only a to z. A follow-up may ask for any character.
n reaches 5 × 10⁴. O(n²) is 2.5 × 10⁹ steps, too slow. O(n log n) and O(n) both fit.
Brute force: cross out letters
For each letter of s, find a matching letter in t and cross it out. If one is missing, the answer is false.
Our example: s = "listen", t = "silent". The cells are the letters of t.
1/6Take l from s. Scan t: s, i, l. Found at 2. Cross it out.
boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
StringBuilder left = new StringBuilder(t); // letters of t not matched yet
for (char ch : s.toCharArray()) {
int i = left.indexOf(String.valueOf(ch)); // a hidden loop over t
if (i == -1) return false;
left.setCharAt(i, '.'); // cross it out
}
return true;
}The loop runs n times, and each search scans up to n cells. (In Python, in and index each scan, so twice the work, but the same growth.) So O(n²): about 2.5 × 10⁹ steps at the limit.
Better: sort both
Sorting puts equal letters side by side, so two anagrams sort to the same string. "listen" and "silent" both become "eilnst".
char[] a = s.toCharArray();
char[] b = t.toCharArray();
Arrays.sort(a);
Arrays.sort(b);
return Arrays.equals(a, b);- Java sorts a
char[]by character code. Python'ssorted(s)returns a sorted list of the characters. - In TypeScript, the default
sort()compares text, which is right for letters. (The trap with numbers is in The TypeScript toolbox for DSA.)
Cost: O(n log n) time and O(n) space for the arrays. It fits, but it puts every letter in order when the question never asked about order.
The key insight: count, do not order
Two strings are anagrams when every letter appears the same number of times in both. That is the tally from the last lesson.
Use one array of 26 counts:
- Each letter of s adds 1.
- Each letter of t takes 1 away.
- If the strings are anagrams, every count ends at 0.
Optimal: one array of 26 counts
- If the lengths differ, return
false. - Make 26 counts, all 0.
- Walk both strings together: add 1 for
s[i], take 1 fort[i]. - Return
truewhen every count is 0.
The cells are the letters of s. The side store shows only the counts that are not 0.
1/7s has l: l goes to 1. t has s: s goes to -1.
boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
int[] count = new int[26];
for (int i = 0; i < s.length(); i++) {
count[s.charAt(i) - 'a']++; // a letter of s adds one
count[t.charAt(i) - 'a']--; // a letter of t takes one away
}
for (int c : count) {
if (c != 0) return false;
}
return true;
}- Time: one walk of n steps, then 26 checks. O(n).
- Space: 26 counts, whatever the length. That is O(1).
- Any character (the follow-up): use a hash map instead of 26 slots (
HashMap<Character, Integer>,dict,Map<string, number>). Same idea, O(k) space for k different characters. - In Python,
Counter(s) == Counter(t)does the whole check in one line, with a hash map inside.
Side by side
| Approach | Idea | Time | Extra space | Fits 5 × 10⁴? |
|---|---|---|---|---|
| Cross out | find each letter of s in t | O(n²) | O(n) | no |
| Sort both | anagrams sort to the same string | O(n log n) | O(n) | yes |
| Count | +1 for s, −1 for t, all zero | O(n) | O(1) | yes |
Other string moves
The rest of this lesson's list uses moves you already know, plus two new tricks.
Two pointers from both ends
Reverse a String II gives the string as an array of characters. Reverse it in place exactly as you reversed an array in First array problems.
Palindrome Check uses the same two pointers, but compares instead of swapping:
┌──────────────────────────────────────────────┐ │ │ │ h a n n a h │ │ l r h = h, step in │ │ l r a = a, step in │ │ l r n = n, step in │ │ r l crossed: true │ │ │ └──────────────────────────────────────────────┘
boolean isPalindrome(String s) {
int l = 0;
int r = s.length() - 1;
while (l < r) {
if (s.charAt(l) != s.charAt(r)) return false;
l++;
r--;
}
return true;
}Scan from the right
Largest Odd Number in a String: a number is odd when its last digit is odd, and a longer number (with no leading zeros) is larger. So find the rightmost odd digit and keep everything up to it.
- "35427" stays "35427".
- "4206" has no odd digit, so the answer is "".
Compare column by column
Longest Common Prefix: read the first string one position at a time. At each position, check that every string has the same character there. Stop at the first mismatch. ["flower", "flow", "flight"] gives "fl".
Two maps
Isomorphic Strings: each character of s must turn into exactly one character of t, and no two characters may turn into the same one. Keep a map from s to t and a map from t to s.
- "egg" and "add" pass.
- "badc" and "baba" fail. Only the second map catches it: b and d would both turn into b.
Double the string
Rotate String: every rotation of s sits inside s + s.
┌────────────────────────────────────────────┐ │ │ │ s a b c d e │ │ s + s a b c d e a b c d e │ │ └───────┘ │ │ goal c d e a b found │ │ │ └────────────────────────────────────────────┘
boolean isRotation(String s, String goal) {
return s.length() == goal.length() && (s + s).contains(goal);
}The length check matters: "a" is inside "aa" + "aa", but "a" is not a rotation of "aa".
Count, then sort
Sort Characters by Frequency: count each character with a hash map, sort the entries by count from high to low, then repeat each character by its count. "tree" becomes "eetr". Sorting k distinct characters costs O(k log k), small next to the O(n) count.
How to spot this pattern
| Signal in the problem | Move |
|---|---|
| "same letters", "rearrange", "anagram" | count letters: 26 slots or a hash map |
| "reads the same backwards", "reverse" | two pointers from both ends |
| "prefix shared by all" | compare column by column |
| "each character maps to one character" | two maps, one each way |
| "rotation", "shift" | look inside s + s |
| "largest odd", "last digit" | scan from the right |
Before any of these, read the character set in the limits. "Lowercase English letters" means 26 counts. "Any character" means a hash map.
- 01
Anagrams have the same count of every letter. Add 1 for s, take 1 for t, and check that all 26 counts end at 0: O(n) time, O(1) space.
- 02
A string is an array you cannot change: read one character by index, and copy it into an array or list to edit. Turn a letter into 0 to 25 by subtracting the code of
a. - 03
Two pointers from both ends reverse a string or check a palindrome with no copy.
- 04
Rotations live inside
s + s. Check the lengths first.
Read more
- String on MDN, including why strings cannot be changed in place
- String.prototype.charCodeAt() on MDN, the letter-to-number step
- Valid Anagram on takeUforward, to solve it yourself
- Rotate String on takeUforward, to try the
s + strick
Next: a new chapter, starting with "What is recursion?": a function that calls itself, and the call stack that keeps track.