Swift 算法實戰之路:棧和隊列

這期的內容有點劍走偏鋒,我們來討論一下棧和隊列。Swift語言中沒有內設的棧和隊列,很多擴展庫中使用Generic Type來實現棧或是隊列。正規的做法使用鏈表來實現,這樣可以保證加入和刪除的時間復雜度是O(1)。然而筆者覺得最實用的實現方法是使用數組,因為 Swift 沒有現成的鏈表,而數組又有很多的 API 可以直接使用,非常方便。本期主要內容有:

  • 棧和隊列的基本Swift實現,以及在iOS開發中應用的實例
  • Facebook棧相關面試題一道
  • 棧和隊列的互相實現及其思想

實現

對于棧來說,我們需要了解以下幾點:

  • 棧是后進先出的結構。你可以理解成有好幾個盤子要壘成一疊,哪個盤子最后疊上去,下次使用的時候它就最先被抽出去。
  • 在iOS開發中,如果你要在你的App中添加撤銷操作(比如刪除圖片,恢復刪除圖片),那么棧是首選數據結構
  • 無論在面試還是寫App中,只關注棧的這幾個基本操作:push, pop, isEmpty, peek, size。
protocol Stack {
  /// 持有的元素類型
  associatedtype Element
  
  /// 是否為空
  var isEmpty: Bool { get }
  /// 棧的大小
  var size: Int { get }
  /// 棧頂元素
  var peek: Element? { get }
  
  /// 進棧
  mutating func push(_ newElement: Element)
  /// 出棧
  mutating func pop() -> Element?
}

struct IntegerStack: Stack {
  typealias Element = Int
  
  var isEmpty: Bool { return stack.isEmpty }
  var size: Int { return stack.count }
  var peek: Element? { return stack.last }
  
  private var stack = [Element]()
  
  func push(_ newElement: Element) {
    stack.append(newElement)
  }
  
  func pop() -> Element? {
    return stack.popLast()
  }
}

對于隊列來說,我們需要了解以下幾點:

  • 隊列是先進先出的結構。這個正好就像現實生活中排隊買票,誰先來排隊,誰先買到票。
  • iOS開發中多線程的GCD和NSOperationQueue就是基于隊列實現的。
  • 關于隊列我們只關注這幾個操作:enqueue, dequeue, isEmpty, peek, size。
protocol Queue {
  /// 持有的元素類型
  associatedtype Element
  
  /// 是否為空
  var isEmpty: Bool { get }
  /// 棧的大小
  var size: Int { get }
  /// 棧頂元素
  var peek: Element? { get }
  
  /// 入隊
  mutating func enqueue(_ newElement: Element)
  /// 出隊
  mutating func dequeue() -> Element?
}

struct IntegerQueue: Queue {
  typealias Element = Int
  
  var isEmpty: Bool { return left.isEmpty && right.isEmpty }
  var size: Int { return left.count + right.count }
  var peek: Element? { return left.isEmpty ? right.first : left.last }
  
  private var left = [Element]()
  private var right = [Element]()
  
  mutating func enqueue(_ newElement: Element) {
    right.append(newElement)
  }
  
  mutating func dequeue() -> Element? {
    if left.isEmpty {
      left = right.reversed()
      right.removeAll()
    }
    return left.popLast()
  }
}

實戰

下面是Facebook一道真實的面試題。

Given an absolute path for a file (Unix-style), simplify it.
For example,
path = "/home/", => "/home"
path = "/a/./b/../../c/", => "/c"

這道題目一看,這不就是我們平常在terminal里面敲的cd啊pwd之類的嗎,好熟悉啊。

根據常識,我們知道以下規則:

  • . 代表當前路徑。比如 /a/. 實際上就是 /a,無論輸入多少個 . 都返回當前目錄
  • ..代表上一級目錄。比如 /a/b/.. 實際上就是 /a,也就是說先進入a目錄,再進入其下的b目錄,再返回b目錄的上一層,也就是a目錄。

