数据结构之探索“队列”的奥秘

找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程(ಥ_ಥ)-CSDN博客

所属专栏:数据结构(Java版)

目录

队列有关概念

队列的使用 

队列模拟实现  

循环队列的模拟实现

 622. 设计循环队列 

双端队列

栈与队列相互转换

232. 用栈实现队列

225. 用队列实现栈 


队列有关概念

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

入队列:进行插入操作的一端称为队尾(Tail/Rear) ;

出队列:进行删除操作的一端称为队头 (Head/Front)。

队列和我们在日常生活中买东西排队或者在食堂打饭的场景是一样的。如下图:

队列的使用 

上面这张图中,Queue 便是队列,可以看到其底层是 LinkedList ,也就是双向链表实现的。

队列的常用方法
方法功能
boolean offer(E e)往队尾添加元素
E poll()获取并删除队头元素
peek()获取队头元素
int size()获取队列中有效元素个数
boolean isEmpty()检测队列是否为空
public class Test {public static void main(String[] args) {Queue<Integer> queue = new LinkedList<>();// 判断队列是否为空System.out.println(queue.isEmpty());// 往队尾插入元素queue.offer(1);queue.offer(2);queue.offer(3);queue.offer(4);queue.offer(5);// 获得并删除对头元素System.out.println(queue.poll());// 获取对头元素System.out.println(queue.peek());// 判断队列是否为空System.out.println(queue.isEmpty());int x = queue.size();System.out.println(x);// 循环遍历队列中的元素并删除for (int i = 0; i < x; i++) {System.out.print(queue.poll()+" ");}System.out.println();// 注意这个poll()方法并不会因为队列为空而抛出空指针异常System.out.println(queue.poll());}
}

因为上面代码中队列是用 LinkedList 实现的,那么我们就应该去其源码下,查看 poll() 方法

队列模拟实现  

// 用链表模拟实现队列
public class MyQueue {static class ListNode {int val;ListNode prev;ListNode next;public ListNode(int val) {this.val = val;}}public ListNode head;public ListNode last;public int size; // 减少时间复杂度去遍历链表// 往队尾添加元素public boolean offer(int val) {// 创建一个新的节点ListNode newNode = new ListNode(val);// 队列为空if (head == null) {head = newNode;last = newNode;} else {// 就是在链表中尾插元素last.next = newNode;newNode.prev = last;last = last.next;}size++;return true;}// 删除并获取对头元素public Integer poll() {// 链表为空就返回nullif (head == null) {return null;} else {int x = head.val;size--;// 链表中只有一个元素就直接删除if (head.next == null) {head = null;last = null;return x;} else {// 链表中有多个节点head = head.next;return x;}}}// 获取对头元素public Integer peek() {// 模范源码的写法return head == null ? null : head.val;}// 获取节点个数public int size() {return size;}// 检测链表是否为空public boolean isEmpty() {return head == null;}
}

上面是用链表模拟实现,队列用链表实现时,就可以把队列看成是一个链表,我们就可以用实现链表的方式来实现队列。又因为链表是用节点组成,因此,我们就要创建节点,来操作链表。

接下来,我们就可以尝试用数组来实现队列。

上面这个数组看起来虽然可以实现队列,但是我们如果去写队列的方法时,根本无法实现。因为删除对头元素时,last到底要不要 -1 呢?如果 -1,那么last 就会指向5位置的下标,不符合要求;如果不 -1,那么对头元素该怎么办呢?有小伙伴可能会说,直接把元素从后往前覆盖,然后再把 last -1,不就完事了吗? 但又有一个问题:移动数组元素所付出的时间是非常大的,因为队列中出队的元素永远是数组的首元素,在移动数组时,全部的元素都得移动,这就会浪费很多的时间。因此上面这种方法是不可行的,就得用一种全新的数组来解决:循环数组。用循环数组实现的队列也叫作循环队列。

循环队列的模拟实现

循环队列有几个要解决的难题:

1、怎么判断这个队列是空还是有元素?

可能有小伙伴会说:看head 与 last 两者的下标是否相等,如果相等,就说明队列为空;如果不相等就说明队列不为空。但是很巧不巧:当队列满了的时候,这个head 与 last 指向的位置还是一样的啊。有三种解决方案:1、使用usedSize 来记录元素的个数。当head 与 last 相遇时,看看usedSize 是否为0,不为0,就说明的确是满了;否则,就没满。2、浪费一个空间来阻止其相遇。既然当两者相遇时,不知道到底是空还是满,那我们就阻止其相遇即可,当last 的下一个位置是head时,就说明队列已经满了,则不往这个位置存放元素。3、使用标记的方式。就是通过维护一个额外的变量,来判断这个队列是否满了。例如:定义一个isEmpty的变量,初始化为true,只要执行了入队操作,就把其变为false,如果在之后的入队操作中,遇到head == last ,并且isEmpty 为false,那么就说明满了。其实这个就是排除了刚开始 head == last的情况。

2、当这个last 或者是 head 为7 时,怎么把其变为 0呢?也就是说怎么把这个下标给正常化?

大佬们给出的方法是 last = (last+1) % Queue.size();   

极端情况:0 = (7+1) % 8           正常情况:1 = (0+1) % 8 、  6 = (5+1) % 8

两种难题都已经解决了,就可以开始着手写循环队列了。

