【数据结构】栈和队列

栈(Stack)

栈的概念

是一种特殊的线性表,只允许在固定一端进行插入和删除元素的操作,进行数据插入和删除操作的一端称为栈顶,另一端则称为栈底。并且栈中的数据遵循先进后出(后进先出)的原则, 可以理解最先进去的在最下面, 就像羽毛球球筒一样,先放进去的羽毛球总是要把上面的羽毛球取出后,才能取到最开始放进去的。

栈的插入操作叫做压栈,也叫进栈,入栈,入数据在栈顶。
栈的删除操作叫做出栈,出数据也在栈顶。

入栈出栈

栈的使用

构造一个空栈,例如:

Stack<Integer> stack = new Stack<>(); 存放 Integer 类型的栈。

将元素 e 入栈,并返回 e,例如:

stack.push(1); 1入栈
stack.push(2); 2入栈
stack.push(3); 3入栈

因为栈遵守先进后出的原则,所以执行上面三条语句后,栈的内部如下:

将栈顶元素出栈并返回,例如:

pop

栈顶元素出栈后,栈的内部如下:
出栈后
所以,要想得到栈底元素 1,只能把它上面的所有元素出栈后,才能得到。

获取栈顶元素,例如:

获取栈顶元素

peek() 是获取栈顶元素,并不是让栈顶元素出栈。

peek获取栈顶元素

获取栈中有效元素个数,例如:

size()

判断栈是否为空,例如:

isEmpty()

模拟实现栈

我们使用数组来模拟实现栈:

public class MyStack {
    //使用数组实现
    public int[] elem;
    //已使用的容量
    public int usedSize;
    public MyStack(){
        //初始化数组大小为10
        this.elem = new int[10];
    }

	//压栈
    public void push(int val) {
        //先判断栈满了没有,没满才能入栈
        if (isFull()) {
            //如果满,就扩容(2倍扩容)
            elem = Arrays.copyOf(elem, 2 * elem.length);
        }
        this.elem[usedSize++] = val;
    }

    //判断是否已满
    public boolean isFull() {
        //数组长度和已使用大小相等则满,返回true
        return elem.length == usedSize;
    }

    //出栈
    public int pop() {
        //先判断栈是否为空
        if (isEmpty()) {
            throw new EmptyException("栈为空");
        }
        //先让usedSize--,因为数组下标从0开始
        return elem[--usedSize];
    }

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

    //取栈顶元素
    public int peek(){
        if (isEmpty()){
            throw new EmptyException("栈空");
        }
        return elem[usedSize - 1];
    }
}

栈的应用场景

1. 改变元素的序列

在这里插入图片描述

2. 逆波兰表达式求值

逆波兰表达式其实就是后缀表达式,通过手段将中缀表达式(正常的表达式 如:a+(b-c))转换成后缀表达式后,只需入栈出栈即可完成表达式的计算。

如何将中缀表达式转换成后缀表达式:

中缀转后缀过程
学会转换后缀表达式后,就可以做题了(虽然题目给的是后缀表达式)。
题目链接:逆波兰表达式求值
题目描述:给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。请你计算该表达式。返回一个表示表达式值的整数。

逆波兰表达式示例一

实现过程:

逆波兰题解过程

代码:

public int evalRPN(String[] tokens) {
    Stack<Integer> stack = new Stack<>();
    //遍历字符串
    for (String s : tokens) {
        //判断是数字还是运算符
        if (!isOperator(s)) {
            //如果是数字就先转换成数字再入栈
            stack.push(Integer.parseInt(s));
        } else {
            //是运算符就先出栈两次,第一个为右操作数,第二个为左操作数
            int rightNum = stack.pop();
            int leftNum = stack.pop();
            switch (s) {
                //根据运算符,完成相应运算并将运算结果入栈
                case "+":
                    stack.push(leftNum + rightNum);
                    break;
                case "-":
                    stack.push(leftNum - rightNum);
                    break;
                case "*":
                    stack.push(leftNum * rightNum);
                    break;
                case "/":
                    stack.push(leftNum / rightNum);
                    break;
            }
        }
    }
    //遍历完成后,栈里面就剩下整个表达式的运算结果,返回即可
    return stack.pop();
}

public boolean isOperator(String s) {
    //题目中的运算符只有这4个
    if (s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/")) {
        return true;
    }
    return false;
}

3. 括号匹配

题目链接:括号匹配
题目描述:
括号匹配描述

实现思路:
在这里插入图片描述
代码:

public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == '(' || c == '{' || c == '[') {
                //如果是上面任意一个左括号就入栈
                stack.push(c);
            } else {
                //遇到右括号
                //如果遇到右括号,并且栈里没有左括号,返回false
                if (stack.empty()) {
                    return false;
                }
                //遇到右括号,取出栈顶的元素,看是否匹配
                char c2 = stack.pop();
                if (!(c2 == '(' && c == ')' || c2 == '[' && c == ']' || c2 == '{' && c == '}')) {
                    return false;
                }                  
            }
        }
        //遍历完,栈中还有数据就不匹配
        return stack.isEmpty();
    }

4. 栈的压入、弹出序列

题目链接:栈的压入、弹出序列
题目描述:输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。

题目示例

实现过程:

入栈出栈匹配

代码:

public boolean IsPopOrder(int [] pushA, int [] popA) {
    Stack<Integer> stack = new Stack<>();
    int j = 0;
    for (int i = 0; i < pushA.length; i++) {
        //pushA数组元素入栈
        stack.push(pushA[i]);
        //比较是否相等,需要注意j如果大于popA的长度,就会空指针异常,
        //栈如果为空,也会异常
        while (j < popA.length && !stack.empty() && stack.peek().equals(popA[j])) {
            //如果栈顶元素和popA中元素相等,就出栈,并且下标加1
            stack.pop();
            j++;
        }
    }
    //pushA遍历完后,如果栈空就是,栈不空就不是
    return stack.empty();
}

