算法 - 栈/队列

本文详细介绍了如何使用链表和数组实现栈和队列,包括如何仅使用队列实现栈的功能以及如何仅使用栈实现队列功能。在设计栈和队列时,通过维护辅助数据结构,如双栈或双队列,实现了push、pop和getMin操作的时间复杂度为O(1)。此外,文章还探讨了如何在限制空间和时间复杂度的情况下优化数据结构的设计,确保高效的数据操作。
摘要由CSDN通过智能技术生成


相信大家都清楚,栈是先进后出,队列是先进先出。具体该怎么实现这两种数据结构呢?

我们可以基于双向链表,或者数组实现:

基于链表

基于双向链表也可以实现队列:
在这里插入图片描述

使用链表的方式,可以实现队列的增强,不仅限于队列头添加节点,队尾弹出节点,在队列的任意节点都可以很方便的实现节点的添加和删除,进需要调整下当前节点的pre和next指针。

栈的实现也是一样,只是栈从头部进,从头部弹出,队列需要从头部进,从尾部弹出。两者底层都可以使用链表来实现。
下面我们简单的来看下基于链表怎么实现:
以队列为例,我们需要一个head和一个tail,同时我们需要一个从头部添加数据,和从尾部弹出数据的方法:

@Data
@Builder
public class MyQueue<T> {
    private Node<T> head;
    private Node<T> tail;

    /**
     * 从头部添加数据
     * @param value
     */
    public void push(T value) {
        // 将当前要添加的数据,构建为链表的节点
        Node cur = Node.builder().value(value).build();

        // 队列没有任何数据,将当前节点设置为头结点
        if (this.head == null) {
            this.head = cur;
            this.tail = cur;
        } else {
            // 队列已经有数据
            // 原头结点变成next节点
            cur.setNext(this.head);
            // 设置原头结点的前节点,为当前节点
            this.head.setPre(cur);
            // 则将当前节点置为头节点
            this.head = cur;
        }
    }

    /**
     * 从队尾弹出数据
     * @return
     */
    public T pop() {
        //  尾结点没有数据,没数据可以弹出
        if (this.tail == null) {
            return null;
        }

        // 将原tail的前节点设置为最终的尾结点
        Node cur = tail;
        tail = tail.getPre();
        tail.setNext(null);

        return (T) cur.getValue();

    }
}

基于数组

假设我们现在基于常规数组来实现队列(或者栈):
在这里插入图片描述
以队列为例:
现在有一个大小为5的数组,已经从头部依次放入了数据1,2,3,4。现在我们需要将1进行弹出队列:
在这里插入图片描述
此时如果5,6均需要入队列:
在这里插入图片描述
当5入列之后,发现6已经没有空间了。此时要么移动数组数据,将2,3,4,5依次在数组中向后移动一位:
在这里插入图片描述

要么6不让添加,不过显然这种方式是不可行的。

这样会带来一个问题,如果使用数组作为栈/队列的实现,随着每次的数据弹出,必然需要将已存在的数据进行一次数据迁移,这样做很明显是不理想的。

为此我们可以将数组抽象成一个环形:
在这里插入图片描述
定义两个指针,一个是记录插入位置,一个记录弹出位置。当数组还有空间可以插入数据时,插入数据后,插入指针指向到新插入的后一个位置。当有数据弹出之后,弹出位置的指针,指向弹出数据的前一个数据。这样就可以避免数据的频繁挪动了。

数组平铺开来示意图如下:
在这里插入图片描述

@Data
@Builder
public class MyQueueArray {
    private Object[] arr;
    /**
     * 已经插入了数据的位置
     */
    private int pushIndex;
    /**
     * 已经弹出了数据的位置
     */
    private int popIndex;
    /**
     * 数组中已经存有数据的总大小
     */
    private int size;

    /**
     * 初始化
     *
     * @param limit
     */
    public MyQueueArray(int limit) {
        arr = new Object[limit];
        pushIndex = 0;
        popIndex = 0;
        size = 0;
    }

    public void push(Object value) {
        if (this.size == arr.length) {
            throw new RuntimeException("数据已经存满,不能再添加数据");
        }

        size++;
        arr[pushIndex] = value;
        // 移动插入指针位置
        pushIndex = nextIndex(pushIndex);
    }

