【算法与数据结构】复杂度深度解析(超详解)

请添加图片描述

文章目录

  • 📝算法效率
  • 🌠 算法的复杂度
  • 🌠 时间复杂度的概念
    • 🌉大O的渐进表示法。
  • 🌠常见复杂度
  • 🌠常见时间复杂度计算举例
    • 🌉常数阶O(1)
    • 🌉对数阶 O(logN)
    • 🌉线性阶 O(N)
    • 🌉平方阶O(N^2)
    • 🌉指数阶O(2^N)
  • 🌠常见复杂度
    • 🌉空间复杂度
    • 🌉空间复杂度为 O(1)
    • 🌉空间复杂度为 O(N)
  • 🚩总结


📝算法效率

如何衡量一个算法的好坏
如何衡量一个算法的好坏呢?比如对于以下斐波那契数列:

long long Fib(int N)
{if(N < 3)return 1;return Fib(N-1) + Fib(N-2);
}

斐波那契数列的递归实现方式非常简洁,但简洁一定好吗?那该如何衡量其好与坏呢?

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。

**时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。**在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

衡量一个算法好坏主要从以下几个方面来看:

  1. 时间复杂度

时间复杂度反映了算法随问题规模增长所需要的计算时间增长情况。时间复杂度越低,算法效率越高。

对于上述斐波那契递归算法,其时间复杂度是O(2^N),随问题规模的增长,需要计算时间呈指数级增长,效率很低。

  1. 空间复杂度

空间复杂度反映了算法需要使用的辅助空间大小,与问题规模的关系。空间复杂度越低,算法效率越高。

递归算法需要在调用栈中保存大量中间结果,空间复杂度很高。

所以对于斐波那契数列来说,简洁的递归实现时间和空间复杂度都很高,不如使用迭代方式。

总的来说,在评价算法好坏时,时间和空间复杂度应该放在首位,然后是代码质量和其他方面。而不是单纯看代码是否简洁。

🌠 算法的复杂度

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。

**时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。**在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

🌠 时间复杂度的概念

时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

// 请计算一下Func1中++count语句总共执行了多少次?
void Func1(int N)
{int count = 0;for (int i = 0; i < N; ++i){for (int j = 0; j < N; ++j){++count;}}for (int k = 0; k < 2 * N; ++k){++count;}int M = 10;while (M--){++count;}printf("%d\n", count);
}

Func1 执行的基本操作次数 :
在这里插入图片描述

N = 10 F(N) = 130
N = 100 F(N) = 10210
N = 1000 F(N) = 1002010

实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。

🌉大O的渐进表示法。

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
使用大O的渐进表示法以后,Func1的时间复杂度为:O(N^2)

N = 10 F(N) = 100
N = 100 F(N) = 10000
N = 1000 F(N) = 1000000

通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
另外有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)

🌠常见复杂度

常数阶O(1)
对数阶O(logN)
线性阶 O(N)
线性对数阶O(nlogN)O(N*logN)
平方阶O(N^2)
K次方阶O(N^k)
指数阶O(2^N)
K次N方阶O(k^N)
N的阶乘O(N!)

🌠常见时间复杂度计算举例

🌉常数阶O(1)

// 计算Func4的时间复杂度?
void Func4(int N)
{int count = 0;for (int k = 0; k < 100; ++ k){++count;}printf("%d\n", count);
}

Func4中有一个for循环,但是for循环的迭代次数是固定的100次,不依赖输入参数N。在for循环内部,只有一个++count操作,这是一个常数时间的操作。打印count也是常数时间的操作。
所以Func4中的所有操作的时间都不依赖输入参数N,它的时间复杂度是常数级别O(1)。
又如int a = 4;int b= 10;那a+b的复杂度是多少?它的时间复杂度是O(1),无论a为2000万,b为10亿,a+b还是O(1),因为a,b都是int 类型,都是32位,固定好的常数操作,&,/…都是O(1)

🌉对数阶 O(logN)

// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{assert(a);int begin = 0;int end = n - 1;// [begin, end]:begin和end是左闭右闭区间,因此有=号while (begin <= end){int mid = begin + ((end - begin) >> 1);if (a[mid] < x)begin = mid + 1;else if (a[mid] > x)end = mid - 1;elsereturn mid;}return -1;
}

