数据结构算法(二) 之 栈和队列

栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
队列(queue)是一种先进先出(First In First Out)的线性表。

一、栈

1.栈的定义

  • 栈顶:允许插入和删除的一端为栈顶

  • 栈底:另一端

栈是一种后进先出(Last In First Out)的线性表,简称 LIFO 结构。既然栈是一种只允许在尾端进行删除操作的线性表,那么线性表的特性它全部都有。根据存储结构的不同,栈可以分成:顺序栈和链栈。

2.顺序栈

顺序栈其实就是一个数组,只不过需要在声明的时候就确定长度。当头指针 top 指向 -1 的时候,表示空栈。一般将头指针 top 指向尾端的元素。

  • 进栈:需要注意栈是否已经满了(top == MAX_SIZE - 1),如果不满,则压入栈,同时 top 指针加一。

  • 出栈:需要注意栈是否为空栈(top == - 1),如果不空,则出栈,同时 top 指针减一。

时间复杂度均为 O(1)。

3.链栈

栈的链式存储结构,其实就是单链表,只不过它的头指针不再指向头结点,而是指向最尾的节点(栈顶元素)。

  • 进栈:将新节点的 next 指针指向 top,然后将 top 指针指向当前的新节点,链表数量加一。

  • 出栈:保存尾节点,将 top 指针指向 top.next,释放刚刚的尾节点,链表数量减一。

时间复杂度均为 O(1)。

4.用途

Java 对栈(Stack)进行了封装,可以直接使用,栈一般用作函数调用栈或者 Activity 栈。

对于函数调用栈,在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。

5.栈的面试题

  • 剑指 Offer 面试题 21:定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min() 函数。在该栈中,调用min(),push() 及 pop() 的时间复杂度都是 O(1)。

    思路:建立一个数据栈和辅助栈,辅助栈的栈顶每次压入数据栈栈顶和辅助栈栈顶两个元素中的最小值,这样当弹出一个数据栈的时候,对应弹出一个辅助栈,因为两者已经关联过大小了,所以当下一次获取最小值的时候必然跟上次已经弹出的元素无关。如果想要获取最小的元素,直接弹出辅助栈的栈顶即可。

show my code

public class MinStack {
    
    //数据栈
    private Stack<Integer> data = new Stack<>();
    //辅助栈
    private Stack<Integer> min = new Stack<>();
    
    /**
     * 压栈
     * @param i
     */
    public void add(Integer item) {
        //数据栈直接入栈
        data.push(item);
        
        //辅助栈需要判断,确保入栈的是最小的元素
        if(min.isEmpty() || item <= min.peek()) {
            min.push(item);
        }else {
            min.push(min.peek());
        }
    }
    
    /**
     * 出栈
     * @return
     */
    public Integer pop() {
        if(data.isEmpty() || min.isEmpty()) {
            return -1;
        }
        
        //弹出辅助栈的栈顶
        min.pop();
        
        //弹出数据栈栈顶
        return data.pop();
    }
    
    /**
     * 获取栈最小的元素
     * @return
     */
    public Integer min() {
        if(data.isEmpty() || min.isEmpty()) {
            return 0;
        }
        
        //直接弹出辅助栈的栈顶
        return min.peek();
        
    }
    
    /**
     * 打印栈
     */
    public void printStack() {
        System.out.print("栈元素: ");
        for(Integer i:data) {
            System.out.print(i+" ");
        }
        System.out.println("");
        System.out.println("*************************");
    }
    
}

测试过程及结果

public static void main(String[] args) {  
        MinStack stack = new MinStack();
        stack.add(10);
        stack.add(999);
        stack.add(23);
        stack.add(654);
       
        stack.printStack();
        
        System.out.println("出栈元素:"+stack.pop());
        stack.printStack();
        
        
        System.out.println("Min 元素:"+stack.min());
        stack.printStack();
    }
MinStack
  • 剑指 Offer 面试题 22:题目:输入两个整数序列,第一个序列表示栈的压入顺序,请判断二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。

思路:解决这个问题很直观的想法就是建立一个辅助栈,把输入的第一个序列中的数字依次压入该辅助栈,并按照第二个序列的顺序依次从该栈中弹出数字。

判断一个序列是不是栈的弹出序列的规律:如果下一个弹出的数字刚好是栈顶数字,那么直接弹出。如果下一个弹出的数字不在栈顶,我们把压栈序列中还没有入栈的数字压入辅助栈,直到把下一个需要弹出的数字压入栈顶为止。如果所有的数字都压入栈了仍然没有找到下一个弹出的数字,那么该序列不可能是一个弹出序列。

show my code

