《Java数据结构基础》“队列的实现”和“刷题实战演练”

简介: 《Java数据结构基础》“队列的实现”和“刷题实战演练”

一、队列的初步认识

队列,和栈一样,也是一种对数据的"存"和"取"有严格要求的线性储存结构。


与栈结构不同的是,队列的两端都"开口",要求数据只能从一端进,从另一端出,如图 1 所示:


90f92e91794fada388b6634eda1c121e.gif

通常,称进数据的一端为 "队尾",出数据的一端为 "队头",数据元素进队列的过程称为 "入队",出队列的过程称为 "出队"。

🌰比如上图中的元素3就是队尾,元素1就是队头。从图中我们也可以看出,元素1是最先进入队列中的,同样他也是最先出队的,所以说队列是一种先进先出的结构。


二、Java中队列的使用

在Java中,队列Queue是个接口,底层是用双向链表实现的。

🌰他主要的方法有一下这几个:


db60681b7c710ca7d986b23e5ade95b.png

🔔但要注意的是,Queue是个接口,我们不能直接对Queue进行实例化,但我们可以用Queue接口实例化LinkedList的对象,因为LinkedList实现了Queue接口。


🌰 使用实例:

7c39ed76291d4f43bbe386647e1407a4.png

三、队列的模拟实现

// 用单链表实现的队列,入队和出队的时间复杂度都是O(1)
public class MyQueue2 {
    class ListNode{
        public int val;
        public ListNode next;
        ListNode(int val) {
            this.val = val;
        }
    }
    ListNode head;
    ListNode last;
    // 入队,从尾入,从头出
    public void offer(int x) {
        ListNode node = new ListNode(x);
        if (empty()) { // 如果此时队列为空,新插入的结点就是头结点和尾巴结点
            head = node;
            last = node;
        }
        else {
            last.next = node;
        }
        last = node;
    }
    // 出队
    public int poll() {
        if (empty()) {
            throw new NullPointerException("当前队列为空,你的操作不合法!");
        }
        if (head == last) head = last = null;
        int tmp = head.val; // 先保留一下头节点的值,然后再更改指向
        head = head.next;
        return tmp;
    }
    // 只是获取将要出队的元素的值,不删除元素
    public int peek() {
        return head.val;
    }
    // 判断当前队列是否为空
    public boolean empty() {
        if (head == null) {
            return true;
        }
        return false;
    }
}

📝思路:  

//用单链表实现的队列,为了入队和出队的时间复杂度都是O(1),
//我们还在单链表中设置了一个对尾结点的引用last,同时还需要保证我们都是尾插入队,头删出队
// 为什么呢?因为单链表只有后驱,没有前驱(即只知道后一个是谁,但不知道前一个是谁)
//如果我们要尾删出队——就必须找到该结点的前一个是谁,就需要遍历链表O(N)时间复杂度
// 而如果头删,我们直接更改当前头结点的指向就好了,时间复杂度自然是O(1)
// 那为啥要尾插入队呢?我们如果从尾巴插入,是不是只要将当前的的尾巴结点指向新插入的结点就行了,
// 此时新插入的结点就变成了新的尾巴结点,时间复杂度也是O(1)


🌰测试:

8e9c50a936e64717878268b75181d3c8.png

四、力扣刷题演练

🍑设计循环队列

题目链接:力扣

defaa9ecf4bd430797b229206b63e8a8.png


分析


我们刚才是用链表实现的队列,而这道题目要求我们用数组这种线性储存结构来实现队列的各种操作,所以我们就需要改变一下设计思想


数组是线性储存元素,那么我们的新增和删除该怎么操作呢?新增和删除时数组的下标是怎样变化的呢?题目中说的循序是怎样进行的呢?


思路


在这个循环队列中,我们用front来表示队头的数组下标、rear表示队尾的数组下标。为了实现循环我们发现,所谓的头和尾其实是在不断变化着的,和链表一样,我们还是从尾巴入队,从头部出队


💖当front == rear是表示当前队列为空,当一个元素入队后,表示队尾的数组下标rear就加一;出队后表示队头的数组下标front就加一,但问题了来了——如何判断当前队列是否满了呢?


💖有三种方法


设置一个标记flag

每增加或减少一个元素后,用计数器usedSize记录当前队列中元素的个数。当usedSize == 数组的长度时说明满了。

空一格,当(rear + 1) % 数组的长度 == front   的时候说明数组满了

