這期的內容有點劍走偏鋒,我們來討論一下棧和隊列。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目錄。
然后針對以上信息,我們可以得出以下思路:
- 首先輸入是個 String,代表路徑。輸出要求也是 String, 同樣代表路徑。
- 我們可以把 input 根據 “/” 符號去拆分,比如 "/a/b/./../d/" 就拆成了一個String數組["a", "b", ".", "..", "d"]
- 創立一個棧然后遍歷拆分后的 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開發中的功能模塊的基礎。這也是一個工程師進階之路理應熟練掌握的兩種數據結構。