/**
     * 检测一个栈的出栈顺序是否存在
     * @param push 数字的入栈顺序
     * @param pop  数字的出栈顺序
     * @return
     */
    public static boolean verifyStackPopOrder(int push[],int pop[]){
        
        //验证输入数据是否合法,压栈和出栈的数组的长度必须一致
        if(push == null || pop == null || push.length == 0 || pop.length == 0
                || push.length != pop.length){
            System.out.println("输入数据不合法");
            return false;
        }
        
        //构造辅助栈,作为数据栈
        Stack<Integer> data = new Stack<>();
        //压栈数组的压入位置
        int pushIndex = 0;
        //出栈数组的出栈位置
        int popIndex = 0;
        
        //遍历出栈的数组,假如发现出栈的数据和压栈的栈顶元素相同,就将压栈数据的栈顶元素弹出,否则一直压入,
        //直到压栈元素和出栈的栈顶元素相等,弹出压栈的栈顶元素,然后处理下一个出栈的栈顶元素
        
        //未处理完出栈的数组
        while(popIndex < pop.length){
            
            //根据一个出栈的栈顶元素,遍历入栈的数组,直到找到相等的元素 或者全部已经入栈
            while(pushIndex < push.length && (data.isEmpty() || data.peek() != pop[popIndex])){
                //压数据进去数据栈
                data.push(push[pushIndex]);
                pushIndex ++;
            }
            
            //出栈栈顶元素找到和入栈的栈顶元素相同的,数据栈栈顶元素出栈,继续处理下一个出栈元素
            if(data.peek() == pop[popIndex]){
                data.pop();
                popIndex ++;
                //如果出栈顺序正确,那么所有数据栈元素都会被出栈,数据栈最后会变为空的栈
            }else {
                //全部已经压入栈,但是找不到和出栈栈顶元素相等的
                return false;
            }
            
        }
        
        //假如能运行到这里说明,已经全部压入栈,而且出栈的元素也已经全部弹出,说明顺序是正确的,这个肯定是true
        return data.isEmpty();
    }

测试过程及结果

public static void main(String[] args){  
        int[] push = {1, 2, 3, 4, 5};
        int[] pop1 = {4, 5, 3, 2, 1};
        int[] pop2 = {3, 5, 4, 2, 1};
        int[] pop3 = {4, 3, 5, 1, 2};
        int[] pop4 = {3, 5, 4, 1, 2};

        System.out.println("人工计算为true,程序得出的出栈顺序为: " + verifyStackPopOrder(push, pop1));
        System.out.println("人工计算为true,程序得出的出栈顺序为: " + verifyStackPopOrder(push, pop2));
        System.out.println("人工计算为false,程序得出的出栈顺序为: " + verifyStackPopOrder(push, pop3));
        System.out.println("人工计算为false,程序得出的出栈顺序为: " + verifyStackPopOrder(push, pop4));
        
        int[] push5 = {1};
        int[] pop5 = {2};
        System.out.println("人工计算为false,程序得出的出栈顺序为: " + verifyStackPopOrder(push5, pop5));

        int[] push6 = {1};
        int[] pop6 = {1};
        System.out.println("人工计算为true,程序得出的出栈顺序为: " + verifyStackPopOrder(push6, pop6));

        System.out.println("人工计算为false,程序得出的出栈顺序为: " + verifyStackPopOrder(null, null));
    }
验证出栈顺序

二、队列

1.队列的定义

  • 队头:允许删除的一端

  • 队尾:允许插入的一端

队列是一种特殊的线性表,所以队列也是具有顺序存储结构和链式存储结构的。

2.队列的顺序存储结构(循环队列)

为了解决用数组来实现队列的“假溢出”问题,我们一般将顺序存储结构的队列头尾相接,这种把队列的头尾相接的顺序存储结构称为循环队列

int front;  // 头指针
int rear; // 尾指针
  • 队列满的条件:(rear+1) % QueueSize == front

  • 计算队列长度公式:length = (rear - front + QueueSize)% QueueSize

3.队列的链式存储结构(链队列)

队列的链式结构其实就是链表,只是有头指针和尾指针。当队列为空的时候,头指针和尾指针都指向头结点。

Node front;  // 头指针
Node rear; // 尾指针
  • 入队:在链表尾端插入一个节点

  • 出队:删除头结点的后继节点

4.队列的面试题

  • 剑指 Offer 面试题 7:用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail()deleteHead(),分别完成在队列尾部插入结点和在队列头部删除结点的功能。
private Stack<T> stack1;
private Stack<T> stack2; 

思路:我们从一个具体的例子来分析队列的插入和删除元素过程,操作两三次,你就会发现删除一个元素的步骤:当 stack2 中不为空的时候,在
stack2 中的栈顶元素就是最先进入队列的元素,可以弹出。如果 stack2 为空时,我们把 stack1 的元素逐个弹出然后压入 stack2 ,这样就能保证
stack2 的栈元素顺序就是进入队列的顺序。如果要使 stack1 的元素出栈,必须要弹完 stack2 的元素,然后将 stack1 的元素弹出压入 stack2 ,再弹出 stack2 的元素。入队的步骤就是将其压入 stack1 中。

show my code

public class CQueue {

    public static void main(String[] args){  
        
        CQueue cQueue = new CQueue();

        cQueue.appendTail(111);
        cQueue.appendTail(222);
        cQueue.appendTail(333);
        System.out.println("出队: " + cQueue.deleteHead());
        System.out.println("出队: " + cQueue.deleteHead());
        
        cQueue.appendTail(444);
        cQueue.appendTail(555);
        System.out.println("出队: " + cQueue.deleteHead());
        System.out.println("出队: " + cQueue.deleteHead());
        System.out.println("出队: " + cQueue.deleteHead());
        
        
    }
    
