后进者先出,先进者后出,这就是典型的“栈”结构。

从栈的操作特性上来看,栈是一种“操作受限”的线性表,只允许在一端插入和删除数据。

如何实现一个“栈”

栈可以用数组实现,也可以用链表实现。用数组实现的叫顺序栈,用链表实现的链式栈

// 基于数组实现的顺序栈
type ArrayStack struct {
    items []string // 数组
    count int      // 栈中元素个数
    n     int      // 栈的大小
}

func NewArrayStack(capacity int) *ArrayStack {
    return &ArrayStack{
        items: make([]string, capacity),
        count: 0,
        n:     capacity,
    }
}

// 入栈
func (s *ArrayStack) Push(item string) bool {
    // 数组空间不够,入栈失败
    if s.count >= s.n {
        return false
    }
    // 将item放到下标为count的位置,并且count加一
    s.items[s.count] = item
    s.count++
    return true
}

// 出栈
func (s *ArrayStack) Pop() string {
    // 栈为空
    if s.count == 0 {
        return ""
    }
    // 返回下标为count-1的数组元素,并且栈中元素个数count减一
    s.count--
    return s.items[s.count]
}

不管是顺序栈还是链式栈,存储数据只需要大小为n的存储空间。在入栈和出栈过程中,只需要一两个临时变量存储空间,所以空间复杂度是O(1)。不管是顺序栈还是链式栈,入栈、出栈只涉及栈顶个别数据的操作,所以时间复杂度都是O(1)。

栈在函数调用中的应用

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

int main() {
    int a = 1;
    int ret = 0;
    int res = 0;
    ret = add(3, 5);
    res = a + ret;
    printf("%d\n", res);
    return 0;
}

int add(int x, int y) {
    int sum = 0;
    sum = x + y;
    return sum;
}

从代码中看出,main()函数调用了add()函数获取计算结果并且与临时变量a相加,最后打印res的值。

栈在表达式求值中的应用

编译器利用两个栈来实现表达式求值。其中一个栈保存操作数,另一个保存运算符。从左向右遍历表达式,遇到数字时压入栈中;遇到运算符就与运算符栈的栈顶元素进行比较。如果比运算符栈顶元素的优先级高,将当前运算符压入栈中,否则取出栈顶运算符,从操作数栈的栈顶取2个操作数进行计算,再把计算完的结果压入操作数栈,之后继续下一轮比较。

栈在括号匹配中的应用

假设表达式中只包含三种括号,圆括号()、方括号[]和花括号{},并且它们可以任意嵌套。

用一个栈来存储未匹配的左括号,从左到右依次扫描字符串。当扫描到左括号时将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,则继续下一轮压栈匹配,否则表示匹配不了,表达式非法。

必知必会

用数组实现一个顺序栈

用链表实现一个链式栈

模拟浏览器的前进、后退功能