Java数据结构(七)——优先级队列与PriorityQueue

文章目录

  • 优先级队列与PriorityQueue
      • 基本概念和性质
      • 建堆
      • 堆的插入
      • 堆的删除
      • 堆的应用
    • PriorityQueue
      • PriorityQueue的构造方法
      • PriorityQueue的常用方法
      • PriorityQueue的模拟实现
    • 经典TopK问题

优先级队列与PriorityQueue

优先级队列是一种特殊类型的队列,其中元素按照优先级进行排序,最高优先级的元素最先被取出。其概念源于对元素进行优先级排序的需求。与普通队列先进先出(FIFO)的原则不同,优先级队列中的元素根据其优先级值进行排列和取出。在许多场景下,如任务调度、交通信号管理等,优先级队列都发挥着重要作用。

Java中的优先级队列(PriorityQueue)是一种利用 数据结构实现的、按照元素优先级排序的队列,它允许元素以任意顺序插入,但取出时会按照优先级高低进行排序。

基本概念和性质

是一种特殊的完全二叉树,其中每个节点的值总是大于或等于(最大堆)或小于或等于(最小堆)其子节点的值。

准确来说,堆在逻辑上是一棵完全二叉树,物理结构上常用数组表示,存储时按照层序遍历的次序存入数组。
所以,我们先要掌握堆的相关知识。

堆的两个特性:

  1. 结构性: 用数组表示一棵完全二叉树
  2. 有序性: 任一结点的关键字是其子树所有节点的最大值(或最小值)。
    “最大堆”,也称“大顶堆”(或“大根堆”),简称大堆。特点是:父亲大于等于孩子
    “最小堆”,也称“小顶堆”(或“小根堆”),简称小堆。特点是:父亲小于等于孩子

在这里插入图片描述

观察图片,最大堆中第二层的7小于第三层的9和11,因为堆只要求父结点大于(或小于)自己的子结点,而不要求大于(或小于)其同层结点的子结点。不要认为第k层的结点都大于(小于)第k+1层!


由于堆的存储方式是数组以及堆性质的特殊性,有以下公式(parent等均表示数组下标,以第一个公式为例,其含义:父结点的下标 = (任一孩子下标 - 1) / 2):

  • parent = (child - 1) / 2
  • leftchild = parent * 2 + 1
  • rightchild = parent * 2 + 2

第一个公式结果小于0时,代表没有父结点;当第二、三个公式的结果大于数组最后一个元素的下标时,代表没有孩子结点。

练习:

下列关键字序列为堆的是()

A. 60,70,65,50,32,100 B. 100,60,70,50,32,65 C. 32,50,100,70,65,60 D. 70,65,100,32,50,60

解: 由于堆逻辑上是一棵完全二叉树,存储时按照层序遍历顺序,所以直接画图就可以解决问题。不想画图,也可以较容易地观察:A中60>70,所以要建立小根堆,第二层70和65均大于60,但第三层的50显然小于70,不满足堆,排除;B中100>60,所以要建立大根堆,第二层60和70均小于100,第三层的50和32小于其父亲60,65小于其父亲70,满足大根堆,故选择B;C、D同理,可分析得不满足堆得性质。


建堆

给我们一个数组,怎样将其建为堆呢?向下调整算法 是最常用的方法,其更高效、更稳定。

向下调整算法的基本思想是 从根节点开始,逐步将父节点与子节点进行比较和必要的交换,直到整个堆满足堆的性质。

向下调整算法的前提是 左右子树均为堆。

向下调整算法的结果是根结点最终到达能使整棵树满足堆性质的位置

向下调整算法的基本思想中涉及父结点与子结点的交换,其规则为:

  • 如果要建立小根堆,则将其与左右孩子中较小的一个作比较,如果比 较小孩子 大(证明较小孩子是父、左右孩子三个结点中最小的),将根结点与较小孩子交换,到达新位置,继续与新的左右孩子的较小值比较,如果比 较小孩子 大,交换,以此类推,直到某次比 较小孩子 小或不存在孩子,结束。
  • 如果要建立大根堆,则将其与左右孩子中较大的一个作比较,如果比 较大孩子 小(证明较大孩子是父、左右孩子三个结点中最大的),将根结点与较大孩子交换,到达新位置,继续与新的左右孩子的较大值比较,如果比 较大孩子 小,交换,以此类推,直到某次比 较大孩子 大或不存在孩子,结束。