说到这里,你可能对取模有点疑惑,为什么要取模呢


📝数组的下标是可以一直增加的——很有可能会超过数组的长度,因为随着数组的增加和删除front和rear都是不断往前走的


📝比如数组中能容纳的元素总个数是8,即最大下标是7。当我们新增了7个元素,rear变成了 6,然后我们又删除了4个元素,即front变成了3


📝此时数组中只剩下了3个元素——也就是还能增加5个元素,即rear还能加5,但rear + 5不就等于11了吗?超过最大下标了呀!但其实此时的增加的确是合法的呀!所以要对数组下标及时的取模。


💖代码实现:

class MyCircularQueue {
    int[] elem; // 我们这个队列是用数组实现的
    int front;  // 表示队头的数组下标
    int rear;   // 表示队尾的数组下标
/**
* 构造方法
* @param 数组长度是k
*/
public MyCircularQueue(int k) {
    elem = new int[k + 1]; // 为啥要k + 1,因为我们在判断队列是否满的时候,浪费了一个数组空间——即定义数组长度为k,但我实际只能放k - 1个就显示满了,所以我们要想放k个元素就要定义k + 1个长度的数组
}
/**
* 入队
* @param value
* @return 插入成功返回true,失败返回false
*/
public boolean enQueue(int value) {
    if (isFull()) {  // 如果当前队列满了,就不能再入队了
        return false;
    }
    else {
        elem[rear] = value;  // 再当前的数组尾巴下标下新增数据
        // 新增数据后要对尾巴下标进行更新
        rear = (rear + 1) % elem.length; // 注意这里,防止数组越界
    }
    return true;
}
/**
* 出队
* @return 出队成功返回true,失败返回false
*/
public boolean deQueue() {
    if (isEmpty()) {
        return false;
    }
    else {
        front = (front + 1) % elem.length;
    }
    return true;
}
/**
* 得到队头元素
* @return 返回队头元素,如果队列为空返回-1
*/
public int Front() {
    if (isEmpty()) {
        return -1;
    }
    return elem[front];
}
/**
* 得到队尾元素 返回队尾元素,如果队列为空返回-1
有小伙伴可能会说,直接返回elem[rear - 1]不就好了吗?但如果此时rear == 0, 数组下标不就越界了吗?
但此时如果此时队列不为空,rear等于0说明当前的队尾下标是 数组长度-1,因为是循环队列呀
* @return
*/
public int Rear() {
    if (isEmpty()) return -1;
    int index = (rear == 0) ? elem.length - 1 : rear - 1;
    return elem[index];
}
/**
* 当前循环队列是否为空
* @return
*/
public boolean isEmpty() {
    if (rear == front) return true; // 他们相遇证明是空的
    return false;
}
/**
* 判断当前队列是否为满
* @return 满了返回true, 不满返回false
*/
public boolean isFull() {
    // 因为我们空了一个,相当于有一个数组下标没有用到,所以说比如我定义了一个长度为4的数组,我就只能放3个元素(就显示数组满了)
    if ((rear + 1) % elem.length == front) { // 空一个格子,和一开始rear == front 为空的情况区分开来
        return true;
    }
    return false;
    }
}

🍑用栈实现队列

题目链接:力扣

704fc3d42d2c4aaabbdbc7e4f988ff7e.png


📝思路

我们入栈的时候把数据都放到s1中

*即用s1来存放数据,s2用来输出数据,如果s2为空,就把s1的数据全部放到s2中

*意思就是push的存的数据都放到了s1中,我们pop输出的数据都是从s2里输出的

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() {
         // 我们输出数据都是从s2里拿的,如果s2为空,就把s1里的元素放到s2里面
        if (s2.empty()) {
            while (!s1.empty()) { // 把s1里的数据全部放到s2里面
                int tmp = s1.pop();
                s2.push(tmp);
            }
            return s2.pop();
        }
        else {
            return s2.pop();
        }
    }
    // 返回队列开头的元素
    public int peek() {
        // 和上面出队的操作相似,不同的是在对s2进行出栈操作时,只是获取栈顶元素的值,而不是删除当前栈顶
        if (s2.empty()) {
            while (!s1.empty()) { 
                int tmp = s1.pop();
                s2.push(tmp);
            }
            return s2.peek();
        }
        else {
            return s2.peek();
        }
    }
    // 判断当前队列是否为空,空返回true,非空返回false
    public boolean empty() {
        return s1.empty() && s2.empty();
    }
}