5. 最小栈

题目链接:最小栈
题目描述:最小栈描述

这题的要求是在常数时间内找出最小元素,那我们就可以多弄一个栈,来存放当前的最小元素,代码:

class MinStack {

//普通栈,存放所给的值
    public Stack<Integer> stack;
    //最小栈,存放较小的值
    public Stack<Integer> minStack;

    public MinStack() {
        stack = new Stack<>();
        minStack = new Stack<>();
    }

    public void push(int val) {
        //所有的都要入普通栈
        stack.push(val);
        //如果最小栈为空,那么当前push的值就是最小的值,也要入最小栈
        if (minStack.empty()){
            minStack.push(val);
        }else {
            //如果不为空,push的值小于等于最小栈栈顶的值,那就需要入最小栈,保证可以在最小栈取到当前最小值
            //取等于是为了pop时,两个栈可以同步
            if (val <= minStack.peek()){
                minStack.push(val);
            }
        }
    }

    public void pop() {
        //在普通栈不为空的情况下
        if (!stack.empty()){
            //记录普通栈出栈的值
            int val = stack.pop();
            //如果和最小栈栈顶元素相等,最小栈出栈
            if (val == minStack.peek()){
                minStack.pop();
            }
        }
    }

    public int top() {
        //在普通栈不为空的情况下
        if (!stack.empty()){
            return stack.peek();
        }
        return -1;
    }

    public int getMin() {
    	//此时的最小元素就在最小栈的栈顶
        return minStack.peek();
    }
}

队列(Queue)

队列: 只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,遵循先进先出的原则。
入队列:进行插入操作的一端称为队尾(Tail / Rear)
出队列:进行删除操作的一端称为队头(Head / Front)
队列结构

队列的实现

在 java 中,Queue 是个接口,通过链表来实现的,所以我们可以通过单向链表和双向链表来实现。
双链表:因为双链表中节点可以指向前驱节点和后继节点,所以我们可以在链表的头部插入删除,也可以在尾部插入删除。
单链表:单链表不知道前一节点的地址,所以我们需要一个尾节点 last,并且只能在 头部删除 尾部插入。

下面就使用单链表来实现队列:

public class MyQueue {
    //使用单链表实现
    public class Node {
        public int val;
        public Node next;

        public Node(int val) {
            this.val = val;
        }
    }

    public Node head;
    public Node last;
    //记录元素个数
    public int usedSize;

    //入队列
    public void offer(int val) {
        Node node = new Node(val);
        //目前还没有结点
        if (head == null) {
            head = node;
            last = node;
        } else {
        	//从尾部插入
            last.next = node;
            last = node;
        }
        usedSize++;
    }

    //出队列
    public int poll() {
        if (empty()) {
            throw new EmptyException("队列为空");
        }
        //记录即将出队列的元素
        int val = head.val;
        //head指向下一个结点后,元素就出列了
        head = head.next;
        //只有一个节点,last也需要为null
        if (head == null) {
            last = null;
        }
        usedSize--;
        //返回记录的值
        return val;
    }

    //判断队列元素个数
    public boolean empty() {
        return usedSize == 0;
    }

    //获取队头元素
    public int peek() {
        if (empty()) {
            throw new EmptyException("队列为空");
        }
        return head.val;
    }
    
    //获取队列中有效元素个数
    public int size() {
        return usedSize;
    }
}

循环队列

循环队列其实就是队列首尾相连(front 为队首,rear 为队尾),形成一个环,通常使用数组实现:
循环队列
如何区分队列为空还是满?
front等于rear
因为 front 等于 rear 时,队列可能为空,也可能为满,所以我们可以通过记录==已使用的个数(usedSize)==来判断队列是否已满,或者 保留一个位置,通过 rear + 1 是否等于 front 来判断队列为满:

保留一个位置

现在就多了一个问题,如何通过从下标 7 的位置到下标 0 的位置?
在环内实现加1

实现循环队列

题目链接:设计循环队列

代码:

class MyCircularQueue {

    public int[] elem;
    public int front;   //表示队头
    public int rear;    //表示队尾

    public MyCircularQueue(int k) {
        //我们使用浪费一个空间来判断队列满没有,所以需要加一
        //如果使用usedSize来记录元素个数,则可以不加一
        this.elem = new int[k + 1];
    }

    //入队列
    public boolean enQueue(int value) {
        //先检查队列满没有
        if (isFull()) {
            return false;
        }
        //入队放在rear的位置
        elem[rear] = value;
        //再更新rear的值
        rear = (rear + 1) % elem.length;
        return true;
    }

    //出队列
    public boolean deQueue() {
        if (isEmpty()) {
            //队列为空,不能出队列
            return false;
        }
        front = (front + 1) % elem.length;
        return true;
    }

    //返回队头元素
    public int Front() {
        if (isEmpty()) {
            //抛异常可能不能通过测试
            return -1;
        }
        return elem[front];
    }

    //返回队尾元素
    public int Rear() {
        if (isEmpty()) {
            //抛异常可能不能通过测试
            return -1;
        }
        //队尾元素为rear下标-1,但是rear可能为0,减一后数组越界,
        //所以如果rear等于0,只需返回数组长度减一处的下标即可,其他位置正常减一
        int index = (rear == 0) ? elem.length - 1 : rear - 1;
        return elem[index];
    }

    public boolean isEmpty() {
        //保留了一个位置,所以两个相遇时,队列为空。
        return rear == front;
    }

    public boolean isFull() {
        return (rear + 1) % elem.length == front;
    }
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值