然后針對以上信息,我們可以得出以下思路:

  1. 首先輸入是個 String,代表路徑。輸出要求也是 String, 同樣代表路徑。
  2. 我們可以把 input 根據 “/” 符號去拆分,比如 "/a/b/./../d/" 就拆成了一個String數組["a", "b", ".", "..", "d"]
  3. 創立一個棧然后遍歷拆分后的 String 數組,對于一般 String ,直接加入到棧中,對于 ".." 那我們就對棧做pop操作,其他情況不錯處理

思路有了,代碼也就有了

func simplifyPath(path: String) -> String {
  // 用數組來實現棧的功能
  var pathStack = [String]()
  // 拆分原路徑
  let paths = path.components(separatedBy: "/")
        
  for path in paths {
    // 對于 "." 我們直接跳過        
    guard path != "." else {
      continue
    }
    // 對于 ".." 我們使用pop操作        
    if path == ".."  {
      if (pathStack.count > 0) {
        pathStack.removeLast()
      }
    // 對于太注意空數組的特殊情況
    } else if path != "" {
      pathStack.append(path)
    }
  }
  // 將棧中的內容轉化為優化后的新路徑      
  let res = stack.reduce("") { total, dir in "\(total)/\(dir)" }
  
  // 注意空路徑的結果是 "/"      
  return res.isEmpty ? "/" : res
}

上面代碼除了完成了基本思路,還考慮了大量的特殊情況、異常情況。這也是硅谷面試考察的一個方面:面試者思路的嚴謹和代碼的風格規范。
隊列會在以后講樹遍歷和圖的廣度優先遍歷時大放異彩,所以本期隊列先按下不表。

轉化

處理棧和隊列問題,最經典的一個思路就是使用兩個棧/隊列來解決問題。也就是說在原棧/隊列的基礎上,我們用一個協助棧/隊列來幫助我們簡化算法,這是一種空間換時間的思路。比如

用棧來實現隊列

struct MyQueue {
  var stackA: Stack
  var stackB: Stack

  var isEmpty: Bool {
    return stackA.isEmpty && stackB.isEmpty;
  }

  var peek: Any? {
    get {
      shift();
      return stackB.peek;
    }
  }

  var size: Int {
    get {
      return stackA.size + stackB.size
    }
  }
  
  init() {
    stackA = Stack()
    stackB = Stack()
  }
  
  func enqueue(object: Any) {
    stackA.push(object);
  }
  
  func dequeue() -> Any? {
    shift()
    return stackB.pop();
  }
  
  fileprivate func shift() {
    if stackB.isEmpty {
      while !stackA.isEmpty {
        stackB.push(stackA.pop()!);
      }
    }
  }
}

用隊列實現棧

struct MyStack {
  var queueA: Queue
  var queueB: Queue
  
  init() {
    queueA = Queue()
    queueB = Queue()
  }

  var isEmpty: Bool {
    return queueA.isEmpty && queueB.isEmpty
  }
  
  var peek: Any? {
    get {
      shift()
      let peekObj = queueA.peek
      queueB.enqueue(queueA.dequeue()!)
      swap()
      return peekObj
    }
  }

  var size: Int {
    return queueA.size
  }
  
  func push(object: Any) {
    queueA.enqueue(object)
  }
  
  func pop() -> Any? {
    shift()
    let popObject = queueA.dequeue()
    swap()
    return popObject
  }

  private func shift() {
    while queueA.size != 1 {
      queueB.enqueue(queueA.dequeue()!)
    }
  }
  
  private func swap() {
    (queueA, queueB) = (queueB, queueA)
  }
}

上面兩種實現方法都是使用兩個相同的數據結構,然后將元素由其中一個轉向另一個,從而形成一種完全不同的數據。

總結

Swift中棧和隊列是比較特殊的數據結構,個人認為最實用的實現方法是利用數組。雖然它們本身比較抽象,卻是很多復雜數據結構和iOS開發中的功能模塊的基礎。這也是一個工程師進階之路理應熟練掌握的兩種數據結構。

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

推薦閱讀更多精彩內容