Swift 算法實戰之路:數組,字符串,集合,與字典


上次講解了基本的語法和一些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中數組、字符串、集合以及字典是最基本的數據結構,但是圍繞這些數據結構的問題層出不窮。幸運的是解決方法也并不是千變萬化、高深莫測,大家做好相應的積累即可。下期我們講鏈表。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容