如何理解栈

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

  • 数组实现的栈:顺序栈
  • 链表实现的栈:链式栈
// 基于数组实现的顺序栈
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;
  }
}

下面是让chatgpt写出的分别基于Java和C++的链式栈 

在下面的代码中,我们使用内部类 Node 表示链式栈的节点,节点包含一个数据域 data 和一个指向下一个节点的指针 next。链式栈本身包含一个指向栈顶节点的指针 top 和一个表示栈元素个数的变量 size。根据链式栈的特点,入栈操作在链表头插入新节点,出栈操作从链表头删除节点。

 // Java基于链表实现的链式栈
public class LinkedListStack<T> {

    // 定义链式栈的节点类
    private static class Node<T> {
        private T data; // 节点存储的数据
        private Node<T> next; // 指向下一个节点的指针

        // 构造方法
        public Node(T data, Node<T> next) {
            this.data = data;
            this.next = next;
        }
    }

    private Node<T> top; // 栈顶节点
    private int size; // 栈的元素个数

    // 构造方法
    public LinkedListStack() {
        top = null;
        size = 0;
    }

    // 判断栈是否为空
    public boolean isEmpty() {
        return top == null;
    }

    // 获取栈的大小
    public int size() {
        return size;
    }

    // 入栈操作
    public void push(T data) {
        Node<T> newNode = new Node<>(data, top); // 创建新节点,指向当前栈顶节点
        top = newNode; // 更新栈顶节点为新节点
        size++; // 元素个数加1
    }

    // 出栈操作
    public T pop() {
        if (isEmpty()) {
            throw new RuntimeException("Stack is empty!");
        }
        T data = top.data; // 获取栈顶节点的数据
        top = top.next; // 更新栈顶节点为下一个节点
        size--; // 元素个数减1
        return data;
    }

    // 获取栈顶元素
    public T peek() {
        if (isEmpty()) {
            throw new RuntimeException("Stack is empty!");
        }
        return top.data;
    }

    // 打印栈中的元素
    public void printStack() {
        Node<T> curr = top;
        System.out.print("Stack: ");
        while (curr != null) {
            System.out.print(curr.data + " ");
            curr = curr.next;
        }
        System.out.println();
    }
}
// C++基于链表实现的链式栈
#include <iostream>
#include <stdexcept>  // 包含 std::out_of_range 异常的定义

template <typename T>
class LinkedStack {
private:
    // 定义内部的节点结构体
    struct Node {
        T data;  // 存储数据的字段
        Node* next;  // 存储下一个节点的指针
        Node(T data) : data(data), next(nullptr) {}  // 节点构造函数,初始化数据和指针
    };

    Node* top;  // 栈顶指针
    int size;   // 栈的元素个数

public:
    LinkedStack() : top(nullptr), size(0) {}  // 栈的构造函数,初始化栈顶指针为 nullptr,元素个数为 0

    // 将元素压入栈中
    void push(T data) {
        Node* newNode = new Node(data);  // 创建一个新节点
        newNode->next = top;  // 新节点指向原先的栈顶节点
        top = newNode;  // 栈顶指针指向新节点
        size++;  // 栈的元素个数加一
    }

    // 弹出并返回栈顶元素
    T pop() {
        if (isEmpty()) {  // 如果栈为空,则抛出一个 std::out_of_range 异常
            throw std::out_of_range("Stack is empty.");
        }
        Node* poppedNode = top;  // 获取栈顶节点
        T poppedData = poppedNode->data;  // 获取栈顶节点的数据
        top = top->next;  // 栈顶指针指向下一个节点
        delete poppedNode;  // 释放栈顶节点的内存空间
        size--;  // 栈的元素个数减一
        return poppedData;  // 返回栈顶节点的数据
    }

    // 返回但不弹出栈顶元素
    T peek() {
        if (isEmpty()) {  // 如果栈为空,则抛出一个 std::out_of_range 异常
            throw std::out_of_range("Stack is empty.");
        }
        return top->data;  // 返回栈顶节点的数据
    }

    // 检查栈是否为空
    bool isEmpty() {
        return size == 0;
    }

    // 返回栈的元素个数
    int getSize() {
        return size;
    }
};

入栈的时间空间复杂度

  • 正常来讲是O(1),时间复杂度和空间复杂度都是。
  • 支持动态扩容的栈时间复杂度,最好情况时间复杂度是O(1),最坏情况时间复杂度是O(n),平均时间复杂度为O(1),按照均摊方法分析,n个正常入栈操作O(1)之后,存在1个特殊的扩容操作,涉及到内存的申请与搬迁,搬迁过程显然为O(n),将1个特殊的操作均摊至n个正常入栈操作,因此均摊之后为O(1)。
  • 空间复杂度都是O(1),不涉及到内存的申请和搬迁。

栈的应用

函数调用

一个main主函数中嵌套一个add函数:

图片来自 极客时间【数据结构与算法之美】课程

 为什么函数调用要用“栈”来保存临时变量呢?用其他数据结构不行吗?

  • 其实,我们不一定非要用栈来保存临时变量,只不过如果这个函数调用符合后进先出的特性,用栈这种数据结构来实现,是最顺理成章的选择。
  • 函数调用之所以用栈,是因为函数调用中经常嵌套,栗子:A调用B,B又调用C,那么就需要先把C执行完,结果赋值给B中的临时变量,B的执行结果再赋值给A的临时变量,嵌套越深的函数越需要被先执行,这样刚好符合栈的特点,因此每次遇到函数调用,只需要压栈,最后依次从栈顶弹出依次执行即可
  • 函数调用和返回符合后进先出原则,而局部变量的生命周期应该和函数一致,所以用栈保存局部变量是合适的,函数出栈的时候同时销毁局部变量

编译器表达式求值

需要两个栈X,Y。从左至右依次遍历表达式,例如3+5*8-6

  • 遇见数字则压入操作数栈X,遇见运算符则压入运算符栈;
  • 遇见的运算符优先级高于栈顶的运算符则继续压入,低于或等于栈顶运算符,则取出该栈顶运算符,并另取出X栈两操作数,计算完毕将结果压入操作数栈X,再继续比较运算符,直至计算完成。
  • 清空栈
图片来自 极客时间【数据结构与算法之美】课程

括号匹配

栈中保存未匹配的左括号。从左到右依次扫描字符串。当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号。如果能够匹配,则继续扫描剩下的字符串。如果扫描的过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。

浏览器的前进、后退

  • 首次浏览,压入栈X
  • 后退:依次出栈X,入栈Y
  • 前进:出栈Y,入栈X
  • 回退后再打开新页面:新页面入栈X,Y中无法前进后退,清空Y

我们都知道,JVM 内存管理中有个“堆栈”的概念。栈内存用来存储局部变量和方法调用,堆内存用来存储 Java 中的对象。那 JVM 里面的“栈”跟我们这里说的“栈”是不是一回事呢?如果不是,那它为什么又叫作“栈”呢? 

尚未解答

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值