上篇文章介绍了数据结构 栈,这篇文章我们介绍队列,以及队列和栈的相互转换。
队列是什么?
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,和栈不同的是,栈是后进先出,队列是先进先出。
实现队列
还是以 Java Queue 的 api 为例,实现一个队列
另加一个 size 方法:
class MyQueue<E> {
list: E[] = [];
offer(e: E) {
this.list.push(e);
}
peek(): E {
return this.list[0];
}
poll(): E | undefined {
return this.list.shift();
}
size(): number {
return this.list.length;
}
}
队列题目实践
这是一个队列实践入门题,先把所有 t 都放入队列中,之后输出时需要判断下如果在 3000 之外,那么就要从队头删除掉:
class RecentCounter {
queue:MyQueue<number>;
constructor() {
this.queue=new MyQueue()
}
ping(t: number): number {
this.queue.offer(t)
while(t-this.queue.peek()>3000){
this.queue.poll()
}
return this.queue.size()
}
}
既然栈和队列我们都了解了,那么下面看下他们两个之间的相互实现。
队列实现栈
题目提示:使用两个队列实现栈。
- push:先把 item offer 到 q2 中,之后如果 q1 非空,再把 q1 的内容都拿出来放到 q2 的后面,这时 q2 里面的内容就是后进先出的了,因为最后进的 item 是最开头的,最后再把 q1 和 q2 交换一下
- pop:q1 直接 poll
- top:q1 直接 peek
- empty:查看 q1.size
class MyStack {
q1:MyQueue<number>
q2:MyQueue<number>
constructor() {
this.q1 = new MyQueue()
this.q2 = new MyQueue()
}
push(x: number): void {
this.q2.offer(x)
while(this.q1.size()){
this.q2.offer(this.q1.poll() as number)
}
let tmp = new MyQueue<number>()
tmp=this.q1
this.q1=this.q2
this.q2=tmp
}
pop(): number {
return this.q1.poll() as number
}
top(): number {
return this.q1.peek()
}
empty(): boolean {
return !this.q1.size()
}
}
题外话,TypeScript 的类型系统没有那么智能,如下代码:
while(this.q1.size()){
this.q2.offer(this.q1.poll() as number)
}
这里必须要写 as number,否则类型系统报错,但明眼人一看就知道,我先是判断了 q1.size,如果非空才 poll,那么就一定是 number 类型而不是 undefined,但 TypeScript 没有那么智能,我们需要手动声明类型。
还有更节省空间的方法,使用一个队列实现栈:
class MyStack {
q1:MyQueue<number>
constructor() {
this.q1 = new MyQueue()
}
push(x: number): void {
let n=this.q1.size()
this.q1.offer(x)
while(n--){
this.q1.offer(this.q1.poll() as number)
}
this.q1.offer(x)
}
pop(): number {
return this.q1.poll() as number
}
top(): number {
return this.q1.peek()
}
empty(): boolean {
return !this.q1.size()
}
}
这个就是 push 的时候,先把 item 插入到队尾,再把队列之前的内容拿出来,重新放到队尾,这样就保证 item 是后进的,但是会先出来,由于每次 push 都执行这样的操作,所以 q1 队列所有元素都保持后进先出。但请注意,这里 push 的复杂度是 O(n),并不能像真正的栈一样做到 O(1)。我认为一个队列的解法要比两个队列简单 😂
栈实现队列
首先复习一下 数据结构 栈 中的实现:
class MyStack<E> {
list: E[] = [];
push(item: E) {
this.list.push(item);
}
pop(): E | undefined {
return this.list.pop();
}
peek(): E {
return this.list[this.list.length - 1];
}
empty(): boolean {
return this.list.length === 0;
}
}
想要后进先出的栈模拟先进先出的队列,那么就要有一个中间栈 s2,正常队列插入,那么 s2 也插入,当需要 pop 或 peek 时,再把 s2 的元素取出,放到 s1 这个新栈中,这时 s1 从栈顶到栈底,就是原数组先进先出的顺序(有点负负得正的感觉)。注意:只有 s1 为空的时候才执行把 s2 的元素取出放到 s1 中,不为空的话就不执行,因为如果不为空还执行了,那么就会导致顺序错乱,你可能还会问,那 s1 有元素并且 s2 也有元素的时候,不会出错吗?不会出错,因为 s1 有元素时,就消耗 s1 的元素,直到消耗完了,再从 s2 取过来,因为队列是先进先出的,先进的元素都在 s1 中,所以不会出现 s1 的没有消耗完就需要 s2 的情况。
class MyQueue {
s1: MyStack<number>
s2: MyStack<number>
constructor() {
this.s1=new MyStack()
this.s2=new MyStack()
}
push(x: number): void {
this.s2.push(x)
}
pop(): number {
if(this.s1.empty()){
while(!this.s2.empty()){
this.s1.push(this.s2.pop())
}
}
return this.s1.pop()
}
peek(): number {
if(this.s1.empty()){
while(!this.s2.empty()){
this.s1.push(this.s2.pop())
}
}
return this.s1.peek()
}
empty(): boolean {
return this.s1.empty() && this.s2.empty()
}
}