Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 4

String Manipulation Fundamentals

Recommended resources

Introduction

In our last lesson, we explored the inner workings of Python's list as a dynamic array, focusing on how its mutability and resizing strategy affect the complexity of its operations. Today, we'll shift our attention to another fundamental data type you use daily: the string. While strings and lists share some sequence-like properties, they are governed by a crucial difference—immutability—which has profound implications for how we manipulate them efficiently.

This lesson will equip you to manipulate strings effectively, a core skill for any coding interview. We will cover the three key areas from our learning outcome: substring extraction using Python's powerful slicing mechanism, various techniques for string reversal with a deep dive into their time and space complexity, and the principles of character-by-character comparison.


Strings: Immutable Sequences

Like lists, strings are sequences, meaning you can access individual characters by their index. For example, my_string[0] gives you the first character. However, unlike lists, strings are immutable. This means that once a string is created, its contents cannot be changed. An attempt like my_string[0] = 'a' will result in a TypeError.

Any operation that appears to "modify" a string, such as concatenation or replacement, actually creates and returns an entirely new string in memory. This is a critical distinction from our previous lesson on dynamic arrays, where operations could modify the list in-place. Understanding this immutability is key to writing performant string manipulation code and avoiding common pitfalls, such as the inefficient use of concatenation in a loop, which can lead to O(n^2) complexity due to repeated memory allocations and copies.


Substring Extraction: The Power of Slicing

One of the most common string manipulations is extracting a portion of it, known as a substring. Python's slicing syntax provides an elegant and efficient way to do this. You're likely familiar with the basics, but a solid grasp of its full capabilities is essential.

Mastering String Indexing and Slicing Techniques in Python

This article provides a comprehensive overview of indexing and slicing. Please read the following sections to refresh your understanding of how to precisely extract parts of a string.

Start by reading the introduction to reinforce the concept of strings as ordered collections. Review the sections on Positive Indexing and Negative Indexing to understand how to access characters from both ends of a string. Focus on the section The Mechanics of String Slicing and the subsequent explanation of start, stop, and step parameters. Finally, read the section on Reversing Strings Through Slicing, which introduces a particularly concise technique we'll discuss next.

Slicing my_string[start:stop:step] is an O(k) operation, where k is the length of the slice being created. This is because a new string of length k must be constructed in memory.


String Reversal: A Classic Interview Problem

Reversing a string is a common task in coding interviews, often used to gauge your understanding of fundamental algorithms, complexity, and language-specific features. Let's explore a few methods, analyzing their trade-offs.

Method 1: The Two-Pointer Swap (In-Place Algorithm)

The canonical interview solution for reversing a sequence is the two-pointer technique. You place one pointer at the beginning of the sequence and another at the end, swap the elements they point to, and then move the pointers towards the center until they meet or cross.

However, due to string immutability in Python, we can't perform this swap directly on a string. The common workaround is to first convert the string to a list of characters, perform the in-place reversal on the list, and then join the list back into a string.

The following video provides an excellent walkthrough of this two-pointer approach.

Reverse String - 3 Ways - Leetcode 344 - Python

This video from NeetCode explains three ways to reverse a string. Focus on the first method presented, which is the optimal in-place algorithm using two pointers.

Watch the segment from the beginning of the explanation of the two-pointer method to its implementation and complexity analysis. Pay close attention to how the left and right pointers move and when the loop terminates.

Let's look at the implementation in Python:

def reverse_string_two_pointers(s: str) -> str:
    char_list = list(s)  # O(n) space to create the list
    left, right = 0, len(char_list) - 1
    
    while left < right:



        # Swap characters
        char_list[left], char_list[right] = char_list[right], char_list[left]



        # Move pointers
        left += 1
        right -= 1
        
    return "".join(char_list) # O(n) time to build the new string
  • Time Complexity: O(n). We iterate through roughly half the list to perform the swaps. The initial conversion to a list and the final join operation are also O(n).
  • Space Complexity: O(n). We need extra space to store the list of characters. Note that if the function signature required you to modify a list of characters in-place (as is common in LeetCode problems, e.g., def reverseString(self, s: List[str]) -> None:), the space complexity would be O(1).

For a more detailed breakdown of this optimized approach, you can refer to the following reading.

Reverse String - LearnYard

This article discusses multiple approaches to reversing a string. The "Optimized Approach" section provides another clear explanation of the two-pointer swap.

