算法之美:栈和队列

本文由玉刚说写做平台提供写做赞助java

原做者:像一只狗node

版权声明:本文版权归微信公众号玉刚说全部,未经许可,不得以任何形式转载程序员

算法,一门既不容易入门,也不容易精通的学问。面试

对于笔者来讲算法算是我程序员生涯很不擅长的技能之一了,自从互联网界招人进入平静期后,愈来愈多的大厂在社招的时候不但会考验面试者的工做所用到的技能,并且会用算法题来考验面试者的逻辑思惟能力和基本数据结构的掌握能力。这也就让想要社招进入大厂的部分同窗有了一些望而却步的心理,毕竟工做中大部分时间在与UI层面的逻辑打交道,数据处理方面及时以前在学校中掌握的还不作,几年的CV生活,估计也忘的差很少了。算法

可是做为一条有梦想的咸鱼,咱们仍是要重拾这些知识的。今天笔者将会挑选几道栈与队列的题目来回顾下相关算法的基本知识。编程

栈与队列分别是两种数据结构,不一样语言对于栈和队列有着不一样的声明,在 java 中 Stack 类是继承自 Vector 集合的子类,Queue 则是以接口形式存在,经常使用的其实现类是 LinkedList 这个双向队列。在C++的标准模版库也是有这两个数据结构定义的具体类的。数组

栈数据结构的特色是 FILO(first in last out) 即先进后出,队列则是 FIFO(first in first out)即先进先出。相信栈与队列的数据结构的基本特色你们也是熟记于胸了。下面就带你们看一道面试题来带你们看下这二者在面试题中的形式。bash

由两个栈实现一个队列 (✭✭✩✩✩)

题目难度两颗星,主要考察了对于栈和队列的数据结构特色。微信

前文介绍了,对于一个栈来讲遵循 pop 操做时从栈的顶部取一个元素,对于队列来讲 poll 操做时从队列队首取一个元素。因此该题翻译过来就是使用两个栈定义一种先放入的元素,最早被取出的数据结构。数据结构

此题应考虑到两种状况,首先最简单的一种状况,假设有 1,2,3,4,5 个元素依次进入自定义的队列,再依次取出。因为是进栈操做都进行完了才进行出栈操做,因此咱们只需在元素出队时,将进栈元素倒入另外一个空栈中便可。示意图以下:

再一种状况是,若是 add poll 操做是交替进行的,那么如何保证数据结构先进先出的定义呢?好比先放入 1,2,3而后要进行一次取出操做取出 1,随后在进行 add 操做放入4,5,这种状况下如何操做两个栈,才能保证以后再取出的时候元素为 2,3,4,5 顺序?实际上咱们只须要保证一下两点就能够:

  1. 不管若是 StackA(最开始add元素的那个栈) 要往 StackB 中压入元素,那么必须选择一次性所有压入。
  2. 不管何时从队列中取元素,必须保证元素是从 StackB 中 pop 出的,也就是说,当 StackB 不为空的时候毫不能再次向 StackB 中压入元素。

为了方便理解能够看下边这幅图:

明白了须要注意的点后就是该写代码的时候了,须要注意的点在图中已经用红色字体标出了,也就是在存入元素一直往 StackA 中存,取元素是从 StackB 中取,但要要注意的是取的时候须要保证 StackB 为空的时候要先将 StackA 中元素一次性压如 StackB 中,在进行从 StackB 中取的操做。

public static class TwoStackQueue<E>{
        private Stack<E> stackA;
        private Stack<E> stackB;

        public TwoStackQueue() {
            stackA = new Stack<>();
            stackB = new Stack<>();
        }

        /** * 添加元素逻辑 * @param e 要添加的元素 * @return 这里只是遵循 Queue 的习惯,这里简单处理返回 true 便可 */
        public boolean add(E e){
            stackA.push(e);
            return true;
        }

