Spiga

用Go撸数据结构(四):栈

2020-06-10 10:06:32

如何理解“栈”

  1. 栈是一种操作受限的数据结构,只支持入栈和出栈操作。
  2. 典型的“栈”结构:后进者先出,先进者后出。
  3. 从栈的操作特性上来看,栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。
  4. 特定的数据结构是对特定场景的抽象,而且,数组或链表暴露了太多的操作接口,操作上的确灵活自由,但使用时就比较不可控,自然也就更容易出错。

顺序栈的实现

type Item interface {
}
type ArrayStack struct {
	items []Item	// 数组 
	count int //栈中元素个数
	size int //栈的大小
}

func CreateStack(cap int) *ArrayStack {
	if cap == 0 {
		panic("不能创建容量为0的栈!")
	}
	return &ArrayStack{
		items: make([]Item, cap),
		count: 0,
		size: cap,
	}
}

func (a *ArrayStack) Push(e Item) {
	if a.size=a.count {
		panic("栈已经满了!") 
	}
	a.items[count] = e
	a.count++
}

func (a *ArrayStack) Pop() *Item {
		if a.count == 0 {
			panic("栈已经空了!") 
		}
		tmp = a.items[a.count-1]
		a.count--
		return &tmp; 
	}
}

链式栈实现

type Item interface {
}
type StackNode struct {
    el      Item  
    next    *StackNode
}
type ListStack struct {
	top     *StackNode
	count int //栈中元素个数
}

func CreateStack() *ListStack {
	return new(LinkStack) 
}

func (l *LinkStack) Push(el Item)  {
    s := StackNode{el: el, next: l.top}
    l.top = &s
    l.count++
}

func (l *LinkStack) Pop() el Item {
    if l.count == 0 {
        panic("栈已经空了!") 
    }
    l.count--
    el = l.top.el
    l.top = l.top.next 
    return el
}

支持动态扩容的顺序栈

  1. 如果要实现一个支持动态扩容的栈,我们只需要底层依赖一个支持动态扩容的数组就可以了。当栈满了之后,我们就申请一个更大的数组,将原来的数据搬移到新数组中。
  2. 出栈的时间复杂度是 O(1)。
    入栈操作,最好情况时间复杂度是 O(1),最坏情况时间复杂度是 O(n)。均摊时间复杂度为O(1)。

栈的应用

栈在表达式求值中的应用

编译器如何利用栈来实现表达式求值?

为了方便解释,我将算术表达式简化为只包含加减乘除四则运算,比如:34+13*9+44-12/3。对于这个四则运算,我们人脑可以很快求解出答案,但是对于计算机来说,理解这个表达式本身就是个挺难的事儿。

实际上,编译器就是通过两个栈来实现的。其中一个保存操作数的栈,另一个是保存运算符的栈。我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较。

如果比运算符栈顶元素的优先级高,就将当前运算符压入栈;如果比运算符栈顶元素的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取 2 个操作数,然后进行计算,再把计算完的结果压入操作数栈,继续比较。

栈在函数调用中的应用

我们知道,操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构, 用来存储函数调用时的临时变量。每进入一个函数,就会将临时变量作为一个栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。

栈在括号匹配中的应用

借助栈来检查表达式中的括号是否匹配。

我们假设表达式中只包含三种括号,圆括号 ()、方括号[]和花括号{},并且它们可以任意嵌套。比如,{[] ()[{}]}或[{()}([])]等都为合法格式,而{[}()]或[({)]为不合法的格式。那我现在给你一个包含三种括号的表达式字符串,如何检查它是否合法呢?

这里也可以用栈来解决。我们用栈来保存未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,比如“(”跟“)”匹配,“[”跟“]”匹配,“{”跟“}”匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。

当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明有未匹配的左括号,为非法格式。

实现浏览器的前进和后退功能

我们使用两个栈,X 和 Y,我们把首次浏览的页面依次压入栈 X,当点击后退按钮时,再依次从栈 X 中出栈,并将出栈的数据依次放入栈 Y。当我们点击前进按钮时,我们依次从栈 Y 中取出数据,放入栈 X 中。当栈 X 中没有数据时,那就说明没有页面可以继续后退浏览了。当栈 Y 中没有数据,那就说明没有页面可以点击前进按钮浏览了。