iq.lab
Python starts when a code cell comes near or you run one
01 Python, one step at a time

Lesson 4 of 6

Strings

A string is a row of characters that never changes. You read it like a list, walk it from either end, and build new strings from a list of pieces.

About 1 hour 5 minutes, with the 2 problems worked in it
By the end you can
  • Index, slice and walk a string, including from the end

  • Build a new string from a list of pieces, and split a string into words

  • Turn a letter into a number from 0 to 25 with ord, and back with chr

  • Count down with a range whose step is -1

A log line arrives as one string: " Load the TABLE ". At work you would trim it, lowercase it and split it into words with SQL's TRIM, LOWER and a split function, or with Python's string methods, and move on.

In an interview the same moves come with a twist: find the length of the last word without splitting, reverse the order of the words, ignore everything that is not a letter. To answer those you need to see a string the way Python does: a row of characters, numbered from 0, that never changes.

This lesson covers strings: reading them by position, walking them from either end, and building new ones. Every string tool here is a loop you could write yourself, so we show the loop first. That way you always know what a tool does at the edges and what it costs. The next lesson adds the tools that work on several values at once: tuples, dicts and sorting.

A string is a row of characters

A string is a sequence of characters, and you read it like a list. s = "spark" holds five characters at positions 0 to 4. len(s) is 5, s[0] is "s", and negative positions count from the end, so s[-1] is "k". A single character is itself a string of length 1.

Slices work as they do for lists: s[1:4] starts at position 1 and stops before position 4. Leave out the start and the slice begins at 0; leave out the stop and it runs to the end. So s[:2] is "sp" and s[2:] is "ark". A third number is the step, how far each move goes. A step of -1 walks backwards, and with the start and stop left out it covers the whole string: s[::-1] is "kraps".

The trace below draws s as a row of cells, with an arrow for the position i. Before you step through, guess what line 3 prints.

Step throughA string is a row of characters
Step 1 of 15
Line 1 is about to run.
s = "spark"
print(len(s), s[0], s[-1])
print(s[1:4])
for i in range(len(s)):
print(i, s[i])
Names
no names yet
Output
(nothing printed yet)

You can also loop over the characters themselves. for c in s: hands out "s", "p", "a", "r", "k", the way for x in nums: hands out items. Use positions when you need a neighbor, or a different order.

PredictSlices of a pipeline

Type exactly what this prints, one line per print. Number the characters of "pipeline" from 0 first.

word = "pipeline"
print(word[0], word[-1])
print(word[:4])
print(word[4:])
print(len(word))

Walking from the end

Some answers sit at the end of a string: the last word, a file extension, the spaces at the end (called trailing spaces). Then the walk starts at the last position, len(s) - 1, and moves left. A while loop fits, because it stops the moment its condition fails. Here we look for the last space in "big data", which tells us where the last word starts. Guess first: at which position does i stop?

Step throughWalking from the end
Step 1 of 13
Line 1 is about to run.
s = "big data"
i = len(s) - 1
while i >= 0 and s[i] != " ":
i -= 1
print(i, s[i + 1:])
Names
no names yet
Output
(nothing printed yet)

Why test i >= 0 at all? Because a negative position is legal in Python: s[-1] is the last character, not an error. Without the test, a string with no space would not stop at the start. It would read the string again from the end until the positions ran out and Python raised an IndexError. Put i >= 0 first: and checks its left side first and skips the right side when the left is false (the short-circuiting from lesson 3), so s[i] is never read with a negative i.

Work the same loop by hand on a string with no space, the case where i >= 0 does its job.

On paperA walk with no space to find

Trace this loop by hand on "etl", a string with no space. Write down i each time the while condition is checked, and what the program prints.

s = "etl"
i = len(s) - 1
while i >= 0 and s[i] != " ":
    i -= 1
print(i, s[i + 1:])

What is i when the loop stops?

