- Published on
知识点
- Authors
- Name
- ArJun
- @Twitter/NibabaAJ
- Name
在本次实验中,我们将从栈的定义开始,先实现一个栈的数据结构,然后再实现栈的基本操作。最后通过实现两个栈的常见的应用场景。通过这种由浅入深的实验方式,加深大家对于栈这个数据结构的理解。
知识点
- 栈的定义
- 栈的基本操作
- 栈的实现
- 栈的应用
栈是一种操作受限制的线性表,将允许进行插入、删除的一端称为栈顶,另一端称为栈底。看到这里你可能会觉得有点绕,其实数据结构很多的定义都很抽象,这是很正常的,下面我将类比一个生活中的常见实例帮助大家理解。
我们在日常生活中洗盘子的时候,摞起来的盘子就是一个典型的栈结构。不管取还是放,总是要放在盘子堆的最上面操作。如果你想从一堆盘子的中间强行取一个盘子,那就有可能酿成大祸。
我们首先还是在当前目录下去新建一个名叫 stack.go
的文件。
接下来我们来定义出栈的数据结构,还记得刚刚的洗盘子问题吗?我们在日常生活中,一摞盘子肯定有很多个而且是连续的,所以我们首先需要一个切片,并且我们日常生活中的盘子是不能无限的摞的,这点在计算机里也是同样的,计算机里也会有 Stack Overflow
的问题。 所以我们还需要一个容量的限制元素。 并且我们还需要频繁的对栈顶元素进行操作,所以我们还需要一个标记位 top
来标记栈顶元素的索引。
分析到这里,栈的数据结构已经呼之欲出了。我们就直接写出栈的数据结构和他的构造方法。
package main
type Stack struct {
// 用来装元素的切片
container []int
// 栈顶标记位
top int
// 容量限制
size int
}
// 初始化时,要传入容量限制
func NewStack(size int) *Stack {
return &Stack{
container: make([]int,size),
// 栈顶指针初始可以指向-1,也可以指向0,想一想为什么。
top: 0,
size: size,
}
}
初始化时要注意,我们这里返回的是一个 Stack 的指针,还记得我们上节学到过引用传递在系统中的开销比较小吗?
到这里,我们对于 Golang 栈的定义以及初始化操作以及完成了,下一小节中我们将学习栈的基本操作。
对于一个栈来说,其基本操作分为以下四种。
- Push(e E) , 将一个数据类型为 E 的元素 e 放到栈顶。
- Pop() , 将栈顶元素取出。
- IsFul() , 栈是否满了。
- IsEmpty() , 栈是否空了。
这里我们还是在 stack.go
中继续实验。实现其上述四个基本功能,并且在主函数中进行测试。代码如下:
package main
import "fmt"
type Stack struct {
// 用来装元素的切片
container []int
// 栈顶标记位
top int
// 容量限制
size int
}
// 初始化时,要传入容量限制
func NewStack(size int) Stack {
return Stack{
container: make([]int, size),
top: 0,
size: size,
}
}
func (s *Stack) Push(e int) bool {
if s.IsFull() {
return false
}
// 把盘子摞上去
s.container[s.top] = e
// 下一个能摞盘子的位置
s.top++
return true
}
func (s *Stack) Pop() (flag bool, ret int) {
// 如果栈空了,你就无法拿到新盘子,所以flag此时为false
if s.IsEmpty() {
return false, ret
}
// 取出盘子
ret = s.container[s.top-1]
// 下一个能取盘子的位置
s.top--
return true, ret
}
func (s *Stack) IsEmpty() bool {
if s.top == 0 {
return true
}
return false
}
func (s *Stack) IsFull() bool {
if s.top == s.size {
return true
}
return false
}
func main() {
stack := NewStack(3)
// 先测试栈为空的时候能否Pop
fmt.Println(stack.Pop())
// 测试Push是否正常
stack.Push(1)
stack.Push(2)
stack.Push(3)
// 如果栈为正常的,这里Pop打印顺序应该是3,2,1
fmt.Println(stack.Pop())
fmt.Println(stack.Pop())
fmt.Println(stack.Pop())
}
我们输入 go run stack.go
查看执行结果。
各位同学在书写 Golang 代码时如果不知道 Golang 的代码风格规范,可以在写完代码后执行 go fmt stack.go
编译器会自动帮我们整理代码风格。
现在,我们已经实现了栈的基本操作,接下来我们将改造一下我们的栈,来解决实际的问题。
在这一小节中,我们将通过解决两个具体的实际问题来巩固栈的知识。
浏览器中的前进后退
假设你现在是 N 年前的 Chrome 浏览器工程师, 你现在很苦恼,有的网页在打开下一个页面后就回不去上一级了,你现在急迫的想要一个后退的功能,请问要怎么样实现呢?
仔细分析之后不难发现,所谓的后退,撤销等操作,其实就是一个栈的 Pop
操作,我们每次点击的网址, 或者进行的操作,都是被程序 Push
到了一个栈中,所以一旦我们点击撤销或后退时,总是可以返回我们最近一次的操作。这就是栈的最广泛的应用。
我们新建一个名叫 browser.go
的文件。然后将我们在 stack.go
中实现的代码除主函数外全都拷贝进去, 并且因为浏览器的网址是字符串类型的,所以我们需要把除了 top
和 size
以外的 int
改为 string
。
代码如下:
package main
import "fmt"
type Stack struct {
// 用来装元素的切片
container []string
// 栈顶标记位
top int
// 容量限制
size int
}
// 初始化时,要传入容量限制
func NewStack(size int) Stack {
return Stack{
container: make([]string, size),
top: 0,
size: size,
}
}
func (s *Stack) Push(e string) bool {
if s.IsFull() {
return false
}
s.container[s.top] = e
s.top++
return true
}
func (s *Stack) Pop() (flag bool, ret string) {
// 如果栈是空的,那么就不能继续 Pop 了
if s.IsEmpty() {
return false, ret
}
ret = s.container[s.top-1]
s.top--
return true, ret
}
func (s *Stack) IsEmpty() bool {
if s.top == 0 {
return true
}
return false
}
func (s *Stack) IsFull() bool {
if s.top == s.size {
return true
}
return false
}
func main() {
back := NewStack(3)
// 模拟每次点击网页时,浏览器会自push你的网址到栈中
back.Push("www.baidu.com")
back.Push("www.bing.com")
back.Push("www.goole.com")
// 每次点击后退时,就相当于是从栈中Pop了一个网址
fmt.Println(back.Pop())
fmt.Println(back.Pop())
fmt.Println(back.Pop())
}
执行结果:
下面关于这道题留下一个思考问题,我们的浏览器不光有后退操作,还是有前进操作的,其实前进也是一个栈,请尝试实现出完善的前进,后退功能。
括号匹配
在现代的 IDE 中,我们的编辑器环境是十分智能的,比如下图:
我少打了一个括号,IDE 就自动给我画了一条红线。是不是非常的神奇。其实这个功能也是用栈实现的。下面来说一下思路:
若遇到左括号入栈,遇到右括号时将栈顶左括号出栈,则遍历完所有括号后 stack
仍然为空;如果遍历之后 stack
不为空,那么说明有多余的左括号。上图中就是这种情况。
我们新建一个名叫 valid.go
的文件,并且将 stack.go
中的代码拷贝进去。把除了 top
和 size
以外的 int
改为 byte
。
然后我们通过实现 isValid 函数去完成括号匹配的功能。完整代码与测试结果如下:
package main
import "fmt"
type Stack struct {
// 用来装元素的切片
container []byte
// 栈顶标记位
top int
// 容量限制
size int
}
// 初始化时,要传入容量限制
func NewStack(size int) Stack {
return Stack{
container: make([]byte,size),
top: 0,
size: size,
}
}
func (s *Stack) Push (e byte) bool {
if s.IsFull() {
return false
}
s.container[s.top] = e
s.top++
return true
}
func (s *Stack) Pop () (flag bool,ret byte) {
// 如果栈是空的,那么就不能继续 Pop 了
if s.IsEmpty() {
return false,ret
}
ret = s.container[s.top-1]
s.top--
return true,ret
}
func (s *Stack) IsEmpty () bool {
if s.top == 0 {
return true
}
return false
}
func (s *Stack) IsFull () bool {
if s.top == s.size {
return true
}
return false
}
func IsValid(s string) bool {
stack := NewStack(100)
// 遍历括号字符串
for _,v := range s {
if v == '(' {
// 由于golang中的字符串默认是unicode编码,所以我们要做一个强制类型转换
stack.Push(byte(v))
}
if v == ')' {
// 如果flag不为true,说明栈已经到底了,可以直返回false
if flag,top := stack.Pop(); flag == true &&top == '(' {
continue
} else {
return false
}
}
}
// 字符串遍历完后如果栈也空了,说明括号匹配
if stack.IsEmpty() {
return true
}
// 如果栈不空,说明栈里还有多余的左括号
return false
}
func main() {
test1 := "()()())"
test2 := "((()"
test3 := "()()()()"
fmt.Println(IsValid(test1))
fmt.Println(IsValid(test2))
fmt.Println(IsValid(test3))
}
测试结果:
现在我们就已经完成了小括号的匹配功能,下面请大家思考一下如果匹配小括号,中括号,大括号混合的字符串,那么我们的 IsValid
函数需要做什么样的修改呢?
在本节中,我们从栈的数据结构开始分析,学习了栈的数据结构,栈的四个基本操作,以及栈的两个常见应用和两个思考题。大家在用栈的时候只要牢记一个定义,栈是后进先出的一种线性结构。如果你的问题是符合后进先出的功能,那么用栈进行实现就肯定没错了。