栈(stack)
栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被称为栈顶,相对地,把另一端称为栈底。
压栈:向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;
出栈:从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
栈的工作原理:后进先出
操作:new(新建)push(入栈)pop(出栈)peek(获取栈顶元素)isEmpty(判断栈是否为空)
public static void main(String[] args) {
Stack<Integer> s = new Stack();
s.push(1);
s.push(2);
s.push(3);
s.push(4);
System.out.println(s.size()); // 获取栈中有效元素个数---> 4
System.out.println(s.peek()); // 获取栈顶元素---> 4
s.pop(); // 4出栈,栈中剩余1 2 3,栈顶元素为3
System.out.println(s.pop()); // 3出栈,栈中剩余1 2 栈顶元素为3
if(s.empty()){
System.out.println("栈空");
}else{
System.out.println(s.size());
}
}
队列(Queue)
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。
队列的使用
***在Java中,Queue是个接口,底层是通过链表实现的。***
注意: Queue 是个接口,在实例化时必须实例化 LinkedList 的对象,因为 LinkedList 实现了 Queue 接口。
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());
System.out.println(q.peek()); // 获取队头元素
q.poll();
System.out.println(q.poll()); // 从队头出队列,并将删除的元素返回
if(q.isEmpty()){
System.out.println("队列空");
}else{
System.out.println(q.size());
}
}
232.用栈实现队列
使用栈实现队列的下列操作:
push(x) -- 将一个元素放入队列的尾部。
pop() -- 从队列首部移除元素。
peek() -- 返回队列首部的元素。
empty() -- 返回队列是否为空。
思路:
这是一道模拟题,不涉及到具体算法,考察的就是对栈和队列的掌握程度。
使用栈来模式队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈一个输入栈,一个输出栈,这里要注意输入栈和输出栈的关系。
在push数据的时候,只要数据放进输入栈就好,
但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入),再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。
最后如何判断队列为空呢?如果进栈和出栈都为空的话,说明模拟的队列为空了。
在代码实现的时候,会发现pop() 和 peek()两个函数功能类似,代码实现上也是类似的,可以思考一下如何把代码抽象一下。
class MyQueue {
//声明一个名为stackIn的整型栈(用于入队操作),全局变量,只能放MyQueue外面
Stack<Integer> stackIn;
Stack<Integer> stackOut;
public MyQueue() {
//初始化队列
stackIn = new Stack<>();//初始化
stackOut = new Stack<>();//同上,用于出队
}
//将元素 x 入栈到 stackIn 中,实现了队列的入队操作
public void push(int x) {
stackIn.push(x);
}
//定义了一个出队方法 pop,用于移除队列头部的元素并返回。
public int pop() {
dumpstackIn();
return stackOut.pop();//这时输出栈弹出来的,就是队列出队的头部元素
}
//定义了一个查看队列头部元素的方法 peek,不移除元素
//这里peek()的实现,直接复用了pop(),要不然,对stOut判空的逻辑又要重写一遍。技巧!效率!
public int peek() {
dumpstackIn(); //同pop过程,调用抽象出来的
return stackOut.peek();
}
public boolean empty() {
return stackIn.isEmpty() && stackOut.isEmpty();//stackIn和stackOut都为空
}
// 如果stackOut为空,那么将stackIn中的元素全部放到stackOut中
public void dumpstackIn(){
if(!stackOut.isEmpty()) return; //if stackOut不为空(为真)
//否则,就是stackOut为空,需要将stackIn中的元素倒入stackOut
while(!stackIn.isEmpty()){
stackOut.push(stackIn.pop());//stackIn中的元素一直弹空为止
}
}
}
/**
* 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();
*/
- 时间复杂度: push和empty为O(1), pop和peek为O(n)
- 空间复杂度: O(n)
扩展:
可以看出peek()的实现,直接复用了pop(), 要不然,对stOut判空的逻辑又要重写一遍。
再多说一些代码开发上的习惯问题,在工业级别代码开发中,最忌讳的就是 实现一个类似的函数,直接把代码粘过来改一改就完事了。
这样的项目代码会越来越乱,一定要懂得复用,功能相近的函数要抽象出来,不要大量的复制粘贴,很容易出问题!(踩过坑的人自然懂)
225. 用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。
实现 MyStack
类:
void push(int x)
将元素 x 压入栈顶。int pop()
移除并返回栈顶元素。int top()
返回栈顶元素。boolean empty()
如果栈是空的,返回true
;否则,返回false
。
思路:
队列模拟栈,其实一个队列就够了,那么我们先说一说两个队列来实现栈的思路。
队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的顺序。
所以用栈实现队列, 和用队列实现栈的思路还是不一样的,这取决于这两个数据结构的性质。
但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用来备份的!
如下面动画所示,用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。
import java.util.LinkedList;
import java.util.Queue;
class MyStack {
// 定义两个队列que1和que2
Queue<Integer> que1;
Queue<Integer> que2; //备份队列
//定义了MyStack类的构造方法,在这个方法里进行了初始化工作。
public MyStack() {
// 初始化两个队列
que1 = new LinkedList<>(); //java中,Queue是个接口,底层是通过链表实现的
que2 = new LinkedList<>(); //初始化 queue2 队列,使用了 LinkedList 类来实现
}
//定义了一个入栈方法 push,参数是要入栈的元素 x
public void push(int x) {
// 将元素x推入que1队列
que1.offer(x);
}
//定义了一个出栈方法pop,用于移除栈顶的元素并返回
public int pop() {
int size = que1.size();
size--; // 减去1,因为我们要留最后一个元素在que1中
// 将que1中的元素依次移到que2中,但要留下最后一个元素
while (size-- > 0) {
que2.offer(que1.poll());
}
// 留下的最后一个元素就是我们要返回的元素
int result = que1.poll();
// 将que2的元素移回que1
while (!que2.isEmpty()) {
que1.offer(que2.poll());
}
// 返回结果
return result;
}
//定义了一个查看栈顶元素的方法 top,不移除元素。
public int top() {
int size = que1.size();
size--; //size-1,保留队列末尾的值
// 将que1中的元素依次移到que2中,但要留下最后一个元素
while (size-- > 0) {
que2.offer(que1.poll());
}
// 留下的最后一个元素就是栈顶元素
int result = que1.peek();
// 把最后一个元素也移到que2中
que2.offer(que1.poll());
// 将que2的元素移回que1
while (!que2.isEmpty()) {
que1.offer(que2.poll());
}
// 返回栈顶元素
return result;
}
//栈 判空
public boolean empty() {
// 检查que1是否为空
return que1.isEmpty();
}
}
------ 其实这道题目就是用一个队列就够了。
一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了。
class MyStack {
Queue<Integer> queue; //声明了一个名为 queue 的整型队列。(全局变量)
public MyStack() { //定义了MyStack类的构造方法,在这个方法里进行了初始化工作。
queue = new LinkedList<>();//初始化 queue 队列,使用了LinkedList类来实现。
}
//定义了一个入栈方法 push,参数是要入栈的元素 x。
public void push(int x) {
queue.add(x);//将元素 x 入队到 queue 中,实现了栈的入栈操作。
}
//定义了一个出栈方法 pop,用于移除栈顶的元素并返回。
public int pop() {
rePosition();//调用了rePosition方法,将队列中的元素重新排列,以实现栈的出栈操作。
return queue.poll(); //从队列中移除并返回队列的头部元素,实现了栈的出栈操作。
}
//定义了一个查看栈顶元素的方法 top,不移除元素。
public int top() {
rePosition();//同样调用了 rePosition 方法,确保队列中的元素重新排列(即现在尾变头了)
int result = queue.poll(); //从队列中移除并记录栈顶的元素 记为result
queue.add(result); // 将记录的栈顶元素重新入队到队列中
return result; //返回记录的栈顶元素,实现了栈的取栈顶元素的操作
}
public boolean empty() {
return queue.isEmpty();
}
//定义了一个重新排列队列元素的方法 rePosition
public void rePosition(){
int size = queue.size(); // 获取队列的大小
size --;
//将队列的大小减一,因为栈是后进先出的结构,所以队列的最后一个元素需要排在队列的头部
//这么做可以保留最后一个元素
while(size-- > 0){ //循环直到队列的大小减为 0
queue.add(queue.poll());//将队列中的元素依次取出并重新加入队列,直到最后一个元素移至队列头部
}
}
}
/**
* Your MyStack object will be instantiated and called as such:
* MyStack obj = new MyStack();
* obj.push(x);
* int param_2 = obj.pop();
* int param_3 = obj.top();
* boolean param_4 = obj.empty();
*/