目录
什么是栈?
栈在函数调用中的应用
栈的应用:如何实现浏览器的前进和后退功能?
每日一练:左右括号匹配
什么是栈?
简单地说,先进后出,后进先出的数据结构就是栈,可以理解为一个纸箱子,往箱子里面放书,一本一本叠上去,取得时候只能从上面取最后放进去的书,最早放进去的最后才会被取出来。栈只允许在一端插入和删除数据,当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,就应该首选“栈”这种数据结构。
与数组或链表相比,栈的操作更为受限,那为什么会出现这种受限的数据结构呢? 从功能上讲,数组或者链表可以替代栈,但问题是数组或者链表的操作过于灵活,对外暴露的接口过多,当数据量很大的时候就会出现一些隐藏的风险。虽然栈限定降低了操作的灵活性,但这使得栈在处理只涉及一端新增和删除数据的问题时效率更高。
栈主要包含两个操作:入栈(在栈顶插入一个数据)和出栈(从栈顶删除一个数据),栈既可以用数组来实现,也可以用链表来实现。下面是在Go语言中分别使用数组和链表实现栈操作的核心代码(完整代码点 这里 查看):
// go-algo-demo/stack/StackArray.go
// 基于数组实现的栈
type StackArray struct {
data []interface{} //栈里面的数据
top int //栈顶指针
}
// 初始化一个栈
func NewArrayStack() *StackArray {
return &StackArray{
data: make([]interface{}, 0, 32),
top: -1,
}
}
// 向栈中插入元素
func (this *StackArray) Push(v interface{}) {
if this.top < 0 {
this.top = 0
} else {
this.top += 1
}
if this.top > len(this.data)-1 {
this.data = append(this.data, v)
} else {
this.data[this.top] = v
}
}
// 从栈中弹出元素
func (this *StackArray) Pop() interface{} {
if this.IsEmpty() {
return nil
}
v := this.data[this.top]
this.top -= 1
return v
}
// 获取当前栈顶的元素
func (this *StackArray) Top() interface{} {
if this.IsEmpty() {
return nil
}
return this.data[this.top]
}
// 测试
func main() {
s := NewArrayStack()
// 向栈中插入元素
s.Push(1)
s.Push(2)
s.Push(3)
s.Print() //3 2 1
// 获取当前栈顶的元素
fmt.Println(s.Top()) //3
// 从栈中弹出元素
fmt.Println(s.Pop()) //3
fmt.Println(s.Pop()) //2
fmt.Println(s.Pop()) //1
fmt.Println(s.Pop()) //<nil>
s.Print() //empty statck
}
栈存储数据只需要一个大小为 n 的数组就够了,所以空间复杂度是 O(1);入栈和出栈只涉及栈顶数据的操作,所以时间复杂度也是 O(1)。
如果要实现一个支持动态扩容的栈,可以使用一个可以动态扩容的数组来实现栈,当栈满了之后就申请一个更大的数组,将原来的数据搬移到新数组中。此时,对于出栈操作来说不会涉及内存的重新申请和数据的搬移,所以出栈的时间复杂度仍然是 O(1)。但是对于入栈操作来说,当栈中有空闲空间时,入栈操作的时间复杂度为 O(1);当空间不够时就需要重新申请内存和数据搬移,时间复杂度就变成了 O(n)。
栈在函数调用中的应用
操作系统给每个线程分配了一块独立的内存空间,这块内存就可以理解为“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量入栈,当函数执行完成之后再将这个临时变量出栈。还记得吗?在 Go语言的函数和defer用法_浮尘笔记的博客-CSDN博客 里面就说过关于栈的逻辑,可以使用defer实现一个函数调用栈,跟踪函数的执行过程。
package main
// 使用 defer 跟踪函数的执行过程
func main() {
defer Trace("main")()
foo()
}
func Trace(name string) func() {
println("enter:", name)
return func() {
println("exit:", name)
}
}
func foo() {
defer Trace("foo")()
bar()
}
func bar() {
defer Trace("bar")()
}
栈的应用:如何实现浏览器的前进和后退功能?
当打开浏览器依次访问页面 A -> B -> C 之后,然后依次点击浏览器的后退按钮,就可以查看之前浏览过的页面 B 和 A;当后退到页面 A的时候,再点击前进按钮,就可以重新进入页面 B 和 C。但是如果后退到页面 B 之后又点击了新的页面 D,那就无法再通过前进和后退功能查看页面 C 了。
要实现这个功能,可以使用栈这种数据结构,使用两个栈来实现。实现逻辑如下:
- 比如顺序查看了 A,B,C 三个页面,就依次把 A,B,C压入栈X, 当通过浏览器的后退按钮从页面 C 后退到页面 A 之后,就依次把 C 和 B页面从栈 X 中弹出,并且依次放入到栈 Y中。
- 如果再次进入页面 B,就把 B 再从栈 Y 中出栈并且放入栈 X 中。
- 如果此时从页面B跳转到新的页面D,那么页面C就无法再通过前进和后退按钮重复查看了,所以需要清空栈 Y。
下面使用Go语言代码实现了这个功能:
// go-algo-demo/stack/Browser.go
package main
import (
"fmt"
)
type Stack interface {
Push(v interface{})
Pop() interface{}
IsEmpty() bool
Top() interface{}
GetTopValue() int
Flush()
}
type Browser struct {
forward Stack //前进的栈
back Stack //后退的栈
}
// 初始化
func NewBrowser() *Browser {
return &Browser{
forward: NewArrayStack(),
back: NewArrayStack(),
}
}
// 打开一个新页面
func (this *Browser) OpenNewPage(addr string) {
fmt.Printf("打开新页面: %v\n", addr)
this.back.Push(addr)
this.forward.Flush() //清空前进的栈
this.forward.Push(addr) //把当前最新打开的页面添加到前进的栈
}
// 从已有页面跳转到下一个页面
func (this *Browser) PushNewPage(addr string) {
fmt.Printf("跳转到: %v\n", addr)
this.back.Push(addr)
}
// 后退
func (this *Browser) Back() {
if this.back.GetTopValue() == 0 {
fmt.Println("已经后退到了第一个页面,无法再次后退")
return
}
this.back.Pop()
top := this.back.Top()
this.forward.Push(top)
fmt.Printf("后退到: %v\n", top)
}
// 前进
func (this *Browser) Forward() {
if this.forward.GetTopValue() == 0 {
fmt.Println("已经前进到了最后一个页面,无法再次前进")
return
}
this.forward.Pop()
top := this.forward.Top()
this.back.Push(top)
fmt.Printf("前进到: %v\n", top)
}
// go run Browser.go StackArray.go
func main() {
browser := NewBrowser()
// 依次访问页面 A -> B -> C
browser.OpenNewPage("www.A.com") //打开新页面: www.A.com
browser.PushNewPage("www.B.com") //跳转到: www.B.com
browser.PushNewPage("www.C.com") //跳转到: www.C.com
// 后退两次, C -> B -> A
browser.Back() //后退到: www.B.com
browser.Back() //后退到: www.A.com
browser.Back() //已经后退到了第一个页面,无法再次后退
browser.Back() //已经后退到了第一个页面,无法再次后退
// 前进一次, A -> B
browser.Forward() //前进到: www.B.com
//打开一个新页面, A -> B -> D
browser.OpenNewPage("www.D.com") //打开新页面: www.D.com
//后退两次, D -> B -> A
browser.Back() //后退到: www.B.com
browser.Back() //后退到: www.A.com
browser.Back() //已经后退到了第一个页面,无法再次后退
//前进两次, A -> B -> D
browser.Forward() //前进到: www.B.com
browser.Forward() //前进到: www.D.com
browser.Forward() //已经前进到了最后一个页面,无法再次前进
browser.Forward() //已经前进到了最后一个页面,无法再次前进
}
每日一练:左右括号匹配
力扣20. 有效的括号
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应的相同类型的左括号。
示例:输入:s = "()[]{}",输出:true;输入:s = "(]",输出:false
思路 1:不使用数据结构和算法,直接用内置的 Replace() 方法将字符串中的 () , [] ,{} 全部替换成空字符串,替换之后如果字符串的长度为0就说明是一个“有效的括号” 字符串。时间复杂度: O(N^2),空间复杂度: O(N)
func isValidBrackets1(s string) bool {
for {
l := len(s)
s = strings.Replace(s, "()", "", -1)
s = strings.Replace(s, "[]", "", -1)
s = strings.Replace(s, "{}", "", -1)
//判断s是否没变过,相当于s不存在(),[],{}
if len(s) == l {
break
}
}
return len(s) == 0
}
func main() {
fmt.Println(isValidBrackets1("[]()")) //true
fmt.Println(isValidBrackets1("[])")) //false
}
思路2:用 栈 来实现,利用栈的后进先出的特性,如果是左括号就入栈,如果是右括号则查看当前栈顶元素是否与当前元素匹配,如果不匹配直接返回 false;如果全部匹配成功则返回 true。时间复杂度: O(N),空间复杂度: O(N)。注意,如果左右括号的个数是奇数则肯定返回false。
func isValidBrackets2(s string) bool {
if len(s)%2 != 0 { //如果左右括号的个数是奇数则肯定返回false
return false
}
stack := make([]rune, len(s))
n := 0
for _, c := range s {
switch c {
case '(', '[', '{':
stack[n] = c
n++
case ')':
if n == 0 || stack[n-1] != '(' {
return false
}
n--
case ']':
if n == 0 || stack[n-1] != '[' {
return false
}
n--
case '}':
if n == 0 || stack[n-1] != '{' {
return false
}
n--
}
}
if n == 0 {
return true
} else {
return false
}
}
func main() {
fmt.Println(isValidBrackets2("[]()")) //true
fmt.Println(isValidBrackets2("[])")) //false
fmt.Println(isValidBrackets2("[]))")) //false
}
源代码:https://gitee.com/rxbook/go-algo-demo/tree/master/stack