目录
一:栈
1.栈的定义
2.栈的模拟实现
3.栈的使用
二:队列
1.队列的定义
2.队列的模拟实现
3.循环队列
3.1循环队列的引入
3.2循环队列的实现
三:面试题
3.1用队列实现栈
3.2用栈实现队列
3.3实现最小栈
栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
- 压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
- 出栈:栈的删除操作叫做出栈。出数据在栈顶。
从上图中可以看到,Stack继承了Vector。Vector和ArrayList类似,都是动态的顺序表,不同的是Vector是线程安全的。值得注意的是,现如今Vector几乎已经被淘汰,所以我们只介绍Stack。
我们先简单看一下java中封装好的栈Stack。它的操作很简单,我们可以根据下表所示的功能,进行栈的模拟实现。
我们只需用一个数组存储栈中的所有元素,为了使栈可以存放任意类型的数据元素,我们在此使用泛型。栈的模拟实现比较简单,在此仅将代码展示如下:
MyStack.java
package MyStack;
import java.util.Arrays;
public class MyStack {
public E[] elem;
public int usedSize;
public MyStack() {
this.elem = (E[])new Object[2];
}
/**
* 入栈
* @param val
*/
public void push(E val) {
if(isFull()){
this.elem = Arrays.copyOf(this.elem,2*elem.length);
}
elem[usedSize] = val;
usedSize++;
}
/**
* 判断当前栈是否为满
* @return
*/
public boolean isFull() {
return usedSize == elem.length;
}
/**
* 出栈
* @return
*/
public E pop() {
if(empty()){
System.out.println("当前栈已空!");
return null;
} else {
E tmp = elem[usedSize-1];
elem[usedSize-1] = null;
usedSize--;
return tmp;
}
}
public boolean empty() {
return usedSize==0;
}
/**
* 获取栈顶元素 不删除!
* @return
*/
public E peek() {
if(empty()){
System.out.println("当前栈已空!");
return null;
} else {
E tmp = elem[usedSize-1];
return tmp;
}
}
/**
* 获取大小
* @return
*/
public int getUsedSize() {
return usedSize;
}
}
Test.java
package MyStack;
public class Test {
public static void main(String[] args) {
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.push(3);
myStack.push(4);
myStack.push(5);
int tmp = (int) myStack.pop();
System.out.println(tmp);
int tmp2 = (int)myStack.pop();
System.out.println(tmp2);
int tmp3 = (int)myStack.peek();
System.out.println(tmp3);
int tmp4 = (int)myStack.pop();
System.out.println(tmp4);
}
}
掌握了栈的模拟实现,栈的使用自然不在话下了。示例代码如下:
public static void main(String[] args) {
Stack myStack = new Stack();
myStack.push(1);
myStack.push(2);
myStack.push(3);
myStack.push(4);
myStack.push(5);
int tmp = (int) myStack.pop();
System.out.println(tmp);
int tmp2 = (int)myStack.pop();
System.out.println(tmp2);
int tmp3 = (int)myStack.peek();
System.out.println(tmp3);
int tmp4 = (int)myStack.pop();
System.out.println(tmp4);
int size = myStack.size();
System.out.println("栈中元素个数为"+size);
boolean isempty = myStack.empty();
if(isempty == false){
System.out.println("当前栈不为空!");
}else{
System.out.println("当前栈为空!");
}
}
运行结果如下:
队列是只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,具有先进先出FIFO(FirstIn First Out)的特点。
- 入队列:进行插入操作的一端称为队尾(Tail/Rear)
- 出队列:进行删除操作的一端称为队头(Head/Front)
在Java中,Queue是个接口,底层是通过链表实现的。
我们先简单看一下Java中封装好的队列Queue。它的操作很简单,我们可以根据下表所示的功能,进行队列的模拟实现。需要注意的是,队列底层是通过链表实现的。
MyLInkedList.java
package MyQueue;
import java.util.Queue;
public class MyLinkedList {
class Node {
public int val;
public Node next;
public Node(int val) {
this.val = val;
}
}
public int usedSize;
public Node head;
public Node last;
/**
* 入队
* @param val
*/
public void offer(int val) {
Node node = new Node(val);
if(head == null){
head = node;
last = node;
} else {
last.next = node;
last = node;
}
usedSize++;
}
/**
* 出队
* @return
*/
public int poll() {
if(isEmpty()){
throw new RuntimeException("当前队列为空!");
}else{
int ret = head.val;
head = head.next;
usedSize--;
return ret;
}
}
/**
* 出队 但是不删除
* @return
*/
public int peek() {
if(isEmpty()){
throw new RuntimeException("当前队列为空!");
}else{
int ret = head.val;
return ret;
}
}
public int size() {
return usedSize;
}
public boolean isEmpty() {
return usedSize == 0;
}
}
Test.java
package MyQueue;
public class Test {
public static void main(String[] args) {
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.offer(12);
myLinkedList.offer(23);
myLinkedList.offer(34);
myLinkedList.offer(45);
myLinkedList.offer(56);
System.out.println(myLinkedList.poll());
System.out.println(myLinkedList.poll());
System.out.println(myLinkedList.peek());
System.out.println("当前队列的长度为:"+myLinkedList.size());
}
}
运行结果如下:
现在我们来思考一个问题。既然栈的实现使用了数组,那为什么队列的实现没有使用数组,而是使用了链表呢?如下图所示为一个数组,假设我们用它来实现队列,那么,你是否能知道队头和队尾分别在哪呢?
显然,随着队列中元素的入队和出队,队头和队尾也在不断发生变化。
显然,如果使用数组实现队列,会造成极大的空间浪费。那么如何才能解决这一问题呢?答案是:把这个数组“卷”起来;当队尾指针来到数组最末端时,不让其指向null,而是指向数组中的第一个元素。这就是我们接下来要谈的“循环队列”。
最终,队列就变成了这个样子:
一开始,head == last,说明该循环队列为空。
各就位,现在开始入队:
Full or not full, this is a question。当循环队列中元素已满时,head == null也成立,那么当这一条件成立时,如何判断当前队列是满还是空呢?以下给出三种方法:
1.通过添加usedSize属性记录;
2.使用标记;
3.保留一个位置。
前两种方法,我们容易理解。添加usedSize属性,判断其等于0还是与数组大小相等即可;使用标记,通过判断last和head的相遇时机,调整标记状态即可。最后一种方法的意思是,当数组中仅有一个位置为空时,我们就认为该数组已满。图解如下:
这里还有一点小问题,那就是在判断队列为满时,last+1=7+1=8,而此时head=0,这二者并不相等。为使数组下标也能循环起来,需要做出这样的调整:
index = (index + 1) % array.length。
保留一个位置表示满时:
class MyCircularQueue {
private int[] elem;
private int front;//表示队头下标
private int rear;//表示队尾下标
public MyCircularQueue(int k) {
elem = new int[k];
}
//入队
public boolean enQueue(int value) {
if (!isFull()) {
elem[rear] = value;
rear = (rear + 1) % (elem.length);
return true;
}
return false;
}
//出队
public boolean deQueue() {
if (!isEmpty()) {
front = (front + 1) % elem.length;
return true;
}
return false;
}
//获取队头元素
public int Front() {
if(isEmpty()){
return -1;
}
return elem[front];
}
//获取队尾元素
public int Rear() {
if(isEmpty()){
return -1;
}
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;
}
}
用usedSize做标记时:
class MyCircularQueue {
private int[] elem;
private int front;//表示队头下标
private int rear;//表示队尾下标
private int usedSize;
public MyCircularQueue(int k) {
elem = new int[k];
usedSize = 0;
}
//入队
public boolean enQueue(int value) {
if (!isFull()) {
elem[rear] = value;
rear = (rear + 1) % (elem.length);
usedSize++;
return true;
}
return false;
}
//出队
public boolean deQueue() {
if (!isEmpty()) {
front = (front + 1) % elem.length;
usedSize--;
return true;
}
return false;
}
//获取队头元素
public int Front() {
if(isEmpty()){
return -1;
}
return elem[front];
}
//获取队尾元素
public int Rear() {
if(isEmpty()){
return -1;
}
int index =(rear ==0 ) ? elem.length-1 : rear-1;
return elem[index];
}
public boolean isEmpty() {
return usedSize == 0;
}
public boolean isFull() {
return usedSize == elem.length;
}
}
链接:
力扣https://leetcode.cn/problems/implement-stack-using-queues/
1.题目:请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(
push
、top
、pop
和empty
)。
2.解题
一个队列,只能实现先进先出,没法实现栈,所以至少需要两个队列。在入队时,永远入到不为空的队列;在出队时,从有元素的队列弹出size-1个元素,放到空的那个队列中,然后再将最后一个元素出队。图解如下:
具体代码如下:
class MyStack {
private Queue qu1;
private Queue qu2;
public MyStack() {
qu1 = new LinkedList<>();
qu2 = new LinkedList<>();
}
//入到不为空队列中
//都为空,入到qu1
public void push(int x) {
if(!qu1.isEmpty()){
qu1.offer(x);
}else if(!qu2.isEmpty()){
qu2.offer(x);
}else{
qu1.offer(x);
}
}
public int pop() {
if(empty()){
return -1;
}
if(!qu1.isEmpty()){
int size = qu1.size();
for(int i = 0;i
要解决这个问题,一定要熟悉队列的操作方法。
链接:
力扣https://leetcode.cn/problems/implement-queue-using-stacks/
1.题目:请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(
push
、pop
、peek
、empty
)。
2.解题
用两个栈实现队列,基本思路是:入的时候,都入到一个栈中;出的时候,都出第二个栈中的元素。如果第二个栈中没有元素了,就把第一个栈中的元素全部倒过来。图解如下:
具体代码如下:
class MyQueue {
Stack s1;
Stack s2;
public MyQueue() {
s1= new Stack<>();
s2= new Stack<>();
}
public void push(int x) {
s1.push(x);
}
public int pop() {
if(empty()){
return -1;
}
if(s2.isEmpty()){
while(!s1.empty()){
s2.push(s1.pop());
}
}
return s2.pop();
}
public int peek() {
if(empty()){
return -1;
}
if(s2.isEmpty()){
while(!s1.empty()){
s2.push(s1.pop());
}
}
return s2.peek();
}
public boolean empty() {
return s1.empty()&&s2.empty();
}
}
/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue obj = new MyQueue();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.peek();
* boolean param_4 = obj.empty();
*/
链接:
力扣https://leetcode.cn/problems/min-stack/
1.题目:设计一个支持
push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
2.解题:
首先明确什么是最小栈。要求设计一个栈,除提供最基础的push,pop
,top
操作外,还能在O(1)的时间复杂度内检索到栈中的最小元素。
思路是:使用两个栈,其中一个s为当前栈,另一个minStack栈,用于保存当前栈中的最小元素。有出栈和入栈两种情况:
当入栈时,比较该元素与minStack栈顶元素的大小,如果该元素小于或等于minStack的栈顶元素,该元素入minStack栈中。这一操作可以保证minStack栈顶元素始终为s栈中最小的元素。
当有元素出栈时,也要比较该元素与minStack栈顶元素的大小,如果二者相等,则minStack的栈顶元素也要出栈,否则不对minStack进行操作。
具体代码如下:
class MinStack {
private Stack s1;
private Stack minStack;
public MinStack() {
s1 = new Stack<>();
minStack = new Stack<>();
}
/**
s1这个栈 一定要放元素的
*/
public void push(int val) {
s1.push(val);
if(minStack.empty()) {
minStack.push(val);
}else{
int x = minStack.peek();
//这里能不能取等号???
if(val <= x) {
minStack.push(val);
}
}
}
public void pop() {
int x = s1.pop();
int x2 = minStack.peek();
if(x == x2) {
minStack.pop();
}
}
//获取当前的栈顶元素不删除 ,不是最小栈的栈顶元素
public int top() {
return s1.peek();
}
public int getMin() {
return minStack.peek();
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(val);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.getMin();
*/
本课主要讲解了栈和队列,了解了栈和队列的模拟实现后,关键是灵活使用java集合类中封装好的栈和队列,尤其注意出栈和入栈使用的是push()和pop()操作,出队和入队使用的是offer()和pull()操作;在栈中,判空使用的是empty()方法;在队列中,判空使用的是isEmpty()方法。而获取栈或队列的长度以及获取栈顶元素(队头元素),其方法名是一致的,为size()和peek()。
最后,通过用两个栈实现队列,用两个队列实现栈,进一步加深了我们对栈和队列的理解。
本课内容结束!