栈和队列
- 一.栈(Stack)
- 1.概念
- 2.栈的使用
- 3.模拟实现一个栈
- 1. 构造方法
- 2. 入栈(push)
- 3. 出栈(pop)
- 4.获取栈顶元素(peek)
- 5.获取元素个数(getSize)
- 6.判断栈是否为空(isEmpty)
- 7.完整代码
- 8.泛型实现
- 二.队列(Queue)
- 1.概念
- 2.队列的使用
- 3.模拟实现一个队列
- 1.构造方法
- 2.入队列(offer)
- 3.出队列(poll)
- 4.获取队头元素(peek)
- 5.获取元素个数(getSize)
- 6.判断队列是否为空(isEmpty)
- 7.完整代码
- 8.泛型实现
- 4. 实现一个循环队列
- 1. 入队列
- 2. 出队列
- 3. 获取队首元素
- 4. 获取队列中元素个数
- 5.判断队列是否为空
- 6. 完整代码
- 三.相关题目
- 1. [有效的括号](https://leetcode.cn/problems/valid-parentheses/)
- 2. [逆波兰表达式求值](https://leetcode.cn/problems/evaluate-reverse-polish-notation/)
- 3. [出栈入栈次序匹配](https://www.nowcoder.com/practice/d77d11405cc7470d82554cb392585106?tpId=13&&tqId=11174&rp=1&ru=/activity/oj&qru=/ta/coding-interviews/question-ranking)
- 4. [实现一个最小栈](https://leetcode.cn/problems/min-stack/)
一.栈(Stack)
1.概念
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈(push):栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。出栈(pop):栈的删除操作叫做出栈。出数据在栈顶。
可以看到,先入栈的元素要等后入栈的元素出栈后才能出栈,栈中的元素总是遵循后进先出LIFO(Last In First Out)的原则。
生活中的例子:
- 堆积的碗
- 羽毛球摆放
注意: 将此处的栈与JVM内存模型中的栈区分开
此处的栈是一种数据结构 JVM栈特指JVM中一段内存区域
JVM : Java虚拟机
2.栈的使用
- Java标准库中提供的栈
- 可以看到,Stack继承了Vector,Vector是动态的顺序表,与ArrayList类似,不同的是,Vector是线程安全的.
- 实现的方法
Stack() | 构造一个空的栈 |
E push(E e) | 将e入栈,并返回e |
E pop() | 将栈顶元素出栈并返回 |
E peek() | 获取栈顶元素 |
int size() | 获取栈中有效元素个数 (继承自Vector) |
boolean empty() | 检测栈是否为空 |
public static void main(String[] args){
Stack<Integer> stack = new Stack<>();
//入栈
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
System.out.println("栈中有效元素个数 : "+ stack.size()); // 输出 4
System.out.println("获取栈顶元素 : "+stack.peek()); // 获取栈顶元素,但是不出栈,栈中元素不变
stack.pop(); // 出栈 元素 4 出栈 ,栈中剩余元素 3,2,1
System.out.println("获取栈顶元素 : " + stack.pop()); // 获取栈顶元素,出栈, 此时栈中剩余 2,1两个元素
System.out.println("栈中有效元素个数 : "+ stack.size()); // 输出 2
System.out.println("stack是否为空 : "+ stack.isEmpty()); // 判断栈中是否为空
}
输出结果:
3.模拟实现一个栈
栈是一个特殊的顺序表,所以采用链表和数组的方式都可实现,但是,一般采用数组的方式实现.
1. 构造方法
class MyStack{
private int[] arr;
// size 记录栈中元素个数
private int size;
public MyStack(){
// 调用无参构造方法 默认最大容量12
this(12);
}
public MyStack(int MaxSize){
this.arr = new int[MaxSize];
}
}
2. 入栈(push)
入栈时判断栈是否已满,如果栈满,需要对数组扩容
// 入栈
public int push(int value){
if(this.size == arr.length){
// 栈满 ,需要扩容
int[] copyArr;
// 复制arr 数组并扩容一倍
copyArr = Arrays.copyOf(arr,2 * arr.length);
arr = copyArr;
}
//将元素添加到size位置
this.arr[size] = value;
// 元素个数加一
this.size++;
// 返回添加元素
return value;
}
3. 出栈(pop)
出栈时,判断栈中元素是否为空,如果为空,抛出异常
// 出栈
public int pop(){
if(this.size == 0){
//没有元素
//抛出运行时异常,此处也可以自定义异常
throw new RuntimeException("栈中没有元素,不能出栈....");
}
// 获得栈顶元素
int value = this.arr[size - 1];
// size - 1 之后, 下一次插入时会覆盖原数据,利用覆盖替代删除
this.size--;
return value;
}
4.获取栈顶元素(peek)
// 获取栈顶元素
public int peek(){
if(this.size == 0){
//没有元素
//抛出运行时异常,此处也可以自定义异常
throw new RuntimeException("栈中没有元素,不能出栈....");
}
return this.arr[this.size - 1];
}
5.获取元素个数(getSize)
//获取元素个数
public int getSize(){
return this.size;
}
6.判断栈是否为空(isEmpty)
//判断元素是否为空
public boolean isEmpty(){
return this.size == 0;
}
7.完整代码
import java.util.Arrays;
public class MyStack{
private int[] arr;
// size 记录栈中元素个数
private int size;
public MyStack(){
// 调用无参构造方法 默认最大容量12
this(12);
}
public MyStack(int MaxSize){
this.arr = new int[MaxSize];
}
// 入栈
public int push(int value){
if(this.size == arr.length){
// 栈满 ,需要扩容
int[] copyArr;
// 复制arr 数组并扩容一倍
copyArr = Arrays.copyOf(arr,2 * arr.length);
arr = copyArr;
}
//将元素添加到size位置
this.arr[size] = value;
// 元素个数加一
this.size++;
// 返回添加元素
return value;
}
// 出栈
public int pop(){
if(isEmpty()){
//没有元素
//抛出运行时异常,此处也可以自定义异常
throw new RuntimeException("栈中没有元素,不能出栈....");
}
// 获得栈顶元素
int value = this.arr[size - 1];
// size - 1 之后, 下一次插入时会覆盖原数据,利用覆盖替代删除
this.size--;
return value;
}
// 获取栈顶元素
public int peek(){
if(isEmpty()){
//没有元素
//抛出运行时异常,此处也可以自定义异常
throw new RuntimeException("栈中没有元素,不能出栈....");
}
return this.arr[this.size - 1];
}
//获取元素个数
public int getSize(){
return this.size;
}
//判断元素是否为空
public boolean isEmpty(){
return this.size == 0;
}
}
8.泛型实现
import java.util.Arrays;
public class MyStack<T>{
private T[] arr;
// size 记录栈中元素个数
private int size;
public MyStack(){
// 调用无参构造方法 默认最大容量12
this(12);
}
public MyStack(int MaxSize){
this.arr = (T[]) new Object[MaxSize];
}
// 入栈
public T push(T value){
if(this.size == arr.length){
// 栈满 ,需要扩容
T[] copyArr;
// 复制arr 数组并扩容一倍
copyArr = Arrays.copyOf(arr,2 * arr.length);
arr = copyArr;
}
//将元素添加到size位置
this.arr[size] = value;
// 元素个数加一
this.size++;
// 返回添加元素
return value;
}
// 出栈
public T pop(){
if(isEmpty()){
//没有元素
//抛出运行时异常,此处也可以自定义异常
throw new RuntimeException("栈中没有元素,不能出栈....");
}
// 获得栈顶元素
T value = this.arr[size - 1];
// size - 1 之后, 下一次插入时会覆盖原数据,利用覆盖替代删除
this.size--;
return value;
}
// 获取栈顶元素
public T peek(){
if(isEmpty()){
//没有元素
//抛出运行时异常,此处也可以自定义异常
throw new RuntimeException("栈中没有元素,不能出栈....");
}
return this.arr[this.size - 1];
}
//获取元素个数
public int getSize(){
return this.size;
}
//判断元素是否为空
public boolean isEmpty(){
return this.size == 0;
}
}
二.队列(Queue)
1.概念
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)
入队列:进行插入操作的一端称为队尾(Tail/Rear) 出队列:进行删除操作的一端称为队头(Head/Front)队列是只允许在一端插入,一端删除的数据结构.
生活中的例子 :
- 排队
2.队列的使用
- java标准库中的队列
可以看出,Java中的Queue是一个接口,需要通过实现这个接口的类来实例化对象,
LinkedList实现了
Queue接口,可以通过
LinkedList实例化,如
Queue<Integer> queue = new LinkedList<>();
- 实现的方法
注意 :
Queue中,插入和删除操作都有2个方法可以实现,当我们使用队列时,通常采用
offer(E)添加元素和
poll删除元素,而不使用
add与
remove方法.
方法 | 功能 |
boolean offer(E e) | 入队列 |
E poll() | 出队列 |
peek() | 获取队头元素 |
int size() | 获取队列中有效元素个数 |
boolean isEmpty() | 检测队列是否为空 |
public static void main(String[] args) {
Queue<Integer> q = new LinkedList<>();
//插入元素
q.offer(1);
q.offer(2);
q.offer(3);
q.offer(4);
q.offer(5);
System.out.println("元素个数 : "+q.size()); // 获取元素个数 输出5
System.out.println("获取队头元素 : "+q.peek()); // 获取队头元素,但不删除元素
q.poll();
System.out.println("出队列元素 : "+q.poll()); // 从队头出队列,并将删除的元素返回
if(q.isEmpty()){
System.out.println("队列空");
}else{
System.out.println("元素个数 : "+q.size());
}
}
输出结果 :
3.模拟实现一个队列
要想实现一个队列,也可以采用链表和数组两种存储数据的方式,那么,到底应该用那种方式实现 对于数组来说,入队列和出队列操作都相对简单,但是可能会造成空间大量浪费,如:
当head在3下标时,下标 < 3 的位置无法在利用,
上面的例子中,数组的大小为9,而最后队列中只有6个元素就已经无法插入新的元素,导致浪费大量的空间,而链表因为存储空间不连续,很好的避免了这一问题,出队列时就可以释放资源,解决了内存利用率低的问题.
所以先采用链表的方式,构造一个队列.
1.构造方法
public class MyLinkedListQueue {
// 结点
static class Node {
public int val;
public Node next;
public Node(int val) {
this.val = val;
}
}
public Node head;
public Node last;
public int useSize;
//全部初始化为空
public MyLinkedListQueue(){
head = null;
last = null;
}
}
2.入队列(offer)
由于是链表,所以入队列时不需要考虑扩容问题,代码执行时,动态分配空间.
//入队列
public void offer(int val) {
//构造一个结点
Node tmp = new Node(val);
//当head为空时,说明队列中没有元素,直接让新结点成为头尾结点
if(head == null){
head = tmp;
last = tmp;
}else{
last.next = tmp;
last = tmp
}
//元素个数加1
useSize++;
}
3.出队列(poll)
出队列时需要判断队列是否为空
public int poll() {
//如果队列为空,抛出异常
if(isEmpty()) {
throw new RuntimeException("队列为空");
}
int val = head.val;
// 让队头指向下一个结点
head = head.next;
//如果head 为空了,说明开始只有一个元素,把last也置空
if(head == null){
last = null;
}
useSize--;
return val;
}
4.获取队头元素(peek)
public int peek() {
if(isEmpty()){
throw new RuntimeException("链表为空");
}
return head.val;
}
5.获取元素个数(getSize)
public int size() {
return useSize;
}
6.判断队列是否为空(isEmpty)
public boolean isEmpty(){
return useSize==0;
}
7.完整代码
public class MyLinkedListQueue {
// 结点
static class Node {
public int val;
public Node next;
public Node(int val) {
this.val = val;
}
}
public Node head;
public Node last;
public int useSize;
public MyLinkedListQueue(){
head = null;
last = null;
}
public void offer(int val) {
Node tmp = new Node(val);
if(head == null){
head = tmp;
last = tmp;
}else{
last.next = tmp;
last = tmp;
}
useSize++;
}
public int poll() {
if(isEmpty()) {
throw new RuntimeException("队列为空");
}
int val = head.val;
head = head.next;
if(head == null){
last = null;
}
useSize--;
return 0;
}
public int peek() {
if(isEmpty()){
throw new RuntimeException("链表为空");
}
return head.val;
}
public int size() {
return useSize;
}
public boolean isEmpty(){
return useSize==0;
}
}
8.泛型实现
public class MyLinkedListQueue<T> {
// 结点
static class Node<T> {
public T val;
public Node<T> next;
public Node(T val) {
this.val = val;
}
}
public Node<T> head;
public Node<T> last;
public int useSize;
public MyLinkedListQueue(){
head = null;
last = null;
}
public void offer(T val) {
Node<T> tmp = new Node<>(val);
if(head == null){
head = tmp;
last = tmp;
}else{
tmp.next = head;
head = tmp;
}
useSize++;
}
public T poll() {
if(isEmpty()) {
throw new RuntimeException("队列为空");
}
T val = head.val;
head = head.next;
if(head == null){
last = null;
}
useSize--;
return val;
}
public T peek() {
if(isEmpty()){
throw new RuntimeException("链表为空");
}
return head.val;
}
public int size() {
return useSize;
}
public boolean isEmpty(){
return useSize==0;
}
}
4. 实现一个循环队列
实际中我们有时还会使用一种队列叫循环队列,循环队列通常使用数组实现。
利用数组实现一个队列可能会浪费大量的空间,那么,就可以使用循环队列,也能解决资源浪费的问题.如图所示循环队列,其本质也是一个数组
如图所示 :
当数组小下标有空闲位置时,head一旦==数组长度就会循环从0开始,如上图,
4,5,63个位置为空闲位置
1. 入队列
//入队列
public void offer(int val){
if(size == elem.length){
//可扩容,此处不实现
throw new RuntimeException("队列已满...");
}
elem[rear] = val;
//如果rear到达数组长度,则置0
if(rear + 1 >= elem.length){
rear = 0;
}else {
rear++;
}
size++;
}
2. 出队列
public int poll(){
if(size == 0){
throw new RuntimeException("队列为空...");
}
int val = elem[front];
if(front + 1 >= elem.length){
front = 0;
}else {
front++;
}
size--;
return val;
}
3. 获取队首元素
//获取队首元素
public int peek(){
if(size == 0){
throw new RuntimeException("队列为空...");
}
return elem[front];
}
4. 获取队列中元素个数
//判断元素个数
public int size(){
return size;
}
5.判断队列是否为空
// 判断是否为空
public boolean isEmpty(){
return size == 0;
}
6. 完整代码
public class MyCircularQueue {
private final int[] elem;
private int front; //队头下标
private int rear; //队尾下标
private int size;
public MyCircularQueue() {
elem = new int[24];
}
//入队列
public void offer(int val){
if(size == elem.length){
//可扩容,此处不实现
throw new RuntimeException("队列已满...");
}
elem[rear] = val;
//如果rear到达数组长度,则置0
if(rear + 1 >= elem.length){
rear = 0;
}else {
rear++;
}
size++;
}
public int poll(){
if(size == 0){
throw new RuntimeException("队列为空...");
}
int val = elem[front];
if(front + 1 >= elem.length){
front = 0;
}else {
front++;
}
size--;
return val;
}
//判断元素个数
public int size(){
return size;
}
//获取队首元素
public int peek(){
if(size == 0){
throw new RuntimeException("队列为空...");
}
return elem[front];
}
// 判断是否为空
public boolean isEmpty(){
return size == 0;
}
}
三.相关题目
1. 有效的括号
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。 有效字符串需满足:
左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。
样例1:
输入:s = "()"
输出:true
样例2:
输入:s = "([)]"
输出:false
- 如果传入字符串长度为奇数,直接返回false
- 如果第一个元素为右括号,则直接false
- 当连续两个元素为不同类型的左右括号时,无法再正确完成匹配,返回false ,如"((]"
- 遍历完成字符串时,还需判断栈中元素是否为空,若不为空,返回false
代码 :
public boolean isValid(String s) {
if(s.length() % 2 != 0){
return false;
}
// 创建一个栈,
Stack<Character> stack = new Stack<>();
//循环遍历每一个字符
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
//如果下一个字符为反括号,进入判断
if((ch == ')' || ch == ']' || ch == '}')){
//如果 此时栈中没有元素,反括号在第一个位置,不可能再匹配,返回false
if(stack.isEmpty()){
return false;
}
char c = stack.peek();
//如果前一个元素为对应左括号,则出栈
if(c == '(' && ch == ')' || c == '[' && ch == ']' || c == '{' && ch == '}') {
stack.pop();
}else {
// 否则会出现两种括号交叉出现,无法正确匹配,返回false
return false;
}
//如果下一个字符为左括号,入栈
}else {
stack.push(ch);
}
}
//遍历完成之后判断栈中元素是否为空,如果为空,说明有多余的括号没有匹配完,返回false
return stack.isEmpty();
}
2. 逆波兰表达式求值
根据 逆波兰表示法,求表达式的值。 有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。 注意 两个整数之间的除法只保留整数部分。 可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
样例1:
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
样例2:
输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:该算式转化为常见的中缀算术表达式为:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
- 逆波兰表达式遵循从左到右的运算,所以采用栈来计算
- 如果遇到数字,将数字入栈
- 如果遇到运算符,将栈内出栈2个元素,通过运算符计算后,将计算结果放入栈中.
public int evalRPN(String[] tokens) {
// 创建一个栈
Stack<Integer> stack = new Stack<>();
// 遍历每一个字符串
for (String token : tokens) {
//判断是否为数字,如果是,加入栈中,如果不是,取出2个元素计算后重新加入栈中
if (token.equals("+") || token.equals("-") || token.equals("*") || token.equals("/")) {
int pop1 = stack.pop();
int pop2 = stack.pop();
switch (token) {
case "+":
stack.push(pop2 + pop1);
break;
case "-":
stack.push(pop2 - pop1);
break;
case "*":
stack.push(pop2 * pop1);
break;
case "/":
stack.push(pop2 / pop1);
break;
}
} else {
stack.push(Integer.parseInt(token));
}
}
//计算完毕后,剩下一个元素,即为结果
return stack.pop();
}
3. 出栈入栈次序匹配
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。
- 0<=pushV.length == popV.length <=1000
- -1000<=pushV[i]<=1000
- pushV 的所有数字均不相同
样例1:
输入:[1,2,3,4,5],[4,5,3,2,1]
返回值:true
说明:可以通过push(1)=>push(2)=>push(3)=>push(4)=>pop()=>push(5)=
>pop()=>pop()=>pop()=>pop()
这样的顺序得到[4,5,3,2,1]这个序列,返回true
样例2:
输入:[1,2,3,4,5],[4,3,5,1,2]
返回值:false
说明:
由于是[1,2,3,4,5]的压入顺序,[4,3,5,1,2]的弹出顺序,
要求4,3,5必须在1,2前压入,且1,2不能弹出,但是这样压入的顺序,
1又不能在2之前弹出,所以无法形成的,返回false
- 创建一个辅助栈,和i,j两下标分别记录入栈与出栈序列
- 遍历 入栈数组
- 如果 入栈元素与出栈元素不相等,将入栈元素加入栈中
- 如果 入栈元素与出栈元素相等,出栈下标往后移
- 通过循环,判断栈顶元素与出栈数组是否相等,如果相等,辅助栈出栈,出栈下标加一,如果辅助栈类为空,结束循环
- 如果全部出栈,出栈下标因该等于出栈数组长度
public boolean IsPopOrder(int[] pushA, int[] popA) {
Stack<Integer> stack = new Stack<>();
//记录popA下标
int j = 0;
//遍历 入栈数组
for (int i = 0; i < pushA.length; i++) {
//如果 入栈元素与出栈元素不相等,将入栈元素加入栈中
if (pushA[i] != popA[j]) {
stack.push(pushA[i]);
//如果 入栈元素与出栈元素相等,出栈下标加一
} else {
j++;
//通过循环,判断栈顶元素与出栈数组是否相等,如果相等,辅助栈出栈,出栈下标加一
//如果辅助栈类为空,结束循环
while (!stack.isEmpty() && stack.peek() == popA[j]){
stack.pop();
j++;
}
}
}
// 如果全部出栈,出栈下标因该等于出栈数组长度
return j == popA.length;
}
4. 实现一个最小栈
设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。 实现 MinStack 类:
MinStack() 初始化堆栈对象。 void push(int val) 将元素val推入堆栈。 void pop() 删除堆栈顶部的元素。 int top() 获取堆栈顶部的元素。 int getMin() 获取堆栈中的最小元素。
实例1 :
输入:
["MinStack","push","push","push","getMin","pop","top","getMin"]
[[],[-2],[0],[-3],[],[],[],[]]
输出:
[null,null,null,null,-3,null,0,-2]
解释:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.getMin(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.getMin(); --> 返回 -2.
来源:力扣(LeetCode)
- 当一个元素要入栈时,取当前最小栈的栈顶存储的最小值,与当前元素比较得出最小值,将这个最小值插入最小栈中
- 出栈时,把最小栈的栈顶元素也一并弹出
- 所以在任意一个时刻,栈内元素的最小值就存储在最小栈的栈顶元素中
class MinStack {
Stack<Integer> stack;
//维护一个最小栈
Stack<Integer> minStack;
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
minStack.push(Integer.MAX_VALUE);
}
public void push(int x) {
stack.push(x);
//将最小值加入最小栈中
minStack.push(Math.min(minStack.peek(), x));
}
public void pop() {
stack.pop();
minStack.pop();
}
public int top() {
return stack.peek();
}
public int getMin() {
return minStack.peek();
}
}