Skip to content
C Codeloom
DSA

Strings: 8 Practice Problems with Solutions

Eight classic string problems with worked Python solutions — Valid Anagram, Group Anagrams, Longest Substring Without Repeating Characters, Longest Palindromic Substring, and more.

·9 min read · By Yash Kesharwani
Intermediate 20 min read

What you'll learn

  • How to apply two-pointer scans to palindrome and reversal problems
  • How to use frequency counters for anagram and unique-character problems
  • The sliding-window pattern for longest-substring problems
  • Expand-around-center for longest palindromic substring
  • How to use a stack to validate matched brackets
  • When to choose KMP vs naive scan for substring search

This post collects eight classic string problems that show up over and over in interviews. Each entry has the problem statement, examples, an approach with Big-O analysis, and a clean Python solution. Type them out yourself — the muscle memory matters more than reading the answer.

1. Valid Anagram

Problem. Given two strings s and t, return True if t is an anagram of s — i.e. they use exactly the same characters with the same multiplicities.

Examples.

s = "anagram", t = "nagaram"   -> True
s = "rat",     t = "car"       -> False

Approach

Compare frequency counts. If the strings are restricted to lowercase ASCII, a fixed-size array of 26 counters runs in O(n) time and O(1) extra space. The lengths must match — a quick early exit.

Time: O(n). Space: O(1) for fixed alphabet, O(k) for general Unicode.

Solution

def is_anagram(s: str, t: str) -> bool:
    if len(s) != len(t):
        return False
    counts = [0] * 26
    for ch in s:
        counts[ord(ch) - ord('a')] += 1
    for ch in t:
        counts[ord(ch) - ord('a')] -= 1
        if counts[ord(ch) - ord('a')] < 0:
            return False
    return True

print(is_anagram("anagram", "nagaram"))   # True
print(is_anagram("rat", "car"))           # False

A one-liner using Counter works too, at the cost of slightly more memory:

from collections import Counter
def is_anagram(s, t):
    return Counter(s) == Counter(t)

2. Reverse String

Problem. Reverse a list of characters in place. You must not allocate a separate array.

Examples.

s = ['h','e','l','l','o']   ->   ['o','l','l','e','h']
s = ['H','a','n','n','a','h'] -> ['h','a','n','n','a','H']

Approach

Two pointers from both ends, swapping until they cross. O(n) time, O(1) extra space.

Solution

def reverse_string(s: list[str]) -> None:
    i, j = 0, len(s) - 1
    while i < j:
        s[i], s[j] = s[j], s[i]
        i += 1
        j -= 1

chars = list("hello")
reverse_string(chars)
print(chars)    # ['o', 'l', 'l', 'e', 'h']

Why the in-place restriction matters: s[::-1] is shorter but allocates an O(n) array. In a system where the input buffer is huge, that doubles your memory.

3. First Unique Character in a String

Problem. Given a string s, return the index of the first character that does not repeat. Return -1 if every character repeats.

Examples.

s = "leetcode"     -> 0    ('l' appears only once)
s = "loveleetcode" -> 2    ('v' appears only once)
s = "aabb"         -> -1

Approach

Two passes. First, count each character’s occurrences. Second, walk the string from the left and return the index of the first character whose count is 1.

Time: O(n). Space: O(1) for ASCII, O(k) for Unicode.

Solution

from collections import Counter

def first_uniq_char(s: str) -> int:
    counts = Counter(s)
    for i, ch in enumerate(s):
        if counts[ch] == 1:
            return i
    return -1

print(first_uniq_char("leetcode"))       # 0
print(first_uniq_char("loveleetcode"))   # 2
print(first_uniq_char("aabb"))           # -1

4. Group Anagrams

Problem. Given a list of strings, group those that are anagrams of one another. Return the groups in any order.

Examples.

strs = ["eat","tea","tan","ate","nat","bat"]
-> [["eat","tea","ate"], ["tan","nat"], ["bat"]]

Approach

Two strings are anagrams iff their sorted character sequences are equal. Use the sorted string as a dictionary key.

Time: O(n · m log m), where n is the number of strings and m is the average length (sorting dominates). Space: O(n · m) for the output.

An alternative key — a 26-tuple of counts — runs in O(n · m) without the log factor:

key = tuple of counts of 'a'..'z' for this word

Solution

from collections import defaultdict

def group_anagrams(strs: list[str]) -> list[list[str]]:
    groups = defaultdict(list)
    for word in strs:
        key = tuple(sorted(word))     # or build a 26-tuple of counts
        groups[key].append(word)
    return list(groups.values())

print(group_anagrams(["eat","tea","tan","ate","nat","bat"]))
# [['eat','tea','ate'], ['tan','nat'], ['bat']]

The count-tuple version, for fixed-alphabet inputs:

def group_anagrams_counts(strs):
    groups = defaultdict(list)
    for word in strs:
        counts = [0] * 26
        for ch in word:
            counts[ord(ch) - ord('a')] += 1
        groups[tuple(counts)].append(word)
    return list(groups.values())

5. Longest Substring Without Repeating Characters

Problem. Given a string s, find the length of the longest substring with no repeated characters.

Examples.

s = "abcabcbb"  -> 3   ("abc")
s = "bbbbb"     -> 1   ("b")
s = "pwwkew"    -> 3   ("wke")

Approach

Classic sliding window. Maintain a window [left, right] containing only unique characters. Track the last seen index of each character in a hash map. When you see a repeat that lies inside the current window, slide left to one past the previous occurrence.

Time: O(n). Each index moves at most once. Space: O(k), where k is the size of the alphabet.

Solution

