Excellent 👏 — you’ve completed Day 1 of your “Deep Python Strings in 3 Days” roadmap.
You now understand immutability, memory model, interning, and foundational string methods.

Let’s move to DAY 2 — Algorithmic Patterns with Strings
Here we’ll focus on building problem-solving muscle memory with real-world LeetCode-style patterns: frequency counting, two pointers, stack, and hash maps.


💎 DAY 2 – String Algorithm Patterns & Problem Solving


🎯 Goal

By the end of Day 2, you’ll:

  • Recognize and apply 4 core string problem-solving patterns
  • Master palindrome and subsequence logic
  • Understand stack-based decoding and string cleanup
  • Build speed and confidence with character maps and Counters

🧩 PART 1 – Frequency & Index Map Patterns

Strings are sequences → most real problems rely on counting frequency or tracking last seen index.


🔹 1️⃣ First Unique Character in a String

👉 Pattern: frequency count + index scan

from collections import Counter

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

print(first_unique_char("leetcode"))  # 0
print(first_unique_char("aabb"))      # -1

⏱ Complexity: O(n) time, O(1) space (since only 26 letters if lowercase).
🔑 Learnings: Counter pattern is a must for frequency problems.


🔹 2️⃣ String Compression (Run-Length Encoding)

👉 Pattern: group consecutive characters.

def compress_string(s: str) -> str:
    if not s:
        return ""
    result = []
    count = 1
    for i in range(1, len(s)):
        if s[i] == s[i - 1]:
            count += 1
        else:
            result.append(s[i - 1] + str(count))
            count = 1
    result.append(s[-1] + str(count))
    compressed = "".join(result)
    return compressed if len(compressed) < len(s) else s

print(compress_string("aabcccccaaa"))  # a2b1c5a3

🔹 3️⃣ Count Vowels and Consonants

👉 Pattern: simple frequency partition.

def count_vowels_consonants(s: str):
    vowels = set("aeiouAEIOU")
    v = c = 0
    for ch in s:
        if ch.isalpha():
            if ch in vowels:
                v += 1
            else:
                c += 1
    return v, c

print(count_vowels_consonants("Python"))

🔹 4️⃣ First Non-Repeating Character

👉 Variation that returns the character, not the index.

def first_non_repeating_char(s: str) -> str | None:
    from collections import Counter
    freq = Counter(s)
    for ch in s:
        if freq[ch] == 1:
            return ch
    return None

print(first_non_repeating_char("racecar"))  # 'e'

🔹 5️⃣ Group Anagrams

👉 Pattern: use sorted characters or frequency tuple as key.

from collections import defaultdict

def group_anagrams(words):
    groups = defaultdict(list)
    for w in words:
        key = "".join(sorted(w))
        groups[key].append(w)
    return list(groups.values())

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

🪞 PART 2 – Palindromes, Subsequence & Rotations


🔹 6️⃣ Check if String is Palindrome (Ignore Non-Alphanumeric)

👉 Pattern: two pointers, clean + compare

def is_palindrome(s: str) -> bool:
    filtered = [ch.lower() for ch in s if ch.isalnum()]
    i, j = 0, len(filtered) - 1
    while i < j:
        if filtered[i] != filtered[j]:
            return False
        i += 1
        j -= 1
    return True

print(is_palindrome("A man, a plan, a canal: Panama"))  # ✅ True

🔹 7️⃣ Valid Palindrome II (Remove at Most One Character)

👉 Pattern: two-pointer + skip once logic

def valid_palindrome_ii(s: str) -> bool:
    def is_pal_range(i, j):
        while i < j:
            if s[i] != s[j]:
                return False
            i += 1
            j -= 1
        return True

    i, j = 0, len(s) - 1
    while i < j:
        if s[i] != s[j]:
            return is_pal_range(i + 1, j) or is_pal_range(i, j - 1)
        i += 1
        j -= 1
    return True

print(valid_palindrome_ii("abca"))  # ✅ True (remove 'b')

🔹 8️⃣ Longest Palindromic Substring

👉 Pattern: expand-around-center

def longest_palindrome(s: str) -> str:
    if not s:
        return ""
    
    def expand(l, r):
        while l >= 0 and r < len(s) and s[l] == s[r]:
            l -= 1
            r += 1
        return l + 1, r

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

