数据结构(Java版)第十一期:栈和队列(二)

专栏: 数据结构(Java版)

个人主页:手握风云

目录

一、队列

[1.1. 队列的概念](#1.1. 队列的概念)

[1.2. 队列的使用](#1.2. 队列的使用)

[1.3. 队列的模拟实现](#1.3. 队列的模拟实现)

[1.4. 循环队列](#1.4. 循环队列)

[1.5. 设计循环队列](#1.5. 设计循环队列)

二、栈和队列面试题

[2.1. 用队列实现栈](#2.1. 用队列实现栈)

[2.2. 用栈实现队列](#2.2. 用栈实现队列)


一、队列

1.1. 队列的概念

队列是只允许在⼀端进⾏插⼊数据操作,在另⼀端进⾏删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)⼊队列:进⾏插⼊操作的⼀端称为队尾(Tail/Rear)出队列:进⾏删除操作 的⼀端称为队头(Head/Front)。

1.2. 队列的使用

|-------------------|--------------|
| 方法 | 功能 |
| boolean offer(E) | 入队列 |
| E poll | 出队列 |
| peek | 获取队头元素 |
| int size() | 获取队列中有效元素的个数 |
| boolean isEmpty() | 检查队列是否为空 |

Queue是个接⼝,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接⼝。

java 复制代码
import java.util.LinkedList;
import java.util.Queue;

public class Main {
    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedList<>();
        System.out.println(queue.isEmpty());
        queue.offer(11);//入队列
        queue.offer(22);
        queue.offer(33);
        queue.offer(44);
        queue.offer(55);
        System.out.println(queue);
        System.out.println(queue.size());//获取有效元素的个数
        System.out.println(queue.isEmpty());//检查队列是否为空
        System.out.println(queue.peek());//获取头元素
        System.out.println(queue.poll());//出队列
        System.out.println(queue.poll());
    }
}

1.3. 队列的模拟实现

队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,这里我们建议使用链表来对队列进行底层的实现。

队列要符合先进先出的原则,我们同时要求时间复杂度为O(1),我们目前所学的单链表是无法实现的。因为我们现在不知道链表的尾部,所以我们必须要去遍历链表,此时时间复杂度为O(n)。如果是从链表的尾部开始进入,时间复杂度为O(n),出时间复杂度为O(1);如果从链表的头部进入,同时需要找到链表的尾部,时间复杂度为O(n)。如果我们知道链表的尾部引用last,从尾部入队,头部出队,使时间复杂度为O(n)。所以单向链表是无法实现时间复杂度为O(1)的队列。

既然单向链表无法实现,我们就可以尝试使用双向链表。

java 复制代码
public class MyQueue {
    static class Node{
        public int val;
        public Node prev;
        public Node next;

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

    /**
     * 规定队尾是链表的尾巴
     * 规定队头是链表的头
     */

    public Node front;//队头
    public Node rear;//队尾
}

对于插入方法offer,如果队列为空的话,那么插入的第一个元素既是队头又是队尾。后进入的元素都可以使用尾插的方法来实现。

java 复制代码
    public void offer(int val){//入队列
        Node node = new Node(val);

        //第一个节点的插入
        if(front == null){
            front = node;
            rear = node;
        }else{
            rear.next = node;
            node.prev = rear;
            rear = node;
        }
    }

对于获取队列的长度size(),我们在链表章节里面多次讲过,这里不再多说。

java 复制代码
    public int size(){
        int count = 0;
        Node cur = front;
        while(cur != null){
            cur = cur.next;
            count++;
        }
        return count;
    }

对于元素出队列的方法poll,front节点的前驱置为空,下一个节点成为对头。

java 复制代码
    public int poll(){//出队列
        if(front == null){
            return -1;
        }
        int val = front.val;
        front = front.next;
        front.prev = null;
        return val;
    }

但此时上面的代码还是有问题的,如果说队列只有一个节点,front直接变为空,根本不存在前驱。

java 复制代码
    public int poll(){//出队列
        if(front == null){
            return -1;
        }
        int val = front.val;
        front = front.next;

        //防止只有一个节点
        if(front != null){
            front.prev = null;
        }
        return val;
    }

对于获取队头元素,可以参照上面的代码实现,直接返回front.val。

java 复制代码
    public int peek(){//获取队头元素
        if(front == null){
            return -1;
        }
        return front.val;
    }

完整代码实现:

java 复制代码
public class MyQueue {
    static class Node{
        public int val;
        public Node prev;
        public Node next;

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

    /**
     * 规定队尾是链表的尾巴
     * 规定队头是链表的头
     */

    public Node front;//队头
    public Node rear;//队尾
    public void offer(int val){//入队列
        Node node = new Node(val);

        //第一个节点的插入
        if(front == null){
            front = node;
            rear = node;
        }else{
            rear.next = node;
            node.prev = rear;
            rear = node;
        }
    }

    public int size(){
        int count = 0;
        Node cur = front;
        while(cur != null){
            cur = cur.next;
            count++;
        }
        return count;
    }

    public int poll(){//出队列
        if(front == null){
            return -1;
        }
        int val = front.val;
        front = front.next;

        //防止只有一个节点
        if(front != null){
            front.prev = null;
        }
        return val;
    }

    public int peek(){//获取队头元素
        if(front == null){
            return -1;
        }
        return front.val;
    }
}

public class Test {
    public static void main(String[] args) {
        MyQueue queue = new MyQueue();
        queue.offer(11);
        queue.offer(22);
        queue.offer(33);
        queue.offer(44);

        System.out.println(queue.size());
        System.out.println(queue.peek());
        System.out.println(queue.poll());
        System.out.println(queue.poll());
    }
}

1.4. 循环队列

上面的队列是链式的,当然也有循环队列。我们可以使用数组来实现循环队列,但是存在一些问题。如下图所示,当元素全部出队列时,如果我们再想把元素放入队列中时,那么前面的内存空间就会出现浪费。我们利用一个圆就可以把空间合理地利用起来。

这时候问题来了:rear如何从7下标走到0下标;怎么判断front和rear相遇时是空还是满。

对于第一个问题,rear=(rear+1)%len(数组的长度)。

对于第二个问题,我们有三种解决方案。第一种,我们可以定义一个size来记录存储的个数,当size等于数组长度时,就说明满了;第二种解决方案是牺牲一个空间,当存储元素的时候,判断下一个位置是否为front,如果是,就不再存储了;第三种解决方案是标记,定义一个isFull方法,初始,先假设isFull是false,每次元素入队时,检查rear和front是否重合,如果重合设置rear==front,isFull方法置为true。

1.5. 设计循环队列

我们先看isFull方法,上面已经讲到了,代码如下:

java 复制代码
    public boolean isFull() {
        if((rear+1)% elem.length == front){
            return true;
        }
        return false;
    }

对于入队列enQueue方法,我们需要先判断队列是否满了,如果满了,直接返回false;如果没满,rear需要后移。

java 复制代码
    public boolean enQueue(int value) {
        if(isFull()){
            return false;
        }
        elem[rear] = value;
        rear = (rear+1) % elem.length;
        return true;
    }

对于出队列deQueue方法,我们也需要先判断队列是否为空。如果为空,我们让front向后移动,然后返回true。

java 复制代码
    public boolean isEmpty() {
        return front == rear;
    }
java 复制代码
    public boolean deQueue() {
        if(isEmpty()){
            return false;
        }
        front = (front+1) % elem.length;
        return true;
    }

对于Front和Rear方法,如果是空,返回-1。如果不是空,Front直接返回elem[front];但Rear方法需要注意一下,如果rear在队列的头,rear-1也会返回-1,所以需要提前判断一下。

java 复制代码
import java.util.Scanner;

public class MyCircularQueue{

    public int[] elem;
    public int front;
    public int rear;

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

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

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

    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 front == rear;
    }

    public boolean isFull() {
        if((rear+1)% elem.length == front){
            return true;
        }
        return false;
    }
}
java 复制代码
import java.util.Scanner;

public class Solution {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while(in.hasNextInt()){
            int k = in.nextInt();
            MyCircularQueue queue = new MyCircularQueue(k);
            System.out.println(queue.enQueue(1));
            System.out.println(queue.deQueue());
            System.out.println(queue.Front());
            System.out.println(queue.Rear());
            System.out.println(queue.isFull());
            System.out.println(queue.isEmpty());
        }
    }
}

二、栈和队列面试题

2.1. 用队列实现栈

我们首先思考以下,一个队列能否实现栈。队列是先进先出的,而栈是先进后出的。如下图所示,左边是队列,只能出10,而右边的栈只能出20,所以一个队列无法实现栈。

我们先定义两个队列qu1,qu2。模拟出栈,先判断哪个队列为空,另一个队列中前n-1个进入的元素进入到空队列中,最后一个元素再出队列。模拟入栈,每次入到不为空的队列当中;如果开始两个队列都为空,则默认放到qu1中。

java 复制代码
import java.util.LinkedList;
import java.util.Queue;

public class MyStack {

    private Queue<Integer> qu1;
    private Queue<Integer> qu2;

    public MyStack() {
        qu1 = new LinkedList<>();
        qu2 = new LinkedList<>();
    }

    //模拟入栈
    public void push(int x) {
         if(empty()){
             qu1.offer(x);
             return;
         }
         if(!qu1.isEmpty()){//qu1不为空,qu2为空
             qu1.offer(x);
         }else{//qu2不为空,qu1为空
             qu2.offer(x);
         }
    }

    //栈顶元素出栈
    public int pop() {
        if(empty()) {
            return -1;
        }
        if(!qu1.isEmpty()) {
            int size = qu1.size();
            while(size-1 != 0) {
                qu2.offer(qu1.poll());
                size--;
            }
            return qu1.poll();
        }else {
            int size = qu2.size();
            while(size-1 != 0) {
                qu1.offer(qu2.poll());
                size--;
            }
            return qu2.poll();
        }
    }

    //返回栈顶元素
    public int top() {
        if(empty()){
            return -1;
        }
        if(! qu1.isEmpty()){
            int size = qu1.size();
            int tmp = -1;
            while(size != 0){
                tmp = qu1.poll();
                qu2.offer(tmp);//size前面的元素出队列,并进入另一个队列
                size--;
            }
            return tmp;
        }else{
            int size = qu2.size();
            int tmp = -1;
            while(size-1 != 0){
                tmp = qu2.poll();
                qu1.offer(tmp);
                size--;
            }
            return tmp;
        }
    }

    //两个队列都是空,说明模拟栈是空的
    public boolean empty() {
        return qu1.isEmpty() && qu2.isEmpty();
    }
}
java 复制代码
public class Solution {
    public static void main(String[] args) {
        MyStack stack = new MyStack();
        System.out.println(stack.empty());
        stack.push(1);
        stack.push(2);
        stack.push(3);
        System.out.println(stack.pop());
        System.out.println(stack.empty());
    }
}

2.2. 用栈实现队列

同上,用栈实现队列,也得同时使用两个栈,并且只能使用标准栈操作。模拟出队列,我们先让元素进入s1中,然后全部进入s2中,再从s2中弹出元素。这样做的好处是可以只使用s2中的元素。如果s2是空的,那么看s1是不是空?s1不是空,s1中的元素全部倒出来。模拟入队列,全部默认放入第一个栈中。

java 复制代码
import java.util.Stack;

public class MyQueue {

    private Stack<Integer> s1;
    private 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()){
            //把s1中的数据全部倒出
            while(! s1.isEmpty()){
                s2.push(s1.pop());
            }
        }
        return s2.peek();
    }

    //获取队头元素
    public int peek() {
        if(empty()){
            return -1;
        }
        if(s2.isEmpty()){
            //把s1中的数据全部倒出
            while(! s1.isEmpty()){
                s2.push(s1.pop());
            }
        }
        return s2.peek();
    }

    //两个栈都为空,则队列为空
    public boolean empty() {
        return s1.isEmpty() && s2.isEmpty();
    }
}
java 复制代码
public class Solution {
    public static void main(String[] args) {
        MyQueue queue = new MyQueue();
        System.out.println(queue.empty());
        queue.push(1);
        queue.push(2);
        queue.push(3);
        System.out.println(queue.pop());
        System.out.println(queue.peek());
        System.out.println(queue.empty());
    }
}
相关推荐
夏末秋也凉1 小时前
力扣-回溯-491 非递减子序列
数据结构·算法·leetcode
老菜鸡mou2 小时前
[OD E 100] 生成哈夫曼树
数据结构·c++
和光同尘@3 小时前
56. 合并区间 (LeetCode 热题 100)
c语言·开发语言·数据结构·c++·算法·leetcode·职场和发展
CS创新实验室4 小时前
计算机考研之数据结构:大 O 记号
数据结构·考研
wen__xvn5 小时前
每日一题洛谷P1914 小书童——凯撒密码c++
数据结构·c++·算法
BUG 劝退师6 小时前
八大经典排序算法
数据结构·算法·排序算法
小小小白的编程日记6 小时前
List的基本功能(1)
数据结构·c++·算法·stl·list
_Itachi__6 小时前
LeetCode 热题 100 283. 移动零
数据结构·算法·leetcode
柃歌6 小时前
【UCB CS 61B SP24】Lecture 5 - Lists 3: DLLists and Arrays学习笔记
java·数据结构·笔记·学习·算法
商bol457 小时前
复习dddddddd
数据结构·c++·算法