作者: 无止无尽 | 来源:发表于2024-07-04 20:53 被阅读0次

栈(stack)是一种遵循先入后出逻辑的线性数据结构。

我们可以把它想象成吃零食的薯片桶,打开薯片桶之后,每次只能拿到最上面的薯片。如图所示,我们把堆叠元素的顶部称为”栈顶“,底部元素称为”栈底“。将把元素添加到栈顶的操作叫做“入栈",删除栈顶元素的操作叫做“出栈”。


栈操作

栈的常用操作

通常情况下,我们可以直接使用编程语言内置的栈类。 然而某些语言可能并不提供专门的栈类,这时我们可以将该语言的“数组”或“链表”当作栈来使用,并在逻辑上忽略栈无关的操作。
栈的常用操作有

  • push(), 元素入栈,将元素添加至栈顶,时间复杂度O(1)
  • pop(), 元素出栈,将栈顶元素删除,时间复杂度尾O(1)
  • peek(),访问栈顶元素,时间复杂度O(1)
var stack []int
// 元素入栈
stack = append(stack, 1)
stack = append(stack,2)

// 元素出栈
pop := stack[len(stack)-1]
stack = stack[:len(stack)-1]

// 访问栈顶元素
peek := stack[len(stack)-1]

// 获取栈的长度
size := len(stack)

// 是否为空判断
isEmpty := len(stack) == 0

栈的实现

栈遵循先入后出的原则,因此我们只能在栈顶添加或删除元素,然而,数组和链表都可以在任意位置添加和删除元素,因此栈可以视为一种受限制的数组或链表.换句话说,我们可以屏蔽数组或链表的部分无关操作,使其对外表现得逻辑符合栈的特性。

1. 基于链表的实现

使用链表实现栈时,我们可以将链表头节点视为栈顶,尾节点视为栈底。因为链表是顺序访问的,头节点是我们最先访问的元素,尾节点是最后访问的元素,这个栈的特性一致。
对于入栈操作,我们只需将元素插入链表头部,这种插入方法成为”头插法“。
对于出栈操作,我们只需将头节点从链表中删除。

package main

import (
    "container/list"
    "fmt"
)

type Stack struct {
    *list.List
}

func NewStack() *Stack {
    return &Stack{
        List: list.New(),
    }
}

// 入栈,将元素添加至栈顶,也就是链表的头节点
func (s *Stack) Push(element interface{}) {
    s.List.PushBack(element)
}

// 出栈,将栈顶元素删除,
func (s *Stack) Pop() interface{} {
    if s.IsEmpty() {
        return nil
    }
    e := s.List.Back()
    s.List.Remove(e)
    return e.Value
}

func (s *Stack) Peek() interface{} {
    return s.List.Front()
}

func (s *Stack) Size() int {
    return s.List.Len()
}

func (s *Stack) IsEmpty() bool {
    return s.List.Len() == 0
}

func main() {
    var s = NewStack()
    s.Push(1)
    s.Push(2)
    s.Push(3)
    fmt.Println(s.Pop(), s.Size())
    fmt.Println(s.Pop(), s.Size())
    fmt.Println(s.Pop(), s.Size())
}
/* 输出结果
3
3 2
2 1
1 0
*/

1. 基于数组的实现

使用数组实现栈时,我们可以将数组的尾部作为栈顶,入栈和出栈操作均在数组尾部进行。时间复杂度为O(1)

type StackArray struct {
    data []interface{}
}

func NewStackArray() *StackArray {
    return &StackArray{}
}

func (s *StackArray) Push(element interface{}) {
    s.data = append(s.data, element)
}

// 出栈,将栈顶元素删除,
func (s *StackArray) Pop() interface{} {
    if s.IsEmpty() {
        return nil
    }

    e := s.data[len(s.data)-1]
    s.data = s.data[:len(s.data)-1]
    return e
}

func (s *StackArray) Peek() interface{} {
    if s.IsEmpty() {
        return nil
    }
    return s.data[len(s.data)-1]
}

func (s *StackArray) Size() int {
    return len(s.data)
}

func (s *StackArray) IsEmpty() bool {
    return len(s.data) == 0
}

两种实现对比

时间效率

在基于数组的实现中,入栈和出栈操作都在预先分配好的连续内存中进行,具有很好的缓存本地性,因此效率较高。如果入栈时超出数组容量,会触发扩容机制,导致该入栈操作的时间复杂度变为O(n)。
在基于链表的实现中,链表的扩容十分灵活,不会出现扩容导致的效率地下问题。但是入栈操作需要初始化节点对象并修改指针,因此效率相对较低。
综上所述,当入栈与出栈操作的元素是基本数据类型时,我们可以得出以下结论:

  • 基于数组的实现的栈在触发扩容机制时效率会降低,但由于扩容时低频操作,因此平均效率更高。
  • 基于链表实现的栈可以提供更加稳定的效率表现。

栈的典型应用

程序内存管理
每次调用函数时,系统都会在栈顶添加一个栈帧,用于记录函数的上下文信息,在递归函数中,向下递推阶段会不断执行入栈操作,而向上回溯阶段则会不断执行出栈操作。

相关文章

  • Java实现栈

    数组栈:压栈、出栈、返回栈顶元素 链式栈:压栈、出栈、返回栈顶元素

  • 数据结构之 栈

    栈结构 链式栈 一.栈结构体 1构建空栈 2栈置空 3判断栈空 4获取栈顶 5入栈 6出栈 7便利栈 二.链式栈 ...

  • 栈和队列

    1、栈 栈是一种先进先出的数据结构。栈顶进栈,栈顶出栈。 数据结构 栈的初始化 进栈 出栈 栈的最小值 2、队列 ...

  • 递归累加数组

    入栈 5入栈 4入栈 3入栈 2入栈 1出栈 [1 0]出栈 [2 1 0]出栈 [3 2 1 0]出栈 [4 3...

  • 栈的逻辑结构和存储结构

    main()进栈s(1)进栈s(0)进栈 s(0)出栈s(1)出栈main()出栈 顺序栈 一个数组 + 指向栈顶...

  • 单调栈 2020-06-12(未经允许,禁止转载)

    1.单调栈 指栈内元素保持单调性的栈结构,分为单调增栈(栈底到栈顶元素递增)和单调减栈(栈底到栈顶元素递减) 2....

  • 链栈的操作

    链栈的定义 链栈的操作 初始化 判断栈空 入栈 出栈

  • 函数调用栈平衡

    栈平衡 栈平衡:函数调用前后的栈顶指针指向的位置不变 内平栈 外平栈 内平栈: 指的是在函数调用返回之前使栈保持...

  • 栈的简单Java实现

    栈栈的特点是先进后出,出栈、入栈都是在栈顶操作。

  • 汇编学习-入栈和出栈

    栈有两个基本的操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素。栈顶的元素总是最后入栈...

网友评论

      本文标题:

      本文链接:https://www.haomeiwen.com/subject/fujpcjtx.html