Why it matters
Most programs build text as they run. A report collects lines, a parser assembles a word one letter at a time and a formatter joins numbers with commas. The obvious way to do this in Python is += inside a loop, and it works. That is why it causes trouble: the program gives the right answer, and in some cases it gets slower much faster than you expect. An interviewer may ask you to explain why, and the answer depends on what the interpreter does with the old text.
Python has no StringBuilder class. The standard pattern is a list that holds the pieces, followed by one call to "".join(pieces). This lesson shows why the list pattern is the usual choice, how += behaves in CPython (the standard Python program) and where the slow case really appears. It also shows the mistakes that give a wrong answer when you edit a list in a loop.
The first lesson explained that a str never changes. This lesson is about the tool you use when you need to add to text many times.
The idea: a notepad versus a printed page
Imagine you are writing a long letter, and each time you add a sentence you have to print the whole letter again on a new sheet. The old sheet is thrown away. After a hundred sentences you have printed a hundred sheets, and each sheet is nearly as long as the letter. That is what happens when you build a string by copying the whole old text on every step.
A list of pieces is a notepad. You write each new piece on the next line of the same page, and at the end you copy the page once into one final string. In code, the two approaches look almost the same:
s = ""
for i in range(5):
s += "x" # may build a new string on every pass
parts = []
for i in range(5):
parts.append("x") # adds one piece to the end of the list
text = "".join(parts) # copies the pieces once, at the endThe loops look alike, but the work they do can differ. The rest of this lesson explains when and why.
What happens inside: why a copy costs time
A str has a fixed size. Once Python creates a string of 10 characters, that object has room for exactly 10 characters. To make it longer, Python must create a new object and copy the old characters into it. Copying 10 characters is cheap. Copying a million characters on every step of a loop is not.
A list is different. It keeps some spare room at the end. When you call append, Python writes the new item into a free slot. When the spare room runs out, Python allocates a bigger block, usually a fraction more than the old size (about one eighth more in CPython), and copies the items across once. That copy is expensive, but it happens only now and then. Across many appends, the average cost of one append stays constant. Computer scientists call this amortized cost: the occasional big copy is spread across all the cheap appends that came before it. You can state it in an interview as "append is O(1) amortized".
Joining the pieces at the end is one pass over all of them, so "".join(parts) costs O(total length). The total work for the whole loop is O(n).
Why += in a loop can be quadratic
Here is the cost of building a string one piece at a time, when every step makes a new copy. Suppose the loop runs five times and adds one character each time.
- Step 1 builds a string of length 1. It copies 0 old characters and writes 1.
- Step 2 builds a string of length 2. It copies the 1 old character and writes 1 new one.
- Step 3 copies 2 characters, step 4 copies 3, and step 5 copies 4.
The total number of copies is 0 + 1 + 2 + 3 + 4 = 10. For n steps the sum is 0 + 1 + ... + (n - 1), which equals n times (n - 1) divided by 2. For large n that is roughly n squared divided by 2, so the time grows with the square of the input. Computer scientists write this as O(n squared).
CPython often avoids the copy
CPython has an optimization for s += piece. When the string has exactly one name pointing at it, CPython can extend the string in place, without a full copy. Many loops therefore run in roughly linear time on CPython. This is an optimization, not a rule of the language. It depends on the implementation, and it stops working when another name or container also holds the old string. The next program shows both sides of that.
The program prints only the sizes, so its output does not depend on the machine:
10000 done
20000 done
40000 doneTo see the timing, change the last loop to print the elapsed time for each function. On one WSL2 Linux machine with Python 3.14.4, one run of the timing loop took about the numbers below. Your numbers will differ, so read only the shape of the rows.
| n | s += "x" (one name) | s += "x" with a snapshot per step | list and join |
|---|---|---|---|
| 10000 | 0.2 ms | 34.8 ms | 0.2 ms |
| 20000 | 0.5 ms | 132.0 ms | 0.3 ms |
| 40000 | 0.9 ms | 559.6 ms | 1.0 ms |
The first column grows slowly because CPython extends the string in place. The second column is the slow shape. Each snapshot keeps a copy of the text alive, so CPython must copy the text on every step, and the memory use grows with the square of n as well. The last doubling made that column about 4 times slower. The list-and-join column stays close to linear everywhere.
The lesson for your own code is simple. Use a list and join when a loop can run many times, when the old text is kept, or when the code may run on another Python implementation. A plain += on one name is acceptable for short loops and for a few pieces.
Do not write very large timing tests on a shared machine. The snapshot example can use a lot of memory, and the growth shape is the lesson, not the exact time.
Worked example 1: the core methods
Python has no builder with methods such as insert and reverse on the text itself. You work on a list and then join it. The next program uses the operations you will use most. Predict each line of output as you read, then run it.
The output is:
abcdef
fedcba
Xfedcba
Xedcba
Zedcba
6
eTrace each step. The list starts as ["a", "b", "c"]. append("def") adds the whole string "def" as one item, so the list has four items, and "".join(parts) gives abcdef. Note that append adds one item, and extend adds each character of an iterable. reversed(text) gives the characters from the end, and join puts them back into a string. insert(0, "X") puts X before index 0 and pushes every other item one place to the right. del chars[1] removes the item at index 1, which is f, and the rest slide left. Assigning chars[0] = "Z" replaces one item. The length is 6, because the list holds six items. Finally result[1] reads a character from the joined string, which is e.
Each operation has a cost, and you should know it before you use it inside a loop.
| Operation | What it does | Cost |
|---|---|---|
lst.append(x) | Adds one item to the end | O(1) amortized |
lst.insert(i, x) | Adds one item at index i | O(n), because later items shift |
del lst[i] | Removes one item | O(n), because later items shift |
lst[i] = x | Replaces one item | O(1) |
lst[i] | Reads one item | O(1) |
lst.pop() | Removes and returns the last item | O(1) |
len(lst) | Returns the number of items | O(1) |
"".join(lst) | Makes one string from the items | O(total length) |
Two rows deserve attention. insert(0, ...) and del lst[i] shift items, so calling them inside a loop of n steps costs O(n squared) as well. If you need to build text in reverse, append the pieces and reverse once at the end, instead of inserting each one at the front.
Calling "".join inside a loop copies the pieces every time, so call it once, at the end, rather than inside the loop. A common slow pattern is len("".join(parts)) inside a loop; use a running counter, or keep the total length in its own variable.
Worked example 2: a comma-separated list
Many programs print numbers with separators. This program reads n from the user and prints the numbers from 1 to n with commas between them, and no comma at the end.
n = int(input())
parts = []
for i in range(1, n + 1):
parts.append(str(i))
if i < n:
parts.append(",")
print("".join(parts))Type this input when the program waits for it:
5The output is:
1,2,3,4,5The key line is the if. The comma goes between two numbers, so it is written only when i is less than n. The number 5 has no comma after it, because i < n is false when i is 5. For input 1 the output is 1, with no comma at all. For input 0 the loop never runs, and print shows an empty line, which is correct for "no numbers".
Notice that str(i) converts the int to text. Python does not join numbers automatically. "".join([1, 2]) raises TypeError: sequence item 0: expected str instance, int found, so the conversion is required.
When the data is already a list of strings, Python has a shorter tool. ",".join(words) puts the separator between the items. Use it when it fits, and write the loop when you need custom logic, such as the rule above or a different format for each item.
words = ["red", "green", "blue"]
print(",".join(words))This prints red,green,blue. For a list of numbers, convert them first, for example ",".join(str(n) for n in nums).
Worked example 3: reversing the words of a sentence
This is one of the most common string questions in interviews. Given a sentence, reverse the order of its words, and keep the words themselves unchanged. The program below does two things: it reverses the word order, and it reverses each word on its own.
def reverse_word_order(s):
words = s.split()
return " ".join(reversed(words))
def reverse_each_word(s):
words = s.split()
return " ".join(w[::-1] for w in words)
line = input()
print(reverse_word_order(line))
print(reverse_each_word(line))Type this input. Keep the spaces exactly as shown: two at the start, three between is and blue, and one at the end.
the sky is blue The output is:
blue is sky the
eht yks si eulbWalk through reverse_word_order first. The call s.split() with no argument breaks the text at every run of whitespace. Leading and trailing spaces are dropped, and the three spaces between is and blue count as one break. The result is the list ["the", "sky", "is", "blue"]. reversed(words) walks the list from the last item to the first, and " ".join puts one space between the words. There is no space at the ends, so nothing needs to be trimmed afterwards.
The second function reverses each word. The slice w[::-1] is a copy of w with the characters in reverse order. The slice has three parts: start, stop and step. An empty start and stop mean "the whole string", and a step of -1 walks backwards. The output eht yks si eulb is each word spelled backwards, in the original order.
The Python split() is close to the Java split("\\s+") call, but it also drops empty pieces at the ends. With no argument it treats any run of spaces, tabs and line breaks as one separator, so you do not write a pattern for it. Use split(" ") only when you want each single space to count as a separator, which gives empty words when spaces repeat.
Both functions run in O(n) time for n characters. Each character is read once, and each appears once in the output, so the total work is proportional to the length of the input.
Deleting while you loop
Removing items inside a loop is a classic trap. The next program removes every a from aabb. It runs two versions on a list of characters: the first moves forward and the second moves backward.
The output is:
forward: abb
backward: bb
bb
bbTrace the forward version. At i = 0 the item is a, so it is deleted, and the list becomes ["a", "b", "b"]. The second a has slid into index 0. The loop then increases i to 1, which is b, so it never looks at index 0 again. The second a survives, and the output still contains an a.
The backward version avoids this. When it deletes an item at index i, every item after i shifts left, but the loop has already passed those positions. The items it has not yet checked are all at lower indexes, and they do not move. Deleting from the back is safe.
Be careful with the simpler form, for i in range(len(wrong)), which is also common. Python computes the range once, at the start. After a deletion the list is shorter, so a later index can be past the end, and the loop raises an error. Here is that form on its own, run on aabb:
text = list("aabb")
for i in range(len(text)):
if text[i] == "a":
del text[i]
print("".join(text))It stops with this traceback:
Traceback (most recent call last):
File "main.py", line 3, in <module>
if text[i] == "a":
~~~~^^^
IndexError: list index out of rangePython makes the mistake visible, but the forward while version fails silently, so it is the one to recognize.
The last two lines show the safe options. str.replace removes every a in one call. A comprehension builds a new string that keeps only the characters you want. Both cost O(n) time and O(n) extra space, and they are often easier to read than an index loop.
String versus list of pieces
Use this table to decide which type to use. The choice is about whether the text will change.
str | list of pieces | |
|---|---|---|
| Can change after creation | No | Yes, in place |
| Good for | Fixed text, dictionary keys, values passed around | Building or editing text in a loop |
Equality with == | Compares characters | Compares items one by one, so join first |
| Thread-safe | Yes, it never changes | No, guard it with a lock if several threads share it |
| Typical cost to add text | O(length of the result) with a copy, unless CPython extends it | O(1) amortized with append |
A simple rule works for most code. Collect the pieces in a list, call "".join once when you are done, and pass the resulting str to the rest of your program. Keep a str for values that other code reads, because a str cannot be changed by accident.
io.StringIO is a third option from the standard library. It is a file-like object that holds text in memory, and you add to it with write. It is useful when a function expects a file, for example when you pass the output to print(..., file=out). Its cost is similar to the list, and its final text comes from getvalue().
import io
out = io.StringIO()
out.write("hello")
out.write(" world")
print(out.getvalue())This prints hello world. For plain loops, the list and join pattern is shorter and more common.
Chaining calls and the return value
Many string methods return a new string, so you can chain them on one line. The chain runs left to right, and each call works on the result of the one before.
text = " ab "
result = text.strip().upper() + "!"
print(result)This prints AB!. Read it one step at a time: strip() removes the spaces, upper() works on the stripped text, and + "!" adds the mark at the end. The original text does not change.
The list methods append, insert, extend and sort return None. They change the list in place and do not give you a new one. This is a common surprise.
parts = ["b", "a"]
result = parts.sort()
print(result)
print(parts)This prints None and then ['a', 'b']. The name result holds None, because sort changed parts and returned nothing. Call the method on its own line, and do not assign its result.
A function you write that returns a str must end with a return that gives a str. A common error is returning the list instead:
def make(parts):
return parts
print(make(["a", "b"]) + "c")This raises TypeError: can only concatenate list (not "str") to list. The fix is return "".join(parts). The message names both types, so read it as "I have a list and you asked for a string; join it".
Worked example 4: a formatted report
Programs often print their results as a table, with the columns lined up. Building that table is a natural job for a list of lines: you add one line at a time, and each line is made of padded fields. The next program reads a small order list and prints a report. The format specs pad each field to a fixed width, and the list collects the lines.
n = int(input())
rows = []
total = 0.0
rows.append(f"{'item':<10}{'qty':>5}{'price':>10}")
rows.append("-" * 25)
for _ in range(n):
name, qty, price = input().split()
qty = int(qty)
price = float(price)
rows.append(f"{name:<10}{qty:>5}{price:>10.2f}")
total += qty * price
rows.append("-" * 25)
rows.append(f"{'total':<15}{total:>10.2f}")
print("\n".join(rows))Type this input:
3
pen 4 1.5
notebook 2 3.25
bag 1 12The output is:
item qty price
-------------------------
pen 4 1.50
notebook 2 3.25
bag 1 12.00
-------------------------
total 24.50The format spec :<10 means "left-aligned in a field 10 characters wide". :>5 means "right-aligned in 5 characters". :>10.2f means "a decimal number, right-aligned, in 10 characters, with 2 digits after the point". The f-string puts the value into the spec, so {name:<10} pads the name. The header uses single quotes inside the double-quoted f-string, which Python allows.
The line name, qty, price = input().split() reads one row and splits it at the spaces. This is simpler than reading tokens one at a time, because input() returns the whole line, and split() breaks it into the three fields.
The total line adds the quantity times the price for every row. The report is built as a list of lines and printed once with "\n".join(rows). Printing once means the output appears all at once, and it is a single write to the terminal rather than one per row.
Reversing in place with a list
Some tasks ask you to reverse text by swapping characters in place. A str cannot be changed, so first turn it into a list of characters, swap in the list, and join the list at the end. The function below uses the two-pointer pattern from the palindrome lesson.
def reverse_text(s):
chars = list(s)
i = 0
j = len(chars) - 1
while i < j:
chars[i], chars[j] = chars[j], chars[i]
i += 1
j -= 1
return "".join(chars)
print(reverse_text("stressed"))This prints desserts. The list is changed in place, so the function uses O(n) space for the single list copy and O(1) extra space for the swaps. The slice s[::-1] gives the same answer with the same cost, and it is shorter to write. The list version is worth knowing, because interviewers often ask for it when they want to see the swap pattern written out.
Python's tuple swap, chars[i], chars[j] = chars[j], chars[i], reads both values before it writes either, so it needs no temporary variable.
Common mistakes
Mistake 1: += inside a loop that can run many times, with old copies kept. The program is correct but slow. Use a list and join once the loop can run more than a few hundred times, or whenever old copies are kept. A loop of 10 items does not need the change.
Mistake 2: comparing a list with a string. ["a", "b"] == "ab" is False, because a list and a string are different types. Join the list first, then compare: "".join(["a", "b"]) == "ab" is True.
Mistake 3: forgetting join when returning a string. Returning the list gives a type error when the caller expects text, as the example above showed. Return "".join(parts).
Mistake 4: deleting or inserting in a forward loop. Items slide into the index you just checked, and the loop skips them. Loop backwards, or build a new list with a comprehension.
Mistake 5: a trailing separator. Appending a comma after every item puts one at the end. Either add the separator before every item except the first, or use ",".join(items).
Mistake 6: calling join in the loop condition. while len("".join(parts)) < 10: joins the whole list on every check. Keep a running total in a variable and test that.
Mistake 7: using insert(0, ...) to build reversed text. Each insert shifts the whole list, so the loop is O(n squared). Append the pieces and call reversed or [::-1] once at the end.
Mistake 8: using a list as a dictionary key. A list cannot be a key. Python raises TypeError: cannot use 'list' as a dict key (unhashable type: 'list'). Use a tuple of the items, or the joined string, which is hashable and never changes. A str built from the list is the simplest key.
Debugging walkthrough: a list with a leading comma
Here is a function that is supposed to join words with commas. It has a bug, and the output shows it.
def join_words(words):
parts = []
for w in words:
parts.append(",")
parts.append(w)
return "".join(parts)
print(join_words(["red", "green", "blue"]))The program prints ,red,green,blue. The comma is in front, but it should only go between words. Debug it in the same way as before: say what each line should do, then check the value after each step.
- Start with an empty list. The first word is
red, and the first pass adds","and then"red". The list holds[",", "red"]. That is already wrong, because the separator should not come before the first word. - The second word adds
","and then"green". That gives a comma betweenredandgreen, which is right, but it is one more comma than the list needs at the start. - The loop ends. There is no separator to remove, because the bug is that the comma is added before every word, including the first.
The fix is to add the separator only when the word is not the first:
def join_words(words):
parts = []
for i, w in enumerate(words):
if i > 0:
parts.append(",")
parts.append(w)
return "".join(parts)
print(join_words(["red", "green", "blue"]))This prints red,green,blue. The enumerate function gives each item and its index, so the if i > 0 test works without a separate counter. The simplest version is ",".join(words), which does the same thing and is what you should write in real code.
Debugging walkthrough: a program that slows down as the input grows
A teammate reports that a log formatter takes a second on a small file and several minutes on a large one. Here is the core of the code.
def format_lines(lines):
out = ""
for i, line in enumerate(lines):
out += f"{i + 1}: {line}\n"
return outStart with the symptom rather than the code. Time the function on 1,000 lines, then on 2,000, then on 4,000. If the time roughly doubles each time the input doubles, the cost is linear, and the bug is somewhere else. If the time roughly quadruples, the cost is O(n squared), and the likely cause is copying inside a loop.
On CPython the += line may be fast in this exact shape, because the name out is the only reference to the text. If the timing still grows fast, look for something that keeps old copies alive, such as a list of partial results, or a log that stores every intermediate string. Check which one applies, then fix that. The fix is the same in both cases: keep the output in a list, and join it once at the end.
def format_lines(lines):
out = []
for i, line in enumerate(lines):
out.append(f"{i + 1}: {line}\n")
return "".join(out)Verify the change in two ways. First, run both versions on the same small input and compare the results with ==. A quick check such as print(old_result == new_result) should print True. Second, time the new version on 1,000, 2,000 and 4,000 lines. The times should now roughly double, which is the linear growth you expect.
The last step is to look for the same pattern elsewhere in the code. A quick search for += inside a loop on a string finds the other places where the same change helps. Make the change only where the loop can run many times. A loop over a list of three items is not worth rewriting, and clear code that is fast enough is better than clever code that nobody can read.
Variations and edge cases
Good list-and-join code handles the inputs you did not expect at first.
- An empty list.
"".join([])is the empty string"". A loop that appends nothing returns"", which is the right answer for an empty list. - Reusing a list.
parts.clear()empties the list in place and keeps the same list object. Use it when you build many strings in one function and want to reuse the variable. - Reading a range. A slice such as
text[a:b]returns a string with the characters fromaup tob, excludingb, just like the first lesson. It costs O(b - a). - Searching.
text.find("x")returns the first index ofx, or -1 if it is missing. The same -1 rule from the first lesson applies. - Extending a list with itself.
parts.extend(parts)appends a copy of the current items, so["ab"]becomes["ab", "ab"]. The call finishes; it does not loop forever, becauseextendreads the current items before it adds the new ones. - Mixing types.
"".join([str(1), "+", str(2.5)])produces1+2.5.joinaccepts only strings, so every item must be converted first. - Removing a range.
del parts[start:end]removes the items fromstartup to but not includingend, the same end-exclusive rule as slices. Use it when you know the range, anddel parts[i]when you know one index. - Replacing a range.
parts[start:end] = ["XYZ"]removes the range and puts the new items in its place. For["a", "b"],parts[1:2] = ["XYZ"]gives["a", "XYZ"]. - Checking for empty.
if not parts:is true for an empty list, and it is the usual Python test. - Turning text into a number. Call
int(text). It raisesValueErrorwhen the text is not a whole number, for exampleint("12a").
Interview view: what to say out loud
When an interviewer asks you to build a string in a loop, say three things before you write code. Say which operation you will use to add text, that s += piece may copy the whole text on each step in general, and that a list with append followed by one join avoids the repeated copies. Then state the cost: O(n) for the whole build with the list, against O(n squared) when every step copies. Add one honest sentence about CPython: "CPython can often extend a string in place, so the simple loop may be fast in practice, but the list pattern is the one that holds on every implementation."
For the word-reversal question, an interviewer usually wants to see the edge cases handled. Ask what happens with leading spaces, with several spaces between words and with a sentence of one word. The code above handles all three: split() with no argument drops the leading and trailing spaces and treats any gap as one break, and a single word produces no separator. Mention that the reversal of word order takes O(n) time and O(n) extra space for the list of words. A version that swaps characters in a list can reduce the extra space, and you should be ready to describe it.
Interviewers also ask about io.StringIO. The answer is that it is a file-like object for writing text in memory, and that a list with join does the same job for most loops with less machinery.
Practice
Try each exercise before you open the solution. Exercises 1 to 4 are short. Exercises 5 to 10 combine several ideas.
- Write a function
range_text(n)that returns"1,2,3,...,n"for n at least 1, and""for n equal to 0. Test it with 0, 1 and 5. - Reverse each word in
"hello world"separately to get"olleh dlrow". Keep the original order of the words. (Hint: split, reverse each word with a slice, then join.) - Explain in two or three sentences why
parts.insert(0, ch)called n times is O(n squared), even thoughappendis O(1) amortized. - Write
remove_spaces(s)that returns the text with every space removed, using one list and a single loop. - Write
collapse_spaces(s)that turns every run of two or more spaces into one space, so"a b c"becomes"a b c". Keep single spaces that already exist. - Write
repeat_char(c, n)that returns the charactercrepeatedntimes, using a list andjoin. Make suren = 0returns the empty string. Then say whetherc * nwould do the same job. - Predict the output of this code, then run it:
parts = ["ab"]followed byparts.extend(parts)andprint("".join(parts)). Explain the result in one sentence. - Write
is_palindrome_copy(s)that builds a reversed copy with a slice and compares it with the original. Then explain why this version uses O(n) extra space, and what a two-pointer version would save. - A function is meant to build
"[1, 2, 3]"from a list of numbers. It returns"[1, 2, 3, ]"instead. Find the bug and fix it, keeping the same output format. - Write
compress_runs(s)using a list, where each run of the same letter becomes the letter followed by its count, always, even when the result is longer. Test it on"aabcccccaaa", which should givea2b1c5a3.
Show the answer
- Use a comprehension:
",".join(str(i) for i in range(1, n + 1)). Forn = 0the range is empty and the function returns"". For 1 it returns1, and for 5 it returns1,2,3,4,5. split()gives the words. For each word,w[::-1]is the reversed word, and" ".joinputs one space between them. The output isolleh dlrow.- Each
insert(0, ch)shifts every item already in the list one place to the right. On step k the list holds about k items, so the shifts add up to 1 + 2 + ... + n, which is O(n squared).appendwrites at the end without shifting anything, so it is O(1) amortized. parts = [], thenfor ch in s: if ch != " ": parts.append(ch), thenreturn "".join(parts).- Keep a flag
last_was_space. Append a character if it is not a space, or if it is a space and the last appended character was not a space. Return"".join(parts). For"a b c"the result is"a b c". - Build a list with
ncopies ofcand join it. Withn = 0the loop does nothing and the empty string comes back.c * ngives the same result in one step, and it is the simpler choice in real code. - The output is
abab.extendreads the current items before it adds new ones, so it appends a copy ofabto the end of the list. return s == s[::-1]. The reversed copy takes O(n) extra space. A two-pointer loop compares characters from both ends without building any copy, so it uses O(1) extra space.- The bug is that the separator is appended after every number, including the last one. Use the first-item check: append
", "only wheni > 0, and append"]"after the loop. The output becomes[1, 2, 3]. - Walk the string with an index
i. For each run, find the endjwhere the letter changes, then appends[i]andj - ias text. Seti = jand repeat. For"aabcccccaaa"the output isa2b1c5a3.
What you now know
- A
strhas a fixed size, so changing it means a new copy. A list has spare room, soappendis O(1) amortized. - A loop that copies the whole text on each step costs O(n squared). A loop that appends to a list and joins once costs O(n).
- CPython can often extend a string in place when only one name refers to it. That makes the simple loop fast in many cases, but it is an optimization, not a guarantee.
insertanddelon a list shift many items, so they cost O(n) each. Avoid them inside a loop.- Call
"".joinonce when you are done, and pass the resultingstron. - When you delete items with an index loop, go backwards, or build a new list with a comprehension.
- Use
",".join(items)for a list of strings, and a list of pieces when the loop has custom rules.
The next lesson uses these tools to solve palindrome and anagram questions, which are asked in almost every string interview.