Strings
A String is a collection of Characters
A Swift String is Unicode-correct. Each Character is what a human sees as one letter. Inside,
one character can be several Unicode scalars (code points).
let a = "é" // one scalar
let b = "e\u{301}" // "e" + a combining accent: two scalars
print(a == b) // true: Swift compares what you see
print(b.count) // 1 character
print(b.unicodeScalars.count) // 2 scalars
print("café".utf8.count) // 5 bytes: "é" takes 2 bytes in UTF-8Because characters have different sizes, Swift cannot jump to "character number 5" in O(1). So
there is no integer subscript on String.
Indexing with String.Index
let s = "swift"
// s[1] // error: no Int subscript
let i = s.index(s.startIndex, offsetBy: 1)
print(s[i]) // "w"
print(s.first!, s.last!) // "s" "t"
let tail = s[s.index(after: s.startIndex)...] // "wift", a Substring
let head = s.prefix(3) // "swi", a Substringindex(_:offsetBy:) walks from the start. It is O(k). Inside a loop, this becomes O(n²).
The interview move: convert to an array once
Convert the string to [Character] at the start. Now every access is O(1).
func isPalindrome(_ s: String) -> Bool {
let chars = Array(s) // O(n) once
var left = 0
var right = chars.count - 1
while left < right {
if chars[left] != chars[right] { return false }
left += 1
right -= 1
}
return true
}
print(isPalindrome("racecar")) // true
print(isPalindrome("")) // trueSay it out loud: "Swift strings have no integer indexing, so I convert to an array first. That costs O(n) extra space."
Bytes for ASCII input
If the input is only ASCII (for example, lowercase English letters), use the UTF-8 bytes. A
[UInt8] is smaller and faster than [Character].
let bytes = Array("hello".utf8) // [104, 101, 108, 108, 111]
let a = UInt8(ascii: "a") // 97
print(Int(bytes[1] - a)) // 4: "e" is letter number 4 (counting from 0)
print(String(decoding: bytes, as: UTF8.self)) // "hello"Ask the interviewer: "Is the input only lowercase English letters?" The answer changes your data structure.
Back to a String
let chars: [Character] = ["s", "w", "i", "f", "t"]
let word = String(chars) // "swift"
let reversedWord = String(word.reversed()) // "tfiws"
let fromSub = String(word.prefix(2)) // "sw": Substring to StringCharacter helpers
let c: Character = "A"
print(c.isLetter) // true
print(c.isUppercase) // true
print(c.isNumber) // false
print(c.isWhitespace) // false
print(c.lowercased()) // "a" (this is a String, not a Character)
print(c.asciiValue!) // 65
print(Character("7").wholeNumberValue!) // 7Careful: isNumber is also true for symbols like "½". For digits, use wholeNumberValue or
c.isASCII && c.isNumber.
Converting between letters and numbers:
func letterIndex(_ c: Character) -> Int {
Int(c.asciiValue! - Character("a").asciiValue!) // "a" -> 0, "z" -> 25
}
func letter(at index: Int) -> Character {
Character(UnicodeScalar(UInt8(97 + index))) // 0 -> "a"
}
print(letterIndex("d")) // 3
print(letter(at: 3)) // "d"Everyday operations
Most coding sites let you import Foundation. Some methods below come from it.
import Foundation
let text = " Hello, World "
print(text.trimmingCharacters(in: .whitespaces)) // "Hello, World"
print(text.lowercased()) // " hello, world "
print(text.contains("World")) // true
print(text.hasPrefix(" He")) // true
print(text.replacingOccurrences(of: "l", with: "L")) // " HeLLo, WorLd "
let words = "a quick brown fox".split(separator: " ")
print(words) // ["a", "quick", "brown", "fox"]: empty parts are dropped
let parts = "a,b,,c".split(separator: ",", omittingEmptySubsequences: false)
print(parts) // ["a", "b", "", "c"]
print(["a", "b", "c"].joined(separator: "-")) // "a-b-c"Numbers and strings:
print(Int("42") as Any) // Optional(42)
print(Int("4x") as Any) // nil
print(String(42)) // "42"
print(String(5, radix: 2)) // "101"
print(Int("101", radix: 2)!) // 5
print(String(repeating: "ab", count: 3)) // "ababab"Substring
split, prefix, suffix and ranges return a Substring. A substring shares memory with the
original string, so it is cheap to make.
let sentence = "hello world"
let firstWord = sentence.split(separator: " ")[0] // Substring
let saved: String = String(firstWord) // copy it before you store it
var seen: Set<String> = []
seen.insert(saved) // a Set<String> needs a StringConvert to String when you store it, return it, or use it as a dictionary key.
Counting characters
A dictionary works for any characters:
func isAnagram(_ s: String, _ t: String) -> Bool {
guard s.count == t.count else { return false }
var counts: [Character: Int] = [:]
for c in s { counts[c, default: 0] += 1 }
for c in t {
counts[c, default: 0] -= 1
if counts[c]! < 0 { return false }
}
return true
}
print(isAnagram("listen", "silent")) // trueFor lowercase English letters, a fixed array of 26 is faster:
func letterCounts(_ s: String) -> [Int] {
var counts = Array(repeating: 0, count: 26)
let a = Int(UInt8(ascii: "a"))
for byte in s.utf8 {
counts[Int(byte) - a] += 1
}
return counts
}
print(letterCounts("eat") == letterCounts("tea")) // true[Int] is Hashable, so this array can be a dictionary key. That is the trick behind "group
anagrams".
Building a string
Use += or append. Both change the string in place.
var result = ""
for word in ["swift", "is", "fun"] {
if !result.isEmpty { result += " " }
result += word
}
print(result) // "swift is fun"When you change characters in the middle, work on a [Character] and convert back at the end:
func reverseVowels(_ s: String) -> String {
var chars = Array(s)
let vowels: Set<Character> = ["a", "e", "i", "o", "u", "A", "E", "I", "O", "U"]
var left = 0
var right = chars.count - 1
while left < right {
if !vowels.contains(chars[left]) { left += 1; continue }
if !vowels.contains(chars[right]) { right -= 1; continue }
chars.swapAt(left, right)
left += 1
right -= 1
}
return String(chars)
}
print(reverseVowels("hello")) // "holle"To reorder whole words, split, reorder and join:
func reverseWords(_ s: String) -> String {
s.split(separator: " ").reversed().joined(separator: " ")
}
print(reverseWords("the sky is blue")) // "blue is sky the"Performance traps
| Trap | Why it is slow | Fix |
|---|---|---|
s.count inside a loop | count walks the whole string: O(n) | Save it once, or use the array's count |
s.index(s.startIndex, offsetBy: i) in a loop | O(i) each time, O(n²) total | Array(s) before the loop |
Array(s) inside a loop | Copies the whole string every time | Do it once, before the loop |
s = s + x in a loop | Builds a new string each time: O(n²) | s += x or s.append(x) |
String(...) of many substrings in recursion | Each one copies | Pass start and end indices instead |
Character work on big ASCII input | Unicode rules cost time | Use s.utf8 bytes |
The same task, slow and fast:
// Slow: O(n^2), because of index(offsetBy:) inside the loop
func countVowelsSlow(_ s: String) -> Int {
var total = 0
for i in 0..<s.count {
let c = s[s.index(s.startIndex, offsetBy: i)]
if "aeiou".contains(c) { total += 1 }
}
return total
}
// Fast: O(n), one pass with a Set lookup
func countVowels(_ s: String) -> Int {
let vowels: Set<Character> = ["a", "e", "i", "o", "u"]
return s.reduce(0) { $0 + (vowels.contains($1) ? 1 : 0) }
}
print(countVowels("interview")) // 4Interview checklist
- Convert to
[Character](or[UInt8]for ASCII) once, at the start. - Ask about the character set: ASCII only, or any Unicode?
c.lowercased()returns aString. Compare withc.lowercased() == d.lowercased().splitdrops empty parts by default. UseomittingEmptySubsequences: falseif you need them.- Convert a
SubstringtoStringbefore you store it.