数据结构-栈

前言

image.png
栈结构:先进后出,后进先出
我们可以把栈理解为叠在一起的盘子。我们平时放盘子的时候,都是从下往上一个个放,取得时候也是从上往下一个个依次取,不能从中间任意抽出。

从栈的操作特性来看,栈是一种操作受限的线性表,只允许在一端插入和删除数据。当某个数据集集合只涉及在一端插入和删除数据,并且满足后进先出,先进后出的特性,我们就应该首选栈这种数据结构。

如何实现一个栈

从刚才栈的定义中,我们可以看出,栈主要分为两个操作:入栈和出栈,也就是在栈顶插入一个数据和栈顶删除一个数据。
实际上,栈可以用数组来实现,也可以用链表来实现。用数组实现的栈叫做顺序栈,用链表实现的栈叫做链式栈。

// 基于数组实现的顺序栈
public class ArrayStack {
  private String[] items;  // 数组
  private int count;       // 栈中元素个数
  private int n;           //栈的大小

  // 初始化数组,申请一个大小为n的数组空间
  public ArrayStack(int n) {
    this.items = new String[n];
    this.n = n;
    this.count = 0;
  }

  // 入栈操作
  public boolean push(String item) {
    // 数组空间不够了,直接返回false,入栈失败。
    if (count == n) return false;
    // 将item放到下标为count的位置,并且count加一
    items[count] = item;
    ++count;
    return true;
  }
  
  // 出栈操作
  public String pop() {
    // 栈为空,则直接返回null
    if (count == 0) return null;
    // 返回下标为count-1的数组元素,并且栈中元素个数count减一
    String tmp = items[count-1];
    --count;
    return tmp;
  }
}
/**
 * 基于链表实现的栈。
 *
 * Author: Zheng
 */
public class StackBasedOnLinkedList {
  private Node top = null;

  public void push(int value) {
    Node newNode = new Node(value, null);
    // 判断是否栈空
    if (top == null) {
      top = newNode;
    } else {
      newNode.next = top;
      top = newNode;
    }
  }

  /**
   * 我用-1表示栈中没有数据。
   */
  public int pop() {
    if (top == null) return -1;
    int value = top.data;
    top = top.next;
    return value;
  }

  public void printAll() {
    Node p = top;
    while (p != null) {
      System.out.print(p.data + " ");
      p = p.next;
    }
    System.out.println();
  }

  private static class Node {
    private int data;
    private Node next;

    public Node(int data, Node next) {
      this.data = data;
      this.next = next;
    }

    public int getData() {
      return data;
    }
  }
}

不管是顺序栈还是链式栈,我么存储数据只需要一个大小为n的数组就够了,在入栈和出栈的过程中,只需要一个两个临时变量存储空间,所以空间复杂度为O(1)。
这里存储数据需要大小为n的数组并不是空间复杂度就是O(n)。因为这个n个空间是必须的,无法省掉。所以我们说空间复杂度的时候,是指除了原本的数据存储空间外,算法运行还需要额外的存储空间。

支持动态扩容的顺序栈

基于数组实现的栈是一个固定的栈,也就是说在初始化栈时需要事先指定栈的大小。当栈满只有,就无法再往栈里添加数据了。尽管链式栈的大小不受限,但要存储next指针,内存消耗比较多。那么如何基于数组实现一个可以支持动态扩容的栈?

如果要实现一个支持动态扩容的栈,我们只需要底层依赖一个支持动态扩容的数组就可以了。当栈满之后,我们就申请一个更大的数组,将原来的数据搬移到新的数组中。
image.png
实际上支持动态扩容的顺序栈我们平时开发用不到。现在我们分析一个支持动态扩容的顺序栈的入栈、出栈的时间复杂度。

对于出栈来说,我们不会设计内存的重新申请和数据搬移,所以出栈的时间复杂度仍然是O(1)。但是对于入栈的操作,情况就不一样了。当栈中有空闲时间时,入栈的时间复杂度为O(1)。当空间不够的时候,就需要重新申请内存和数据搬移,所以时间复杂度就变为了O(n)。

栈在函数调用的应用

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

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

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

从代码中看出,main调用量add方法,获取计算结果,并且与临时变量a
相加,最后打印res的值。如下图:
image.png

栈在表达式求值中的应用

有一个四则运算:34+13*9+44-12/3
实际上编译器是通过两个栈来实现的。其中一个保存操作数的栈,另一个保存运算符的栈。从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈,当遇到运算符,就与运算符栈的栈顶元素进行比较。
如果比运算符栈顶元素的优先级高,就将当前运算符压入栈。如果比运算符栈顶的优先级低或者相同,从运算符栈中取栈顶运算符,从操作数栈的栈顶取两个操作数,进行运算,再把运算后的记过压入操作数栈,继续比较。
image.png

栈在括号匹配中的应用

假设表达式中只包括三个括号,圆括号()、方括号[]和花括号{},并且可以任意嵌套。
{[] ()[{}]}或[{()}([])]等都为合法格式,而{[}()]或[({)]为不合法的格式

那如何检查它是否合法?
我们用栈来保存为匹配的左括号,从左到右扫描字符串。当扫描左括号时,则将其压入栈中;当扫描右括号时,从栈顶取一个左括号,如果能够匹配,比如(),[],{}匹配,则扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明非法格式。
当所有括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则说明有未匹配的左括号,为非法格式。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值