快速排序及归并排序的实现与排序的稳定性
目录
快速排序
一. 快速排序递归的实现方法
1. 左右指针法
步骤思路
为什么要让end先走?
2. 挖坑法
步骤思路
3. 前后指针法
步骤思路
二. 快速排序的时间和空间复杂度
1. 时间复杂度
2. 空间复杂度
三. 快速排序的优化方法
1. 三数取中优化
2. 小区间优化
四. 使用栈来实现非递归快排
步骤思路
归并排序
编辑
一. 归并排序的递归实现
步骤思路
二. 时间复杂度与空间复杂度
1. 时间复杂度
2. 空间复杂度
三. 非递归实现归并排序
步骤思路
排序算法的稳定性
快速排序
一. 快速排序递归的实现方法
1. 左右指针法
步骤思路
(假设排升序)将数组a最左边的下标用begin记录下来,最右边用end记录下来,定义一个key为begin或end
(假设key定义为begin)end先向左查找找到<a[key]的数停下,begin再向右查找找到>a[key]的值停下,此时将begin指向的值与end指向的值交换,以此类推直到end的值<=begin,将此时的a[key]与begin与end相遇坐标的值交换,我们发现此时的a[key],左边的值都比其小,右边的值都比其大,那就说明key所指向的值在数组中已经排好位置了
如以下代码,即完成了单趟
int key = left;int begin = left, end = right;while (begin < end){while (a[end] >= a[key] && begin < end){end--;}while (a[begin] <= a[key] && begin < end){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[key], &a[begin]);
我们在end和begin寻找比a[key]大或小的值的时候不要忘记也要判断循环成立的条件
既然key已经在数组排好位置,我们接下来递归就不需要加上key了,只需要递归key的左右区间即可,直到递归的区间左边与右边相等即只有一个数
完整代码如下
void QuickSort1(int* a, int left,int right)
{if (left >= right)return;int mid = GetMid(a, left, right);Swap(&a[mid], &a[left]);int key = left;int begin = left, end = right;while (begin < end){while (a[end] >= a[key] && begin < end){end--;}while (a[begin] <= a[key] && begin < end){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[key], &a[begin]);QuickSort1(a, left, begin - 1);QuickSort1(a, begin + 1, right);
}
为什么要让end先走?
左边做key右边先走,可以保证相遇位置比key小
相遇场景分析
begin遇end:end先走,停下来,end停下条件是遇到比key小的值,end停下来的位置一定比key小,begin没有找到大的遇到end停下了
end遇begin:end先走,找小,没有找到比key更小的,直接跟begin相遇了。begin停留的位置是上一轮交换的位置(即,上一轮交换,把比key小的值,换到begin的位置了)
同样道理让右边做key,左边先走,可以保证相遇位置比key要大
2. 挖坑法
步骤思路
(假设排升序,给数组a)将最左边的值定义key存储起来,最左边的下标用bigen记录,最右边的下标用end记录,定义pivot记录为最左边的下标,即将最左边视为坑位
然后end向左寻找比key小的值放到pivot所指向的位置即坑位中,并将这个地方(end所找到的)视作新的坑(更新pivot的值)。
begin向右寻找比key大的值,放到坑位中,并将这个地方视作新的坑(更新pivot的值)
重复以上步骤直到end<=begin
然后将key填进pivot中,再通过递归,即可完成排序
由于与左右指针法类似就不写单趟,直接上完整代码
void QuickSort2(int* a, int left, int right)
{if (left >= right)return;int key = a[left];int begin = left, end = right;int pivot = left;while (begin < end){while (a[end] >= key && begin < end){end--;}a[pivot]=a[end];pivot = end;while (a[begin] <= key && begin < end){begin++;}a[pivot] = a[begin];pivot = begin;}a[pivot] = key;QuickSort2(a, left, pivot - 1);QuickSort2(a, pivot + 1, right);
}
3. 前后指针法
步骤思路
(假设排升序)定义key为数组最左边的下标,并定义,prev=key与after=key+1
after在找到比key指向的值小的值时,prev++,并将after指向的值与现在的prev(即prev++后的值)交换
以此往复,直到after>数组的值
然后将prev所指向的值与key所指向的值交换
代码如下
我们要注意,当prev++后的值==after就会发生与自身交换
完成一次后,效果依然是a[key]左区间的值比其小,右区间的值比其大
int key = left;int prev = left, after = left + 1;while (after<=right){while (a[after] < a[key]&&++prev!=after){Swap(&a[prev], &a[after]);}after++;}Swap(&a[prev], &a[key]);
递归是和上面两种方法同样的道理
完整代码如下
void QuickSort3(int* a,int left,int right)
{if (left >= right)return;int key = left;int prev = left, after = left + 1;while (after<=right){while (a[after] < a[key]&&++prev!=after){Swap(&a[prev], &a[after]);}after++;}Swap(&a[prev], &a[key]);QuickSort3(a, left, prev - 1);QuickSort3(a, prev + 1, right);
}
二. 快速排序的时间和空间复杂度
1. 时间复杂度
①最好情况
每次的划分都使得划分后的子序列长度大致相等,一般在数据已经部分有序或者随机分布的情况下发生。此时时间复杂度为O(Nlog₂N)
②最坏情况
在待排序序列有序的情况下,每一次划分的两个区间都有一个为0,此时快速排序的时间复杂度退化为O(N²)
③平均情况
实际应用中快速排序的平均情况大概会接近于最好情况,因为待排序序列通常不是有序的,我们还可以通过三数取中来优化,减少最坏情况的可能性,所以快速排序的时间复杂度为O(Nlog₂N)
2. 空间复杂度
由于需要递归调用,相当于求递归树的深度,
①最坏情况
当数组接近有序时,递归深度很深,空间复杂度为O(N)
②最好情况
当数组无序时,递归树基本相当与完全二叉树,空间复杂度为O(log₂N)
③平均情况
实际应用中,平均情况大概会接近最好情况,同样可以用三数取中优化
所以快速排序空间复杂的为O(log₂N)
三. 快速排序的优化方法
1. 三数取中优化
为了让每次左右区间长度接近,我们可以使用三数取中,即最左边最右边与中间的值取不大也不小的一个值并返回
int GetMid(int* a, int left, int right)
{int mid = (left + right) / 2;if (a[left] < a[mid]){if (a[mid] < a[right])return mid;else if (a[left] < a[right])//上面if条件不成立可得a[right]<a[mid]return right;else//又可得 a[left] > a[right]return left;}else//a[left]>=a[mid]{if (a[mid] > a[right])return mid;else if (a[left]< a[right])//上面if条件不成立可得a[right]>a[mid]return left;else//又可得 a[left] < a[right]return right;}}
将返回值接收并将其指向位置与最左边的值交换,代码如下
if (left >= right)return;int mid = GetMid(a, left, right);Swap(&a[mid], &a[left]);int key = left;
2. 小区间优化
当快速排序要排的数据很长时,越递归到后面区间越小递归的层数越多,我们可以考虑,当要递归区间小于10的时候用别的排序来代替,这样就可以省去80%到90%的递归
代码如下
void QuickSort1(int* a, int left,int right)
{if ( (right-left+1)<10)//小区间优化{InsertSort(a+left, right - left + 1);//a+left 有可能是后半段区间//减少递归层数}else{if (left >= right)return;int mid = GetMid(a, left, right);Swap(&a[mid], &a[left]);int key = left;int begin = left, end = right;while (begin < end){while (a[end] >= a[key] && begin < end){end--;}while (a[begin] <= a[key] && begin < end){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[key], &a[begin]);QuickSort1(a, left, begin - 1);QuickSort1(a, begin + 1, right);}
}
四. 使用栈来实现非递归快排
栈的实现可以看一下我以前的博客
栈的实现详解-CSDN博客
步骤思路
初始化栈后,将数组的最右边与最左边分别放入栈(即将一个区间放入栈中)
进入循环(当栈为空时循环结束),用begin和begin1接收栈顶端的值,再删除栈的值,再用end和end1接收栈顶端的值,再删除栈的值,使用左右指针法(挖坑法,前后指针法皆可)(用begin与end来寻找值,begin1与end1不变)进行一趟排序,
如果right1>=begin+1 就往栈里存 right1(当前排序区间的最右边) 和 begin+1 反之不存
如果left1<=begin-1 就往栈里存 begin-1 和 left1(当前排序区间的最左边) 反之不存
最后不要忘记销毁栈
代码如下
void StackQuickSort(int* a, int left, int right)
{ST s;StackInit(&s);StackPush(&s, right);StackPush(&s, left);while (!StackEmpty(&s)){int begin = StackTop(&s);int left1 = begin;StackPop(&s);int end = StackTop(&s);int right1= end;StackPop(&s);int key = begin;//int mid = GetMid(a, begin, end);//Swap(&a[mid], &a[begin]);while (begin < end){while (a[end] >= a[key] && begin < end){end--;}while (a[begin] <= a[key] && begin < end){begin++;}Swap(&a[begin], &a[end]);}Swap(&a[key], &a[begin]);if(right1>=begin+1){StackPush(&s,right1);StackPush(&s, begin + 1);}if(left1<=begin-1){StackPush(&s, begin - 1);StackPush(&s, left1);}}StackDestroy(&s);
}
归并排序
一. 归并排序的递归实现
步骤思路
malloc一个临时数组进入子函数(创建子函数递归会更方便),进行递归,子函数利用分治思想一直递归直到left>=right 开始执行下面操作
k赋初值为当前区间最左边,begin1 , end1来记录左数组最左边和最右边,定义begin2 ,end2 来记录右数组的最左边和最右边,将两个数组从头比较,较小的赋值给临时数组,直到有一方赋完值,再将没赋完值的数组给临时数组赋值。最后给要排序数组left到right赋值为临时数组left到right
代码如下
//递归
void _MergeSort(int* a,int* tmp, int left, int right)
{if(left>=right){return;}int mid = (left + right) / 2;//如果[left,mid][mid+1,right]有序就可以归并了_MergeSort(a,tmp, left, mid);_MergeSort(a,tmp, mid + 1, right);int begin1 = left;int end1 = mid;int begin2 = mid + 1;int end2 = right;int k=left;while (begin1 <= end1&&begin2<=end2){if(a[begin1]<a[begin2]){tmp[k++] = a[begin1++];}else{tmp[k++] = a[begin2++];}}while (begin1 <= end1){tmp[k++] = a[begin1++];}while (begin2 <= end2){ tmp[k++] = a[begin2++];}for (int i = left; i <= right; i++){a[i] = tmp[i];}}void MergeSort(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fail");return;}//_MergeSort(a, tmp, 0, n - 1);_MergeSort2(a,tmp, n);free(tmp); tmp = NULL;
}
二. 时间复杂度与空间复杂度
1. 时间复杂度
归并排序的时间复杂度是稳定的,不受输入数组的初始顺序影响
将数组分成两个子数组的时间复杂度为O(1),递归对子数组进行排序,假设每个子数组长度为n
则两个子数组排序的总时间复杂度为O(NlogN),将两个有序数组合并为一个有序数组时间复杂度为O(N),所以归并排序时间复杂度为O(NlogN)
2. 空间复杂度
调用栈所需要的额外空间为O(logN),因为我们需要一个额外数组来存储数据所以又额外消耗O(N)的空间,我们将较小的O(logN)忽略可以得到归并排序的空间复杂度为O(N)
三. 非递归实现归并排序
步骤思路
开辟动态空间后定义一个数gap=1来控制区间(gap相当于每组数据个数),(每一次gap*2,使每次区间扩大)gap<数组长度
设计一个for循环i+=gap*=2
每次分两组[i][i+gap-1]和[i+gap][i+2*gap-1] (i每次+=正好跳过这些数据)
将两个区间的值比较放入新开辟的数组,再拷贝到原数组
代码如下
//非递归
void _MergeSort2(int* a,int* tmp,int n)
{int gap = 1;while(gap<n){for (int i = 0; i < n; i += 2 * gap){int begin1 = i;int end1 = i + gap - 1;;int begin2 = i + gap;int end2 = i + 2 * gap - 1;int k = i;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[k++] = a[begin1++];}else{tmp[k++] = a[begin2++];}}while (begin1 <= end1){tmp[k++] = a[begin1++];}while (begin2 <= end2){tmp[k++] = a[begin2++];}//memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));for (int j = i; j < k; j++){a[j] = tmp[j];}}gap *= 2;}
}
但是我们发现,这样如果会发生越界的现象
一共三种可能
1. [begin1,end1][begin2,end2] end2越界
2. [begin1,end1][begin2,end2] begin2,end2越界
3. [begin1,end1][begin2,end2] end1,begin2,end2越界
第2,3种我们可以直接不递归了,因为后面区间的不存在前面区间的在上一次已经递归好了,
第一种呢我们需要把区间(即end)给修正一下
修正代码如下
//非递归
void _MergeSort2(int* a,int* tmp,int n)
{int gap = 1;while(gap<n){for (int i = 0; i < n; i += 2 * gap){int begin1 = i;int end1 = i + gap - 1;;int begin2 = i + gap;int end2 = i + 2 * gap - 1;int k = i;if (begin2 >= n)//第二种情况,第二组不存在,不需要归并break;if (end2 >= n)//第一种情况,需要修正一下end2 = n - 1;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[k++] = a[begin1++];}else{tmp[k++] = a[begin2++];}}while (begin1 <= end1){tmp[k++] = a[begin1++];}while (begin2 <= end2){tmp[k++] = a[begin2++];}//memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));for (int j = i; j < k; j++){a[j] = tmp[j];}}gap *= 2;}
}
排序算法的稳定性
假定在待排序的记录序列中,存在多个具有相同关键字的记录,若经过排序,这些记录的相对次序保持不变
即原序列中 r[i]=r[j],且r[i]在r[j]之前而在排序后的序列中r[i]仍在r[j]前,则称这种排序算法是稳定的,否则是不稳定的
冒泡选择 | 稳定 | |
选择排序 | 不稳定*** | 只会考虑自身,假如找到最小值1下标为3,将其与下标为0(假设此处为6)处交换若下标为1处也是6,就改变了 |
直接插入排序 | 稳定 | |
希尔排序 | 不稳定(分组) | 预排序时相同的值可能分到不同的组 |
堆排序 | 不稳定 | 建堆时可能就乱了 |
归并排序 | 稳定 | 当两个数相等,让第一个下来就是稳定的(可以控制) |
快速排序 | 不稳定 | end先找到 j 和begin交换了,在找到 i 和bigin交换,显然改变了 |
这篇文章就到这里了,感谢大家阅读
(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤
相关文章:

快速排序及归并排序的实现与排序的稳定性
目录 快速排序 一. 快速排序递归的实现方法 1. 左右指针法 步骤思路 为什么要让end先走? 2. 挖坑法 步骤思路 3. 前后指针法 步骤思路 二. 快速排序的时间和空间复杂度 1. 时间复杂度 2. 空间复杂度 三. 快速排序的优化方法 1. 三数取中优化 2. 小区…...

【系统架构设计】数据库系统(一)
数据库系统(一) 数据库模式与范式数据库的结构与模式数据模型关系代数数据的规范化反规范化 数据库设计事务管理备份与恢复分布式数据库系统数据仓库数据挖掘NoSQL大数据 数据库模式与范式 数据库的结构与模式 数据库技术中采用分级的方法将数据库的结…...

泛微e-cology WorkflowServiceXml SQL注入漏洞(POC)
漏洞描述: 泛微 e-cology 是泛微公司开发的协同管理应用平台。泛微 e-cology v10.64.1的/services/接口默认对内网暴露,用于服务调用,未经身份认证的攻击者可向 /services/WorkflowServiceXml 接口发送恶意的SOAP请求进行SQL注入,…...

<Rust><GUI>rust语言GUI库tauri体验:前、后端结合创建一个窗口并修改其样式
前言 本文是rust语言下的GUI库:tauri来创建一个窗口的简单演示,主要说明一下,使用tauri这个库如何创建GUI以及如何添加部件、如何编写逻辑、如何修改风格等,所以,这也是一个专栏,将包括tauri库的多个方面。…...

OBD诊断(ISO15031) 09服务
文章目录 功能简介ISO 9141-2、ISO 14230-4和SAE J1850的诊断服务定义1、请求车辆信息请求消息(读取支持的INFOTYPE)2、请求车辆信息响应消息(报告支持INFOTYPE)3、请求车辆信息请求消息(读取INFOTYPE值)4、请求车辆信息响应消息&…...

客户端与服务端之间的通信连接
目录 那什么是Socket? 什么是ServerSocket? 代码展示: 代码解析: 补充: 输入流(InputStream): 输出流(OutputStream): BufferedReader 是如何提高读取效率的&a…...

Font Awesome 图表图标
Font Awesome 图表图标 Font Awesome 是一个广泛使用的图标库,它提供了大量的图标,可以轻松地用于网页设计和开发中。在本文中,我们将重点介绍 Font Awesome 中的图表图标,探讨它们的特点、使用方法,并展示一些实际的…...

React Native 自定义 Hook 获取组件位置和大小
在 React Native 中自定义 Hook useLayout 获取 View、Pressable 等组件的位置和大小的信息 import {useState, useCallback} from react import {LayoutChangeEvent, LayoutRectangle} from react-nativeexport function useLayout() {const [layout, setLayout] useState&l…...

如何在SpringCloud中使用Kafka Streams实现实时数据处理
使用Kafka Streams在Spring Cloud中实现实时数据处理可以帮助我们构建可扩展、高性能的实时数据处理应用。Kafka Streams是一个基于Kafka的流处理库,它可以用来处理流式数据,进行流式计算和转换操作。 下面将介绍如何在Spring Cloud中使用Kafka Streams实…...

InterSystems IRIS使用python pyodbc连接 windows环境,odbc驱动安装,DSN配置,数据源配置
一、创建的数据库和数据 SELECT 1SELECT $ZVERSIONCREATE TABLE MyApp.Person ( ID INT PRIMARY KEY, Name VARCHAR(100) NOT NULL, Age INT, Gender CHAR(1) );CREATE TABLE MyApp.Person2 ( ID INT PRIMARY KEY, Name VARCHAR(100) NOT NULL, Age INT, Gender CHA…...

JVM:运行时数据区
文章目录 一、总览二、程序计数器1、介绍2、程序计数器在运行中会出现内存溢出吗? 三、栈1、介绍2、栈帧的组成部分(1)局部变量表(2)操作数栈(3)帧数据(3)栈内存溢出&…...

spring-boot2.x整合Kafka步骤
1.pom依赖添加 <properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</ma…...

信创学习笔记(四),信创之数据库DB思维导图
创作不易 只因热爱!! 热衷分享,一起成长! “你的鼓励就是我努力付出的动力” 一. 信创学习回顾 1.信创内容 信创内容思维导图 2.信创之CPU芯片架构 信创之CPU芯片架构思维导图 3.信创之操作系统OS 信创之操作系统OS思维导图 二. 信创之国产数据库DB思维导图 …...

SCP 使用教程
SCP(Secure Copy Protocol)是一种通过加密的方式在本地主机和远程主机之间安全地传输文件的协议。它是基于SSH协议的扩展,允许用户在不同主机之间进行文件复制和传输,是Linux和Unix系统中常用的工具之一。本教程将详细介绍SCP的基…...

python自动化之用flask校验接口token(把token作为参数)
用到的库:flask 实现效果: 写一个接口,需要token正确才能登录 代码: # 导包 from flask import Flask,request,jsonify,json # 创建一个服务 appFlask(__name__) # post请求,路径:/query app.route(/query, met…...

旗晟巡检机器人的应用场景有哪些?
巡检机器人作为现代科技的杰出成果,已广泛应用于各个关键场景。从危险的工业现场到至关重要的基础设施,它们的身影无处不在。它们以精准、高效、不知疲倦的特性,担当起保障生产、守护安全的重任,为行业发展注入新的活力。那么&…...

vue2迁移到vue3注意点
vue2迁移到vue3注意点 1、插槽的修改 使用 #default , 以及加上template 模板 2、 类型的定义,以及路由,vue相关资源(ref, reactive,watch)的引入等 3、类装饰器 1)vue-class-component是vue官方库,作…...

使用windows批量解压和布局ImageNet ISLVRC2012数据集
使用的系统是windows,找到的解压命令很多都linux系统中的,为了能在windows系统下使用,因此下载Git这个软件,在其中的Git Bash中使用以下命令,因为Git Bash集成了很多linux的命令,方便我们的使用。 ImageNe…...

css实现每个小盒子占32%,超出就换行
代码 <div class"visitors"><visitor class"item" v-for"(user,index) in userArr" :key"user.id" :user"user" :index"index"></visitor></div><style lang"scss" scoped&…...

C++的链接指示extern “C“
目录 链接指示extern "C"A.What(概念)B.Why(extern "C"的作用)C.How (如何使用链接指示extern "C") 链接指示extern “C” A.What(概念) extern&quo…...

私域运营 组织架构
**揭秘私域社群运营的神秘面纱:角色与职能一网打尽!** 在私域社群运营的大舞台上,每个角色都扮演着不可或缺的重要角色。今天,就让我们一起揭开这个神秘世界的面纱,看看这些角色们是如何协同作战,共同创造…...

Netty HTTP
Netty 是一个高性能的异步事件驱动的网络应用程序框架,支持快速开发可维护的高性能协议服务器和客户端。它广泛应用于开发网络应用程序,如服务器和客户端协议的实现。Netty 提供了对多种传输类型的抽象,如 TCP/IP 和 UDP/IP 等,使…...

什么是边缘计算技术和边缘计算平台?
随着物联网、5G技术和人工智能的不断发展,数据的规模和种类也在快速增加。在这种背景下,传统的云计算模式面临着一些问题,例如延迟高、网络拥塞等,这些问题限制了数据的处理速度和效率,降低了用户的使用体验。为了解决…...
自然语言处理(NLP)——法国工程师IMT联盟 期末考试题
1. 问题1 (法语)En langue arabe lcrasante majorit des mots sont forms par des combinaisons de racines et de schmes. Dans ce mcanisme... (英语)In Arabic language the vast majority(十之八九) of…...

Linux内核编译安装 - Deepin,Debian系
为什么要自己编译内核 优点 定制化:你可以根据自己的硬件和需求配置内核,去掉不必要的模块,优化性能。性能优化:移除不需要的驱动程序和特性,减小内核体积,提高系统性能。最新特性和修复:获取…...

安全防御,防火墙配置NAT转换智能选举综合实验
目录: 一、实验拓扑图 二、实验需求 三、实验大致思路 四、实验步骤 1、防火墙的相关配置 2、ISP的配置 2.1 接口ip地址配置: 3、新增设备地址配置 4、多对多的NAT策略配置,但是要保存一个公网ip不能用来转换,使得办公区的…...

追溯源码观察HashMap底层原理
引言(Map的重要性) 从事Java的小伙伴,在面试的时候几乎都会被问到Map,Map都被盘包浆了。Map是键值集合,使用的场景有很多比如缓存、数据索引、数据去重等场景,在算法中也经常出现,因为在Map中获…...

为什么渲染农场渲染的是帧,而不是视频?
在3D动画产业的壮阔画卷中,渲染农场作为幕后英雄,以其庞大的计算能力支撑起无数视觉奇观的诞生。这些由高性能计算机集群构成的系统,通过独特的逐帧渲染策略,解锁了单机难以企及的创作自由与效率。本文将深入剖析这一策略背后的逻…...

windows镜像下载网站
一个专注于提供精校、完整、极致的Windows系统下载服务的仓储站,网站托管于Github。 网站:https://hellowindows.cn/ 下载方式:ED2k,BT,百度网盘 MSDN - 山己几子木,提供Windows 11、Windows 10等不同版本…...

ollama + fastgpt 搭建免费本地知识库
目录 1、ollama ollama的一些操作命令: 使用的方式: 2、fastgpt 快速部署: 修改配置: config.json: docker-compose.yml: 运行fastgpt: 访问OneApi: 添加令牌和渠道: 登陆fastgpt,创建知识库和应用 3、总结: 附录: 1. 11434是ollama的端口: 2. m3e 测…...