Skip to Content
Course content

221: Solving Array and String Problems in Swift

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're standing in front of a long conveyor belt at a distribution center. Boxes are sliding past you one by one. If I asked you to find two boxes that together weigh exactly 50 lbs, you wouldn't just pick up the first box and then walk back to the start of the belt for every single other box to check the weight. That would be exhausting and incredibly slow. Instead, you'd probably use your hands as markers—maybe one hand at the start and one at the end—and slide them toward each other based on the total weight you're seeing. Or, if I asked you to find the longest stretch of boxes that are all the same color, you'd keep a "window" of your vision focused on a section of the belt, expanding it as long as the color matches and shrinking it the moment it doesn't.

In Swift, solving array and string problems is exactly like managing that conveyor belt. The "belt" is your Array or String, and your "hands" are your indices. The trick isn't just knowing the syntax; it's knowing which movement pattern to use so you aren't looping through the same data a thousand times.

Moving the Markers: The Two-Pointer Technique

I see a lot of developers default to nested for loops whenever they need to compare elements. But nested loops are often a sign that you're doing more work than necessary. The two-pointer approach is your best friend when you're dealing with sorted data or looking for pairs.

Take a palindrome check, for example. You don't need to reverse the whole string and compare it to the original—that allocates extra memory. Instead, put one pointer at the start and one at the end, and move them toward the center.

func isPalindrome(_ text: String) -> Bool {
    let characters = Array(text.lowercased())
    var left = 0
    var right = characters.count - 1
    
    while left < right {
        if characters[left] != characters[right] {
            return false
        }
        left += 1
        right -= 1
    }
    return true
}

Notice how I converted the string to an Array first? I'll get to why in a second, but for now, focus on the logic: we only traverse the list once. That's a massive win for performance.

Expanding and Contracting the Sliding Window

Sometimes you aren't looking for a pair, but a segment. This is where the "Sliding Window" comes in. Think of it like a magnifying glass that you slide across the array. You expand the right side to include more data, and when a certain condition is broken, you shrink the left side until the condition is valid again.

Let's say you need to find the length of the longest substring without repeating characters. You can't just use a simple loop because the "valid" part of your string changes size dynamically.

func longestUniqueSubstring(_ s: String) -> Int {
    let chars = Array(s)
    var maxLength = 0
    var start = 0
    var seenChars = Set<Character>()
    
    for end in 0..

The while loop inside the for loop might look like it's making the code slow, but it's not. Each element is added to the set once and removed once. In the industry, we call this O(n) time complexity. It's the difference between a professional implementation and a prototype that crashes on large datasets.

The Swift String Index Trap

Here is a bit of a warning: Swift strings are not like arrays. You can't just call myString[5]. Because Swift supports Unicode (meaning one "character" might actually be multiple bytes under the hood), calculating the position of a character takes time. This is why, in the examples above, I cast the strings to [Character] using Array(text).

If you're working with massive strings where converting to an array would eat too much memory, you'll have to use String.Index. It's clunkier—you have to use index(after:) and index(before:)—but it's the "correct" way to handle strings without copying them into a new array. I usually start with the array conversion for clarity during interviews or quick tasks, but keep the index methods in your back pocket for production-level memory optimization.

Tracking Frequency with Dictionaries

Finally, when the problem asks about "the first unique character" or "how many times X appears," stop thinking about loops and start thinking about Dictionaries. A dictionary acts as your "notepad" on the conveyor belt, allowing you to remember everything you've seen in a single pass.

If you need to find the first character that doesn't repeat, you do two passes: one to count everything, and one to find the first character whose count is exactly one. It's clean, it's fast, and it avoids the "nested loop" nightmare.




📋 Practical Task

Build a "Most Frequent Word" Analyzer

You're building a feature for a text analysis tool. Your task is to write a function that takes a string of text and returns the word that appears most frequently. If there is a tie, any of the most frequent words will do.

Requirements:

  • The function should be case-insensitive (e.g., "Swift" and "swift" are the same word).
  • Ignore punctuation marks like commas and periods.
  • Return the word as a string, or nil if the input string is empty.

Starter Code:

func findMostFrequentWord(in text: String) -> String? {
    // Your implementation here
}

// Test it out:
let sampleText = "Swift is great. Learning Swift is fun, and Swift is powerful!"
print(findMostFrequentWord(in: sampleText) ?? "No word found") 
// Expected output: "swift"
Rating
0 0

There are no comments for now.

to be the first to leave a comment.