 622. 设计循环队列 

设计你的循环队列实现。 循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”。

循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。

你的实现应该支持如下操作:

  • MyCircularQueue(k): 构造器,设置队列长度为 k 。
  • Front: 从队首获取元素。如果队列为空,返回 -1 。
  • Rear: 获取队尾元素。如果队列为空,返回 -1 。
  • enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
  • deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
  • isEmpty(): 检查循环队列是否为空。
  • isFull(): 检查循环队列是否已满。

示例:

MyCircularQueue circularQueue = new MyCircularQueue(3); // 设置长度为 3
circularQueue.enQueue(1);  // 返回 true
circularQueue.enQueue(2);  // 返回 true
circularQueue.enQueue(3);  // 返回 true
circularQueue.enQueue(4);  // 返回 false,队列已满
circularQueue.Rear();  // 返回 3
circularQueue.isFull();  // 返回 true
circularQueue.deQueue();  // 返回 true
circularQueue.enQueue(4);  // 返回 true
circularQueue.Rear();  // 返回 4

提示:

  • 所有的值都在 0 至 1000 的范围内;
  • 操作数将在 1 至 1000 的范围内;
  • 请不要使用内置的队列库。

思路:其实把上面的难题解决之后,循环队列也就容易实现了。

class MyCircularQueue {// 用数组来实现循环队列public int[] elem; // 循环数组public int usedSize; // 记录数组的有效元素的个数public int head; // 记录对头的位置public int last; // 记录队尾的位置public int lastValue; // 获取队尾的值public MyCircularQueue(int k) {this.elem = new int[k];}public boolean enQueue(int value) {// 如果满了,就插入失败if (isFull()) {return false;}// 往数组中插入元素elem[last] = value;lastValue  = elem[last];last = (last+1) % elem.length;// 注意:不能简单的++了usedSize++;return true;}public boolean deQueue() {if (isEmpty()) {return false;}// 删除对头元素// head 往后走就行,其余不用管head = (head+1) % elem.length;usedSize--;return true;}public int Front() {if (isEmpty()) {return -1;} else {return elem[head];}}public int Rear() {if (isEmpty()) {return -1;} else {return lastValue;}}public boolean isEmpty() {return usedSize == 0;}public boolean isFull() {return usedSize == elem.length;}
}

注意:因为这里 last 的位置会在插入元素之后发生改变, 因此,我们得把改变前的位置存起来或者把改变前的队尾值存起来,以便后面的 Rear() 方法。

这里我用的是usedSize 记录位置看是否满,也可以用其他的方法。下面是用浪费一个空间的方法:(为了更好的观察,就只给出了改动代码)

class MyCircularQueue {public MyCircularQueue(int k) {// 既然浪费了一个空间,那么我们就偷偷地多申请一个空间this.elem = new int[k+1];}public boolean isEmpty() {// 两者只有在队列为空时,才能相遇return head == last;}public boolean isFull() {// 如果 last+1 == head,说明此时队列已经满了return (last+1) % elem.length == head;}
}

双端队列

前面我们学习的队列都是队尾进,对头出。现在我们来学习一种全新的队列:双端队列。

双端队列(deque)是指允许两端都可以进行入队和出队操作的队列,deque 是 “double ended queue” 的简称。 那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。 

从上图也可以看出Deque 拓展了 Queue 的功能,并且 ArrayList 与 LinkedList 都是实现了该接口的,也就说明既有 线性的双端队列,也有链式的双端队列。

其他的方法与我们在上面的方法差不多,都是这样的,这里就不过多的赘述了。

上面是ArrayDeque 的offer() 方法。

注意:Java 8 和 Java 17 在针对ArrayDeque的无参构造方法上设计的有些不一样。

上面这个就是在浪费一个空间作为判断循环队列是否已满的情况。和我们前面的处理是一样。 

由于双端队列可以在一端进,一端出,这也就表明其可以作为栈来使用了。

栈与队列相互转换

232. 用栈实现队列

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

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:

