Java数据结构与算法第五课——栈和队列

目录

一:栈

1.栈的定义

2.栈的模拟实现

3.栈的使用

二:队列

1.队列的定义

 2.队列的模拟实现

 3.循环队列

3.1循环队列的引入

3.2循环队列的实现

三:面试题

3.1用队列实现栈

3.2用栈实现队列

3.3实现最小栈


一:栈

1.栈的定义

        栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。

  • 压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
  • 出栈:栈的删除操作叫做出栈。出数据在栈顶。

2.栈的模拟实现

        从上图中可以看到,Stack继承了Vector。Vector和ArrayList类似,都是动态的顺序表,不同的是Vector是线程安全的。值得注意的是,现如今Vector几乎已经被淘汰,所以我们只介绍Stack。

        我们先简单看一下java中封装好的栈Stack。它的操作很简单,我们可以根据下表所示的功能,进行栈的模拟实现。

         我们只需用一个数组存储栈中的所有元素,为了使栈可以存放任意类型的数据元素,我们在此使用泛型。栈的模拟实现比较简单,在此仅将代码展示如下:

MyStack.java

package MyStack;


import java.util.Arrays;

public class MyStack<E> {

    public E[] elem;
    public int usedSize;

    public MyStack() {
        this.elem = (E[])new Object[2];
    }

    /**
     * 入栈
     * @param val
     */
    public void push(E val) {
        if(isFull()){
            this.elem = Arrays.copyOf(this.elem,2*elem.length);
        }
        elem[usedSize] = val;
        usedSize++;

    }

    /**
     * 判断当前栈是否为满
     * @return
     */
    public boolean isFull() {
        return usedSize == elem.length;
    }

    /**
     * 出栈
     * @return
     */
    public E pop() {
        if(empty()){
            System.out.println("当前栈已空!");
            return null;
        } else {
            E tmp = elem[usedSize-1];
            elem[usedSize-1] = null;
            usedSize--;
            return tmp;
        }
    }

    public boolean empty() {
        return usedSize==0;
    }

    /**
     * 获取栈顶元素 不删除!
     * @return
     */
    public E peek() {
        if(empty()){
            System.out.println("当前栈已空!");
            return null;
        } else {
            E tmp = elem[usedSize-1];
            return tmp;
        }
    }

    /**
     * 获取大小
     * @return
     */
    public int getUsedSize() {
        return usedSize;
    }

}

Test.java

package MyStack;

public class Test {
    public static void main(String[] args) {
        MyStack myStack = new MyStack();
        myStack.push(1);
        myStack.push(2);
        myStack.push(3);
        myStack.push(4);
        myStack.push(5);
        int tmp = (int) myStack.pop();
        System.out.println(tmp);
        int tmp2 = (int)myStack.pop();
        System.out.println(tmp2);
        int tmp3 = (int)myStack.peek();
        System.out.println(tmp3);
        int tmp4 = (int)myStack.pop();
        System.out.println(tmp4);
    }
}

3.栈的使用

        掌握了栈的模拟实现,栈的使用自然不在话下了。示例代码如下:

public static void main(String[] args) {
        Stack myStack = new Stack();
        myStack.push(1);
        myStack.push(2);
        myStack.push(3);
        myStack.push(4);
        myStack.push(5);
        int tmp = (int) myStack.pop();
        System.out.println(tmp);
        int tmp2 = (int)myStack.pop();
        System.out.println(tmp2);
        int tmp3 = (int)myStack.peek();
        System.out.println(tmp3);
        int tmp4 = (int)myStack.pop();
        System.out.println(tmp4);

        int size = myStack.size();
        System.out.println("栈中元素个数为"+size);

        boolean isempty = myStack.empty();
        if(isempty == false){
            System.out.println("当前栈不为空!");
        }else{
            System.out.println("当前栈为空!");
        }
    }

运行结果如下:

二:队列