    public Object pop() {
        if (size == 0) {
            throw new RuntimeException("无数据可以弹出");
        }

        size--;
        Object obj = arr[popIndex];
        // 移动弹出指针位置
        popIndex = nextIndex(popIndex);
        return obj;
    }

    /**
     * 返回下一个位置
     *
     * @param currentIndex
     * @return
     */
    private int nextIndex(int currentIndex) {
        return currentIndex < arr.length - 1 ? currentIndex + 1 : 0;
    }
}

设计栈(队列),push,pop,getMin时间复杂度为O(1)

分析下这道算法题,要求我们对栈的操作,入栈,出栈,以及获取栈中所有数据的最小值,其时间复杂度均为O(1)。
现在push,pop的操作肯定是O(1),要做的就是将getMin的复杂度也能支持O(1)。
如果我们在getMIn的时候,采用遍历栈内所有元素的方式来实现,肯定是不行,时间复杂度达不到O(1)。在这样的情况下,我们需要借助额外空间来存储栈内最小元素的信息。
在这里插入图片描述
在我们自定义的栈中,需要有两块存储区域,data部分用来存储当前栈元素信息,min部分,只存储当前栈内最小元素信息。
当7入栈时,data内没有数据,min内也没有数据,直接在data和min中压入
当3入栈时,data内压入3,min中栈顶元素是7,此时3<7,在min中将3压入
当2入栈时,data内压入2,min中此时栈顶元素是3,2<3,在min中将2压入
当5入栈时,data内压入5,min中此时栈顶元素是2,5>2,在min中再次将2压入。

也就是说,min中随着data的入栈操作,同步入栈。但是需要经历一步比较,如果data新入栈的数据,比min的栈顶元素要小,得将该数据直接压入min栈,如果data新入栈的元素比min栈顶元素大,则将min的栈顶元素再次压入min栈。

这样,min的栈顶元素,永远是当前data栈中最小的那条数据。如图所示,data中入栈7,3,2,5。此时min的栈顶元素是2,即data栈中最小元素为2。

ok,那现在还有一个问题,一旦data中有数据出栈了怎么办呢?很简单,min栈随着data同步出栈。
data栈中栈顶元素出栈,min中栈顶元素也出栈
在这里插入图片描述
data和min的同步出栈操作,可以保证min的栈顶永远是data中的最小元素。

@Data
@Builder
public class MyQueueArrayTwo {
    private MyQueueArray data;
    private MyQueueArray min;

    public void push(Object value) {
        // data栈中始终入栈
        data.push(value);

        // min中进行比较之后再入栈
        Object minObj = min.peek();
        if (minObj == null) {
            // min栈中还没有元素,直接入栈
            min.push(value);
        }
        // 进行比较
        if (value > minObj) {
            min.push(value);
        } else {
            min.push(minObj);
        }
    }

    public Object pop() {
        // data 和min 同步出栈
        min.pop();
        return data.pop();
    }

    public Object getMin() {
        // 直接返回当前min的栈顶元素
        return min.peek();
    }
}

在此种方案中,min中存储的数据量和data是一致的。还有另一种,min中只存储最小值:

在这里插入图片描述

data中7入栈,min中7也入栈
data中3入栈,min中栈顶元素7,由于3<=7 ,所以min中3也入栈
data中2入栈,min中栈顶元素3,由于2<=3,所以min中2也入栈
data中2入栈,min中栈顶元素2,由于2<=2,所以min中2也入栈
data中5入栈,min中栈顶元素2,由于5>2,此时5不再入栈
也就说说,当data中新入栈的数据,小于或者等于 min的栈顶元素时,min中才进行入栈操作。

在此方案下,data中数据出栈时,与前面方案会稍有不同。
data中5出栈,此时min的栈顶是2,说明5并不是data中最小的元素,min中无元素出栈
data中2出栈,此时min的栈顶是2,说明2已结是data中的最小元素,data中最小元素已结出栈,那min中的元素也需要同步出栈
data中2出栈,此时min的栈顶是2,说明2已结是data中的最小元素,data中最小元素已结出栈,那min中的元素也需要同步出栈