  • 你 只能 使用标准的栈操作 —— 也就是只有 push to toppeek/pop from topsize, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

示例 1:

输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]解释:
MyQueue myQueue = new MyQueue();
myQueue.push(1); // queue is: [1]
myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)
myQueue.peek(); // return 1
myQueue.pop(); // return 1, queue is [2]
myQueue.empty(); // return false

提示:

  • 1 <= x <= 9
  • 最多调用 100 次 pushpoppeek 和 empty
  • 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)

思路:先取一个栈让其作为第一次存放元素的栈, 如果进行peek 或者 pop 操作,就把有元素的栈中所有元素出栈到另一个空栈中即可,empty 就是看两个栈是否都为空。

class MyQueue {public Stack<Integer> stack1;public Stack<Integer> stack2;public MyQueue() {stack1 = new Stack<>();stack2 = new Stack<>();}public void push(int x) {// 如果两个栈都为空,随便选取一个作为存放元素的栈if (empty()) {stack1.push(x);} else if (!stack1.empty()) {// 如果栈1不为空,存放到栈1中,反之则存放到栈2中stack1.push(x);} else {stack2.push(x);}}public int pop() {// 把有元素的栈中所有元素存放到另一个栈中if (!stack1.empty()) {int size = stack1.size();for (int i = 0; i < size; i++) {stack2.push(stack1.pop());}// 再颠倒过来int x = stack2.pop();size = stack2.size();for (int i = 0; i < size; i++) {stack1.push(stack2.pop());}return x;} else {int size = stack2.size();for (int i = 0; i < size; i++) {stack1.push(stack2.pop());}// 再颠倒过来int x = stack2.pop();size = stack2.size();for (int i = 0; i < size; i++) {stack1.push(stack2.pop());}return x;}}public int peek() {// 把有元素的栈中所有元素存放到另一个栈中if (!stack1.empty()) {int size = stack1.size();for (int i = 0; i < size; i++) {stack2.push(stack1.pop());}// 再颠倒过来// 这个peek()方法的顺序没有关系int x = stack2.peek();size = stack2.size();for (int i = 0; i < size; i++) {stack1.push(stack2.pop());}return x;} else {int size = stack2.size();for (int i = 0; i < size; i++) {stack1.push(stack2.pop());}// 再颠倒过来int x = stack2.peek();size = stack2.size();for (int i = 0; i < size; i++) {stack1.push(stack2.pop());}return x;}}public boolean empty() {// 当两个栈同时为空时,才为空return stack1.empty() && stack2.empty();}
}

 上面这个代码有些过于繁琐了,可以简化为下面这样:

class MyQueue {public Stack<Integer> stack1;public Stack<Integer> stack2;public MyQueue() {stack1 = new Stack<>();stack2 = new Stack<>();}public void push(int x) {// 指定在一个中存放stack1.push(x);}public int pop() {while (stack2.empty()) {while (!stack1.empty()) {stack2.push(stack1.pop());}}return stack2.pop();}public int peek() {while (stack2.empty()) {while (!stack1.empty()) {stack2.push(stack1.pop());}}return stack2.peek();}public boolean empty() {// 当两个栈同时为空时,才为空return stack1.empty() && stack2.empty();}
}

这个代码就是把一个栈专门用来存放元素,另一个栈专门用来出元素。

225. 用队列实现栈 

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppop 和 empty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

注意:

  • 你只能使用队列的标准操作 —— 也就是 push to backpeek/pop from frontsize 和 is empty 这些操作。
  • 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

示例:

输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False

提示:

