栈无处不在。下面是一些常见的例子,你会把东西叠起来。
煎饼
书籍
纸张
现金
栈数据结构在概念上与物理上的物体栈相同。当你向一个栈添加一个项目时,你把它放在栈的顶部。当你从一个栈中删除一个项目时,你总是删除最上面的项目。
好消息:一叠薄饼。坏消息:你只能吃最上面的煎饼。
栈是有用的,也是极其简单的。建立栈的主要目的是强制执行你如何访问你的数据。
栈只有两个基本操作。
推送。将一个元素添加到栈的顶部。
pop。移除栈顶部的元素。
将接口限制在这两个操作上,意味着你只能从数据结构的一侧添加或删除元素。在计算机科学中,栈被称为LIFO(后进先出)数据结构。最后被推入的元素是第一个被弹出的。
栈在编程的所有学科中都有突出的应用。举几个例子。
iOS使用导航栈将视图控制器推入和移出视图。
内存分配在架构层面上使用栈。局部变量的内存也是使用栈管理的。
搜索和征服算法,如寻找走出迷宫的路径,使用栈来促进回溯。
打开本章的起始游戏场。在playground的Source文件夹中,创建一个名为Stack.swift的文件。在该文件中,写下以下内容。
public struct Stack<Element> { 
  private var storage: [Element] = [] 
  public init() { } 
} 
extension Stack: CustomDebugStringConvertible { 
  public var debugDescription: String { 
    """
    ----top----
    \(storage.map { "($0)" }.reversed().joined(separator:"\n"))
    -----------
    """
  }
}
在这里,你已经定义了你的栈的后备存储。为你的栈选择正确的存储类型是很重要的。数组是一个明显的选择,因为它通过append和popLast在一端提供持续的插入和删除。这两个操作的使用将促进栈的后进先出性质。
对于CustomDebugStringConvertible协议所要求的debugDescription中花哨的函数调用链,你正在做三件事。
创建一个数组,通过storage.map { "\ ($0)" 将元素映射到String。}.
创建一个新的数组,通过reversed()来反转之前的数组。
使用join(separator:)将数组扁平化为一个字符串。你用换行符"/n "分隔数组中的元素。
这创建了一个可打印的Stack类型的表示,你可以用来进行调试。
给你的Stack添加以下两个操作。
public mutating func push(_ element: Element) { 
  storage.append(element) 
}
@discardableResult 
public mutating func pop() -> Element? { 
  storage.popLast() 
}
相当直截了当! 在playground的页面上,写下以下内容。
example(of: "using a stack") {
  var stack = Stack<Int>()
  stack.push(1) 
  stack.push(2) 
  stack.push(3) 
  stack.push(4)
  print(stack)
  if let poppedElement = stack.pop() { 
    assert(4 == poppedElement) 
    print("Popped: \(poppedElement)") 
  }
}
你应该看到以下输出。
---Example of using a stack---
----top---
4 
3 
2 
1
----------Popped: 4
push和pop都有一个O(1)的时间复杂度。
有几个不错的操作可以使栈更容易使用。在Stack.swift中,给Stack添加以下内容。
public func peek() -> Element? { 
  storage.last
}
public var isEmpty: Bool { 
  peek() == nil 
}
一个栈接口通常包括一个peek操作。peek的概念是在不改变其内容的情况下查看栈的顶部元素。
你可能已经想知道你是否可以为栈采用Swift的收集协议。栈的目的是限制访问数据的方式的数量。采用Collection这样的协议会违背这个目标,因为它通过迭代器和下标暴露了所有的元素。在这种情况下,少即是多!
你可能想利用一个现有的数组并将其转换为栈,以保证访问顺序。当然,也可以循环浏览数组元素并推送每个元素。
然而,由于你可以写一个初始化器来设置底层私有存储。在你的栈实现中添加以下内容。
public init(_ elements: [Element]) { 
  storage = elements 
}
现在,把这个例子添加到主playground。
example(of: "initializing a stack from an array") {
  let array = ["A", "B", "C", "D"]
  var stack = Stack(array) 
  print(stack) stack.pop()
}
这段代码创建了一个字符串的栈,并弹出了最上面的元素 "D"。注意,Swift编译器可以从数组中推断出元素的类型,所以你可以使用Stack而不是更粗略的Stack
你可以更进一步,使你的栈可以从一个数组字头初始化。把这个添加到你的栈实现中。
extension Stack: ExpressibleByArrayLiteral { 
  public init(arrayLiteral elements: Element...) { 
    storage = elements 
  } 
}
现在回到主playground页面并添加。
example(of: "initializing a stack from an array literal") { 
  var stack: Stack = [1.0, 2.0, 3.0, 4.0] 
  print(stack) 
  stack.pop() 
}
这将创建一个双倍数的栈,并弹出最上面的值4.0。同样,类型推理使你不必输入更多的Stack
栈对于搜索树和图的问题至关重要。想象一下,在一个迷宫中寻找你的路。每当你来到一个左、右或直的决策点时,你可以把所有可能的决策推到你的栈中。当你遇到死胡同时,只需从栈中跳出,继续往回走,直到你逃脱或遇到另一个死胡同。
栈是一种后进先出的数据结构。
尽管如此简单,栈是许多问题的关键数据结构。
栈的唯一两个基本操作是增加元素的push方法和删除元素的pop方法。
| 上一章 | 目录 | 下一章 | 
|---|