🍑最小栈

题目链接:力扣

bd6c5e5d0ffa4facac2ac7357db71f4b.png

📝分析

在本题中,push,pop,top等操作功能和普通的栈都相同,不同的就是多了一个getMin()获取栈中元素最小值的操作

📝思路

为了让我们能够在常数时间内检测到当前栈中的最小元素,我们不妨用两个栈s1和minStack,s1栈就正常的进行数据的存放和处理,另一个minStack栈就专门存放当前栈中的最小元素,并随着s1栈中元素的变化不断的进行更新


💖代码实现:

class MinStack {
    Stack<Integer> s1;
    Stack<Integer> minStack;
    public MinStack() {
        s1 = new Stack<>();
        minStack = new Stack<>();
    }
    // 入栈
    public void push(int val) {
        s1.push(val); // 把元素都先放到s1栈中
        if (minStack.empty()) { // 如果当前最小栈minStack中为空,直接把当前元素放到minStack就行
            minStack.push(val);
        }
        else if (val <= minStack.peek()){ // 下面有详细解释
            minStack.push(val);
        }
    }
    public void pop() {
        int x = s1.pop();
        // 当s1中要出栈的元素等于最小栈中的元素,即此时栈中元素的最小值已经发生了变化,所以最小栈中元素也要出栈
        if (x == minStack.peek()) {
            minStack.pop();
        }
    }
    public int top() {
        return s1.peek();
    }
    public int getMin() {
        if (!minStack.empty()) {
            return minStack.peek();    
        }
        return -1;
    }
}

🔔对其中一些代码的解释:

// 入栈
    public void push(int val) {
        s1.push(val); // 把元素都先放到s1栈中
        if (minStack.empty()) { // 如果当前最小栈minStack中为空,直接把当前元素放到minStack就行
            minStack.push(val);
        }
        // 如果minStack不为空,就需要进行判断,
       // 如果新添加的元素比minStack中的元素小或等于就放进去
        else if (val <= minStack.peek()){ 
            minStack.push(val);
        }
    }
为啥要包含等于这种情况
比如s1栈中放的是0、1、0,如果没有等于minStack中存放的只有一个0,
但如果s1中的元素发生了变化——栈顶元素0出栈了,按我们下面出栈的规则来说的话,
此时minStack中的0也要出栈,此时栈不就为空了吗?

今天的博客就到这里了,一起加油!!!

926cc22bd3be4694a9bd2799f618f7d9.jpg

相关文章
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
159 9
|
2月前
|
存储 Java
Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。
【10月更文挑战第19天】本文详细介绍了Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。HashMap以其高效的插入、查找和删除操作著称,而TreeMap则擅长于保持元素的自然排序或自定义排序,两者各具优势,适用于不同的开发场景。
47 1
|
2月前
|
存储 Java
告别混乱!用Java Map优雅管理你的数据结构
【10月更文挑战第17天】在软件开发中,随着项目复杂度增加,数据结构的组织和管理至关重要。Java中的Map接口提供了一种优雅的解决方案,帮助我们高效、清晰地管理数据。本文通过在线购物平台的案例,展示了Map在商品管理、用户管理和订单管理中的具体应用,有效提升了代码质量和维护性。
93 2
|
2月前
|
存储 Java 开发者
Java Map实战:用HashMap和TreeMap轻松解决复杂数据结构问题!
【10月更文挑战第17天】本文深入探讨了Java中HashMap和TreeMap两种Map类型的特性和应用场景。HashMap基于哈希表实现,支持高效的数据操作且允许键值为null;TreeMap基于红黑树实现,支持自然排序或自定义排序,确保元素有序。文章通过具体示例展示了两者的实战应用,帮助开发者根据实际需求选择合适的数据结构,提高开发效率。
71 2
|
4天前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
23 5
|
14天前
|
Java 程序员
Java基础却常被忽略:全面讲解this的实战技巧!
小米,29岁程序员,分享Java中`this`关键字的用法。`this`代表当前对象引用,用于区分成员变量与局部变量、构造方法间调用、支持链式调用及作为参数传递。文章还探讨了`this`在静态方法和匿名内部类中的使用误区,并提供了练习题。
18 1
|
18天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
42 5
|
25天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
48 6
|
24天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
1月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
47 6