Find And Replace in String

Find And Replace in String trips people up because of one word: offsets. You replace parts of a string at given spots. But every replacement changes the length. So the spots shift. The interviewer wants to see if you handle that shifting cleanly instead of getting lost in moving positions.

🎯 The Problem

You run replacement jobs on a string, but only where the job truly matches.

  • Each job has a position, a source word to look for, and a target word to swap in.
  • A job fires only if the source word really sits at that position.
  • If it matches, swap in the target. If not, leave that spot alone.
  • All positions point at the original string, not the changed one. So check every match against the original.

For "abcd": one job is at position 0, look for "a", replace with "eee". Another is at position 2, look for "cd", replace with "ffff". Both match. The result is "eeebffff".

Input: s = "abcd", indices = [0, 2], sources = ["a", "cd"], targets = ["eee", "ffff"]
Output: "eeebffff"
Explanation: at 0 "a" -> "eee", letter b stays, at 2 "cd" -> "ffff".

Here is how each job maps onto the original string.

abcd

job at 0: a matches -> eee

index 1: b has no job -> keep

job at 2: cd matches -> ffff

eee

b

ffff

result eeebffff

🐒 Approach 1: Replace Left to Right and Shift (Brute Force)

The idea in one line: edit the string job by job and track how much each edit moves the later positions.

The idea:

  • Do the jobs one by one from left to right.
  • Cut out the source. Paste in the target.

How it works:

  • Each replacement changes the length, so later positions move.
  • That shift is called an offset. It is the move in positions caused by an earlier change.
  • After adding "eee" for "a", the next job’s position is off by two. You add that shift yourself.

Why it is weak:

  • The offset math is easy to get wrong.
  • One mistake and the whole answer is off.

Here is the left-to-right replacement code:

find_and_replace_brute_force.py
def find_replace_string(s, indices, sources, targets):
shift = 0
jobs = sorted(zip(indices, sources, targets))
for index, source, target in jobs:
real_index = index + shift
if s.startswith(source, real_index):
s = s[:real_index] + target + s[real_index + len(source):]
shift += len(target) - len(source)
return s

⚑ Approach 2: Sort Jobs and Build Once (Best)

The idea in one line: never edit the original, build a fresh string while sweeping left to right.

The idea:

  • Do not touch the original string.
  • Build a brand new string from left to right.
  • First sort the jobs by position, from smallest to largest, so we can sweep once.

How it works:

  • Walk a pointer along the original string.
  • If a job starts here and its source matches the original, add the target and jump the pointer past the source.
  • If no job starts here, copy the current character and move the pointer by one.

Why it is fast:

  • We always check matches against the original, so positions never shift.
  • There is no offset math at all.

Here is the build sweep on the example.

pointer at 0

job here, a matches -> add eee, jump to 1

pointer at 1, no job -> copy b, go to 2

pointer at 2, job here, cd matches -> add ffff, jump to 4

pointer at 4, end

result eeebffff

Steps to Solve

  1. Pair each job’s position with its source and target. Sort these jobs by position.
  2. Start a pointer at the beginning of the original string and an empty result.
  3. Walk the pointer along the string.
  4. If a job starts at the pointer and its source matches the original string there, add the target to the result and move the pointer past the source.
  5. If no job matches here, copy the current character to the result and move the pointer by one.
  6. When the pointer reaches the end, return the result.

This Python version builds a map from each index to its job, then sweeps the string once from left to right.

find_replace_in_string.py
def find_replace(s, indices, sources, targets):
job_at = {} # index -> job number
for j in range(len(indices)):
job_at[indices[j]] = j
result = []
i = 0
while i < len(s):
if i in job_at: # a job starts at this index
j = job_at[i]
if s.startswith(sources[j], i): # source matches the original here
result.append(targets[j]) # add the target word
i += len(sources[j]) # jump past the matched source
continue
result.append(s[i]) # no match, copy one character
i += 1
return "".join(result)
s = "abcd"
indices = [0, 2]
sources = ["a", "cd"]
targets = ["eee", "ffff"]
print(find_replace(s, indices, sources, targets))

The output of the above code will be:

eeebffff

Let us walk through the Python version line by line, because it shows how we dodge the offset problem.

job_at = {} is a map from an index to its job number. We build it so that when our pointer reaches some index, we can ask in one step β€œis there a job here?”. The lookup is instant.

for j in range(len(indices)): job_at[indices[j]] = j fills that map. For each job we store its index pointing to its job number. So index 0 points to job 0, and index 2 points to job 1.

result = [] is where we build the new string. We use a list and join at the end because that is faster than adding to a string over and over.

while i < len(s): sweeps the pointer i along the original string. Notice we walk the original, never a changed copy. That is the whole reason the offsets never bite us.

if i in job_at: checks whether a job starts at the current index. If yes, we grab its job number with j = job_at[i].

if s.startswith(sources[j], i): checks the source actually sits at index i in the original. The startswith with a start position is the clean way to test a match at a spot. A job only fires if its source really matches.

result.append(targets[j]) adds the target word in place of the source. Then i += len(sources[j]) jumps the pointer past the whole source. So we never re-read the letters we just replaced. The continue sends us back to the top of the loop.

result.append(s[i]); i += 1 is the no-match path. If no job starts here, or the source did not match, we copy the single character and move on by one.

return "".join(result) glues all the kept and replaced pieces into the final string.

⏱️ Time and Space Complexity

The naive shift approach edits the string again and again, and tracking offsets is error-prone and can be slow. The sweep approach walks the string once and does a quick match check at each job index. Sorting the jobs costs O(k log k), where k is the number of jobs. The sweep itself is O(n) for the string length. The new string takes O(n) space. So this is fast and, more importantly, it never gets tangled in shifting positions, which keeps it correct.

Approach Time Complexity Space Complexity
Replace left to right with offsets O(n * k) O(n)
Sort jobs and build once O(n + k log k) O(n)

Tip

The trick that wins this question is checking every match against the original string and building a fresh one. Say that out loud. It tells the interviewer you saw the offset trap and stepped around it.

🧩 Key Takeaways

  • βœ… All job positions point at the original string, so always check matches there.
  • βœ… Build a new string instead of editing the old one, so positions never shift.
  • βœ… A job only fires if its source word truly sits at that index.
  • βœ… When a job fires, jump the pointer past the whole source word.
  • βœ… This avoids all the offset math that makes the naive approach buggy.

Check Your Knowledge

4 questions Show quiz Hide quiz

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    Where do the job positions point?

    Why: Every index points at the original string, so we always check matches against the original.

  2. 2

    Why does building a new string avoid the offset problem?

    Why: Since we never edit the original, the indices stay fixed and there is no shifting to track.

  3. 3

    When does a replacement job actually happen?

    Why: A job fires only when its source word truly sits at the given index in the original string.

  4. 4

    After a job replaces a source, how far does the pointer move?

    Why: We jump the pointer past the entire matched source so we do not re-read those characters.

πŸš€ What’s Next?