下图演示了对根结点27进行向下调整算法建小根堆的过程:

在这里插入图片描述


但是,我们不能保证所给数组天生满足根结点的左右子树均为堆,即不能从整棵的根结点开始执行向下调整算法。

既然不能从根结点开始向下调整,我们考虑从叶子结点开始执行向下调整,但叶子结点不存在左右孩子,即叶子结点作为根结点的树本身就满足堆,所以我们要从倒数第一个非叶子结点开始

堆存储在数组里,我们怎么确定倒数第一个非叶子结点的下标呢?

倒数第一个非叶子结点就是倒数第一个结点(即数组最后一个元素)的父结点,根据公式parent = (child - 1) / 2child传入数组最后一个元素的下标,就能得到向下调整算法开始执行的结点。

对倒数第一个非叶子结点执行向下调整算法后,继续对倒数第二个结点执行,以此类推,直到整棵树的根结点执行完向下调整算法,即可建堆成功!


代码实现:(以建小堆为例)

    public void createHeap(int[] array) {//从倒数第一个非叶子结点开始向下调整for(int i = (array.length - 1 - 1) / 2; i >= 0; i--) {//向下调整代码:int parent = i;int child = parent * 2 + 1;while(child < array.length) {//寻找较小结点if (child + 1 < array.length && array[child + 1] < array[child]) {child += 1;}//无需再判断child + 1 < array.lengthif (array[parent] > array[child]) {int tmp = array[parent];array[parent] = array[child];array[child] = tmp;parent = child;child = parent * 2 + 1;}else {break;}}}}
  • 每一次向下调整: 进循环前先保存父结点下标,再计算出左孩子的下标(假设左孩子是较小孩子);判断child下标是否存在,尝试进入循环;进入循环后,寻找较小孩子,(注意判断条件必须包含child + 1 < array.length且必须是第一个条件)如果之前认为左孩子是较小孩子的假设不成立,那么就更新child的值;判断父结点和较小孩子结点的大小,如果父结点小于较小结点,证明父结点是三个结点中最小的,直接跳出循环,否则,交换较小孩子和父结点,更新parent,接着计算新的父结点的左孩子下标并更新。继续尝试进入循环…

  • 特别注意当找到较小结点后,判断父结点和较小结点大小时,不需要先判断child + 1 < array.length,因为

    参与调整的结点一定存在孩子,否则不会进入while循环,一定存在则child的值是有效值。可能存在调整结点只有左孩子没有右孩子的情况,这时在寻找较小结点时,child的值不会变化,表示存在的左孩子的下标,调整的结点只要有孩子,就必须与孩子进行比较来维持堆的结构,如果此时判断父结点和较小结点大小时,加上child + 1 < array.length条件,就会因为调整结点没有右孩子而导致左孩子没有与父结点比较,这导致建堆可能失败。例如非堆序列 [32,50,100,70,65,60] ,如果犯了上述错误,则建堆结果与原序列一致,建堆失败!


堆的插入

当向堆中插入新元素时,先将其存储在数组已有元素后一个位置,然后 向上调整 到合适的位置以保证堆的结构。

下图为向一个大根堆中插入新元素11的向上调整的过程,整个过程要不断与父亲结点比较并不断上调,直到满足堆的性质。

实现堆(push/pop)_堆的pop-CSDN博客

    public void offer(int val) {if(isFull()) {throw new ElemIsFullException("the elemArray is full!:已满");}elem[usedSize] = val;shiftUp(usedSize);usedSize++;}//向上调整private void shiftUp(int child) {int parent = (child - 1) / 2;while(parent >= 0) {if(elem[child] < elem[parent]) {int tmp = elem[child];elem[child] = elem[parent];elem[parent] = tmp;child = parent;parent = (child - 1) / 2;}else {break;}}}
  • 此处堆插入的代码实现是模拟实现PriorityQueue代码的一部分,掌握插入的逻辑以及向上调整算法。其余的代码会在模拟实现阶段再见。

堆的删除

堆的删除一般指删除堆顶元素,具体操作是:将堆顶元素与最后一个元素交换,然后在认为最后一个元素被删除的情况下向下调整,继续维持堆的结构。

    public int poll() {if(isEmpty()) {throw new ElemIsEmptyException("elem is empty!:无元素可以删除!");}int ret = elem[usedSize-1];elem[usedSize-1] = elem[0];elem[0] = ret;//注意交换后认为元素已被删除,接下来要向下调整维持堆结构usedSize--;shiftDown(0, usedSize);return ret;}//向下调整private void shiftDown(int root, int len) {int parent = root;int child = parent * 2 + 1;while(child < len) {if (child + 1 < len && elem[child + 1] < elem[child]) {child += 1;}if (elem[parent] > elem[child]) {int tmp = elem[parent];elem[parent] = elem[child];elem[child] = tmp;parent = child;child = parent * 2 + 1;}else {break;}}}
  • 此处堆删除的代码实现是模拟实现PriorityQueue代码的一部分,掌握删除的逻辑是先交换头尾再向下调整维持堆结构。其余的代码会在模拟实现阶段再见。

堆的应用

堆最常见的应用除了实现优先级队列外,还有:

  • 堆排序
  • TopK问题
  • 频率统计和数据压缩
  • 合并有序小文件

堆排序是十分重要的一种排序方法,后续会在排序章节介绍,TopK问题是本文最后一个问题。


PriorityQueue

在Java的集合框架中,PriorityQueue 是一个基于优先级堆实现的的无界优先级队列。

其基本特性如下:

  • 不允许使用 null 元素也不允许插入不可比较的对象(即没有实现 Comparable 接口的对象,或者在创建时没有提供 Comparator 的情况下)。
  • 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
  • 默认情况下为小根堆,即每次获取到的元素都是最小元素

更多具体内容如下:

PriorityQueue的构造方法

构造方法说明
PriorityQueue()创建一个空的优先级队列,默认容量为11
PriorityQueue(int initialCapacity)创建一个初始容量为initalCapacity的优先级队列,但不能指定小于1,否则会抛出异常
PriorityQueue(Collection<? extends E> c)创建一个包含指定集合元素的 PriorityQueue优先级队列
PriorityQueue(int initialCapacity, Comparator<? super E> comparator)创建一个具有指定初始容量和指定比较器的空 PriorityQueue优先级队列
PriorityQueue(Collection<? extends E> c, Comparator<? super E> comparator)创建一个包含指定集合元素的 PriorityQueue,并根据提供的 Comparator 进行排序。

PriorityQueue默认是小顶堆,如果我们需要一个大顶堆,则需要提供一个自定义的Comparator来实现这一点,该自定义的Comparator需要反转排序规则,这样就能得到一个大顶堆。

Comparator.reverseOrder() 是 Java 中 Comparator 接口的一个静态方法,它返回了一个实现了 Comparator 接口的 Comparator 实例,这个实例可以对实现了 Comparable 接口的对象集合进行逆序排序。换句话说,它创建了一个比较器,该比较器会将较大的元素视为较低(或“更好”的)优先级,从而与默认的自然顺序(通常是升序)相反。

例如:

//默认小根堆
PriorityQueue<Integer> priorityQueue1 = new PriorityQueue<>();//指定为大根堆
PriorityQueue<Integer> priorityQueue2 = new PriorityQueue<>(Comparator.reverseOrder());

PriorityQueue的常用方法

方法功能
boolean offer(E e)插入元素,注意插入的元素不能为空且要可比较
E peek()获取优先级最高的元素(堆顶元素),如果优先级队列为空,返回null
E poll()删除优先级最高的元素(堆顶元素),如果优先级队列为空,返回null
int size()获取有效元素个数
void clear()清空
boolean isEmpty()检测优先级队列是否为空

PriorityQueue的模拟实现

以int类型为例对关键逻辑进行了实现,同时模拟实现的没有涉及扩容,这与PriorityQueue不同,我们主要掌握建堆、堆的删除、堆的插入操作。

public class ExceedTheCapacityException extends RuntimeException {public ExceedTheCapacityException(String message) {super(message);}
}public class ElemIsFullException extends RuntimeException {public ElemIsFullException(String message) {super(message);}
}public class ElemIsEmptyException extends RuntimeException {public ElemIsEmptyException(String message) {super(message);}
}public class MyPriorityQueue {public int[] elem;public int usedSize;//默认分配的容量private static final int DEFAULT_INIT_CAPACITY = 11;public MyPriorityQueue() {this(DEFAULT_INIT_CAPACITY);}public MyPriorityQueue(int capacity) {this.elem = new int[capacity];}private void initElem(int[] array) {for (int i = 0; i < array.length; i++) {elem[i] = array[i];usedSize++;}}public void createHeap(int[] array) {if(array.length > elem.length) {throw new ExceedTheCapacityException("exceed the capacity!:传入的数组超出了容量");}initElem(array);for(int i = (array.length - 1 - 1) / 2; i >= 0; i--) {shiftDown(i, this.usedSize);}}private void shiftDown(int root, int len) {int parent = root;int child = parent * 2 + 1;while(child < len) {//寻找较小结点if (child + 1 < len && elem[child + 1] < elem[child]) {child += 1;}//无需再判断child + 1 < array.lengthif (elem[parent] > elem[child]) {int tmp = elem[parent];elem[parent] = elem[child];elem[child] = tmp;parent = child;child = parent * 2 + 1;}else {break;}}}public void offer(int val) {if(isFull()) {throw new ElemIsFullException("the elemArray is full!:已满");}elem[usedSize] = val;shiftUp(usedSize);usedSize++;}private void shiftUp(int child) {int parent = (child - 1) / 2;while(parent >= 0) {if(elem[child] < elem[parent]) {int tmp = elem[child];elem[child] = elem[parent];elem[parent] = tmp;child = parent;parent = (child - 1) / 2;}else {break;}}}public boolean isFull() {return elem.length == usedSize;}public int poll() {if(isEmpty()) {throw new ElemIsEmptyException("elem is empty!:无元素可以删除!");}int ret = elem[usedSize-1];elem[usedSize-1] = elem[0];elem[0] = ret;usedSize--;shiftDown(0, usedSize);return ret;}public boolean isEmpty() {return usedSize == 0;}public int peek() {return elem[0];}
}

经典TopK问题

TopK问题,即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。比如,世界500强,游戏排行榜。

TopK问题一定可以使用排序来解决问题,但是当数据量比较庞大时,排序就很难高效解决问题了。这时,使用堆就能更好的解决问题,具体思想如下:

  1. 用数据集合中前K个元素来建堆
    • 前K个最大的元素,建小堆
    • 前K个最小的元素,建大堆
  2. 用剩余的N-K个元素依次与堆顶元素进行比较,不满足则替换堆顶元素

例如,对于500条数据,寻找前K个最大的数据元素,将集合前K个元素建立小堆,此时堆顶元素就是这K个建堆元素中最小的,也是最有可能被替换的,然后,遍历剩余的N-K个元素,分别与堆顶元素比较,如果比堆顶元素大,说明该元素可以入选当前的前10,替换堆顶元素。我们可以利用PriorityQueue很好的解决这一类问题,而不需要特意编写堆的插入和删除代码,这是我们学习集合框架的意义。

【例题】

设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。

class Solution {public int[] smallestK(int[] arr, int k) {//补充代码}
}

利用上述思路即可解决问题:

class Solution {public int[] smallestK(int[] arr, int k) {int[] ret = new int[k];if(k == 0) {return ret;}//“小堆转大堆”PriorityQueue<Integer> q = new PriorityQueue<>(Comparator.reverseOrder());//将前k个元素offerfor(int i = 0; i < k; i++) {q.offer(arr[i]);}//遍历剩余元素,与堆顶元素比较,如果比堆顶元素小,则删除现堆顶,插入该元素for(int i = k; i < arr.length; i++) {if(arr[i] < q.peek()) {q.poll();q.offer(arr[i]);}}//将PriorityQueue中的元素都放入数组中,返回for(int i = 0; i < k; i++) {ret[i] = q.poll();}return ret;}
}

原题链接:面试题 17.14. 最小K个数 - 力扣(LeetCode)


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/413485.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

audiocraft - 免费文本转音乐、AI音乐生成、AI音乐创作工具,Facebook开源,本地一键整合包下载

AudioCraft 是一个由Facebook Research开发的PyTorch库&#xff0c;专注于深度学习在音频生成领域的研究。这个强大的工具包集成了两个最新的AI音频生成模型&#xff1a;AudioGen和MusicGen&#xff0c;能够产生高质量的声音和音乐。 今天的一键包也包含了 AudioGen 和 MusicG…

java一键生成数据库说明文档html格式

要验收项目了&#xff0c;要写数据库文档&#xff0c;一大堆表太费劲了&#xff0c;直接生成一个吧&#xff0c;本来想用个别人的轮子&#xff0c;网上看了几个&#xff0c;感觉效果不怎么好&#xff0c;自己动手写一个吧。抽空再把字典表补充进去就OK了 先看效果&#xff1a; …

如何使用Smart-Doc高效生成Java WebSocket接口文档

前言 Smart-Doc 是一款强大的文档生成工具&#xff0c;可以帮助开发者轻松地为Java 项目生成清晰、详细的 API 文档。随着WebSocket技术的普及&#xff0c;Smart-Doc 在3.0.7版本开始也增加了对 WebSocket 接口的支持。本文将详细介绍如何使用 Smart-Doc 生成 Java WebSocket …

最简单监控方案:域名、证书 SSL、服务器全搞定!发送钉钉告警消息

需求 有时候域名太多&#xff0c;时间一长&#xff0c;你会不记得快要续期了服务器太多&#xff0c;需要监控&#xff0c;这应该是刚需证书申请来申请去&#xff0c;自动续签鬼知道会不会出问题 痛点 监控还要安装各种东西&#xff0c;会出岔子&#xff0c;折腾够呛&#xf…

C++ 洛谷 哈希表(对应题库:哈希,hash)习题集及代码

马上就开学了&#xff0c;又一个卷季&#xff0c;不写点东西怎么行呢&#xff1f;辣么&#xff0c;我不准备写那些dalao们都懂得&#xff0c;熟练的&#xff0c;想来想去&#xff0c;最终还是写哈希表吧&#xff01;提供讲解&题目&代码解析哦&#xff01; 奉上题目链接…

软件测试——论坛系统测试用例

功能测试 其他测试 测试用例 用例编号 用例描述 优先级 预置条件 操作步骤 测试数据 预期结果 测试结果Bug ID软件版本测试员SNS_User_Register_001注册成功使用合法的数据成功注册一个新账号P11、已打开注册页面 2、准备一个未注册用户信息1、输入用户昵称 2、输入用户名 3、…

【前端开发必备小技巧】前端代码规范Vue篇

文章目录 &#x1f7e2; 前端代码规范&#x1f7e2; 一、前端代码规范Vue篇&#x1f449;1、Vue编码基础&#x1f449;1.1、组件规范&#x1f449;1.2、模板中使用简单的表达式&#x1f449;1.3、指令都使用缩写形式&#x1f449;1.4、 标签顺序保持一致&#x1f449;1.5、必须…

【IEEE独立出版 | 往届快至会后2个月检索】2024年第四届电子信息工程与计算机科学国际会议(EIECS 2024,9月27-29)

2024年第四届电子信息工程与计算机科学国际会议&#xff08;EIECS 2024&#xff09;将于2024年9月27日至29日在中国延吉举行。会议由长春理工大学主办&#xff0c;延边大学、长春理工大学电子信息工程学院、长春理工大学计算机学院、长春理工大学人工智能学院承办&#xff0c;多…

生产环境变态开启devtools(redux篇)

前沿 默认都安装了谷歌的redux-devtools插件哦 没有亮,说明关闭了生产环境的redux devtools工具, 接下来跟着博主一起变态启用它 如果看了我上一篇的小伙伴,应该会很熟练了,如果没有看上一篇的,也没关系,博主会手摸手的教你们打开它。 正常的解决方案(适用内部开发人员…

学院个人信息|基于SprinBoot+vue的学院个人信息管理系统(源码+数据库+文档)

学院个人信息管理系统基于SprinBootvue的学院个人信息管理系统 一、前言 二、系统设计 三、系统功能设计 系统功能实现 后台模块实现 管理员模块实现 学生模块实现 教师模块实现 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获…

浅谈常见的分布式ID生成方案

一、UUID UUID是通用唯一标识码的缩写&#xff0c;其目的是让分布式系统中的所有元素都有唯一的辨识信息&#xff0c;而不需要通过中央控制器来指定唯一标识。 优点&#xff1a; &#xff08;1&#xff09;降低全局节点的压力&#xff0c;使得主键生成速度更快&#xff1b; &…

青蛙跳台阶与汉诺塔问题

hello&#xff0c;各位小伙伴们上次我们复习了C语言小tip之函数递归&#xff0c;这次我们来使用函数递归来完成青蛙跳台阶和汉诺塔问题&#xff01; 青蛙跳台阶问题 青蛙跳台阶问题&#xff1a;一只青蛙跳n阶台阶&#xff0c;一次可以跳1阶或者两阶&#xff0c;问有多少种情况…

list类底层逻辑实现

list的底层逻辑是一个双向带头链表。那么list的底层其实就跟我们之前实现的带头双向链表相同&#xff0c;都是开辟一个一个单独的节点&#xff0c;最后再通过指针将各个单独的节点链接起来即可。 我们来类比之前编写的双向带头链表实现具体的内容。 创建一个list类的主体 就像我…

Bazel 快速入门与核心知识

Bazel 快速入门与核心知识 Bazel 简介 Bazel 是一款与 Make、Maven 和 Gradle 类似的开源构建和测试工具。 它使用人类可读的高级构建语言。Bazel 支持多种语言的项目 (C/C, Java, Python, …)&#xff0c;可为多个平台构建输出。Bazel 支持跨多个代码库和大量用户的大型代码…

ncnn之yolov5(7.0版本)目标检测pnnx部署

一、pnxx介绍与使用 pnnx安装与使用参考&#xff1a; https://github.com/pnnx/pnnxhttps://github.com/Tencent/ncnn/wiki/use-ncnn-with-pytorch-or-onnxhttps://github.com/Tencent/ncnn/tree/master/tools/pnnx 支持python的首选pip&#xff0c;否则就源码编译。 pip3 …

opencv/c++的一些简单的操作(入门)

目录 读取图片 读取视频 读取摄像头 图像处理 腐蚀 膨胀 调整图像大小 裁剪和缩放 绘制 绘制矩形 绘制圆形 绘制线条 透视变换 颜色检测 轮廓查找 人脸检测 检测人脸 检测嘴巴 可适当调整参数 读取图片 读取路径widows使用vis sto一定是\斜杠 #include <o…

界面控件Telerik UI for ASP.NET Core 2024 Q2亮点 - AI与UI的融合

Telerik UI for ASP.NET Core是用于跨平台响应式Web和云开发的最完整的UI工具集&#xff0c;拥有超过60个由Kendo UI支持的ASP.NET核心组件。它的响应式和自适应的HTML5网格&#xff0c;提供从过滤、排序数据到分页和分层数据分组等100多项高级功能。 本文将介绍界面组件Teler…

【服务对接】✈️SpringBoot 项目整合华为云 obs 对象存储服务

目录 &#x1f44b;前言 &#x1f440;一、环境准备 &#x1f331;二、整合实现 1.依赖引入 2.准备 AK 和 SK ​ 3.配置类 4.obs 工具类封装 &#x1f49e;️三、测试使用 &#x1f37b;四、 obs 客户端 &#x1f4eb;五、章末 &#x1f44b;前言 小伙伴们大家好&…

Oracle查询优化--分区表建立/普通表转分区表

本文介绍了Oracle表分区的方法&#xff0c;将已有的非分区表转化为分区表&#xff0c;也可以直接建立新的分区表&#xff0c;从而实现大表查询的优化。主要通过DBMS_REDEFINITION 和 alter table xxx modify 方法&#xff0c;DBMS_REDEFINITION 适用于所有版本&#xff0c;操作…

计算机毕业设计选题推荐-大学生竞赛管理系统-Java/Python项目实战

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…