基本数据结构 - Stack(栈)

1,271 阅读3分钟
  • 介绍

数据结构对数据的基本操作非常简单,只在栈顶对数据进行添加和移除操作。在计算机语言中称之为 LIFO(后进先出 last-in-first-out)数据结构。

它只有两个必要操作:

  • push: 添加一个元素到栈顶
  • pop: 从栈顶移除一个元素

使用栈数据结构的有下面的一些例子:

  • UINavigationConrtoller 使用栈数据结构对 ViewController 进行 push pop 操作

  • 内存分配结构层中使用栈数据结构;局部变量的内存管理也使用栈数据结构

  • 迷宫地图的前进或者原路返回中使用栈进行路径查询

  • 实现

public struct Stack<Element> {
    
    private var storage: [Element] = []
    
    public init() {
        
    }
}

extension Stack: CustomStringConvertible {
    
    public var description: String {
        """
        ------- top -------
        \(storage.map { "\($0)" }.reversed().joined(separator: "\n"))
        -------------------
        """
    }
    
}

在这个栈里,使用一个数组进行栈元素存储。因为栈的数据结构操作方式为 LIFO(后进先出),所以我们可以在数组的最后面执行 push 和 pop 操作,这能很好的利用数组的数据结构特性,此时对数组来说 push 和 pop 操作的时间复杂度都是 O(1)

最后我们还加了一个辅助描述属性 description,主要是为了更好的查看栈元素的输出结构。

  • push 和 pop 操作

    public mutating func push(_ element: Element) {
        storage.append(element)
    }
    
    @discardableResult
    public mutating func pop() -> Element? {
        return storage.popLast()
    }

方法都很简单明了。下面来测试一下:

先添加一个测试辅助方法:

func example(of des: String, block:() -> ()) {
    print("---Example of \(des)---")
    block()
}

测试代码:

        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 popedElement = stack.pop() {
                assert(popedElement == 4)
                print("Popped: \(popedElement)")
            }
        }

可以看到输出为:

---Example of using a stack---
------- top -------
4
3
2
1
-------------------
Popped: 4

push pop 时间复杂度都是 O(1)

  • 非必须的辅助方法

    public func peek() -> Element? {
        return storage.last
    }
    
    public var isEmpty: Bool {
        return peek() == nil
    }

peek(): 数据不删除,只是看一看栈顶数据

isEmpty: 判断栈是否为空

  • 额外的初始化方法

由于在这个栈底部的元素存储是数组的形式,我们还可以使用传递数组来进行栈的初始化操作

直接使用数组做参数:

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

测试一下:

        example(of: "initializing a stack from an array") {
            let stack = Stack([1, 2, 3, 4])
            print(stack)
        }

代码输出:

---Example of initializing a stack from an array---
------- top -------
4
3
2
1
-------------------

使用数组字面量:

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

测试一下:

        example(of: "initializing a stack from an array literal") {
            let stack: Stack = [1.0, 2.0, 3.0, 4.0]
            print(stack)
        }

代码输出:

---Example of initializing a stack from an array literal---
------- top -------
4.0
3.0
2.0
1.0
-------------------
  • 要点总结

  • 栈是后进先出(LIFO)数据结构
  • 虽然栈很简单,但是它是很多场景的关键数据结构
  • 它只有两个必须方法: push 添加数据, pop 删除数据

更多来自:data-structures-and-algorithms-in-swift

  • 试题

  1. 实现数组元素倒序输出

实现思路:

由于栈的底部存储是一个数组,所以直接把外部的数组变量元素写入栈里面就是一个倒序的数据结构了。

实现代码:

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

此时代码的时间复杂度和空间复杂度都是 O(n)

  1. 判断字符串内包含的括号是否平衡对齐

比如 "(((ee)))we" 就是对齐的, xyz(xx( 就不是对齐的。

实现思路:

在栈中 push 和 pop 是对应操作的,如果要判断给出数据的A元素和B元素的个数是否相等,操作可以是遇到一个 A, push A 进去, 遇到一个 B,pop 一个 A。最后判断栈是否为空,栈为空就是对齐的。

实现代码:

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

此时代码的时间复杂度和空间复杂度都是 O(n)