一篇图文彻底搞懂栈
定义
维基百科上对栈的说明如下示:
栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素
简单定义:栈就是一种只允许在表尾进行插入和删除操作的线性表
也可以这么理解:
先进后出,先进队的数据最后才出来。在英文的意思里,stack 可以作为一叠的意思,这个排列是垂直的,你将一张纸放在另外一张纸上面,先放的纸肯定是最后才会被拿走,因为上面有一张纸挡住了它。
上面说半天定义可能也不是很直观的能想象到栈到底是个啥玩意,话不多说,直接上图:
![6d88ff8fb1d20952e581aebf39698840.png](https://i-blog.csdnimg.cn/blog_migrate/c3307e8232efce32270b37b936e302fd.png)
![a937f57a7d63cddf6bc0fe75156de420.png](https://i-blog.csdnimg.cn/blog_migrate/9ae4f630b735213177f4443373d83a39.png)
怎么更加形象的理解呢,举一个生活中的例子:我在一个储物箱中,堆了一堆衣服,我的一件球衣在最下面,而我要拿这件衣服,就意味着我必须将上面的衣服全部拿出来才可以,但是由于箱子只有一个口,我也只能从上面拿东西,心里还默默想着,当初就不该将球衣早早的放进去,导致结果就是先进后出!这其实就是栈。
栈的专业术语说明
- 栈顶:允许进行插入和进行删除操作的一段成为栈顶
- 栈底:表的另一端称为栈底 (第一个元素进入的位置)
- 压栈(入栈、进栈):在栈顶位置插入元素的操作叫做压栈,或入栈、进栈
- 出栈(弹栈、退栈):删除栈顶元素的操作叫做出栈,也叫作弹栈,或者退栈
- 空栈:不含元素的空表
- 栈溢出:当栈满的时候,如果再有元素压栈,则发生上溢,当栈空的时候,再出栈则发生下溢
如何实现
我们可以用数据结构:链表(可连续或不连续的将数据与数据关联起来的结构),或 数组(连续的内存空间,按索引取值) 来实现 栈(stack)。
- 数组实现 能快速随机访问存储的元素,通过下标 index 访问,支持随机访问,查询速度快,但存在元素在数组空间中大量移动的操作,增删效率低。
- 链表实现 只支持顺序访问,在某些遍历操作中查询速度慢,但增删元素快。
本章我们也将用数组和链表两种形式实现栈。
栈的实现
首先,我们定义一下栈操作的基本接口:
// 定义栈接口
type Stack interface {
Push(v interface{}) //入栈
Pop() (v interface{}) //出栈
IsEmpty() (ok bool) //判断是否为空
Top() (v interface{}) //获取栈顶元素
Reset() //重置栈
Print() //打印栈
}
顺序栈
栈是线性表的特例,所以栈的顺序存储结构其实就是线性表顺序存储结构的简称,我们简称为顺序栈。线性表是用数组来实现的,对于栈这种只能一头插入删除的线性表来说,用数组下标为0(栈底不变,只需要跟踪栈顶的变化即可)的一端作为栈底比较合适。
形象的表示图如下所示:
![3db47a64c1263f2a3908caa8a262abd9.png](https://i-blog.csdnimg.cn/blog_migrate/c6de4eef4c0edea1e5f9a37ec6ce036a.png)
栈操作
我们重点讲解下入栈和出栈操作,直观的示意图如下示:
![771b677619fa36babab561ea38828209.png](https://i-blog.csdnimg.cn/blog_migrate/bb56ef91ba12dc0ad29496e438c74b33.jpeg)
在golang中,我们可以使用切片来实现顺序栈,定义如下示:
type StackOnArray struct {
data []interface{} //栈的元素列表
size int // 栈的元素数目
mu sync.Mutex // 为了并发安全使用的锁
}
入栈
结合上面形象的示意图来理解,入栈操作很简单,用代码表示如下示:
func (this *StackOnArray) Push(v interface{}) {
this.mu.Lock()
defer this.mu.Unlock()
this.data = append(this.data, v)
this.size++
return
}
入栈时直接把元素放在数组的最后面,然后元素数量加 1。性能损耗主要花在切片追加元素上,切片如果容量不够会自动扩容,底层损耗的复杂度我们这里不计,所以时间复杂度为 O(1)。
出栈
结合上面形象的示意图,那么出栈操作也很easy,用代码表示如下示:
func (this *StackOnArray) Pop() (v interface{}) {
this.mu.Lock()
defer this.mu.Unlock()
if this.IsEmpty() {
return nil
}
v = this.data[this.size-1]
this.data = this.data[0 : this.size-1]
this.size--
return v
}
如果栈大小为0,那么不允许出栈,否则从数组的最后面拿出元素。
其余操作也很简单了,下面我们直接给出完整的代码示例
完整的代码示例
package stack
import (
"fmt"
"sync"
)
/*
* 基于数组实现的栈
*/
type StackOnArray struct {
data []interface{} //栈的元素列表
size int // 栈的元素数目
mu sync.Mutex // 为了并发安全使用的锁
}
func NewStackOnArray() *StackOnArray {
return &StackOnArray{}
}
func (this *StackOnArray) Push(v interface{}) {
this.mu.Lock()
defer this.mu.Unlock()
this.data = append(this.data, v)
this.size++
return
}
func (this *StackOnArray) Pop() (v interface{}) {
this.mu.Lock()
defer this.mu.Unlock()
if this.IsEmpty() {
return nil
}
v = this.data[this.size-1]
this.data = this.data[0 : this.size-1]
this.size--
return v
}
func (this *StackOnArray) IsEmpty() (ok bool) {
return this.size == 0
}
func (this *StackOnArray) Top() (v interface{}) {
if this.IsEmpty() {
return nil
}
return this.data[this.Size()-1]
}
func (this *StackOnArray) Reset() {
this.data = make([]interface{}, 0)
this.size = 0
return
}
func (this *StackOnArray) Size() int {
return this.size
}
func (this *StackOnArray) Print() {
if this.IsEmpty() {
fmt.Printf("stack is empty!n")
return
}
fmt.Printf("stack size is:[%v]n", this.size)
fmt.Printf("print stack elem......n")
for i := this.size - 1; i >= 0; i-- {
fmt.Printf("index:%v, elem:[%v]n", i, this.data[i])
}
return
}
链表栈
把栈顶放在单链表的头部,用链表来存储栈的的数据结构称为链栈。
一个直观的示意图如下示:
![adeadeb1596db5b56365cd833e6c3007.png](https://i-blog.csdnimg.cn/blog_migrate/a9113a9725c808236f36fc611c69e464.png)
如果对链表的操作还不是很了解的话,可以参阅我之前的链表讲解部分。
栈操作
这里我们也只重点讲解一下入栈和出栈使用链表如何去实现。
链表形式的数据结构定义如下示:
type LinkNode struct {
data interface{}
next *LinkNode
}
type StackOnLinklist struct {
top *LinkNode //栈顶元素
size int //栈的元素数目
mu sync.Mutex
}
进栈
进栈示意图如下示:
![6ef6048959ce24a6371c87f7b4eee2c9.png](https://i-blog.csdnimg.cn/blog_migrate/6e35ba513fd7e2d7a25a631c57b0b34a.jpeg)
代码示例如下:
func (this *StackOnLinklist) Push(v interface{}) {
this.mu.Lock()
defer this.mu.Unlock()
if this.top == nil {
node := &LinkNode{data: v}
this.top = node
} else {
preNode := this.top
newNode := &LinkNode{data: v}
newNode.next = preNode
this.top = newNode
}
this.size++
return
}
结合代码和示意图理解如下:
如果栈里面的底层链表为空,表明没有元素,那么新建节点并设置为链表起点:stack.top = new(LinkNode)。
否则取出老的节点:preNode := stack.top,新建节点:newNode := new(LinkNode),然后将原来的老节点链接在新节点后面: newNode.Next = preNode,最后将新节点设置为链表起点 stack.top = newNode。
时间复杂度为:O(1)。
出栈
出栈示意图如下示:
![f89fe15075e10f4daa6fc2f4ccc7ec89.png](https://i-blog.csdnimg.cn/blog_migrate/94fee3d06e6e1dbe23af4cb62e526de6.png)
代码示例如下:
func (this *StackOnLinklist) Pop() (v interface{}) {
this.mu.Lock()
defer this.mu.Unlock()
if this.IsEmpty() {
return nil
}
v = this.top.data
this.top = this.top.next
this.size--
return v
}
结合示意图和代码直观讲解:
元素出栈。如果栈大小为0,那么出栈元素为空。
直接将链表的第一个节点 topNode := stack.top 的值取出,然后将表头设置为链表的下一个节点:stack.top = stack.top.Next,相当于移除了链表的第一个节点。
时间复杂度为:O(1)。
完整代码
package stack
import (
"fmt"
"sync"
)
type LinkNode struct {
data interface{}
next *LinkNode
}
type StackOnLinklist struct {
top *LinkNode //栈顶元素
size int //栈的元素数目
mu sync.Mutex
}
func NewStackOnLinklist() *StackOnLinklist {
return &StackOnLinklist{
top: nil,
size: 0,
}
}
func (this *StackOnLinklist) Push(v interface{}) {
this.mu.Lock()
defer this.mu.Unlock()
if this.top == nil {
node := &LinkNode{data: v}
this.top = node
} else {
preNode := this.top
newNode := &LinkNode{data: v}
newNode.next = preNode
this.top = newNode
}
this.size++
return
}
func (this *StackOnLinklist) Pop() (v interface{}) {
this.mu.Lock()
defer this.mu.Unlock()
if this.IsEmpty() {
return nil
}
v = this.top.data
this.top = this.top.next
this.size--
return v
}
func (this *StackOnLinklist) IsEmpty() (ok bool) {
return this.size == 0
}
func (this *StackOnLinklist) Top() (v interface{}) {
if this.IsEmpty() {
return nil
}
return this.top.data
}
func (this *StackOnLinklist) Reset() {
this.size = 0
this.top = nil
return
}
func (this *StackOnLinklist) Size() int {
return this.size
}
func (this *StackOnLinklist) Print() {
if this.IsEmpty() {
fmt.Printf("stack is empty!n")
return
}
fmt.Printf("stack size is:[%v]n", this.size)
fmt.Printf("print stack elem......n")
curNode := this.top
for curNode != nil {
fmt.Printf("elem:[%v]n", curNode.data)
curNode = curNode.next
}
return
}