Swift数据结构和算法03_栈

前言

有需要的同学可以订阅专栏:Swift数据结构和算法专题
代码地址:Swift数据结构和算法代码

正文

栈数据结构在概念上与对象的物理堆栈相同。将项目添加到栈时,会将其放置在栈的顶部。当我们从堆栈中移除一个项目时,我们始终会移除最顶层的项目。

栈操作

栈很有用,也非常简单。构建栈的主要目标是强制执行我们访问数据的方式。

栈只有两个基本操作:

• push:向栈顶添加一个元素。

• pop:移除栈顶元素。

将接口限制为这两个操作意味着我们只能从数据结构的一侧添加或删除元素。在计算机科学中,栈被称为 LIFO(后进先出)数据结构。最后推入的元素是最先弹出的元素。

堆栈在所有编程学科中都被广泛使用。列举几个:

• iOS 使用导航栈将视图控制器推入和弹出视图。

• 内存分配在架构级别使用栈。局部变量的内存也使用栈进行管理。

• 搜索算法,例如从迷宫中寻找路径,使用栈来促进回溯。

执行

打开本章的starter playground。在 PlaygroundSources 文件夹中,创建一个名为 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"))
    -----------
    """
  }
}

在这里,我们已经定义了 Stack 的后备存储。为我们的栈选择正确的存储类型很重要。数组是一个显而易见的选择,因为它通过 appendpopLast 在一端提供恒定时间的插入和删除。这两个操作的使用将促进堆栈的 LIFO 特性。

对于 CustomDebugStringConvertible 协议所需的 debugDescription 中花哨的函数调用链,我们正在做三件事:

  1. 创建一个数组,通过 storage.map { "\ ($0)" } 将元素映射到 String

  2. 创建一个新数组,使用 reversed() 反转前一个数组。

  3. 使用joined(separator:) 将数组展平为字符串。我们使用换行符“\n”分隔数组的元素。

这将创建可用于调试的可打印表示的栈。

push和pop操作

将以下两个操作添加到我们的栈中:

public mutating func push(_ element: Element) {               
  storage.append(element) 
}

@discardableResult 
public mutating func pop() -> Element? { 
  storage.popLast() 
}

继续添加以下内容来测试一下:

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 中,将以下内容添加到栈:

public func peek() -> Element? { 

  storage.last 
}

public var isEmpty: Bool { 
  peek() == nil 
 }

栈接口通常包括 peek 操作。 peek 的想法是在不改变其内容的情况下查看栈的顶部元素。

少即是多

我们可能想知道是否可以为栈采用 Swift collection协议。栈的目的是限制访问数据的方式数量Collection 之类的协议会违背这个目标,因为它会通过迭代器和下标公开所有元素。在这种情况下,少即是多!

我们可能希望获取现有数组并将其转换为堆栈以保证访问顺序。当然,可以循环遍历数组元素并推送每个元素。

但是,因为我们可以编写一个设置底层私有存储的初始化程序。将以下内容添加到我们的栈实现中:

public init(_ elements: [Element]) { 
  storage = elements 
}

现在,将此示例添加到main 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<String>。我们可以更进一步,使我们的栈可从数组文字初始化。将此添加到我们的栈实现中:

extension Stack: ExpressibleByArrayLiteral { 
  public init(arrayLiteral elements: Element...) {         
    storage = elements 
  } 
}

现在回到main 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() 
}

这将创建一个 Doubles 堆栈并弹出顶部值 4.0。同样,类型推断使我们不必键入更冗长的 Stack<Double>。栈对于搜索树和图的问题至关重要。想象一下在迷宫中寻找出路。每次你来到一个左、右或直的决策点时,我们都可以将所有可能的决策推到你的栈上。当我们遇到死胡同时,只需从堆栈中弹出并继续回溯,直到我们逃脱或遇到另一个死胡同。

关键点

• 栈是一种后进先出,后进先出的数据结构。

• 尽管非常简单,但栈是许多问题的关键数据结构。

• 栈仅有的两个基本操作是用于添加元素的push 方法和用于移除元素的pop 方法。

栈进阶

堆栈是一种简单的数据结构,具有惊人的大量应用程序。打开启动项目开始。在其中,我们会发现以下问题。

问题1:反转数组

创建一个函数,该函数使用栈以相反的顺序打印数组的内容。

问题2:平衡括号

检查平衡括号。给定一个字符串,检查是否有 ( 和 ) 字符,如果字符串中的括号是平衡的,则返回 true。例如:

// 1 
h((e))llo(world)() // 平衡的括号

// 2 
(hello world // 不平衡的括号

问题1解决方案

堆栈的主要用例之一是便于回溯。如果我们将一系列值压入栈,则顺序弹出堆栈将以相反的顺序为我们提供值。

func printInReverse<T>(_ array: [T]) {
  var stack = Stack<T>()
  for value in array { 
    stack.push(value) 
  }

  while let value = stack.pop() { 
    print(value) 
  }
}

将节点压入堆栈的时间复杂度为 O(n)。弹出堆栈以打印值的时间复杂度也是 O(n)。总的来说,这个算法的时间复杂度是O(n)。

由于我们在函数内部分配容器(栈),因此还会产生 O(n) 空间复杂度成本。

注意:我们应该在生产代码中反转数组的方法是调用标准库提供的 reversed() 方法。对于Array,这个方法在时间和空间上都是O(1)。这是因为它是惰性的,并且只会在原始集合中创建反向视图。如果我们遍历项目并打印出所有元素,则可以预见的是,它会在时间上使其成为 O(n),而在空间中保持 O(1)。

问题2解决方案

要检查字符串中是否存在平衡括号,我们需要遍历字符串的每个字符。当我们遇到左括号时,我们可以将其压入栈。反之亦然,如果遇到右括号,则应弹出栈。

代码如下所示:

func checkParentheses(_ string: String) -> Bool {
  var stack = Stack<Character>()
  for character in string {
    if character == "(" { 
      stack.push(character) 
    } else if character == ")" { 
      if stack.isEmpty { 
        return false 
      } else { 
        stack.pop() 
      } 
    }
  } 
  return stack.isEmpty
}

该算法的时间复杂度为 O(n),其中 n 是字符串中的字符数。由于使用了栈数据结构,该算法还会产生 O(n) 的空间复杂度成本。

上一章 目录 下一章
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容