        /** * 去除元素的时候须要判断两个地方,StackA & StackB 是否都为空 * StackB 为空的时候讲StackA中的元素所有依次压入 StackB * @return 返回队列中的元素 若是队列为空返回 null */
        public E poll(){
            //若是队列中没有元素则直接返回空,也能够选择抛出异常
            if (stackB.isEmpty() && stackA.isEmpty()){
                return null;
            }
            
            if (stackB.isEmpty()){
                while (!stackA.isEmpty()){
                    stackB.add(stackA.pop());
                }
            }
            
            return stackB.pop();
        }

        /** * peek 操做不取出元素,只返回队列头部的元素值 * @return 队列头部的元素值 */
        public E peek(){
            //若是队列中没有元素则直接返回空,也能够选择抛出异常
            if (stackB.isEmpty() && stackA.isEmpty()){
                return null;
            }

            if (stackB.isEmpty()){
                while (!stackA.isEmpty()){
                    stackB.add(stackA.pop());
                }
            }

            return stackB.peek();
        }
    }
复制代码

对应的 C++ 解法:

#include <stdio.h>
#include <stack>
using namespace std;

template <typename T> class TStackQueue {
public:
    void add(T t);
    T poll();
    
private:
    stack<T> stackA;
    stack<T> stackB;
};

template <typename T> void TStackQueue<T>::add(T node) {
    stackA.push(node);
}
template<typename T> T TStackQueue<T>::poll(){
    if (stackB.empty() && stackA.empty()) {
        return NULL;
    }
    
    if (stackB.empty()) {
        while (!stackA.empty()) {
            stackB.push(stackA.top());
            stackA.pop();
        }
    }
    T node = stackB.top();
    stackB.pop();
    return node;
}
复制代码

两个队列实现一个栈 (✭✭✩✩✩)

上道题咱们完成了两个栈实现一个队列的题目,那么两个队列实现一个栈又该注意哪些呢?

首先队列是先进先出,咱们能够发现队列不管怎么倒,咱们不能逆序一个队列。既然不能套用上题的解法,那么就得另谋出路,可是能够预知无非就是两个队列进行交替的入队出队操做,那么惟一要作的就是判断目前出队的值是不是按照放入元素顺序中最后放入的元素。 依旧画图举例

这里咱们只看首次取出操做,那么须要注意一点, 如何判断哪一次取出操做后 QueueA 为空?

事实上做为 Queue 做为容器,咱们能够经过事先定义好的方法 queue.size() 去判断一个队列中元素的个数,有人可能说这是犯规,其实不是的。题目中给出是让你用队列去实现,那么队列中公共 API 都是你能够用的。因此能够想象出下面的伪代码:

//若是 queueA 的大小不为 0 则循环取出元素
while(queueA.size() > 0){
    //被取出的元素
    int result = queueA.poll();
    // 这里注意咱们取出元素后再去判断一次,队列是否为空,若是为空表明是最后一个元素
    if(queueA.size() != 0){
        queueB.add(result)
    }else{
        return result;
    }
}
复制代码

上文咱们只是说了一次取出操做,那么一次取出操做后,再次放入元素应该怎么放,咱们彷佛又遇到了困难。

与上题不一样的是,咱们应该先思考若是连续两次取出应该怎么操做,上面一次取出后 QueueA 空了,因此咱们若是按照相同的思路将 B 中的元素倒入 A 中,那么将会获得 3 ,这看起来没什么问题。那么若是下一步进行的 push 操做,那么应该放入 QueueA 仍是 QueueB 中才能保证元素先进后出的规则呢,很容易想到是放入 B 中。 那么总结一下操做要点:

  1. 任什么时候候两个队列总有一个是空的。
  2. 添加元素老是向非空队列中 add 元素。
  3. 取出元素的时候老是将元素除队尾最后一个元素外,导入另外一空队列中,最后一个元素出队。

接上图咱们开看第一次取出操做后可能的两种操做状况:

思路缕清楚了,那么时候写代码了:

public static class TwoQueueStack<E> {
   private Queue<E> queueA;
   private Queue<E> queueB;

   public TwoQueueStack() {
       queueA = new LinkedList<>();
       queueB = new LinkedList<>();
   }