Work it on real paper: writing each step is the point. Then check your final answer here and compare your working with the walk-through.

i when the loop stops

Type a number: 12, -1, 0.5 and 3/4 all work. Enter checks.

The next problem needs two of these walks, one after the other. The string may end in spaces, so first move left past them. Then move left through the last word, counting letters, and stop at a space or at the start.

easyLoops and basics target 15 min

Length of the last word

A string s is made of English letters and spaces. A word is a stretch of letters side by side, with a space or an end of s on each side: in "big data" the words are big and data, and dat is not a word. Spaces can sit at the start, at the end, or several in a row.

Return the number of letters in the last word of s. There is always at least one word.

Example 1
Inputs = "load the table "Output5

The last word is table. The two spaces after it are not a word.

Example 2
Inputs = " sort merge join"Output4

The two spaces at the start and the three between sort and merge change nothing: the last word is join.

Example 3
Inputs = "a"Output1

One word of one letter, starting at position 0.

Constraints
  • 1 ≤ len(s) ≤ 104

  • Every character is an English letter or a space.

  • At least one character is a letter.

Plan it first

Write a line for each before you code, then say them out loud. Compare with the Approach tab afterwards.

⌘+Enter runs 0:00Python starts when a code cell comes near or you run one
Run examples checks the examples. Submit runs every test, including edge cases and, when the problem has one, a speed check on a large input.

The counting walk is the loop you traced, with a counter added. The problem's solution also shows a one-line version with split, a tool this lesson covers below. If an interviewer asks for a version without split, the walk is it: it reads only the end of the string and builds no list.

So far: a string is a row of characters with positions from 0, read with s[i], slices and loops like a list. To find something at the end, start at len(s) - 1 and walk left, testing i >= 0 before s[i].

Strings never change: build a list, then join

Try to change one character and Python refuses:

s = "spark"
s[0] = "S"   # TypeError: 'str' object does not support item assignment

A string cannot be changed after it is made. The word for this is immutable. No string method changes the string it is called on: s.upper() hands back a new string, "SPARK", and s is still "spark". To get "Spark", you build a new string: "S" + s[1:].

So to build a string piece by piece, collect the pieces in a list and join them once at the end. "".join(pieces) glues a list of strings into one, with nothing between them. Adding to a string with += in a loop also works, but in general each += makes a new string and copies everything built so far, so the copying grows with every piece.

Here is the pattern on a cleanup familiar from data work: keep only letters and digits, in lowercase. Two methods do the per-character work. c.isalnum() is True when c is a letter or a digit, and c.lower() hands back the lowercase version. Guess what clean("Top 3.") returns before you step through.

Step throughBuild a list, then join
Step 1 of 22
Line 1 is about to run.
def clean(s):
out = []
for c in s:
if c.isalnum():
out.append(c.lower())
return "".join(out)
Names
no names yet
Output
(nothing printed yet)

Characters are numbers

Every character has a number, its code. ord(c) gives the code of c, and chr(k) gives the character whose code is k. The lowercase letters have the codes 97 to 122, in alphabetical order, so subtracting the code of "a" turns a letter into its place in the alphabet, counting from 0:

cord(c)ord(c) - ord("a")
"a"970
"b"981
"z"12225
"A"65-32

ord(c) - ord("a") maps the letters a to z onto the positions 0 to 25. The last row is the warning: capitals have their own codes, 65 to 90, so lowercase first or check the range. chr goes back the other way: chr(ord("a") + 2) is "c".

So a plain list of 26 numbers can count letters: position 0 counts the a's, position 25 the z's. The trace counts the letters of "dead". [0] * 26 makes a list of 26 zeros (* on a list repeats its items). Before you step through, guess what the last line prints: the counts for a, b, c, d and e.