    //入队的栈
    private Stack<Integer> stack1 = new Stack<>();
    // 出队的栈
    private Stack<Integer> stack2 = new Stack<>();
    
    /**
     * 入队
     * @param item
     */
    public void appendTail(Integer item){
        stack1.push(item);
    }
    
    /**
     * 出队
     * @return
     */
    public Integer deleteHead(){
        
        //假如 stack2 为空栈,那么当前的队列的头结点肯定在 stack1 中,将 stack1 的元素全部弹出,然后入栈 stack2
        if(stack2.isEmpty()){
            while(stack1.size() > 0){
                Integer i = stack1.peek();
                stack1.pop();
                stack2.push(i);
            }
        }
        
        if(stack2.isEmpty()){
            System.out.println("队列为空,删除失败");
            return -1;
        }
        
        Integer head = stack2.peek();
        stack2.pop();
        return head;
    }
}
验证出队顺序
  • 有个类似的题目:用两个队列实现栈。

思路:假设有两个队列Q1和Q2,当二者都为空时,入栈操作可以用入队操作来模拟,可以随便选一个空队列,假设选Q1进行入栈操作,现在假设a,b,c依次入栈了(即依次进入队列Q1), 这时如果想模拟出栈操作,则需要将c出栈,因为在栈顶,这时候可以考虑用空队列Q2,将a,b依次从Q1中出队, 而后进入队列Q2,将Q1的最后一个元素c出队即可,此时Q1变为了空队列,Q2中有两个元素,队头元素为a,队尾元素为b,接下来如果再执行入栈操作,则需要将元素进入到Q1和Q2中的非空队列,即进入Q2队列,出栈的话,就跟前面的一样,将Q2除最后一个元素外全部出队,并依次进入队列Q1,再将Q2的最后一个元素出队即可。

show my code

public class QueueToStack<T> {
     
    //链队
    Queue<T> queueA = new LinkedList<>();
    //链队
    Queue<T> queueB = new LinkedList<>();
     
    /**
     * 入栈
     * @param value
     */
    public void push(T value) {
        if (queueA.size() == 0 && queueB.size() == 0) {//如果两个队列都是空的话,则随便选择一个队列执行入栈操作
            queueA.add(value);
        }else if (queueA.size() == 0 && queueB.size() != 0){///如果不是两个队列都是为空的话,则选择非空的队列入栈
            queueB.add(value);
        }else if (queueA.size() != 0 && queueB.size() == 0){
            queueA.add(value);
        }
    }
     
    /**
     * 出栈
     * @return
     */
    public T pop() {
        if (queueA.size() == 0 && queueB.size() == 0){
            return null;
        }
        
        T result = null;
        //将非空的队列的元素按顺序出队,转移到空的队列,直到只有一个元素在非空队列
        if (queueA.size() == 0 && queueB.size() != 0){
             while (queueB.size() > 0){
                 result = queueB.poll();
                 if (queueB.size()!=0){
                     queueA.add(result);
                 }
             }
         }else if (queueA.size() != 0 && queueB.size() == 0){
             while (queueA.size() > 0){
                 result = queueA.poll();
                 if (queueA.size()!=0){
                      queueB.add(result);
                 }
             }
         }
         return result;
    }
     
    public static void main(String[] args) {
        QueueToStack<Integer> stack=new QueueToStack<>();
        int tmp=0;
        stack.push(1);
        stack.push(2);
        stack.push(3);
        tmp=stack.pop();
        System.out.println(tmp);//3
        stack.push(4);
        tmp=stack.pop();
        System.out.println(tmp);//4
        tmp=stack.pop();
        System.out.println(tmp);//2
        stack.push(5);
        stack.push(6);
        tmp=stack.pop();
        System.out.println(tmp);//6
        tmp=stack.pop();
        System.out.println(tmp);//5
        tmp=stack.pop();
        System.out.println(tmp);//1
    }

}
出栈顺序

三、诗和远方

好了,最后两分钟,念几句我在初学栈和队列时写的人生感悟的小诗,希望也能引起你们的共鸣。

人生,就像是一个很大的栈演变。 出生时你赤条条地来到人世,慢慢地长大,渐渐地变老,最终还得赤条条地离开世间。

人生,又仿佛是一天一天小小的栈重现。 童年父母每天抱你不断地进出家门,壮年你每天奔波于家与事业之间,老年你每天独自路跚于养老院的门里屋前。

人生,更需要有进栈出栈精神的体现。在哪里跌倒,就应该在哪里爬起来。无论陷入何等困境,只要抬头就能仰望蓝天,就有希望,不断进取,你就可以让出头之日重现。困难不会永远存在,强者才能勇往直前。

人生,其实就是一个大大的队列演变。无知童年,快乐少年,稚傲青年,成熟中年,安逸晚年。

人生,又是一个又一个小小的队列重现。 春夏秋冬轮回年年,早中晚夜循环天天。变化的是时间,不变的是你对未来执著的信念。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容