   /** * 选一个非空的队列入队 * * @param e * @return */
   public E push(E e) {
       if (queueA.size() != 0) {
           System.out.println("从 queueA 入队 " + e);
           queueA.add(e);
       } else if (queueB.size() != 0) {
           System.out.println("从 queueB 入队 " + e);
           queueB.add(e);
       } else {
           System.out.println("从 queueA 入队 " + e);
           queueA.add(e);
       }
       return e;
   }

   public E pop() {
       if (queueA.size() == 0 && queueB.size() == 0) {
           return null;
       }

       E result = null;
       if (queueA.size() != 0) {
           while (queueA.size() > 0) {
               result = queueA.poll();
               if (queueA.size() != 0) {
                   System.out.println("从 queueA 出队 并 queueB 入队 " + result);
                   queueB.add(result);
               }
           }
           System.out.println("从 queueA 出队 " + result);

       } else {
           while (queueB.size() > 0) {
               result = queueB.poll();
               if (queueB.size() != 0) {
                   System.out.println("从 queueB 出队 并 queueA 入队 " + result);
                   queueA.add(result);
               }
           }
           System.out.println("从 queueB 出队" + result);
       }
       return result;
   }
}
复制代码

为了方便你们理解我将文章进行下测试:

public static void main(String[] args) {
        TwoQueueStack<Integer> queueStack = new TwoQueueStack<>();
        queueStack.push(1);
        queueStack.push(2);
        queueStack.push(3);
        queueStack.push(4);
        queueStack.pop();
        queueStack.pop();
        queueStack.push(5);
        queueStack.pop();
    }
复制代码

结果为下面所示,看上去咱们的代码是对的

从 queueA 入队 1
从 queueA 入队 2
从 queueA 入队 3
从 queueA 入队 4
从 queueA 出队 并 queueB 入队 1
从 queueA 出队 并 queueB 入队 2
从 queueA 出队 并 queueB 入队 3
从 queueA 出队 4
从 queueB 出队 并 queueA 入队 1
从 queueB 出队 并 queueA 入队 2
从 queueB 出队3
从 queueA 入队 5
从 queueA 出队 并 queueB 入队 1
从 queueA 出队 并 queueB 入队 2
从 queueA 出队 5
复制代码

附C++ 代码实现:

#include <stdio.h>
#include<queue>
#include<exception>

using namespace std;

template <typename T> class TQueueStack {
public:
    void push(const T& node);
    T pop();
    
private:
    queue<T> queueA;
    queue<T> queueB;
};

// 插入元素
template<typename T> void TQueueStack<T>::push(const T& node)
{
    
    //插入到非空队列,若是均为空则插入到queueB中
    if (queueA.size() == 0)
    {
        queueB.push(node);
    }
    else
    {
        queueA.push(node);
    }
}

template<typename T> T TQueueStack<T>::pop()
{
    if (queueA.size() == 0 && queueB.size() == 0)
    {
        return NULL;
    }
    T head;
    if (queueA.size() > 0)
    {
        while (queueA.size()>1)
        {
            //queueA中的元素依次删除,并插入到queueB中,其中queueA删除最后一个元素
            //至关于从栈中弹出队尾元素
            T& data = queueA.front();
            queueA.pop();
            queueB.push(data);
        }
        head = queueA.front();
        queueA.pop();
    }
    else
    {
        while (queueB.size()>1)
        {
            //queueB 中的元素依次删除,并插入到 queueA 中,其中 queueB 删除最后一个元素
            //至关于从栈中弹出队尾元素
            
            T& data = queueB.front();
            queueB.pop();
            queueA.push(data);
        }
        head = queueB.front();
        queueB.pop();
    }
    return head;
}
复制代码

判断出栈顺序是否符合要求(✭✭✭✩✩)

经历了上两道题,你们是否是感受对栈和队列更反感,哦不对是更了解了呢。(额~ 一不当心把实话说出来了)。下面咱们来看第二道题这是一个有关于出栈顺序的判断的题目:

