数据结构与算法03:栈

目录

什么是栈?

栈在函数调用中的应用

栈的应用:如何实现浏览器的前进和后退功能?

每日一练:左右括号匹配


什么是栈?

简单地说,先进后出,后进先出的数据结构就是栈,可以理解为一个纸箱子,往箱子里面放书,一本一本叠上去,取得时候只能从上面取最后放进去的书,最早放进去的最后才会被取出来。栈只允许在一端插入和删除数据,当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,就应该首选“栈”这种数据结构。

与数组或链表相比,栈的操作更为受限,那为什么会出现这种受限的数据结构呢? 从功能上讲,数组或者链表可以替代栈,但问题是数组或者链表的操作过于灵活,对外暴露的接口过多,当数据量很大的时候就会出现一些隐藏的风险。虽然栈限定降低了操作的灵活性,但这使得栈在处理只涉及一端新增和删除数据的问题时效率更高。

栈主要包含两个操作:入栈(在栈顶插入一个数据)和出栈(从栈顶删除一个数据),栈既可以用数组来实现,也可以用链表来实现。下面是在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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浮尘笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值