队列实现栈以及栈实现队列

100 阅读4分钟

image.png

上篇文章介绍了数据结构 栈,这篇文章我们介绍队列,以及队列和栈的相互转换。

队列是什么?

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端进行删除操作,而在表的后端进行插入操作,和栈不同的是,栈是后进先出,队列是先进先出。

实现队列

还是以 Java Queue 的 api 为例,实现一个队列

image.png

另加一个 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;
    }
}

队列题目实践

933.  最近的请求次数

image.png

image.png

这是一个队列实践入门题,先把所有 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()
    }
}

既然栈和队列我们都了解了,那么下面看下他们两个之间的相互实现。

队列实现栈

225. 用队列实现栈

image.png

题目提示:使用两个队列实现栈。

  • 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)。我认为一个队列的解法要比两个队列简单 😂

栈实现队列

232. 用栈实现队列

image.png

首先复习一下 数据结构 栈 中的实现:

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()
    }
}

参考:队列实现栈以及栈实现队列LeetCode 题解