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
| Category | Core Idea | Example Problems |
|---|---|---|
| Frequency Map | Count chars using Counter | Anagram, First Unique Char |
| Two Pointers | Compare start & end simultaneously | Palindrome, Subsequence |
| Expand Around Center | Check from a center outward | Longest Palindromic Substring |
| Stack | Process nested or adjacent rules | Decode String, Remove Duplicates |
⚙️ PRACTICE TASKS (Self Study)
- Modify
decode_stringto handle multi-digit numbers (e.g.12[a]→ 12 repeats). - Write
is_mirror_palindrome(s)that treats"b" ↔ "d","p" ↔ "q"as mirrors. - Create a function to check if two strings are one edit distance apart (
"abc"vs"ab"or"adc").
🧾 Day 2 Summary
| Concept | You Learned |
|---|---|
| Frequency Maps | Counting and grouping efficiently |
| Stack | Handle nesting and adjacent duplicates |
| Two Pointers | Compare characters efficiently |
| Sliding Centers | Used for palindrome discovery |
| String Rotation | Simple concatenation trick |
| Decode String | Recursive/Stack-based parsing pattern |
🧠 End-of-Day Reflection Questions
- Why is Counter() useful for anagram problems?
- What’s the time complexity of checking if two strings are rotations?
- How would you reverse the order of words without
.split()? - How does stack-based decoding handle nested brackets?
- 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.