1.队列的定义

        队列是只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,具有先进先出FIFO(FirstIn First Out)的特点。

  1. 入队列:进行插入操作的一端称为队尾(Tail/Rear)
  2. 出队列:进行删除操作的一端称为队头(Head/Front)

 2.队列的模拟实现

        在Java中,Queue是个接口,底层是通过链表实现的。 

        我们先简单看一下Java中封装好的队列Queue。它的操作很简单,我们可以根据下表所示的功能,进行队列的模拟实现。需要注意的是,队列底层是通过链表实现的。

MyLInkedList.java 

package MyQueue;

import java.util.Queue;

public class MyLinkedList {

    class Node {
        public int val;
        public Node next;
        public Node(int val) {
            this.val = val;
        }
    }

    public int usedSize;
    public Node head;
    public Node last;

    /**
     * 入队
     * @param val
     */
    public void offer(int val) {
        Node node = new Node(val);
        if(head == null){
            head = node;
            last = node;
        } else {
            last.next = node;
            last = node;
        }
        usedSize++;

    }

    /**
     * 出队
     * @return
     */
    public int poll() {
        if(isEmpty()){
            throw new RuntimeException("当前队列为空!");
        }else{
            int ret = head.val;
            head = head.next;
            usedSize--;
            return ret;
        }
    }
    /**
     * 出队 但是不删除
     * @return
     */
    public int peek() {
        if(isEmpty()){
            throw new RuntimeException("当前队列为空!");
        }else{
            int ret = head.val;
            return ret;
        }

    }
    public int size() {
        return usedSize;
    }
    public boolean isEmpty() {
        return usedSize == 0;
    }
}

Test.java

package MyQueue;

public class Test {
    public static void main(String[] args) {
        MyLinkedList myLinkedList = new MyLinkedList();
        myLinkedList.offer(12);
        myLinkedList.offer(23);
        myLinkedList.offer(34);
        myLinkedList.offer(45);
        myLinkedList.offer(56);
        System.out.println(myLinkedList.poll());
        System.out.println(myLinkedList.poll());
        System.out.println(myLinkedList.peek());
        System.out.println("当前队列的长度为:"+myLinkedList.size());
    }

}

 运行结果如下:

 3.循环队列

3.1循环队列的引入

        现在我们来思考一个问题。既然栈的实现使用了数组,那为什么队列的实现没有使用数组,而是使用了链表呢?如下图所示为一个数组,假设我们用它来实现队列,那么,你是否能知道队头和队尾分别在哪呢?

        显然,随着队列中元素的入队和出队,队头和队尾也在不断发生变化。

         显然,如果使用数组实现队列,会造成极大的空间浪费。那么如何才能解决这一问题呢?答案是:把这个数组“卷”起来;当队尾指针来到数组最末端时,不让其指向null,而是指向数组中的第一个元素。这就是我们接下来要谈的“循环队列”。

                最终,队列就变成了这个样子:

         一开始,head == last,说明该循环队列为空。

        各就位,现在开始入队:

         Full or not full, this is a question。当循环队列中元素已满时,head == null也成立,那么当这一条件成立时,如何判断当前队列是满还是空呢?以下给出三种方法:

1.通过添加usedSize属性记录;

2.使用标记;

3.保留一个位置。

        前两种方法,我们容易理解。添加usedSize属性,判断其等于0还是与数组大小相等即可;使用标记,通过判断last和head的相遇时机,调整标记状态即可。最后一种方法的意思是,当数组中仅有一个位置为空时,我们就认为该数组已满。图解如下:

         这里还有一点小问题,那就是在判断队列为满时,last+1=7+1=8,而此时head=0,这二者并不相等。为使数组下标也能循环起来,需要做出这样的调整:

index = (index + 1) % array.length。     

3.2循环队列的实现

保留一个位置表示满时:

class MyCircularQueue {
    private int[] elem;
    private int front;//表示队头下标
    private int rear;//表示队尾下标

    public MyCircularQueue(int k) {
        elem = new int[k];
    }

    //入队
    public boolean enQueue(int value) {
        if (!isFull()) {
            elem[rear] = value;
            rear = (rear + 1) % (elem.length);
            return true;
        }
        return false;
    }