BinarySearch的时间复杂度是O(logN)

原因:

BinarySearch采用二分查找算法,每次都将搜索区间缩小一半, while循环里面计算mid点和比较a[mid]与x的操作都是常数时间复杂度的, 最坏情况下,需要log2N次循环才能找到元素或判断不存在。所以BinarySearch的时间复杂度取决于while循环迭代的次数,而循环次数是与输入规模N成对数级别的关系,即O(logN)。基本操作执行最好1次,最坏O(logN)次,时间复杂度为 O(logN) ps:logN在算法分析中表示是底数为2,对数为N。有些地方会写成lgN。

🌉线性阶 O(N)

// 计算Func2的时间复杂度?
void Func2(int N)
{int count = 0;for (int k = 0; k < 2 * N; ++k){++count;}int M = 10;while (M--){++count;}printf("%d\n", count);
}

Func2里面有一个外层for循环,循环次数是2N,for循环内部的++count是常数时间操作,基本操作执行了2N+10次,通过推导大O阶方法知道,时间复杂度为 O(N)

🌉平方阶O(N^2)

// 计算BubbleSort的时间复杂度?
void BubbleSort1(int* a, int n)
{assert(a);for (size_t end = n; end > 0; --end){int exchange = 0;for (size_t i = 1; i < end; ++i){if (a[i - 1] > a[i]){Swap(&a[i - 1], &a[i]);exchange = 1;}}if (exchange == 0)break;}
}

