Strings in C++ | Lecture 39: Strings Interview Problems on LeetCode – Add Strings & Sort Vowels in a String
Building a rock-solid foundation in string processing is a mandatory prerequisite for mastering advanced string algorithms like the Knuth-Morris-Pratt (KMP) algorithm. This chapter covers two foundational LeetCode interview problems: Sort Vowels in a String (LeetCode 2785) and Add Strings (LeetCode 415). Through these problems, we explore frequency mapping, ASCII-based sorting, placeholder marker strategies, string reversal, and arbitrary-precision integer arithmetic without relying on big-integer libraries.
1. Problem 1: Sort Vowels in a String (LeetCode 2785)
Given a 0-indexed string s, we are required to permute its characters such that:
- All vowels (
'a','e','i','o','u','A','E','I','O','U') are sorted in non-decreasing order of their ASCII values. - All consonants remain strictly in their original positions.
Problem Constraints & Twist
- Uppercase and lowercase vowels are distinct (
'A''a'). - ASCII values determine relative order: Uppercase vowels (
'A'=65,'E'=69,'I'=73,'O'=79,'U'=85) have smaller ASCII values than lowercase vowels ('a'=97,'e'=101,'i'=105,'o'=111,'u'=117). - Uppercase vowels must always precede lowercase vowels in the final sorted order.
2. Mental Model & ASCII Invariants for Vowel Sorting
To sort vowels by ASCII values without invoking a heavy generic sorting algorithm, we leverage Frequency Mapping (Counting Sort) over fixed alphabetic bounds ( letters for uppercase and letters for lowercase).
By populating frequency counts for uppercase vowels first, then lowercase vowels, we implicitly produce an ASCII-sorted sequence of vowels in time.
3. Algorithmic Strategy: The Marker Strategy vs. Multi-Pass Traversal
A naive approach requires repeated 10-way conditional checks (s[i] == 'a' || s[i] == 'e' ...) every time we attempt to re-insert sorted vowels back into the original string.
The Placeholder Marker Strategy ('#' Replacement)
To simplify the second pass and eliminate repetitive conditional checks, we use a placeholder character ('#'):
- Pass 1 (Extract & Mark): Iterate through the string
s. When a vowel is encountered, increment its frequency count in the upper/lower frequency arrays and overwrites[i]with a neutral placeholder marker'#'. - Pass 2 (Construct Sorted Vowels): Build a linear string
vowelcontaining all extracted vowels ordered sequentially from'A'to'u'using the frequency maps. - Pass 3 (In-Place Substitution): Iterate through string
susing two pointers:- Pointer
firstscanss. - Pointer
secondscansvowel. - Whenever
s[first] == '#', overwrites[first]withvowel[second]and incrementsecond.
- Pointer
4. Complete Flow Walkthrough: Sort Vowels
Let us trace the input string s = "lEetcOde":
5. C++ Implementation: Sort Vowels in a String
#include <iostream>
#include <string>
#include <vector>
class Solution {
public:
string sortVowels(string s) {
// Frequency arrays for fixed alphabet size 26
vector<int> lower(26, 0);
vector<int> upper(26, 0);
// Pass 1: Count frequency and mark vowel positions with '#'
for (int i = 0; i < s.size(); i++) {
// Lowercase vowel check
if (s[i] == 'a' || s[i] == 'e' || s[i] == 'i' || s[i] == 'o' || s[i] == 'u') {
lower[s[i] - 'a']++;
s[i] = '#'; // Placeholder marker
}
// Uppercase vowel check
else if (s[i] == 'A' || s[i] == 'E' || s[i] == 'I' || s[i] == 'O' || s[i] == 'U') {
upper[s[i] - 'A']++;
s[i] = '#'; // Placeholder marker
}
}
// Pass 2: Build the sorted vowel string (Uppercase first, then Lowercase)
string vowel = "";
// Collect Uppercase Vowels
for (int i = 0; i < 26; i++) {
char c = 'A' + i;
while (upper[i] > 0) {
vowel += c;
upper[i]--;
}
}
// Collect Lowercase Vowels
for (int i = 0; i < 26; i++) {
char c = 'a' + i;
while (lower[i] > 0) {
vowel += c;
lower[i]--;
}
}
// Pass 3: In-place substitution using two pointers
int first = 0; // Traverses modified string 's'
int second = 0; // Traverses sorted string 'vowel'
while (second < vowel.size()) {
if (s[first] == '#') {
s[first] = vowel[second];
second++;
}
first++;
}
return s;
}
};
6. Complexity Analysis & Common Misconceptions
Time Complexity
- Pass 1 (Marking & Counting): , where is the length of string
s. - Pass 2 (Building Sorted Vowel String): . The inner
whileloops execute a total of times (where is the number of vowels). Hence, this step takes time. - Pass 3 (Pointer Substitution): as
firstincrements at most times. - Total Time Complexity: .
Note on Nested Loops: A common misconception is assuming that nested loops (the
forloop of size 26 combined with thewhileloop) result in time complexity. Because the total number of decrements across allupper[i]andlower[i]buckets is bounded strictly by the total vowel count , the innerwhileloop runs at most times in total across all 26 outer iterations. Thus, it remains strictly linear, .
Space Complexity
- Frequency Storage: Two
vector<int>arrays of size 26 take auxiliary space. - Vowel String: Stores at most characters, requiring extra memory.
- Total Space Complexity: .
7. Problem 2: Add Strings (LeetCode 415) & Large Integer Storage
In competitive programming and real-world system design, handling integers that exceed standard primitive bit limits is a fundamental challenge.
Memory & Primitive Limits
- Standard
int(32-bit signed): Max value ( digits). - Standard
long long(64-bit signed): Max value ( digits). - Unsigned 128-bit integers: Max value ( digits).
When evaluating functions like (which contains 158 digits), standard types suffer catastrophic integer overflow.
Real-World Engineering Solution
To represent an arbitrarily large number (e.g., 100+ digits), we store the number as a std::string. Each digit is stored as a character requiring of memory. A 100-digit number consumes exactly 100 bytes of memory and can grow dynamically without integer overflow risks.
Conceptually: Languages like Python natively implement arbitrary-precision integers (
BigInt). In C++, building manual string arithmetic constructs the foundational mechanics behindBigIntlibraries.
8. Manual Arithmetic Simulation: Handling Digits & Carry
To add two numeric strings num1 and num2 without converting them directly into integers, we simulate manual elementary school addition:
- Align numbers by their least significant digits (right-to-left).
- Maintain a running
carryvariable (initialized to0). - Extract numeric values using ASCII conversion:
- Compute current column sum:
- Determine output digit and forward carry:
- Convert output digit back to character:
9. Step-by-Step Evolution of the Code Logic
Handling Variable String Lengths cleanly
Instead of writing complex nested loops to guard against out-of-bound errors when num1 and num2 have different lengths, we can enforce a clean structural invariant:
- Structural Wrapper: Ensure
num1always holds the string with equal or greater length (num1.size() >= num2.size()). Ifnum1.size() < num2.size(), dynamically delegate execution toadd(num2, num1).
Three-Phase Loop Process
- Phase 1: Iterate while
index2 >= 0(processing bothnum1andnum2digits simultaneously along withcarry). - Phase 2: Iterate while
index1 >= 0(processing remaining left-over digits ofnum1along withcarry). - Phase 3: Post-loop check. If
carry > 0, append'1'to the result string. - Final Step: Reverse the result string (since digits were appended from least significant to most significant).
10. C++ Implementation: Add Strings
#include <iostream>
#include <string>
#include <algorithm>
class Solution {
private:
// Core helper method assuming num1.size() >= num2.size()
string add(string num1, string num2) {
string answer = "";
int index1 = num1.size() - 1;
int index2 = num2.size() - 1;
int sum = 0;
int carry = 0;
// Phase 1: Process common digit positions from right to left
while (index2 >= 0) {
sum = (num1[index1] - '0') + (num2[index2] - '0') + carry;
carry = sum / 10;
char c = '0' + (sum % 10);
answer += c;
index2--;
index1--;
}
// Phase 2: Process remaining digits of longer string (num1)
while (index1 >= 0) {
sum = (num1[index1] - '0') + carry;
carry = sum / 10;
char c = '0' + (sum % 10);
answer += c;
index1--;
}
// Phase 3: Append final carry if present
if (carry > 0) {
answer += '1'; // Carry can at most be 1 when adding two digits
}
// Reverse answer string to restore correct left-to-right order
reverse(answer.begin(), answer.end());
return answer;
}
public:
string addStrings(string num1, string num2) {
// Enforce structural invariant: num1 must be the longer string
if (num1.size() < num2.size()) {
return add(num2, num1);
} else {
return add(num1, num2);
}
}
};
11. Edge Cases & Debugging Real-World Failures
1. The Leftover Most Significant Carry
- Scenario: Adding
"99"and"1". - Pitfall: Phase 1 processes
'9' + '1' = 10(append'0',carry = 1). Phase 2 processes remaining'9' + 1 = 10(append'0',carry = 1). If Phase 3 is omitted, the string result becomes"00", which reverses to"00"instead of"100". - Fix: Explicitly check
if (carry > 0)after all pointer loops terminate.
2. Disproportionate String Lengths
- Scenario: Adding
"99999"and"1". - Pitfall: Index out-of-bounds access if pointers are managed within a single unified loop without safe empty checks.
- Fix: The helper delegation technique ensures
num1is always longer, isolating remainingnum1processing cleanly in Phase 2.
3. ASCII Off-by-One Conversions
- Scenario: Forgetting
- '0'during digit extraction or+ '0'during character reconstruction. - Result: Adding ASCII value (char
'5') and ASCII value (char'8') yields (char'm'), corrupting integer representation.
12. Summary and Key Takeaways
- Fixed Alphabet Mapping Optimization: Sorting small character subsets (like vowels) can be optimized to using frequency maps across fixed array bounds ( positions), bypassing comparison sorts.
- The Placeholder Strategy: Replacing target characters with neutral markers (e.g.,
'#') streamlines multi-pass string mutations by avoiding repetitive, expensive multi-variable conditional logic during the substitution phase. - ASCII Character Invariants: Uppercase letters have lower ASCII values than lowercase letters (
'A'=65 vs'a'=97). Sorting according to ASCII naturally clusters uppercase characters before lowercase characters. - Large Integer Memory Model: Native integer types max out at 64 bits ( digits). String representations store large numbers dynamically at , preventing arithmetic overflow.
- Char-to-Int ASCII Math: Always subtract
'0'to convert a digit character to its scalar integer value (ch - '0'), and add'0'to convert a scalar integer back to its character equivalent (val + '0'). - Structural Invariant Delegation: Delegating function logic to ensure standard bounds (e.g., guaranteeing
num1.size() >= num2.size()) eliminates redundant conditional branches and keeps code maintainable. - Post-Loop Carry Propagation: Always handle the final arithmetic carry after primary digit iteration loops terminate (e.g., ).
- In-Place Append & Reverse: Appending digits to a string (
answer += c) and reversing at the end withstd::reverseoperates in linear time , which is significantly faster than repeatedly prepending characters to a string (answer = c + answer), which causes expensive memory shifts.