    //出队
    public boolean deQueue() {
        if (!isEmpty()) {
            front = (front + 1) % elem.length;
            return true;
        }
        return false;

    }

    //获取队头元素
    public int Front() {
        if(isEmpty()){
            return -1;
        }
        return elem[front];

    }

    //获取队尾元素
    public int Rear() {
        if(isEmpty()){
            return -1;
        }
        int index =(rear ==0 ) ? elem.length-1 : rear-1;
        return elem[index];
    }

    public boolean isEmpty() {
        return rear == front;
    }

    /*
    浪费一个空间表示满
     */
    public boolean isFull() {
        return (rear + 1) % elem.length == front;
    }
}

用usedSize做标记时:

class MyCircularQueue {
    private int[] elem;
    private int front;//表示队头下标
    private int rear;//表示队尾下标
    private int usedSize;

    public MyCircularQueue(int k) {
        elem = new int[k];
        usedSize = 0;
    }

    //入队
    public boolean enQueue(int value) {
        if (!isFull()) {
            elem[rear] = value;
            rear = (rear + 1) % (elem.length);
            usedSize++;
            return true;
        }
        return false;
    }

    //出队
    public boolean deQueue() {
        if (!isEmpty()) {
            front = (front + 1) % elem.length;
            usedSize--;
            return true;
        }
        return false;

    }

    //获取队头元素
    public int Front() {
        if(isEmpty()){
            return -1;
        }
        return elem[front];

    }

    //获取队尾元素
    public int Rear() {
        if(isEmpty()){
            return -1;
        }
        int index =(rear ==0 ) ? elem.length-1 : rear-1;
        return elem[index];
    }

    public boolean isEmpty() {
        return usedSize == 0;
    }

    public boolean isFull() {
        return usedSize == elem.length;
    }
}

三:面试题

3.1用队列实现栈

链接:

力扣https://leetcode.cn/problems/implement-stack-using-queues/

1.题目:请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。

2.解题

        一个队列,只能实现先进先出,没法实现栈,所以至少需要两个队列。在入队时,永远入到不为空的队列;在出队时,从有元素的队列弹出size-1个元素,放到空的那个队列中,然后再将最后一个元素出队。图解如下:

 具体代码如下:

class MyStack {
        private Queue<Integer> qu1;
        private Queue<Integer> qu2;

    public MyStack() {
        qu1 = new LinkedList<>();
        qu2 = new LinkedList<>();
    }
    
    //入到不为空队列中
    //都为空,入到qu1
    public void push(int x) {
            if(!qu1.isEmpty()){
                qu1.offer(x);
            }else if(!qu2.isEmpty()){
                qu2.offer(x);
            }else{
                qu1.offer(x);
            }
    }
    
    public int pop() {
        if(empty()){
            return -1;
        }
        if(!qu1.isEmpty()){
            int size = qu1.size();
            for(int i = 0;i<size-1;i++){
                qu2.offer(qu1.poll());
            }
            return qu1.poll();
        }else{
            int size = qu2.size();
            for(int i = 0;i<size-1;i++){
                qu1.offer(qu2.poll());
            }
            return qu2.poll();            
        }
    }
    
    public int top() {
             if(empty()){
            return -1;
        }
        int tmp = -1;
        if(!qu1.isEmpty()){
            int size = qu1.size();   
            for(int i = 0;i<size;i++){
                 tmp = qu1.poll();
                 qu2.offer(tmp);
            }
            return tmp;
        }else{
        int size = qu2.size();
        for(int i = 0;i<size;i++){
            tmp = qu2.poll();
            qu1.offer(tmp);
        }
        return tmp;            
        }
    }
    
    public boolean empty() {
       if(qu1.isEmpty() &&qu2.isEmpty()){
           return true;
       }
       return false;
    }
}

/**
 * Your MyStack object will be instantiated and called as such:
 * MyStack obj = new MyStack();
 * obj.push(x);
 * int param_2 = obj.pop();
 * int param_3 = obj.top();
 * boolean param_4 = obj.empty();
 */

        要解决这个问题,一定要熟悉队列的操作方法。