Focus your reading on the section titled Optimized Approach. The text and diagrams reinforce the intuition behind swapping s[i] with s[n-1-i] and why you only need to loop until the middle of the array.

Other Reversal Methods in Python

While the two-pointer method is a fundamental algorithm to know, Python offers more concise ways to achieve the same result. It's crucial to know their complexities as well.

This table summarizes the time and space complexities for common string reversal techniques in Python. Notice that while all efficient methods have O(n) time complexity, the naive loop-based concatenation is O(n²).

Let's break down the methods from the table:

  1. Slicing [::-1]: reversed_s = s[::-1]. This is the most Pythonic and often the fastest method in practice because its implementation is highly optimized in C. It creates a reversed copy of the string.
  2. reversed() + join(): reversed_s = "".join(reversed(s)). The reversed() function returns an iterator that yields items from the string in reverse order. "".join() then consumes this iterator to build a new string. This is also very efficient and memory-friendly.
  3. List conversion + .reverse(): This is essentially our two-pointer logic encapsulated. char_list = list(s); char_list.reverse(); reversed_s = "".join(char_list).
  4. Loop-based concatenation: This is the inefficient approach we mentioned earlier. Creating a new string in each iteration (rev_s = char + rev_s) leads to O(n^2) time complexity and should be avoided for all but the smallest strings.

Character and String Comparison

Comparing strings is another fundamental operation. In Python, the comparison operators (<, >, ==, etc.) work lexicographically. This means strings are compared character by character from left to right based on their Unicode ordinal values.

The comparison stops as soon as a pair of characters differs. For example, "apple" < "apricot" because at index 2, 'p' is equal to 'p', but at index 3, 'l' (Unicode 108) comes before 'r' (Unicode 114).

This diagram illustrates how Python compares 'chaya' and 'chavi'. It proceeds character by character until it finds a difference at the fourth position ('a' vs 'v'). Since the Unicode value of 'y' (121) is greater than 'v' (118) in the subsequent comparison shown in the example code ('y' vs 'v'), the expression `name < another_name` evaluates to False when comparing 'chaya' and 'chavi' because `y` > `v` is false if we look at the unicode values. The example image seems to have a typo, it compares 'y' and 'v' where it should be comparing 'a' and 'v'. In the string 'chaya' vs 'chavi', the first difference is at index 3, where 'a' (97) < 'v' (118), so 'chaya' < 'chavi' would be True.

Tying It All Together: A Practical Example

Many interview problems require you to combine these manipulation techniques. A classic example is "Valid Palindrome," where you must check if a string reads the same forwards and backward, ignoring case and non-alphanumeric characters.

Solving this involves:

  1. Filtering and Normalization: Iterating through the string, keeping only alphanumeric characters, and converting them to a consistent case (e.g., lowercase). Methods like .lower() and .isalnum() are useful here.
  2. Comparison: Checking if the cleaned string is equal to its reverse.

The following video demonstrates a clean solution to this problem, applying several of the string methods and the slicing reversal technique we've discussed.

Strings in Python Tutorial - Data Structures for Coding Interviews

This video covers many useful Python string methods and applies them to solve the "Valid Palindrome" problem from LeetCode.

Watch the final segment starting from the introduction of the LeetCode problem. Observe how a new string is built using only alphanumeric characters, and how slicing [::-1] is used for the final palindrome check.


Conclusion

In this lesson, we dissected the core principles of string manipulation in Python, always with an eye toward efficiency and interview-readiness. You are now equipped to handle common string operations with a clear understanding of the underlying mechanics.

Key Takeaways:

  • Immutability: Strings in Python cannot be changed in-place. All "modification" operations create new strings, a fact with critical performance implications.
  • Slicing: The [start:stop:step] syntax is a powerful and efficient tool for extracting substrings. It runs in O(k) time, where k is the length of the slice.
  • Reversal: You have multiple tools for reversal. The two-pointer swap (on a list of characters) is a fundamental algorithm to know. For concise, practical code, slicing ([::-1]) and "".join(reversed(s)) are excellent O(n) time and O(n) space solutions. Avoid O(n^2) loop-based concatenation.
  • Comparison: String comparisons are lexicographical, based on the Unicode values of characters from left to right.

In our next lesson, we will move beyond sequential data and introduce hash maps. You'll learn how this ingenious data structure allows for near-instantaneous lookups, insertions, and deletions, opening the door to solving a whole new class of problems with remarkable efficiency.