上次講解了基本的語法和一些Swift的小技巧。這期我們來看幾個最基本的數據結構:數組,字符串,集合和字典。
數組
數組是最基本的數據結構。Swift中改變了以前Objective-C時代NSMutableArray和NSArray分開的做法,統一到了Array唯一的數據結構。雖然看上去就一種數據結構,其實它的實現有三種:
ContiguousArray<Element>:效率最高,元素分配在連續的元素上。如果數組是值類型(棧上操作),Swift會自動調用Array的這種實現;如果注重效率,推薦聲明這種類型,尤其是在大量元素是類時,這樣做效果會很好。
Array<Element>:會自動橋接到 Objective-C 中的 NSArray,如果是值類型,其性能與ContiguousArray無差別。
ArraySlice<Element>:它不是一個新的數組。只是一個片段,內存上與原數組享用同一區域。
下面是數組最基本的一些運用。
// 聲明一個不可修改的數組
let nums = [1, 2, 3]
let nums = [Int](repeating: 0, count: 5)
// 聲明一個可以修改的數組
var nums = [3, 1, 2]
// 增加一個元素
nums.append(4)
// 對原數組進行升序排序
nums.sort()
// 對原數組進行降序排序
nums.sort(by: >)
// 將原數組除了最后一個以外的所有元素賦值給另一個數組
let anotherNums = Array(nums[0 ..< nums.count - 1])
不要小看這些簡單的操作:數組可以依靠它們實現更多的數據結構。Swift雖然不像Java中有現成的隊列和棧,但我們完全可以用數組配合最簡單的操作實現這些數據結構,下面就是用數組實現棧的示例代碼。
// 用數組實現棧
class Stack {
var stack: [AnyObject]
var isEmpty: Bool { return stack.isEmpty }
var peek: AnyObject? { return stack.last }
init() {
stack = [AnyObject]()
}
func push(object: AnyObject) {
stack.append(object)
}
func pop() -> AnyObject? {
if (!isEmpty()) {
return stack.removeLast()
} else {
return nil
}
}
}
最后特別強調一個操作:reserveCapacity()
。它用于為原數組預留空間,防止數組在增加和刪除元素時反復申請內存空間或是創建新數組,特別適用于創建和removeAll()時候進行調用,為整段代碼起到提高性能的作用。
字典和集合
這兩個數據結構經常使用的原因在于,查找數據的時間復雜度為O(1)。一般字典和集合要求它們的Key都必須遵守Hashable協議,Cocoa中的基本數據類型都滿足這一點;自定義的class需要實現Hashable,而又因為Hashable是對Equable的擴展,所以還要重載 == 運算符。
下面是關于字典和集合的一些實用操作:
let oddNums: Set = [1, 3, 5, 7, 9]
let primeNums: Set = [3, 5, 7, 11, 13]
// 交集、并集、差集
let oddAndPrimeNums = primeNums.intersection(oddNums)
let oddOrPrimeNums = primeNums.union(oddNums)
let oddNotPrimeNums = oddNums.subtracting(primeNums)
// 用字典和高階函數計算字符串中每個字符的出現頻率
Dictionary("hello".map { ($0, 1) }, uniquingKeysWith: +)
集合和字典在實戰中經常與數組配合使用,請看下面這道算法題:
給一個整型數組和一個目標值,判斷數組中是否有兩個數字之和等于目標值
這道題是傳說中經典的2Sum,我們已經有一個數組記為nums,也有一個目標值記為target,最后要返回一個Bool值。
最粗暴的方法就是每次選中一個數,然后遍歷整個數組,判斷是否有另一個數使兩者之和為target。這種做法時間復雜度為O(n^2)。
采用集合可以優化時間復雜度。在遍歷數組的過程中,用集合每次保存當前值。假如集合中已經有了目標值減去當前值,則證明在之前的遍歷中一定有一個數與當前值之和等于目標值。這種做法時間復雜度為O(n),代碼如下。
func twoSum(nums: [Int], _ target: Int) -> Bool {
var set = Set<Int>()
for num in nums {
if set.contains(target - num) {
return true
}
set.insert(num)
}
return false
}
如果把題目稍微修改下,變為
給定一個整型數組中有且僅有兩個數字之和等于目標值,求兩個數字在數組中的序號
思路與上題基本類似,但是為了方便拿到序列號,我們采用字典,時間復雜度依然是O(n)。代碼如下。
func twoSum(nums: [Int], _ target: Int) -> [Int] {
var dict = [Int: Int]()
for (i, num) in nums.enumerated() {
if let lastIndex = dict[target - num] {
return [lastIndex, i]
} else {
dict[num] = i
}
}
fatalError("No valid output!")
}
字符串
字符串在算法實戰中極其常見。在Swift中,字符串不同于其他語言(包括Objective-C),它是值類型而非引用類型。首先還是列舉一下字符串的通常用法:
// 字符串和數字之間的轉換
let str = "3"
let num = Int(str)
let number = 3
let string = String(num)
// 字符串長度
let len = str.count
// 訪問字符串中的單個字符,時間復雜度為O(1)
let char = str[str.index(str.startIndex, offsetBy: n)]
// 修改字符串
str.remove(at: n)
str.append("c")
str += "hello world"
// 檢測字符串是否是由數字構成
func isStrNum(str: String) -> Bool {
return Int(str) != nil
}
// 將字符串按字母排序(不考慮大小寫)
func sortStr(str: String) -> String {
return String(str.sorted())
}
下面是本篇的精華所在,我們來一起看一道以前的Google面試題。
Given an input string, reverse the string word by word.
A word is defined as a sequence of non-space characters.
The input string does not contain leading or trailing spaces and the words are always separated by a single space.
For example,
Given s = "the sky is blue",
return "blue is sky the".
Could you do it in-place without allocating extra space?
這道題目一看好簡單,不就是翻轉字符串的翻版嗎?這種方法有以下兩個問題
- 每個單詞長度不一樣
- 空格需要特殊處理
這樣一來代碼寫起來會很繁瑣而且容易出錯。不如我們先實現一個字符串翻轉的方法。
fileprivate func _reverse<T>(_ chars: inout [T], _ start: Int, _ end: Int) {
var start = start, end = end
while start < end {
_swap(&chars, start, end)
start += 1
end -= 1
}
}
fileprivate func swap<T>(_ chars: inout [T], _ p: Int, _ q: Int) {
(chars[p], chars[q]) = (chars[q], chars[p])
}
有了這個方法,我們就可以實行下面兩種字符串翻轉:
- 整個字符串翻轉,"the sky is blue" -> "eulb si yks eht"
- 每個單詞作為一個字符串單獨翻轉,"eulb si yks eht" -> "blue is sky the"
整體思路有了,我們就可以解決這道問題了
func reverseWords(s: String?) -> String? {
guard let s = s else {
return nil
}
var chars = Array(s), start = 0
_reverse(&chars, 0, chars.count - 1)
for i in 0 ..< chars.count {
if i == chars.count - 1 || chars[i + 1] == " " {
_reverse(&chars, start, i)
start = i + 2
}
}
return String(chars)
}
時間復雜度還是O(n),整體思路和代碼簡單很多。
總結
Swift中數組、字符串、集合以及字典是最基本的數據結構,但是圍繞這些數據結構的問題層出不窮。幸運的是解決方法也并不是千變萬化、高深莫測,大家做好相應的積累即可。下期我們講鏈表。