3.2用栈实现队列

链接:

力扣https://leetcode.cn/problems/implement-queue-using-stacks/

1.题目:请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty)。

2.解题

        用两个栈实现队列,基本思路是:入的时候,都入到一个栈中;出的时候,都出第二个栈中的元素。如果第二个栈中没有元素了,就把第一个栈中的元素全部倒过来。图解如下:

 具体代码如下:

class MyQueue {
        Stack<Integer> s1; 
        Stack<Integer> s2; 
    public MyQueue() {
        s1= new Stack<>();
        s2= new Stack<>();
    }
    
    public void push(int x) {
        s1.push(x);
    }
    
    public int pop() {
        if(empty()){
            return -1;
        }

        if(s2.isEmpty()){
            while(!s1.empty()){
            s2.push(s1.pop());                
            }
        }
        return s2.pop();
    }
    
    public int peek() {
        if(empty()){
            return -1;
        }

        if(s2.isEmpty()){
            while(!s1.empty()){
            s2.push(s1.pop());                
            }
        }
        return s2.peek();
    }
    
    public boolean empty() {
        return s1.empty()&&s2.empty();
    }
}

/**
 * Your MyQueue object will be instantiated and called as such:
 * MyQueue obj = new MyQueue();
 * obj.push(x);
 * int param_2 = obj.pop();
 * int param_3 = obj.peek();
 * boolean param_4 = obj.empty();
 */

3.3实现最小栈

链接:

力扣https://leetcode.cn/problems/min-stack/

1.题目:设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。

2.解题:

        首先明确什么是最小栈。要求设计一个栈,除提供最基础的push,poptop 操作外,还能在O(1)的时间复杂度内检索到栈中的最小元素。

        思路是:使用两个栈,其中一个s为当前栈,另一个minStack栈,用于保存当前栈中的最小元素。有出栈和入栈两种情况:

         当入栈时,比较该元素与minStack栈顶元素的大小,如果该元素小于或等于minStack的栈顶元素,该元素入minStack栈中。这一操作可以保证minStack栈顶元素始终为s栈中最小的元素。

         当有元素出栈时,也要比较该元素与minStack栈顶元素的大小,如果二者相等,则minStack的栈顶元素也要出栈,否则不对minStack进行操作。

具体代码如下:

class MinStack {


    private Stack<Integer> s1;
    private Stack<Integer> minStack;

    public MinStack() {
        s1 = new Stack<>();
        minStack = new Stack<>();
    }
    
    /**
    s1这个栈 一定要放元素的
     */
    public void push(int val) {
        s1.push(val);
        if(minStack.empty()) {
            minStack.push(val);
        }else{
            int x = minStack.peek();
            //这里能不能取等号???
            if(val <= x) {
                minStack.push(val);
            }
        }
    }
    
    public void pop() {
        int x = s1.pop();
        int x2 = minStack.peek();
        if(x == x2) {
            minStack.pop();
        }
    }
    
    //获取当前的栈顶元素不删除 ,不是最小栈的栈顶元素
    public int top() {
        return s1.peek();
    }
    
    public int getMin() {
        return minStack.peek();
    }
}


/**
 * Your MinStack object will be instantiated and called as such:
 * MinStack obj = new MinStack();
 * obj.push(val);
 * obj.pop();
 * int param_3 = obj.top();
 * int param_4 = obj.getMin();
 */

       本课主要讲解了栈和队列,了解了栈和队列的模拟实现后,关键是灵活使用java集合类中封装好的栈和队列,尤其注意出栈和入栈使用的是push()和pop()操作,出队和入队使用的是offer()和pull()操作;在栈中,判空使用的是empty()方法;在队列中,判空使用的是isEmpty()方法。而获取栈或队列的长度以及获取栈顶元素(队头元素),其方法名是一致的,为size()和peek()。

        最后,通过用两个栈实现队列,用两个队列实现栈,进一步加深了我们对栈和队列的理解。

        本课内容结束!


  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值