Step throughCounting letters by position
Step 1 of 12
Line 1 is about to run.
26 zeros: position 0 counts the a's, position 25 the z's.
counts = [0] * 26
for c in "dead":
counts[ord(c) - ord("a")] += 1
print(counts[:5])
Names
no names yet
Output
(nothing printed yet)

The list holds 26 counters however long the string is, so it costs O(1) extra space, and the walk costs one step per character, O(n) time. The next lesson counts with a dict instead, which works for any character, and for whole words too.

Work it outA letter as a number

What is ord("k") - ord("a")? Type a whole number.

ord("k") - ord("a")

Type a number: 12, -1, 0.5 and 3/4 all work. Enter checks.

For practice, the bank has To lower case (LeetCode 709): lower a string by hand with ord and chr, building the answer in a list. Try it after this lesson.

Words: split and join

s.split() with no argument cuts a string into its words. It splits at every run of spaces (one or more spaces side by side) and drops the spaces at both ends. sep.join(words) goes the other way: it glues a list of strings into one, with sep between neighbors.

To walk the words from last to first with a for loop, give range a third number. range(start, stop, step) moves by step each time and stops before stop. A step of -1 counts down: range(2, -1, -1) gives 2, 1, 0. The stop is -1, not 0, because a range stops before its stop, and 0 is the last position we want.

The trace splits a line with messy spacing, walks the words from the end, and joins them back with -. Guess what the last line prints.

Step throughWords out, and back in
Step 1 of 11
Line 1 is about to run.
line = " load the table"
words = line.split()
for i in range(len(words) - 1, -1, -1):
print(words[i])
print("-".join(words))
Names
no names yet
Output
(nothing printed yet)

Now put split, a list and join together in the right order, to turn "extract transform load" into its initials, "ETL".

Put in orderInitials

Put the lines in order to build initials(s), which returns the first letter of each word as a capital: initials("extract transform load") is "ETL". Words may be separated by several spaces. Indentation is already part of each line. Two of the lines do not belong.

word[0].upper() is the first character of word as a capital.

Your program

Add lines from the list below, in order.

    Unused lines (some do not belong)
    • return "".join(out)
    • def initials(s):
    • return out
    • for word in s.split():
    • for word in s.split(" "):
    • out.append(word[0].upper())
    • out = []

    Reversing the order of the words takes all three moves: split to get the words, walk them from the last to the first while collecting them in a list, and join once with one space.

    mediumLoops and basics target 25 min

    Reverse the words

    A string s is made of English letters, digits and spaces. A word is a stretch of letters and digits side by side, with a space or an end of s on each side. Spaces can sit between words, at the start, at the end, or several in a row.

    Return a new string with the words in reverse order: the last word first and the first word last. Each word keeps its own characters in their original order. Put exactly one space between neighboring words and no space at the start or the end. There is always at least one word.

    Example 1
    Inputs = " read the logs "Output"logs the read"

    The two spaces at the start and the one at the end are gone, and the three spaces between the and logs shrink to one. One space separates each pair of words.

    Example 2
    Inputs = "extract transform load"Output"load transform extract"
    Example 3
    Inputs = "batch"Output"batch"

    One word in reverse order is the same word.

    Constraints
    • 1 ≤ len(s) ≤ 2 × 106

    • Every character is an English letter, a digit or a space.

    • At least one character is not a space.

    Plan it first

    Write a line for each before you code, then say them out loud. Compare with the Approach tab afterwards.

    ⌘+Enter runs 0:00Python starts when a code cell comes near or you run one
    Run examples checks the examples. Submit runs every test, including edge cases and, when the problem has one, a speed check on a large input.

    The problem's speed test, with 300,000 words, is immutability in action. Putting each word in front of a growing string copies that string every time. Collecting a list and joining once copies each character once.

    So far: strings never change, so build new ones from a list of pieces and join once. split() gives the words, join glues them back, and ord(c) - ord("a") turns a lowercase letter into a number from 0 to 25.

    Next: Tuples, dicts and the toolkit