前言
本文主要讲一讲栈这种非常基础的数据结构,以及其如何用Golang来实现的3种方式,简单用golang bench做一个性能比对,以字符串括号匹配的一个例子来看其一个简单的应用场景。
栈的特性
栈是一种FILO类型的数据结构,FILO 即 Fisrt In Last Out,也就是先进后出,也可以说是后进先出。它很像一个只有一个出口的站立的杯子,后边进入杯子的东西,会被最先取出来。
栈实现的三种方式
首先,实现栈,远非只有这三种方式,这里,只是举例3种相对比较典型的方式。每一种实现方式都很简单,也没什么需要太费周章讲的必要。
1、利用Golang的slice
1 | package main |
此方式特点:
- 利用Golang的Slice,足够简单。
- 允许添加任意类型的元素。
- Push和Pop有加锁处理,线程安全。
- 在一些文章里(Reddit)提到,使用slice作为Stack的底层支持,速度更快。但是,slice最大的问题其实是存在一个共用底层数组的的问题,哪怕你不断的Pop,但可能对于Golang来说,其占用的内存,并不一定减少。
性能测试如下:
1 | package main |
1 | $ go test -test.bench=".*" -benchmem -v |
2、利用Golang的container/list内置包
1 | package main |
简单来说,container/list 是一个链表。用链表模拟栈,要么都向链表的最后做push和pop,要么都向链表的起点做push和pop,这种方法也非常简单。
性能测试如下:
1 | $ go test -test.bench=".*" -benchmem -v -count=1 |
3、godoc的实现参考(自定义数据结构实现)
1 | package main |
此方式特点:
- 实现比较精巧,唯一需要理解的地方就是 node 结构体中,prev 的部分,它指向的是前一个node成员。
- 允许添加任意类型的元素。
- Push和Pop是线程安全的。
性能测试如下:
1 | $ go test -test.bench=".*" -benchmem -v |
对比
三种方式,总的来看,第三种基于自定义数据结构的实现方式,在push上效率最高,而且实现也比较精巧。个人其实是推荐使用这种方式的。其次,是基于 container/list 实现的方式。
特性对比 | push速度 | pop速度 | push内存分配 | pop内存分配 |
---|---|---|---|---|
基于slice | 222ns/op | 65ns/op | 94B/op | 0B/op |
container/list链表 | 222ns/op | 73.5ns/op | 48B/op | 0B/op |
自定义数据结构 | 178ns/op | 75ns/op | 32B/op | 0B/op |
栈数据结构的用途
栈这种数据结构通途很多。典型的应用,比如编辑器的撤销(undo)操作,以及校验字符匹配问题。下面主要举一个校验字符匹配的例子:
题目:给定一个包含了右侧三种字符的字符串, ‘(‘, ‘)’, ‘{‘, ‘}’, ‘[‘ and ‘]’,判断字符串是否合法。合法的判断条件如下:
- 必须使用相同类型的括号关闭左括号。
- 必须以正确的顺序关闭打开括号。
示例1:
1 | Input: "()[]{}" |
示例2:
1 | Input: "([)]" |
参考实现:
1 | func isValid(s string) bool { |