题目: 输入两个整数数组,第一个表示一个栈的压入序列,请写一个函数,判断第二个数组是否为该栈的出栈序列,假设数组中的全部数字均不相等。例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1 是该压栈序列对应的一个弹出序列,但 4,3,5,1,2 就不多是该压栈序列的弹出序列。

看到这道题咱们首先应该去理解题目中的怎么去判断是否符合出栈顺序,其实题目想要表达的意思是若是以数组 A 的方式进栈但并非一次所有进栈,好比咱们先进栈1,2,3,4 而后出栈 4,而后进栈 5,而后在出栈 5,3,2,1。 那么什么状况下是不可能知足的出栈顺序呢?好比 1,确定是比 2 先进栈的,因此 2确定比 1先出栈。因此解题的关键就在于,如何判断数组2 中的元素,是按数组1 中某种进栈顺序操做的出栈序列。

思路是若是咱们在进栈的同时维护一个出栈角标,若是栈顶元素等于 popA[popIndex] 的时候,将角标加一,并出栈该元素,并继续判断下一个栈顶元素,若是栈顶元素不等于 popA[popIndex] 的时候继续入栈元素,直到全部元素入栈完毕若是,栈不为空则表示 popA 不是一个出栈序列。经过下图能够更好的理解题目要考察的内容:

因此在编程的只须要注意一下三点:

  1. 执行放入操做后,若是栈顶的元素等于对应角标在 popA 数组中的元素值,那么就须要出栈该元素,同事角标加1
  2. 若是栈顶的元素不等于对应角标在 popA 数组中的元素值,那么就执行放入操做
  3. 待全部的元素都被放入栈中,此时若是栈为空,那么 popA 就是一个出栈序列,反之则不是。

下面看代码实现:

public static class Solution {

   public boolean IsPopOrder(int[] pushA, int[] popA) {
       int len = pushA.length;

       Stack<Integer> stack = new Stack<>();
       for (int pushIndex = 0, popIndex = 0; pushIndex < len; pushIndex++) {
           stack.push(pushA[pushIndex]);
           //若是栈顶元素等于 popA[popIndex] 则一直出栈且 popIndex++
           while (popIndex < popA.length && popA[popIndex] == stack.peek()) {
               stack.pop();
               popIndex++;
           }
       }
       return stack.isEmpty();
   }
}
复制代码

C++实现以下

class Solution {
public:
    bool IsPopOrder(vector<int> pushA, vector<int> popA) {
        if(pushA() == 0) return false;
        vector<int> stack;
        for(int i = 0,j = 0 ;i < pushA.size();){
            stack.push_back(pushA[i++]);
            while(j < popA.size() && stack.back() == popA[j]){
                stack.pop_back();
                j++;
            }       
        }
        return stack.empty();
    }
};
复制代码

测试结果以下:

public static void main(String[] args) {

   Solution solution = new Solution();
   int[] pushA = new int[]{1, 2, 3, 4, 5};
   int[] popA1 = new int[]{4, 3, 5, 1, 2};
   int[] popA2 = new int[]{4, 5, 3, 2, 1};

   System.out.println("popA1 是不是出栈队列 " + solution.IsPopOrder(pushA, popA1));
   System.out.println("popA2 是不是出栈队列 " + solution.IsPopOrder(pushA, popA2));
}
// 结果
//popA1 是不是出栈队列 false
//popA2 是不是出栈队列 true
复制代码

总结

本文列举了栈和队列的一些面试题目,经过这些面试题目咱们能够了解到一些面试中算法的考点,对于运算相关题目,咱们仍是须要多加练习,可是不要惧怕本身某些地方不会限制了解题思路,经过多加练习,记住见过的解题中的规律,相信通过一段时间练习后,也会感觉到自个人提升。

最后欢迎你们关注个人掘金专栏,不定时分享一些本身的学习工做总结。

像一只狗的掘金专栏

参考: 《剑指 offer 第二版》 《程序员代码面试指南 - 左程云》

欢迎关注个人微信公众号,接收第一手技术干货
相关文章
相关标签/搜索