也就是,当data中出栈的元素,等于min的栈顶元素时,min栈顶元素才出栈:
在这里插入图片描述
在此方案下,min栈中会存储相应较少的数据,节省空间

如何仅使用队列实现栈的功能

如何仅使用队列实现栈的功能呢?
比如数据入列顺序是1,2,3,4,5,6,那数据的出队顺序也是1,2,3,4,5,6
如何让调用端收到的数据顺序是6,5,4,3,2,1呢?
在这里插入图片描述
此时我们对外提供的栈MyStack中,底层实现只能是队列。

我们可以在MyStack中定义两个队列,queue1和queue2。
当执行入栈操作时,仅操作queue1:
当调用端将数据1,2,3进行入栈,实际底层是将1,2,3压入队列queue1。此时queue1中有数据1,2,3,其出队列的顺序也是1,2,3。queue2中无数据。
当调用端进行出栈pop操作时,预期弹出数据3,在MySatack的pop方法中:
1. queue1先进行一次出队列操作,数据为1,将数据1入队列queue2,此时queue1还有数据2,3;queue2中有数据1
2. queue1再进行出队列操作,数据为2,将数据2入队列queue2,此时queue1还有数据3,queue2中有数据1,2
3. queue1再进行出队列操作,数据为3,直接返回给客户端。
当调用端进行出栈pop操作时,预期弹出数据2:
此时queue1中已经没有数据,queue2中有数据1,2。
方法和前面类似,只是queue1和queue2的角色进行对换
1. queue2先进行一次出队列操作,数据为1,将数据1入队列queue1,此时queue2还有数据2;queue2中有数据2
2. queue1再进行出队列操作,数据为2,直接返回给客户端。

算法核心:在MyStack内部,不是直接将queue的出队列结果返回给调用端,而是先将queue中前面的所有数据进行出队列,然后将这些数据暂存在一个辅助队列中,queue中仅存的最后一条数据出队列返回给调用端,模拟出栈先进后出的效果。

如何仅使用栈实现队列功能

和仅使用队列实现栈类型,在MyQueue中需要定义两个stack,在push stack出栈时,需要先将数据入栈到pop stack,push stack仅返回最后一次的出栈结果给调用端。
在这里插入图片描述
但是在此方案下,需要注意一点:
调用端先将1,2,3push如队列,然后pop出一条数据。按照我们的算法逻辑,此时push stack先开始入栈1,2,3,然后将3,2出栈,再将3,2入栈pop stack,push stack再次将1进行出栈。走完流程之后,push stack里面没有数据了,pop stack里面还有数据3,2。

此时调用端再将4进行push如队列,然后再次调用pop进行出队列。这个过程我们来分析下,如果我们对push stack -> pop stack不加限制等的话,其执行过程将会是这样:
1. 调用端push 4,5
2. push stack 先将4,5入栈,此时push stack中有数据4,5
3. 调用端pop
4. push stack 将5出栈放入pop stack,然后再将4进行出栈返回调用端。此时pop stack中有数据1,2,5。

调用端的执行过程是:push 1,2,3, pop
push 4,5,pop
其第一次pop的返回值应该是1,这个没有问题,而第二次pop的返回值应该是2,而我们返回的是4。原因是我们在pop stack还有数据的情况下,又手动往里面添加了数据,这样其实就打破了前一次数据迁移后的数据顺序。
在这里插入图片描述
当调用端第二次pop时,如果我们的pop stack中有数据,应该直接从pop stack中弹出数据返回,依次弹出数据3,2。
当调用端再次pop时,pop stack已经没有数据了,此时又需要将push stack中的5出栈,然后压入pop stack ,重复执行前面的步骤。

所以当我们需要将push stack中的数据压入pop stack时,需要遵循两个原则:

  1. 只有pop stack里面没有数据的时候,才能进行入栈操作,这样可以保证已经入栈了的数据,顺序不会错乱
  2. 一旦发生从push stack 入栈到pop stack,一定要将数据全部压入pop stack,这样保证本次的入栈数据,顺序不会错乱。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值