print(longest_palindrome("babad"))  # "bab" or "aba"

🔹 9️⃣ Is Subsequence

👉 Pattern: Two pointers moving independently.

def is_subsequence(s: str, t: str) -> bool:
    i = 0
    for ch in t:
        if i < len(s) and s[i] == ch:
            i += 1
    return i == len(s)

print(is_subsequence("abc", "ahbgdc"))  # ✅ True

🔹 🔟 Check if Two Strings Are Rotations

👉 Pattern: use concatenation trick

def are_rotations(s1: str, s2: str) -> bool:
    if len(s1) != len(s2):
        return False
    return s2 in (s1 + s1)

print(are_rotations("abcd", "cdab"))  # ✅ True

🧱 PART 3 – Stack-Based String Manipulation

These are some of the most common interview patterns.


🔹 11️⃣ Decode String (3[a2[c]] → accaccacc)

👉 Pattern: stack of (string, repeat_count)

def decode_string(s: str) -> str:
    num_stack, str_stack = [], []
    curr, num = [], 0

    for ch in s:
        if ch.isdigit():
            num = num * 10 + int(ch)
        elif ch == "[":
            num_stack.append(num)
            str_stack.append("".join(curr))
            num = 0
            curr = []
        elif ch == "]":
            repeat = num_stack.pop()
            prev = str_stack.pop()
            curr = [prev + "".join(curr) * repeat]
        else:
            curr.append(ch)
    return "".join(curr)

print(decode_string("3[a2[c]]"))  # accaccacc

🔹 12️⃣ Remove Adjacent Duplicates (Revisited)

👉 Pattern: stack push/pop cleanup

def remove_adjacent_duplicates(s: str) -> str:
    stack = []
    for ch in s:
        if stack and stack[-1] == ch:
            stack.pop()
        else:
            stack.append(ch)
    return "".join(stack)

print(remove_adjacent_duplicates("abbaca"))  # "ca"

🔹 Mini Challenge: Reverse Words in a Sentence

👉 Reverse word order, not characters.

def reverse_words(sentence: str) -> str:
    words, word = [], []
    for ch in sentence:
        if ch == " ":
            if word:
                words.append("".join(word))
                word = []
        else:
            word.append(ch)
    if word:
        words.append("".join(word))
    return " ".join(reversed(words))

print(reverse_words("Python is fun"))  # "fun is Python"

🧠 PATTERN RECAP TABLE

CategoryCore IdeaExample Problems
Frequency MapCount chars using CounterAnagram, First Unique Char
Two PointersCompare start & end simultaneouslyPalindrome, Subsequence
Expand Around CenterCheck from a center outwardLongest Palindromic Substring
StackProcess nested or adjacent rulesDecode String, Remove Duplicates

⚙️ PRACTICE TASKS (Self Study)

  1. Modify decode_string to handle multi-digit numbers (e.g. 12[a] → 12 repeats).
  2. Write is_mirror_palindrome(s) that treats "b" ↔ "d", "p" ↔ "q" as mirrors.
  3. Create a function to check if two strings are one edit distance apart ("abc" vs "ab" or "adc").

🧾 Day 2 Summary

ConceptYou Learned
Frequency MapsCounting and grouping efficiently
StackHandle nesting and adjacent duplicates
Two PointersCompare characters efficiently
Sliding CentersUsed for palindrome discovery
String RotationSimple concatenation trick
Decode StringRecursive/Stack-based parsing pattern

🧠 End-of-Day Reflection Questions

  1. Why is Counter() useful for anagram problems?
  2. What’s the time complexity of checking if two strings are rotations?
  3. How would you reverse the order of words without .split()?
  4. How does stack-based decoding handle nested brackets?
  5. What’s the intuition behind expanding around a palindrome center?

Coming Next → Day 3:
Sliding Windows, Substring Search, and Numeric String Logic (Roman, Binary, Arithmetic)
You’ll learn the most advanced string algorithms — from minimum windows to calculator evaluation.


Would you like me to generate a Day 2 Jupyter notebook (with explanations + all runnable cells + short hints for self-attempt sections)?
That notebook will make it easy to code + test in your local environment.