String basics

Reverse, compare and count the characters of strings: palindromes, anagrams and rotations.

Pattern: Strings25 min read
Quick recall3 questions
  1. 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.

  2. 02How do you reverse an array in place?

    Two pointers at the two ends. Swap them, step both inward, stop when they meet.

  3. 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.
TaskJavaPythonTypeScript
Character at is.charAt(i)s[i]s[i]
Lengths.length()len(s)s.length
Editable copys.toCharArray()list(s)[...s]
Letter to 0..25c - '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.

Each letter has a number code. Subtract 97, the code of a, and a to z become indexes 0 to 25.

That last fact turns a letter into an index for an array of 26 counts.

Read the problem

Problem: Valid Anagramn ≤ 5 × 10⁴
it asks
Return true when t uses exactly the same letters as s, each the same number of times, in any order.
example 1
in s = "anagram", t = "nagaram" out true
example 2
in s = "rat", t = "car" out false

t has a c and no t.

limits
  • 1 ≤ s.length, t.length ≤ 5 × 10⁴
  • lowercase English letters
budget
About 10⁸ steps run in one second. With n up to 5 × 10⁴, aim for O(n log n) or faster.
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 false before 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.

find each letter of listen in silentcomparisons 3
s
0
i
1
l
2
e
3
n
4
t
5

1/6Take l from s. Scan t: s, i, l. Found at 2. Cross it out.

21 comparisons for 6 letters. The scans get longer as more cells are crossed 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's sorted(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

  1. If the lengths differ, return false.
  2. Make 26 counts, all 0.
  3. Walk both strings together: add 1 for s[i], take 1 for t[i].
  4. Return true when every count is 0.

The cells are the letters of s. The side store shows only the counts that are not 0.

+1 for listen, −1 for silent, in one walksteps 1
l
0
i
1
s
2
t
3
e
4
n
5
counts not 0{l:1s:-1}

1/7s has l: l goes to 1. t has s: s goes to -1.

6 steps for 6 letters, against 21 comparisons for the brute force.
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

ApproachIdeaTimeExtra spaceFits 5 × 10⁴?
Cross outfind each letter of s in tO(n²)O(n)no
Sort bothanagrams sort to the same stringO(n log n)O(n)yes
Count+1 for s, −1 for t, all zeroO(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:

l and r start at the two ends. Compare, then step both inward. One mismatch means false.
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.

abcde rotated left by 2 is cdeab, and it appears inside s + s.
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 problemMove
"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.

Remember4 points
  1. 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.

  2. 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.

  3. 03

    Two pointers from both ends reverse a string or check a palindrome with no copy.

  4. 04

    Rotations live inside s + s. Check the lengths first.

Read more

Next: a new chapter, starting with "What is recursion?": a function that calls itself, and the call stack that keeps track.

Practice

Solve these on the sheet after the lesson. Try each one for 20 minutes before you open its solution.

Striver's A2Z sheet0/9 solved
Open the sheet