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

栈的插入操作叫做压栈,也叫进栈,入栈,入数据在栈顶。
栈的删除操作叫做出栈,出数据也在栈顶。
栈的使用
构造一个空栈,例如:
Stack<Integer> stack = new Stack<>();
存放 Integer 类型的栈。
将元素 e 入栈,并返回 e,例如:
stack.push(1);
1入栈
stack.push(2);
2入栈
stack.push(3);
3入栈
因为栈遵守先进后出的原则,所以执行上面三条语句后,栈的内部如下:

将栈顶元素出栈并返回,例如:
栈顶元素出栈后,栈的内部如下:
所以,要想得到栈底元素 1,只能把它上面的所有元素出栈后,才能得到。
获取栈顶元素,例如:
peek() 是获取栈顶元素,并不是让栈顶元素出栈。
获取栈中有效元素个数,例如:
判断栈是否为空,例如:
模拟实现栈
我们使用数组来模拟实现栈:
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 时,队列可能为空,也可能为满,所以我们可以通过记录==已使用的个数(usedSize)==来判断队列是否已满,或者 保留一个位置,通过 rear + 1 是否等于 front 来判断队列为满:
现在就多了一个问题,如何通过从下标 7 的位置到下标 0 的位置?
实现循环队列
题目链接:设计循环队列
代码:
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;
}
}