def length_of_longest_substring(s: str) -> int:
    last_seen = {}
    left = 0
    best = 0
    for right, ch in enumerate(s):
        if ch in last_seen and last_seen[ch] >= left:
            left = last_seen[ch] + 1
        last_seen[ch] = right
        best = max(best, right - left + 1)
    return best

print(length_of_longest_substring("abcabcbb"))   # 3
print(length_of_longest_substring("bbbbb"))      # 1
print(length_of_longest_substring("pwwkew"))     # 3

The check last_seen[ch] >= left is the subtle bit — characters seen earlier than the current window do not force the window to shrink.

6. Longest Palindromic Substring

Problem. Return the longest substring of s that is a palindrome.

Examples.

s = "babad"   -> "bab"   (or "aba")
s = "cbbd"    -> "bb"

Approach

Expand around center. Every palindrome has a center — either a single character (odd length) or a gap between two characters (even length). There are 2n−1 such centers; expand outward from each and track the longest result.

Time: O(n²) — best straightforward approach. Space: O(1).

A linear-time solution exists (Manacher’s algorithm) but is rarely required.

Solution

def longest_palindrome(s: str) -> str:
    if not s:
        return ""

    def expand(left: int, right: int) -> tuple[int, int]:
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left -= 1
            right += 1
        return left + 1, right - 1   # last valid window

    start, end = 0, 0
    for i in range(len(s)):
        l1, r1 = expand(i, i)         # odd-length center
        l2, r2 = expand(i, i + 1)     # even-length center
        if r1 - l1 > end - start:
            start, end = l1, r1
        if r2 - l2 > end - start:
            start, end = l2, r2
    return s[start:end + 1]

print(longest_palindrome("babad"))   # 'bab' or 'aba'
print(longest_palindrome("cbbd"))    # 'bb'

7. Valid Parentheses

Problem. Given a string containing only ()[]{}, return True if the brackets are matched and properly nested.

Examples.

"()"        -> True
"()[]{}"    -> True
"(]"        -> False
"([)]"      -> False
"{[]}"      -> True

Approach

Use a stack. Push opening brackets. For each closing bracket, peek at the top of the stack — if it is the matching opener, pop; otherwise return False. At the end the stack must be empty.

Time: O(n). Space: O(n) for the stack in the worst case.

Solution

def is_valid(s: str) -> bool:
    pairs = {')': '(', ']': '[', '}': '{'}
    stack = []
    for ch in s:
        if ch in "([{":
            stack.append(ch)
        else:
            if not stack or stack[-1] != pairs[ch]:
                return False
            stack.pop()
    return not stack

print(is_valid("()"))         # True
print(is_valid("()[]{}"))     # True
print(is_valid("(]"))         # False
print(is_valid("([)]"))       # False
print(is_valid("{[]}"))       # True

The trick that makes this clean is the pairs dictionary — one lookup per closer, no nested conditionals.

Problem. Given two strings haystack and needle, return the index of the first occurrence of needle in haystack, or -1 if it is not present. Return 0 if needle is empty.

Examples.

haystack = "sadbutsad", needle = "sad"    -> 0
haystack = "leetcode",  needle = "leeto"  -> -1
haystack = "hello",     needle = ""       -> 0

Approach

For interview purposes the naive O(n · m) scan is acceptable. For the optimal answer, use KMP for O(n + m). We will show both.

Naive solution

def str_str_naive(haystack: str, needle: str) -> int:
    if not needle:
        return 0
    n, m = len(haystack), len(needle)
    for i in range(n - m + 1):
        if haystack[i:i + m] == needle:
            return i
    return -1

print(str_str_naive("sadbutsad", "sad"))    # 0
print(str_str_naive("leetcode", "leeto"))   # -1

KMP solution

def build_lps(pattern: str) -> list[int]:
    lps = [0] * len(pattern)
    length, i = 0, 1
    while i < len(pattern):
        if pattern[i] == pattern[length]:
            length += 1
            lps[i] = length
            i += 1
        elif length != 0:
            length = lps[length - 1]
        else:
            lps[i] = 0
            i += 1
    return lps

def str_str_kmp(haystack: str, needle: str) -> int:
    if not needle:
        return 0
    lps = build_lps(needle)
    i = j = 0
    while i < len(haystack):
        if haystack[i] == needle[j]:
            i += 1
            j += 1
            if j == len(needle):
                return i - j
        elif j != 0:
            j = lps[j - 1]
        else:
            i += 1
    return -1

print(str_str_kmp("sadbutsad", "sad"))     # 0
print(str_str_kmp("mississippi", "issip")) # 4

The naive version is fine to submit; mentioning KMP shows you know the optimal bound.

Try it yourself. Pick three problems from this list and re-solve them without scrolling up. Aim to write the function signature first, then a one-sentence approach in a comment, then the code. The discipline of writing the approach down first catches off-by-one errors before they cost you twenty minutes.

Stretch problem. Modify length_of_longest_substring to return the longest substring itself, not just its length. You should only need two more variables.

Recap

The patterns that solved these eight problems:

  • Two pointers — Reverse String
  • Frequency counter — Valid Anagram, First Unique Character
  • Hash-map key — Group Anagrams (sorted string or count tuple)
  • Sliding window — Longest Substring Without Repeating Characters
  • Expand around center — Longest Palindromic Substring
  • Stack — Valid Parentheses
  • Naive scan or KMP — Implement strStr()

These six patterns cover the majority of string interview questions. If a problem feels unfamiliar, ask which of them fits.

Next steps

The next two posts shift from strings to hashing: how hash functions work, the trade-offs of collision strategies, and a follow-up practice set covering Two Sum, Subarray Sum, and the other classics built on hash maps.

→ Next: Hashing and Hash Maps in DSA

Questions or feedback? Email codeloomdevv@gmail.com.