Introduction
Array and string manipulation problems are fundamental in programming interviews and real-world applications. These problems test your ability to work with data structures efficiently, handle edge cases, and implement algorithms that operate in-place or with minimal space complexity.
In this article, we’ll explore various array and string manipulation techniques through practical LeetCode problems, providing clean, well-explained solutions in both C# and Python.
Common Techniques
Two Pointers
Using two pointers to traverse arrays or strings, often reducing time complexity from O(n²) to O(n).
In-Place Operations
Modifying arrays or strings without using extra space, often using swapping or overwriting.
String Parsing
Processing strings character by character, handling whitespace, signs, and edge cases.
Pattern Recognition
Identifying mathematical patterns or cycles in problems to optimize solutions.
Problem 1: Move Zeroes
Problem Statement
Given an integer array nums, move all 0‘s to the end of it while maintaining the relative order of the non-zero elements.
Note that you must do this in-place without making a copy of the array.
Example
Input: nums = [0,1,0,3,12]
Output: [1,3,12,0,0]
Input: nums = [0]
Output: [0]
Understanding the Problem
We need to rearrange the array so all zeros are at the end, but non-zero elements maintain their original relative order. This must be done in-place.
Solution Strategy
Two Pointers Approach:
- Use
pospointer to track where the next non-zero element should be placed - Iterate through array with
i - When we find a non-zero element:
- If
i != pos, swap elements (or copy and zero out) - Increment
pos
- If
- All zeros naturally end up at the end
C# Solution
public class Solution
{
public void MoveZeroes(int[] nums)
{
// pos tracks where next non-zero element should be placed
int pos = 0;
for (int i = 0; i < nums.Length; i++)
{
if (nums[i] != 0)
{
// Only swap if positions are different
if (i != pos)
{
nums[pos] = nums[i];
nums[i] = 0;
}
pos++;
}
}
}
}
Python 3 Solution
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
pos = 0
for i in range(len(nums)):
if nums[i] != 0:
if i != pos:
nums[pos] = nums[i]
nums[i] = 0
pos += 1
Complexity Analysis
- Time Complexity: O(n) where n is the length of nums
- Space Complexity: O(1) – only using constant extra space
Key Insight
The pos pointer always points to the position where the next non-zero element should go. By swapping (or copying and zeroing), we maintain the relative order while moving zeros to the end.
Problem 2: Is Subsequence
Problem Statement
Given two strings s and t, return true if s is a subsequence of t, or false otherwise.
A subsequence of a string is a new string that is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters.
Example
Input: s = "abc", t = "ahbgdc"
Output: true
Input: s = "axc", t = "ahbgdc"
Output: false
Understanding the Problem
We need to check if all characters of s appear in t in the same order (but not necessarily consecutively).
Solution Strategy
Two Pointers Approach:
- Use pointer
ifor stringsandjfor stringt - Traverse
twithj - When
s[i] == t[j], moveiforward (found a match) - Always move
jforward - If
ireaches end ofs, all characters found in order
C# Solution
public class Solution
{
public bool IsSubsequence(string s, string t)
{
int i = 0, j = 0;
while (i < s.Length && j < t.Length)
{
// If characters match, move pointer in s
if (s[i] == t[j])
{
i++;
}
// Always move pointer in t
j++;
}
// If we've matched all characters in s, it's a subsequence
return i == s.Length;
}
}
Python 3 Solution
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
i = j = 0
while i < len(s) and j < len(t):
if s[i] == t[j]:
i += 1
j += 1
return i == len(s)
Complexity Analysis
- Time Complexity: O(n) where n is the length of t
- Space Complexity: O(1)
Key Insight
We only advance i when we find a match. If i reaches the end of s, we’ve found all characters in order, making s a subsequence of t.
Problem 3: Next Permutation
Problem Statement
A permutation of an array of integers is an arrangement of its members into a sequence or linear order.
The next permutation of an array of integers is the next lexicographically greater permutation of its integer. If such arrangement is not possible, the array must be rearranged as the lowest possible order (i.e., sorted in ascending order).
Given an array of integers nums, find the next permutation of nums.
The replacement must be in place and use only constant extra memory.
Example
Input: nums = [1,2,3]
Output: [1,3,2]
Input: nums = [3,2,1]
Output: [1,2,3]
Input: nums = [1,1,5]
Output: [1,5,1]
Understanding the Problem
We need to find the next lexicographically greater permutation. The algorithm:
- Find the largest index
isuch thatnums[i] < nums[i+1] - Find the largest index
j > isuch thatnums[j] > nums[i] - Swap
nums[i]andnums[j] - Reverse the suffix starting at
i+1
Solution Strategy
Algorithm Steps:
- Traverse from right to find first decreasing element (pivot)
- If no pivot found, reverse entire array (already last permutation)
- Find smallest element greater than pivot in suffix
- Swap pivot with that element
- Reverse the suffix to get lexicographically smallest suffix
C# Solution
public class Solution
{
public void NextPermutation(int[] nums)
{
int n = nums.Length;
if (n < 2)
return;
// Step 1: Find the largest index i such that nums[i] < nums[i+1]
int index = n - 1;
while (index > 0)
{
if (nums[index - 1] < nums[index])
break;
index--;
}
// If no such index exists, array is in descending order
// Reverse to get first permutation (ascending order)
if (index == 0)
{
ReverseSort(nums, 0, n - 1);
return;
}
// Step 2: Find the largest index j > index-1 such that nums[j] > nums[index-1]
int val = nums[index - 1];
int j = n - 1;
while (j >= index)
{
if (nums[j] > val)
break;
j--;
}
// Step 3: Swap nums[index-1] and nums[j]
Swap(nums, j, index - 1);
// Step 4: Reverse the suffix starting at index
ReverseSort(nums, index, n - 1);
}
private void Swap(int[] num, int i, int j)
{
int temp = num[i];
num[i] = num[j];
num[j] = temp;
}
private void ReverseSort(int[] num, int start, int end)
{
if (start > end)
return;
for (int i = start; i <= (end + start) / 2; i++)
{
Swap(num, i, start + end - i);
}
}
}
Python 3 Solution
class Solution:
def nextPermutation(self, nums: List[int]) -> None:
def swap(nums: List[int], i: int, j: int) -> None:
temp = nums[i]
nums[i] = nums[j]
nums[j] = temp
def reverseSort(nums: List[int], start: int, end: int) -> None:
if start > end:
return
for i in range(start, (start + end) // 2 + 1):
swap(nums, i, start + end - i)
n = len(nums)
if n < 2:
return
# Step 1: Find pivot
index = n - 1
while index > 0:
if nums[index - 1] < nums[index]:
break
index -= 1
# If no pivot, reverse entire array
if index == 0:
reverseSort(nums, 0, n - 1)
return
# Step 2: Find element to swap with pivot
val = nums[index - 1]
j = n - 1
while j >= index:
if nums[j] > val:
break
j -= 1
# Step 3: Swap
swap(nums, j, index - 1)
# Step 4: Reverse suffix
reverseSort(nums, index, n - 1)
Complexity Analysis
- Time Complexity: O(n) where n is the length of nums
- Space Complexity: O(1) – only using constant extra space
Key Insight
The algorithm finds the rightmost position where we can make a lexicographically larger permutation by swapping with a larger element from the suffix, then makes the suffix as small as possible.
Problem 4: String to Integer (atoi)
Problem Statement
Implement the myAtoi(string s) function, which converts a string to a 32-bit signed integer (similar to C/C++’s atoi function).
The algorithm for myAtoi(string s) is as follows:
- Read in and ignore any leading whitespace.
- Check if the next character (if not already at the end of the string) is
'-'or'+'. Read this character in if it is either. This determines if the final result is negative or positive respectively. Assume the result is positive if neither is present. - Read in next the characters until the next non-digit character or the end of the input is reached. The rest of the string is ignored.
- Convert these digits into an integer (i.e.
"123"->123,"0032"->32). If no digits were read, then the integer is0. Change the sign as necessary (from step 2). - If the integer is out of the 32-bit signed integer range
[-2³¹, 2³¹ - 1], then clamp the integer so that it remains in the range. Specifically, integers less than-2³¹should be clamped to-2³¹, and integers greater than2³¹ - 1should be clamped to2³¹ - 1. - Return the integer as the final result.
Example
Input: s = "42"
Output: 42
Input: s = " -42"
Output: -42
Input: s = "4193 with words"
Output: 4193
Understanding the Problem
We need to parse a string and convert it to an integer, handling:
- Leading whitespace
- Optional sign (+ or -)
- Digits only
- Integer overflow/underflow
Solution Strategy
Step-by-Step Parsing:
- Skip leading whitespace
- Read optional sign
- Read digits and build number
- Check for overflow before multiplying by 10
- Return clamped value if overflow occurs
C# Solution
public class Solution
{
public int MyAtoi(string s)
{
int sign = 1;
int result = 0;
int index = 0;
int n = s.Length;
// Step 1: Discard all spaces from the beginning
while (index < n && s[index] == ' ')
{
index++;
}
// Step 2: Read optional sign
if (index < n && s[index] == '+')
{
sign = 1;
index++;
}
else if (index < n && s[index] == '-')
{
sign = -1;
index++;
}
// Step 3: Read digits and build number
while (index < n && Char.IsDigit(s[index]))
{
int digit = s[index] - '0';
// Step 4: Check overflow before multiplying
if (result > Int32.MaxValue / 10 ||
(result == Int32.MaxValue / 10 && digit > Int32.MaxValue % 10))
{
// Clamp to integer limits
return sign == 1 ? Int32.MaxValue : Int32.MinValue;
}
// Append current digit to the result
result = 10 * result + digit;
index++;
}
// Step 5: Return result with sign
return sign * result;
}
}
Python 3 Solution
class Solution:
def myAtoi(self, s: str) -> int:
sign = 1
result = 0
index = 0
n = len(s)
INT_MAX = pow(2, 31) - 1
INT_MIN = -pow(2, 31)
# Step 1: Discard all spaces from the beginning
while index < n and s[index] == ' ':
index += 1
# Step 2: Read optional sign
if index < n and s[index] == '+':
sign = 1
index += 1
elif index < n and s[index] == '-':
sign = -1
index += 1
# Step 3: Read digits and build number
while index < n and s[index].isdigit():
digit = int(s[index])
# Step 4: Check overflow before multiplying
if (result > INT_MAX // 10 or
(result == INT_MAX // 10 and digit > INT_MAX % 10)):
# Clamp to integer limits
return INT_MAX if sign == 1 else INT_MIN
# Append current digit to the result
result = 10 * result + digit
index += 1
# Step 5: Return result with sign
return sign * result
Complexity Analysis
- Time Complexity: O(n) where n is the length of the string
- Space Complexity: O(1)
Key Insight
We check for overflow before multiplying by 10 and adding the digit. This prevents actual integer overflow and allows us to clamp to the limits correctly.
Problem 5: Zigzag Conversion
Problem Statement
The string "PAYPALISHIRING" is written in a zigzag pattern on a given number of rows like this:
P A H N
A P L S I I G
Y I R
And then read line by line: "PAHNAPLSIIGYIR"
Write the code that will take a string and make this conversion given a number of rows.
Example
Input: s = "PAYPALISHIRING", numRows = 3
Output: "PAHNAPLSIIGYIR"
Input: s = "PAYPALISHIRING", numRows = 4
Output: "PINALSIGYAHRPI"
Explanation:
P I N
A L S I G
Y A H R
P I
Understanding the Problem
We need to write the string in a zigzag pattern and then read it row by row. The pattern has a cycle length of 2 * numRows - 2.
Solution Strategy
Pattern-Based Approach:
- For each row, identify the pattern of character positions
- The cycle length is
2 * numRows - 2 - For row
i:- Characters appear at positions:
j + iwherejis a multiple of cycle length - For middle rows, there’s also a character at
j + cycleLen - i
- Characters appear at positions:
C# Solution
public class Solution
{
public string Convert(string s, int numRows)
{
// Edge case: single row
if (numRows == 1)
return s;
StringBuilder ret = new StringBuilder();
int n = s.Length;
int cycleLen = 2 * numRows - 2;
for (int i = 0; i < numRows; i++)
{
for (int j = 0; j + i < n; j += cycleLen)
{
// Add character from main column
ret.Append(s[j + i]);
// For middle rows, add character from diagonal
if (i != 0 && i != numRows - 1 && j + cycleLen - i < n)
{
ret.Append(s[j + cycleLen - i]);
}
}
}
return ret.ToString();
}
}
Python 3 Solution
class Solution:
def convert(self, s: str, numRows: int) -> str:
# Edge case: single row
if numRows == 1:
return s
result = []
n = len(s)
cycleLen = 2 * numRows - 2
for i in range(numRows):
j = 0
while j + i < n:
# Add character from main column
result.append(s[j + i])
# For middle rows, add character from diagonal
if i != 0 and i != numRows - 1 and j + cycleLen - i < n:
result.append(s[j + cycleLen - i])
j += cycleLen
return ''.join(result)
Complexity Analysis
- Time Complexity: O(n) where n is the length of the string
- Space Complexity: O(n) for the result string
Key Insight
The zigzag pattern has a repeating cycle. For each row, we can calculate exactly which characters belong to that row based on the cycle length and row index.
Key Takeaways
Array Manipulation Techniques:
1. Two Pointers
- Use when you need to process elements from different positions
- Often reduces time complexity
- Examples: Move Zeroes, Is Subsequence
2. In-Place Operations
- Modify arrays without extra space
- Use swapping or overwriting
- Examples: Move Zeroes, Next Permutation
3. Pattern Recognition
- Identify mathematical patterns or cycles
- Use formulas to calculate positions
- Examples: Zigzag Conversion
String Manipulation Techniques:
1. Character-by-Character Parsing
- Process strings one character at a time
- Handle edge cases carefully
- Examples: String to Integer (atoi)
2. Index Calculation
- Use mathematical formulas for positions
- Consider cycles and patterns
- Examples: Zigzag Conversion
3. State Tracking
- Track sign, whitespace, digits
- Handle transitions between states
- Examples: String to Integer (atoi)
Common Patterns:
1. Two Pointers
- One pointer for each array/string
- Move pointers based on conditions
- Often O(n) time complexity
2. In-Place Swapping
- Swap elements to rearrange
- Use temporary variable or XOR
- Maintain relative order when needed
3. Overflow Handling
- Check before operations
- Use limits to clamp values
- Handle both positive and negative
Best Practices:
1. Handle Edge Cases
- Empty arrays/strings
- Single element
- All zeros or all same values
- Integer overflow
2. Optimize Space
- Use in-place operations when possible
- Reuse existing arrays/strings
- Avoid unnecessary copies
3. Clear Variable Names
- Use descriptive names for pointers
- Name variables by their purpose
- Make code self-documenting
4. Test Boundary Conditions
- Minimum and maximum values
- Empty inputs
- Single character/element
- Edge cases in algorithms
Conclusion
Array and string manipulation problems are fundamental and appear frequently in coding interviews. Mastering these techniques—two pointers, in-place operations, pattern recognition, and careful parsing—will help you solve a wide variety of problems efficiently.
Key techniques to remember:
- Two Pointers: Process elements from different positions simultaneously
- In-Place Operations: Modify data structures without extra space
- Pattern Recognition: Identify cycles and mathematical relationships
- Careful Parsing: Handle edge cases and state transitions
Practice these problems to build intuition for when to apply each technique. The more you practice, the better you’ll become at recognizing patterns and implementing efficient solutions.
Remember: Always consider edge cases, optimize for both time and space complexity, and write clean, readable code that handles all possible inputs correctly.
Happy coding!