BubbleSort2的时间复杂度是O(n^2)
原因:
BubbleSort采用冒泡排序算法,它有两个循环,外层循环从n遍历到1,循环n次,内层循环每次比较相邻元素,从1遍历到end-1,循环从n-1到1次,所以内层循环的总时间复杂度是Σ(n-1)+(n-2)+...+1 = n(n-1)/2 = O(n^ 2) ,外层循环n次,内层循环每个都为O(n), 所以整体时间复杂度是外层循环次数乘内层循环时间复杂度,即O(n)×O(n)=O(n^ 2 ), 其他操作如交换等都是常数时间,对总时间影响不大,基本操作执行最好N次,最坏执行了(N*(N+1)/2次,通过推导大O阶方法+时间复杂度一般看最坏,时间复杂度为 O(N^2)

不要用代码结构来判断时间复杂度,比如只有一个while循环的冒泡排序,

计算BubbleSort2的时间复杂度?
void bubbleSort2(int[] arr) 
{if (arr == null || arr.length < 2) {return;}int n = arr.length;int end = n - 1, i = 0;while (end > 0) {if (arr[i] > arr[i + 1]) {swap(arr, i, i + 1);}if (i < end - 1) {i++;} else {end--;i = 0;}}}void swap(int[] arr, int i, int j) {int tmp = arr[i];arr[i] = arr[j];arr[j] = tmp;}

冒泡排序每一轮循环都可以使得最后一个元素"沉底",即升序排列, 数组长度为n的排序,需要进行n-1轮比较才能完成排序,每一轮循环需要进行n-1次元素比较,最坏情况下每次比较都需要交换元素,所以总共需要进行(n-1)+(n-2)+...+1 = n(n-1)/2次元素比较,每次元素比较和交换的时间复杂度都是O(1),所以冒泡排序的时间复杂度是O(n^2)
总之,判断算法时间复杂度应该基于操作次数的估算,而不仅仅看代码结构,如循环、递归等。

又比如:N/1+N/2+N/3 ...+N/N,这个流程的时间复杂度是O(N*logN),著名的调和级数

for (int i = 1; i <= N; i++) 
{for (int j = i; j <= N; j += i) {// 这两个嵌套for循环的流程,时间复杂度为O(N * logN)// 1/1 + 1/2 + 1/3 + 1/4 + 1/5 + ... + 1/n,也叫"调和级数",收敛于O(logN)// 所以如果一个流程的表达式 : n/1 + n/2 + n/3 + ... + n/n// 那么这个流程时间复杂度O(N * logN)}
}

对于这个代码,时间复杂度分析需要更仔细:外层循环i1N,循环次数是O(N),内层循环j的起始点是i,终止点是N,但是j的步长是i,也就是j每次增加i,那么内层循环每次迭代的次数大致是N/i,所以总体循环迭代次数可以表示为:∑(N/i) = N*(H(N) - 1) ,其中H(N)是哈密顿数,也就是1N的和,约为O(logN),所以这个算法的时间复杂度是:O(N*(logN)) = O(NlogN)

当然举个例子就更清晰了:

for (int i = 1; i <= N; i++) 
{for (int j = i; j <= N; j += i) 1 2 3 4 5 6 7 8 9 10 11 12.......N
第一轮: 1 2 3 4 5 6 7 8 9 10 11 12.......i=1,j每次加1,都遍历为N
第二轮:	  2   4   6   8   10    12.......i=2,j每次加2,以2的倍数来遍历为N/2
第三轮:     3     6     9       12.......i=3,j每次加3,以3的倍数来遍历为N/3
第四轮:        4       8        12.......i=4,j每次加4,以4的倍数来遍历为N/4....i=N,j每次加N,以N的倍数来遍历为N/NN/1+N/2+N/3+N/4+....N/N
1+1/2+1/3+1/4+1/5+......1/N-->O(logN)
N/1+N/2+N/3+N/4+....N/N-->N*(1+1/2+1/3+1/4+1/5+......1/N)->O(N*logN)

我们可以看出:对于循环嵌套,我们需要考虑所有细节,不能简单下定论,给出一个更准确的时间复杂度分析。

🌉指数阶O(2^N)

// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{if(N < 3)return 1;return Fib(N-1) + Fib(N-2);
}

斐波那契递归Fib函数的时间复杂度是O(2^N)

原因:

斐波那契数列的递归定义是:Fib(N) = Fib(N-1) + Fib(N-2),每次调用Fib函数,它会递归调用自己两次。

可以用递归树来表示斐波那契递归调用的关系:

       Fib(N)  /        \Fib(N-1) Fib(N-2)/   \     /     \
...

可以看出每次递归会产生两条子节点,形成一个二叉树结构。

二叉树的高度就是输入N,每一层节点数都是2N次方,根据主定理,当问题可以递归分解成固定数目的子问题时,时间复杂度就是子问题数的对数,即O(c^ N )。这里每次都分解成2个子问题,所以时间复杂度是O(2^ N)Fib递归函数的时间复杂度是指数级的O(2^N),属于最坏情况下的递归。

🌠常见复杂度

🌉空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定

🌉空间复杂度为 O(1)

// 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n)
{assert(a);for (size_t end = n; end > 0; --end){int exchange = 0;for (size_t i = 1; i < end; ++i){if (a[i - 1] > a[i]){Swap(&a[i - 1], &a[i]);exchange = 1;}}if (exchange == 0)break;}
}

BubbleSort的空间复杂度是O(1)

原因:

BubbleSort是一种原地排序算法,它不需要额外的空间来排序,算法中只使用了几个大小为常数的变量,如end、exchange等,交换元素也是直接在原数组上操作,不需要额外空间,整个排序过程中只使用了固定数量的变量空间,不会随着输入规模n的增加而增加,常数空间对空间复杂度的影响可以忽略不计。所以,BubbleSort的空间复杂度取决于它使用的变量空间,而变量空间不随n的增加而增加,是固定的O(1)级别。

🌉空间复杂度为 O(N)

// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
{if (n == 0)return NULL;long long* fibArray = (long long*)malloc((n + 1) * sizeof(long long));fibArray[0] = 0;fibArray[1] = 1;for (int i = 2; i <= n; ++i){fibArray[i] = fibArray[i - 1] + fibArray[i - 2];}return fibArray;
}

斐波那契数列递归算法Fibonacci的空间复杂度是O(n)

原因:
算法使用了一个长整型数组fibArray来存储计算出来的前n项斐波那契数列,这个数组需要的空间大小是n+1,随着输入n的增加而线性增长,除此之外,递归过程中没有其他额外空间开销, 所以空间消耗完全取决于fibArray数组的大小,即O(n),常数因子可以忽略,所以算法的空间复杂度为O(n)。

// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{if(N == 0)return 1;return Fac(N-1)*N;
}

阶乘递归算法Fac的空间复杂度是O(N)

原因:

Fac函数是递归定义的,每递归一次就会在函数调用栈中push一个栈帧,递归深度等于输入N,随着N增加而增加,每个栈帧中保存的信息(如参数N值等)大小为常量,所以总的栈空间大小就是递归深度N乘以每个栈帧大小,即O(N),Fac函数内部没有其他额外空间开销。阶乘递归算法Fac之所以空间复杂度为O(N),是因为它使用递归调用栈的深度正比于输入N,而栈深度决定了总空间需求。


🚩总结

感谢你的收看,如果文章有错误,可以指出,我不胜感激,让我们一起学习交流,如果文章可以给你一个小小帮助,可以给博主点一个小小的赞😘

请添加图片描述

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=34m59s418000k

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

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

相关文章

高防服务器托管应注意什么

选择高防服务器托管主要考虑的因素&#xff1a;1.服务商的服务器大小。2.服务器的防御值大小。3.服务器机房的位置以及机房的资质。 具体内容如下&#xff1a; 1&#xff0e;服务器大小是按照U来定的&#xff0c;U是一种表示服务器外部尺寸的单位(计量单位&#xff1a;高度或厚…

揭示预处理中的秘密!(二)

目录 ​编辑 1. #运算符 2. ##运算符 3. 命名约定 4. #undef 5. 命令行定义 6. 条件编译 7. 头文件的被包含的方式 8.嵌套文件包含 9. 其他预处理指令 10. 完结散花 悟已往之不谏&#xff0c;知来者犹可追 …

微信小程序引入Vant插件

Vant官网&#xff1a;Vant Weapp - 轻量、可靠的小程序 UI 组件库 先查看官网的版本 新建一个package.json页面&#xff0c;代码写上&#xff1a;&#xff08;我先执行的npm安装没出package页面&#xff0c;所以先自己创建了一个才正常&#xff09; {"dependencies"…

【软件测试】--功能测试4-html介绍

1.1 前端三大核心 html:超文本标记语言&#xff0c;由一套标记标签组成 标签&#xff1a; 单标签&#xff1a;<标签名 /> 双标签:<标签名></标签名> 属性&#xff1a;描述某一特征 示例:<a 属性名"属性值"> 1.2 html骨架标签 <!DOC…

web组态软件

1、强大的画面显示web组态功能 2、良好的开放性。 开放性是指组态软件能与多种通信协议互联&#xff0c;支持多种硬件设备&#xff0c;向上能与管理层通信&#xff0c;实现上位机和下位机的双向通信。 3、丰富的功能模块。 web组态提供丰富的控制功能库&#xff0c;满足用户的测…

【数据分享】2019-2023年我国地级市逐月新房房价数据(Excel/Shp格式)

房价是一个城市发展程度的重要体现&#xff0c;一个城市的房价越高通常代表这个城市越发达&#xff0c;对于人口的吸引力越大&#xff01;因此&#xff0c;房价数据是我们在各项城市研究中都非常常用的数据&#xff01;之前我们分享过2011-2023年我国地级市逐月二手房房价数据&…

【pytorch】tensor.detach()和tensor.data的区别

文章目录 序言相同点不同点测试实例应用 序言 .detach()和.data都可以用来分离tensor数据&#xff0c;下面进行比较pytorch0.4及之后的版本&#xff0c;.data仍保留&#xff0c;但建议使用.detach() 相同点 x.detach()和x.data返回和x相同数据的tensor&#xff0c;这个新的t…

rpmrebuild 重新制作 rpm

重新制作 rpm 有两种方式 方式一&#xff1a; 1、rpm2cpio xxx.rpm |cpio -idv 2、修改相关文件 3、rpmrebuild -s my.spec xxx.rpm 4、rpmbuild -ba xxx.spec 方式二&#xff1a; 1、rpmrebuild -m /bin/bash -np rpm/xxx.rpm 2、此时我们得到一个交互shell&#xff0c; 3、比…

算法打卡day5|哈希表篇01|Leetcode 242.有效的字母异位词 、19.删除链表的倒数第N个节点、202. 快乐数、1. 两数之和

哈希表基础知识 哈希表 哈希表关键码就是数组的索引下标&#xff0c;然后通过下标直接访问数组中的元素&#xff1b;数组就是哈希表的一种 一般哈希表都是用来快速判断一个元素是否出现集合里。例如要查询一个名字是否在班级里&#xff1a; 要枚举的话时间复杂度是O(n)&…

docker发布dubbo服务 外部程序访问不到问题处理

一、问题简述 程序通过docker向zookeeper注册服务时&#xff0c;会将容器地址(内网地址)作为服务的通信地址。当我们的程序都在容器内相互通信时&#xff0c;可以凭借容器地址相互通信。但是外部程序想要直接通过宿主机取访问服务时&#xff0c;从zk上取到providers的地址&…

【数据结构】从链表到LinkedList类

&#x1f9e7;&#x1f9e7;&#x1f9e7;&#x1f9e7;&#x1f9e7;个人主页&#x1f388;&#x1f388;&#x1f388;&#x1f388;&#x1f388; &#x1f9e7;&#x1f9e7;&#x1f9e7;&#x1f9e7;&#x1f9e7;数据结构专栏&#x1f388;&#x1f388;&#x1f388;&…

标准库中的String类 String(C++)【2】

文章目录 String常用的接口&#xff08;黑框标记的是常用接口&#xff09;string类对象的反向遍历操作第一种第二种 容量string的扩容机制 String常用的接口&#xff08;黑框标记的是常用接口&#xff09; string类对象的反向遍历操作 第一种 通过下表进行遍历 void TestSt…

计算机网络期末复习笔记

一、引言 计算机网络是现代信息技术的核心&#xff0c;涉及计算机、通信、电子等多个领域。在信息时代&#xff0c;计算机网络已经深入到人们生活的各个方面&#xff0c;如社交、购物、教育、科研等。因此&#xff0c;理解和掌握计算机网络的基本概念和原理&#xff0c;对于计…

springboot226经方药食两用服务平台

经方药食两用服务平台的设计与实现 摘要 近年来&#xff0c;信息化管理行业的不断兴起&#xff0c;使得人们的日常生活越来越离不开计算机和互联网技术。首先&#xff0c;根据收集到的用户需求分析&#xff0c;对设计系统有一个初步的认识与了解&#xff0c;确定经方药食两用…

Redis、Memcache、MongoDB三者区别?

使用Redis、Memcache和MongoDB这些工具或数据库取决于具体的需求和应用场景。每种技术都有自己的优势和适用范围。 Redis&#xff1a; 高性能的内存存储&#xff1a;Redis是一个基于内存的数据存储系统&#xff0c;因此读取和写入速度非常快。 丰富的数据结构支持&#xff1a…

RK3568平台 RTC时间框架

一.RTC时间框架概述 RTC&#xff08;Real Time Clock&#xff09;是一种用于计时的模块&#xff0c;可以是再soc内部&#xff0c;也可以是外部模块。对于soc内部的RTC&#xff0c;只需要读取寄存器即可&#xff0c;对于外部模块的RTC&#xff0c;一般需要使用到I2C接口进行读取…

nebula容器方式安装:docker 安装nebula到windows

感谢阅读 基础环境安装安装docker下载nebula 安装数据库命令行安装查询network nebula-docker-compose_nebula-net并初始化查询安装初始使用root&#xff08;God用户类似LINUX的root&#xff09; 关闭服务 安装UI 基础环境安装 安装docker 点我下载docker 下载nebula 数据…

(2024,Sora 逆向工程,DiT,LVM 技术综述)Sora:大视觉模型的背景、技术、局限性和机遇回顾

Sora: A Review on Background, Technology, Limitations, and Opportunities of Large Vision Models 公和众和号&#xff1a;EDPJ&#xff08;进 Q 交流群&#xff1a;922230617 或加 VX&#xff1a;CV_EDPJ 进 V 交流群&#xff09; 目录 0. 摘要 1. 简介 2. 背景 2.1…

Presto简介、部署、原理和使用介绍

Presto简介、部署、原理和使用介绍 1. Presto简介 1-1. Presto概念 ​ Presto是由Facebook开发的一款开源的分布式SQL查询引擎&#xff0c;最初于2012年发布&#xff0c;并在2013年成为Apache项目的一部分&#xff1b;Presto 作为现在在企业中流行使用的即席查询框架&#x…

Python全栈工程师课件教程

【百战程序员】Python全栈工程师 描述&#xff1a;Python 被广泛应用于数据分析、机器学习以及 Web 开发等领域&#xff0c;尤其是近些年人工智能的发展更加助推了 Python 的火热程度。 学习 Python 是非科班出身进入程序员岗位的选择。课程体系全新升级&#xff0c;满足学员提…