  • 1 <= x <= 9
  • 最多调用100 次 pushpoptop 和 empty
  • 每次调用 pop 和 top 都保证栈不为空

思路:一个队列存放元素,在pop时,把有元素的队列的前n-1个元素给到新队列,再根据需要处理最后一个元素即可。 

class MyStack {public Queue<Integer> queue1;public Queue<Integer> queue2;public MyStack() {queue1 = new LinkedList<>();queue2 = new LinkedList<>();}public void push(int x) {// 如果两个都为空,则任选一个if (!empty()) {queue1.offer(x);} else if (!queue1.isEmpty()) {queue1.offer(x);} else {queue2.offer(x);}}public int pop() {// 把不为空的队列的前n-1个元素出队if (!queue1.isEmpty()) {int size = queue1.size();for (int i = 0; i < size-1; i++) {queue2.offer(queue1.poll());}// 将原队列的队尾元素出队return queue1.poll();} else {int size = queue2.size();for (int i = 0; i < size-1; i++) {queue1.offer(queue2.poll());}return queue2.poll();}}public int top() {// 把不为空的队列的前n-1个元素出队if (!queue1.isEmpty()) {int size = queue1.size();for (int i = 0; i < size-1; i++) {queue2.offer(queue1.poll());}// 得到原队列的队尾元素,并插入新队列int x = queue1.peek();queue2.offer(queue1.poll());return x;} else {int size = queue2.size();for (int i = 0; i < size-1; i++) {queue1.offer(queue2.poll());}int x = queue2.peek();queue1.offer(queue2.poll());return x;}}public boolean empty() {// 两个都为空,则返回空return queue1.isEmpty() && queue2.isEmpty();}
}

当然本题可以使用双端队列,只不过题目要求要用两个队列,而且双端队列来实现栈比用两个队列更为简单,因为只需要维护一个队列即可。 

双端队列 思路:创建一个双端队列,正常入队元素,但出队是从队尾出。

综上:栈与队列之间的相互转换还是不难的,只要掌握了栈与队列的特点,再根据各自的特点去实现即可。 

好啦!本期 数据结构之探索“队列”的奥秘 的学习之旅就到此结束了,我们下一期再一起学习吧!

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

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

相关文章

C++ 84 之 文件读写

#include <iostream> #include <cstring> #include <string> using namespace std; #include <fstream> // 文件流的头文件int main() {// 写入: 文件内容// (文件位置&#xff0c; 如果这个不存在&#xff0c; 就新建一个)// 写法1&#xff1a; of…

深度学习项目十六:根据训练好的权重文件推理图片--YOLO系列

文章目录 根据训练好的权重文件推理图片--YOLO系列一、自己构建YOLOv5推理代码1.1 对数据集进行模型训练1.2 对数据集进行模型推理检测1.3 自己编写推理函数1.3.1 针对单张进行推理1.3.2 针对文件夹下的图片进行推理二、自己构建YOLOv8推理代码2.1 对数据集进行模型训练2.2 对数…

安装pytorch环境

安装&#xff1a;Anaconda3 通过命令行查显卡nvidia-smi 打开Anacanda prompt 新建 conda create -n pytorch python3.6 在Previous PyTorch Versions | PyTorch选择1.70&#xff0c;安装成功&#xff0c;但torch.cuda.is_available 返回false conda install pytorch1.7.0…

报表工具数据源的取数处理方式大对比

根据报表的需求&#xff0c;很多报表中的指标数据需要进行预处理&#xff0c;以满足快速抽取和展示的需要。对于帆软报表类似的产品&#xff0c;一般通过建立视图、合并数据表&#xff0c;形成直接应用于模板设计的数据集&#xff0c;报表直接和数据集进行交互、关联。当用户发…

Antd - 上传图片 裁剪图片

目录 本地上传方法【input type"file"】&#xff1a;upload组件【antd】默认接口上传&#xff1a;自定义接口上传&#xff1a;【取消默认上传接口】antd的upload组件beforeUpload还有个比较坑的地方 upload结合裁剪1、antd官方裁剪组件&#xff1a;![在这里插入图片描…

Vue - 第3天

文章目录 一、Vue生命周期二、Vue生命周期钩子三、工程化开发和脚手架1. 开发Vue的两种方式2. 脚手架Vue CLI基本介绍&#xff1a;好处&#xff1a;使用步骤&#xff1a; 四、项目目录介绍和运行流程1. 项目目录介绍2. 运行流程 五、组件化开发六、根组件 App.vue1. 根组件介绍…

python学习笔记-08

面向对象基础(OOP)-上 1. 面向对象概述 面向过程&#xff1a;根据业务逻辑从上到下写代码 函数式&#xff1a;将某功能代码封装到函数中&#xff0c;日后便无需重复编写&#xff0c;仅调用函数即可 面向对象(object oriented programming)&#xff1a;将数据与函数绑定到一起…

微信小程序毕业设计-电影院订票选座系统项目开发实战(附源码+论文)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;微信小程序毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计…

【JS重点14】内置构造函数

目录 一:Object构造函数 1 创建对象说明 2 关于Object的三个常用静态方法 Object.keys() Object.values() Object.assign() 二:Array构造函数 1 数组对象的常见实例方法 2 详解reduce实例方法 语法规则&#xff1a; 运行细节&#xff1a; 案例&#xff1a; 3 map()…

【C++高阶】高效搜索的秘密:深入解析搜索二叉树

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ ⏩收录专栏⏪&#xff1a;C “ 登神长阶 ” &#x1f921;往期回顾&#x1f921;&#xff1a;C多态 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀二叉搜索树 &#x1f4d2;1. 二叉搜索树&…

一键解压,无限可能——BetterZip,您的Mac必备神器!

BetterZip for Mac 是一款高效、智能且安全的解压缩软件&#xff0c;专为Mac用户设计。它提供了直观易用的界面&#xff0c;使用户能够轻松应对各种压缩和解压缩需求。 这款软件不仅支持多种压缩格式&#xff0c;如ZIP、RAR、7Z等&#xff0c;还具备快速解压和压缩文件的能力。…

qt 5.6 qmake手册

qt 5.6 qmake手册 &#xff08;笔者翻译的qmake手册&#xff0c;多数是机翻&#xff0c;欢迎评论区纠错修正&#xff09; Qmake工具有助于简化跨不同平台开发项目的构建过程。它自动生成Makefile&#xff0c;因此创建每个Makefile只需要几行信息。您可以将qmake用于任何软件项目…

32.双击列表启动目标游戏

上一个内容&#xff1a;31.加载配置文件中的游戏到辅助列表 以 31.加载配置文件中的游戏到辅助列表 它的代码为基础进行修改 效果图&#xff1a; 添加列表双击事件 实现代码&#xff1a; LPNMITEMACTIVATE pNMItemActivate reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR…

考研数学强化,880+660正确打开方式

1800题基础做完了&#xff1f;做的怎么样&#xff01; 之所以问你做的怎么样&#xff0c;是因为1800题做的好坏&#xff0c;直接决定了你要不要开始做880题和660题。 有的同学1800题做的很好&#xff0c;做完1800题之后开始做880660没毛病 但是有的同学就是纯纯的为了做题而…

python使用哪种数据库

MySQL 是一个关系型数据库管理系统&#xff0c;由瑞典MySQL AB 公司开发&#xff0c;目前属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一&#xff0c;在 WEB 应用方面&#xff0c;MySQL是最好的 RDBMS (Relational Database Management System&#xff0c;关…

阿里云SSL免费证书部署(nginx)

1.先在阿里云领取免费证书 创建证书 下载证书 得到nginx证书和密钥的压缩包 2.配置nginx 将两个文件放进nginx的opt目录下 先检查有没有ngx_http_ssl_module模块 ngixn -V 如果有进入下一步&#xff0c;没有继续 1.找到你nginx的文件 2.进入添加模块 ./configure --with-h…

git的Cherry pick

Cherry pick Git Cherry Pick详解 https://blog.csdn.net/jam_yin/article/details/131594716 目标: 将开发分支A中提交的部分内容合并到B分支(可能是测试分支) 步骤: vscode安装 点击下图标进入graph

最新版本IntelliJ IDEA安装与“坤活”使用

最新版本IntelliJ IDEA安装与“科学”使用 IntelliJ IDEA安装与坤活下载安装坤活idea1.将下面两个压缩文件解压到安装位置&#xff0c;注意路径不要包含中文空格等特殊符号2.双击 install-all-users.vbs &#xff0c;然后点击确定&#xff0c;等到出现 Done的弹窗3. 打开idea复…

远程桌面另一台服务器连接不上,局域网IP如何访问另一台服务器

在IT运维工作中&#xff0c;远程桌面连接是日常工作中不可或缺的一部分。然而&#xff0c;当尝试远程桌面连接至另一台服务器时&#xff0c;如果连接不上&#xff0c;可能会引发一系列问题&#xff0c;影响到工作效率和信息安全。特别是在局域网环境中&#xff0c;确保能够正确…

AI口语练习APP的技术难点

AI口语练习APP旨在帮助用户练习口语&#xff0c;因此其核心功能是语音识别和语音评测。以下是一些AI口语练习APP的主要技术难点。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1. 语音识别 语音识别是将语音信号转换为文本的过程。…