快速排序算法详解:hoare、挖坑法、lomuto前后指针与非递归实现
目录
- 前言
- 一、选择排序
- 1.1 直接选择排序
- 二、交换排序
- 2.1 快速排序
- 2.1.1 hoare版本
- 2.1.2 挖坑法
- 2.1.3 lomuto前后指针
- 2.1.4 非递归版本
- 三、完整源码
- 结语


前言
大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~
一、选择排序
选择排序的基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完
1.1 直接选择排序

如图,对该数组循环一遍,找最小的值(2),将其放在下标为0的位置


剩下数据的最小值为3,将其放到下标为1的位置

接下来继续在剩下的数据中找最小值,依此类推

最后在剩下的数据中找最小的值(9),将其放到最后一个位置


该算法的时间复杂度很好看,就是O(n2),该算法的时间复杂度是无法优化的,但是可以排序一次既找到最小值(最小值依次从前往后放),又找到最大值(最大值依次从后向前放)
这样原本外层循环找最小值要循环n次数,同时加入找最大值后,n就变为n/2

这是对10w个数据找最小值所需要花费的时间,直接选择排序需要2605毫秒
这里定义begin来指向最小的数据要保存的位置,end指向最大的数据要保存的位置,

接下来的一次循环中就要既找最大,也找最小,找到后,最小值和begin位置交换,最大值和end交换

接下来begin加加,end减减,再在begin和end的范围内找最大和最小值


接下来依旧最小和begin位置交换,最大值和end交换

交换之后begin加加,end减减

接下来在下标2和3的位置内找最小值和最大值

交换之后位置不变,begin再加加,end再减减

begin大于end时,说明无法再进行交换了
根据图示推理代码实现如下:


然而该代码是有bug的,根据代码复推逻辑

找到最小最大值后和begin,end进行交换


接下来在3到5的范围内找最大值和最小值

和begin,end交换


再在5到7的范围内找最大值和最小值

和begin,end交换


此时新的范围内,最小值为4,最大值为6

然后先把最小值(4)放在begin的位置(6),最大值此时变为了4,放到end的位置(6),这就相当于没有交换了,这就是程序bug所在
这里直接选择排序最大值放到了begin的位置,最小值若直接放在begin会把最大值替换掉,所以这里还需要特殊处理
当最大值在begin的位置时候,就让最大值(maxi)先指向最小值(mini)的位置

当最小值和begin交换的时候,6就到了此时maxi的位置

之后maxi在和end交换,6还是在end的位置

交换之后继续begin加加,end减减,begin等于end的时候就不用交换了,跳出循环



直接插入排序,选择排序,冒泡排序的时间复杂度都为O(n2)
直接选择排序就是O(n2),没有优化的成分,无论数组是否有序,都会在范围内找最大值和最小值。而冒泡排序还存在最好的情况,如果数组有序,时间复杂度为O(n)
所以冒泡排序和直接选择排序的性能是最差的
二、交换排序
2.1 快速排序
2.1.1 hoare版本
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
如图数组中有很多乱序的数据,现在找一个基准值为6,6通过一定的排序算法放到当前数组中的指定位置,其他数据按照上面规则放在其左右

然后按照基准值把数组一分为二,接着对这两个序列中找基准值,然后经过一定排序算法,按规则将值放到对应的位置

根据基准值,再把数组一分为二

至此2,3,6,7,8。变为一个有序的数组,这样不断根据基准值一分为二,只不过图中最后两个序列没有右序列,这就是快速排序是一种二叉树结构的交换排序思想,通过不断找基准值,把基准值放在该放的位置,而其他的数据不用管

这样不断一分为二是通过二叉树递归的结构实现的,要找基准值,就要先确定对哪个范围找基准值,如图只要left<right就可以找基准值,在2,3序列中基准值为3,一分为二,左序列就是left和right,此时left和right指向同一个数据的下标,只有一个数据就没有办法一分为二了

基本代码框架如图

接下来就要找基准值,并把基准值放到指定的位置上,依旧是给定一个数组

这里先随便指定数组最左侧的值为基准值,定义两个变量left和right,分别指向数组最左边和最右边,然后让left指向基准值右边一个数据,让其他数据依次和基准值比较。然后让right从右向左走,找比基准值小的数据,left从左向右走,找比基准值大的数据

right此时指向的数据3比基准值要小,right就不动了,left循环++找比基准值要大的值,直到left指向7为止

此时若依旧是left<=right,就让left和right交换,这样大的数据就来到了右边,小的数据在左边

交换之后left++,right减减

此时left和right相等,在相等的情况下,right继续从右向左找比基准值要小的值,此时left准备走,但是left此时指向9,比right大了。此刻left就不循环向右找比基准值要大的值了

最后一步,right指向的值和基准值进行交换

此时基准值为6,对应下标为3,其左边的值比基准值要小,其右边的值比基准值要大,就这样的排序之后,6一定在下标为3的位置
继续重复前面的步骤,按照基准值将序列一分为二


继续重复前面的步骤找基准值,right从右向左走找比基准值要小的数据,就是2。left从左向右走找比基准值要大的数据,直到left大于right且越界。

继续拿基准值和right交换

基准值就到了下标为2的位置,且其左边的值比基准值要小,基准值右边没有值
再看右子序列,继续找基准值

left指向假定基准值右边的一个数据后

left和right相等,right从右向左找比基准值要小的数据(7),left从左向右找比基准值要大的数据,直到left>right且越界,就结束寻找

接下来让right和基准值交换,此时基准值为9,其下标为5,且其左边都是比基准值要小的数据

依旧将序列一分为二,只有左序列没有右序列,该左序列left和right相等,前面的代码中left等于right便直接返回,不再一分为二

左边的序列按照基准值一分为二

接下来right从右向左找比基准值要小的值(1),left从左向右找比基准值要大的值,left大于right且left越界,停止寻找,right此时指向的位置就是基准值的位置

然后right和基准值交换

此时基准值为2,基准值的左边比基准值要小,继续根据基准值一分为二,之后只有一个数据(1),该数据范围的left和right相等,直接返回

至此,数据放到了下标为0的位置

接下来把找基准值的代码单独封装一个方法


由于该算法是个递归结构,根据递归时间复杂度 = 单词递归时间复杂度 * 递归次数
先看外层代码是没有循环的,进入找基准值代码后,仅管有两次while循环,但是内部找基准值的逻辑是left从左往右走,right从右向左走,只是对数组循环一遍,所以找基准值的时间复杂度为N(单次递归时间复杂度),而递归的次数(logn)是该二叉树的高度(左侧函数栈帧销毁完之后才会递归数组右序列,该高度是指在最多的时候会占用多少函数栈帧),补充:满二叉树的总结点个数n = 2k-1,现在已知结点个数(数组数据个数),所以k(二叉树的高度)= log以2为底(n+1)的对数,换成时间复杂度就是logn
快速排序的时间复杂度也有最好最差的情况,单次递归的时间复杂度为n不会改变,因为只会对数组遍历一遍。而对于递归的次数,每次并不一定会一分为二
现在有一个数组,对其找基准值,找完基准之后,该基准值放到下标为0的位置。若按照基准值一分为二,下次递归的时候就递归n-1个数据找基准值,基准值依旧在最左边的位置,按照基准值一分为二之后,再对剩下的n-2个数据找基准值,不断递归…若按照这样的顺序去递归的话,会递归n次,只有一个数据的时候就不再递归了。此时递归次数的复杂度就不是logn了,logn是在理想情况下,找到基准值之后,基准值恰好在数组的中间


在上面的代码中,right从右向左找比基准值要小值,若right和基准值相等,该代码的处理方式是不进入循环,right此时指向和基准值相等的位置等待交换。这样处理的方式是因为若加上等于号,会在下面的场景下出现死循环

right此时从右向左找比基准值要小的值,一直找不到持续减减,直到right来到left的左侧,就不能再进入while循环

跳出while循环进行交换,交换基准值和right位置,此时基准值在下标为0的位置,之后按照基准值一分为二,对n-1个数据继续找基准值,然而下一次和这次的结果是一样的


这就是不用等于的原因,若用等于且数组中的数据重复,循环的时间复杂度就为N2,

以上代码的一些细节写法都在这些场景下可以体现

快速排序的时间复杂度为nlogn,这是10w个数据时快速排序排10w个数据要花费的时间,这是一种平均的情况,当数组已经有序时就是最差的情况(找第一个数据为基准值,下一次对n-1个数据递归,再下一次对n-2个数据递归,递归的次数为n,这种情况下时间复杂度为n2),其花费时间如下图

可以看到比直接插入排序(时间复杂度为n2)的时间还长
2.1.2 挖坑法
思路:
创建左右指针。首先从右向左找出比基准值小的数据,找到后立即放入左边坑中,当前位置变为新的“坑”,然后从左向右找出比基准值大的数据,找到后立即放入右边坑中,当前位置变为新的“坑”,结束循环后将最开始存储的分界值放入当前的“坑”中,返回当前“坑”的下标(即分界值下标)
初始情况下定义一个坑位,指向数组最左边的数据,初始情况下再定义left和right

此时把坑中的6拿出来,表示该值是基准值,然后right从右向左找比基准值要小的值,然后将该值填到坑位中

right位置的数据拿过去填坑后,right位置就变成了新的坑位

然后left从左向右找比基准值要大的值,将找到的值填到坑中,新的坑就是letf指向的位置

然后right继续找比基准值要小的值

拿4填坑位

此时right指向的位置又是新的坑位,然后left++找比基准值要大的值

拿9填了坑位后,left指向的位置就是新的坑位了,然后right继续从右向左找比基准值要小的数据

之后拿3填坑,right指向的位置为新的坑位

left继续从左往右找比基准值小的数据,走到下一个位置left和right重合,由于right指向的位置就是坑,就无法再和基准值比较了,所以当left = right的时候,该坑位就是基准值要放的位置

此时基准值6的左边的值都比基准值要小,6的右边的值都比基准值要大

在前面的hoare版本中,left初始的位置在下标为1的位置,但是在挖坑法中就不可以了



若left初始指向下标为1的位置,left和right相等就不能进入循环了,而是直接将基准值给坑,基准值在6这个位置,而6的右侧比基准值要小,显然是不对的
2.1.3 lomuto前后指针
创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边
在一个乱序的数组中创建两个变量名,prev是cur的前一个数据,cur表示当前数据,依旧定义数组中最左侧的数据为基准值,整体思路如下,和双指针法有些类似:


cur指向的数据比基准值小,++prev,cur和prev交换值,不变

++cur

cur指向的数据比基准值小,++prev,cur和prev交换值,不变

++cur

cur指向的数据不必基准值小,++cur

此时cur指向的数据比基准值小,++prev

cur和prev交换值,++cur

此时cur指向的数据比基准值小,++prev

cur和prev交换值,++cur

此时cur指向的数据比基准值小,++prev

cur和prev交换值,++cur

cur指向的数据不比基准值要小,++cur

cur指向的数据不比基准值要小,++cur

此时cur越界,prev指向的位置就是基准值的位置,接下来让prev指向的值和基准值交换

基准值左边的值都比基准值要小,基准值右边的值都比基准值要大


2.1.4 非递归版本

该数组是通过第一遍的lomuto双指针法找到的基准值,下面是还是用二叉树的递归结构不断对基准值一分为二得到的有序数组

若不用递归,想实现不断划分左右两个序列就需要借助数据结构栈,利用找完基准值划分的左右两个序列,将这两个序列存起来,这样才能够知道后续该对数组中哪个区间实现找基准值,排序
刚开始创建一个空栈,要对第一个序列进行排序,要把该区间的left和right存起来,这样栈就不为空了,若栈为空,便认为当前数组中所有的序列都排完了。right和left哪个先入栈都可以,这里例如先让right入栈,随后left

此时栈中存储了两个整数数据,栈不为空,循环两次取栈顶一次,出栈一次

取到left和right之后,对0到5这个区间利用lumoto双指针法找基准值(6,下标为3的位置),利用基准值把序列一分为二,也确定了左右序列的范围(分别用left和right确定范围),接下来让两个序列入栈(首先让右序列的left和right入栈)

然后让左序列入栈

接下来对左序列找基准值,此时栈不为空,循环两次取栈顶一次,出栈一次

此时要排序和找基准值的left和right是0,2,找到基准值后,基准值的下标为2,再划分左右两个区间(右区间非有效,所以不入栈),把按照基准值划分的左序列入栈(right先入栈,left后入栈)

继续重复前面的步骤,只要栈不为空,循环两次取栈顶一次,出栈一次,这次得到的区间0到1,在该区间排序,找基准值,找到基准值2后(下标为1),按照基准值一分为二,若序列中只有一个数据(left=right的时候),就不需要入栈了,右序列不是一个有效的区间(2,1),更不用入栈了

此时左边的序列就排完了,接下来4到5这个区间还没有找基准值排序,此时栈不为空,继续循环两次取栈顶一次,出栈一次

得到的范围是4到5,接下来对数组4到5这个范围找基准值排序,这里基准值对应的下标为5,按照基准值划分左右两个序列,左边的序列区间是[4,4],右序列是[6,5](非有效区间),此时只有一个数据,便不用入栈了

此时栈为空,就不能取栈顶了,至此快速排序的非递归版本结束了,此时数组也有序了



三、完整源码
Sort.h
#pragma once
#include<stdio.h>
#include<time.h>
#include<stdlib.h>//插入排序
//1)直接插入排序n^2
void InsertSort(int* arr, int n);
//2)希尔排序n^1.3
void ShellSort(int* arr, int n);//选择排序
//1)直接选择排序n^2
void SelectSort(int* arr, int n);
//2)堆排序 nlogn
void HeapSort(int* arr, int n);//交换排序
//1)冒泡排序
void BubbleSort(int* arr, int n);
//2)快速排序
void QuickSort(int* arr, int left, int right);
//非递归版本快速排序
void QuickSortNorR(int* arr, int left, int right);
Stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>//定义栈的结构
typedef int STDataType;
typedef struct Stack {STDataType* arr;int top; //指向栈顶的位置---刚好就是栈中有效数据个数int capacity;//栈的空间大小
}ST;//初始化
void STInit(ST* ps);
//销毁
void STDesTroy(ST* ps);//入栈——栈顶
void STPush(ST* ps, STDataType x);
//出栈———栈顶
void STPop(ST* ps);
//取栈顶元素
STDataType STTop(ST* ps);//栈是否为空
bool STEmpty(ST* ps);
//获取栈中有效元素个数
int STSize(ST* ps);
Sort.c
#define _CRT_SECURE_NO_WARNINGS 666
#include"Sort.h"
#include"Stack.h"//1)直接插入排序
void InsertSort(int* arr, int n)
{for (int i = 0; i < n - 1; i++){int end = i;int tmp = arr[end + 1];while (end >= 0){if (arr[end] > tmp){arr[end + 1] = arr[end];end--;}else {break;}}arr[end + 1] = tmp;}
}//希尔排序
void ShellSort(int* arr, int n)
{int gap = n;while (gap > 1){gap = gap / 3 + 1;for (int i = 0; i < n - gap; i++){int end = i;//当前组的下一个位置数据int tmp = arr[end + gap];while (end >= 0){if (arr[end] > tmp){arr[end + gap] = arr[end];end -= gap;}else {break;}}arr[end + gap] = tmp;}}
}void Swap(int* x, int* y)
{int tmp = *x;*x = *y;*y = tmp;
}//向下调整算法 logn
void AdjustDown(int* arr, int parent, int n)
{int child = parent * 2 + 1;while (child < n){//建大堆:<//建小堆: >if (child + 1 < n && arr[child] < arr[child + 1]){child++;}//孩子和父亲比较//建大堆:>//建小堆:<if (arr[child] > arr[parent]){Swap(&arr[child], &arr[parent]);parent = child;child = parent * 2 + 1;}else {break;}}
}//堆排序————使用的是堆结构的思想 n * logn
void HeapSort(int* arr, int n)
{//向下调整算法——建堆nfor (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(arr, i, n);}////向上调整算法建堆n*logn//for (int i = 0; i < n; i++)//{// AdjustUp(arr, i);//}//n*lognint end = n - 1;while (end > 0){Swap(&arr[0], &arr[end]);AdjustDown(arr, 0, end);//lognend--;}
}//冒泡排序
void BubbleSort(int* arr, int n)
{int exchange = 0;for (int i = 0; i < n; i++){for (int j = 0; j < n - i - 1; j++){if (arr[j] > arr[j + 1]){exchange = 1;Swap(&arr[j], &arr[j + 1]);}}//经历一次循环exchange没有改变,没有发生交换//说明数组本身有序,此时时间复杂度达到最好(n)if (exchange == 0){break;}}
}////1)直接选择排序
//void SelectSort(int* arr, int n)
//{
// //找最小值
// for (int i = 0; i < n; i++)
// {
// int mini = i;
// for (int j = i + 1; j < n; j++)
// {
// if (arr[j] < arr[mini])
// {
// mini = j;
// }
// }
// //遍历完一遍后找到了最小值mini
// //将其放到下标为0的位置...
// Swap(&arr[mini], &arr[i]);
// }
//}//1)直接选择排序
void SelectSort(int* arr, int n)
{int begin = 0, end = n - 1;while (begin < end){//初始情况假定最大最小在下标为0的位置int mini = begin;int maxi = begin;//在begin和end的范围内找最大和最小for (int i = begin + 1; i <= end; i++){if (arr[i] < arr[mini]){mini = i;}if (arr[i] > arr[maxi]){maxi = i;}}//特殊情况处理,当maxi和begin在同一位置时if (maxi == begin){maxi = mini;}//找到最小最大值,和begin和end位置交换Swap(&arr[begin], &arr[mini]);Swap(&arr[end], &arr[maxi]);begin++;end--;}
}//找基准值 hoare版本
int _QuickSort1(int* arr, int left, int right)
{int keyi = left; //初始基准值在数组最左边的位置left++; //初始left在基准值右边while (left <= right){//right:从右往左走找比基准值小的值while (left <= right && arr[right] > arr[keyi]){right--;}//left:从左往右找比基准值大的值while (left <= right && arr[left] < arr[keyi]){left++;}//left和right交换if (left <= right){Swap(&arr[left++], &arr[right--]);}}//right的位置就是基准值的位置Swap(&arr[keyi], &arr[right]);//返回当前基准值的下标return right;
}//找基准值 挖坑法
int _QuickSort2(int* arr, int left, int right)
{int hole = left;int key = arr[hole];while (left < right){//right 从右向左找比基准值小的值while (left<right && arr[right]>key){--right;}arr[hole] = arr[right];hole = right;//left 从左向右找比基准值大的值while (left < right && arr[left] < key){++left;}arr[hole] = arr[left];hole = left;}arr[hole] = key;return hole;
}//找基准值 lumoto双指针法
int _QuickSort3(int* arr, int left, int right)
{int prev = left, cur = prev + 1;int keyi = left;while (cur <= right){//cur数据和基准值比较if (arr[cur] < arr[keyi] && ++prev != cur){Swap(&arr[cur], &arr[prev]);}//cur指向的数据不比基准值要小或prev++之后和cur指向同一个位置cur++;}Swap(&arr[keyi], &arr[prev]);return prev;
}//2)快速排序
void QuickSort(int* arr, int left, int right)
{if (left >= right){return;}//找基准值int keyi = _QuickSort3(arr, left, right);//左序列[left,keyi-1] 右序列[keyi+1,right]QuickSort(arr, left, keyi - 1);QuickSort(arr, keyi + 1, right);
}//非递归版本的快速排序---栈
void QuickSortNorR(int* arr, int left, int right)
{ST st;STInit(&st);STPush(&st, right);STPush(&st, left);while (!STEmpty(&st)){//取栈顶两次int begin = STTop(&st);STPop(&st);int end = STTop(&st);STPop(&st);//[begin,end]---找基准值int keyi = begin;int prev = begin, cur = prev + 1;while (cur <= end){if (arr[cur] < arr[keyi] && ++prev != cur){Swap(&arr[prev], &arr[cur]);}++cur;}Swap(&arr[prev], &arr[keyi]);keyi = prev;//begin keyi end//左序列[begin,keyi-1]//右序列[keyi+1,end]if (keyi + 1 < end){STPush(&st, end);STPush(&st, keyi + 1);}if (begin < keyi - 1){STPush(&st, keyi - 1);STPush(&st, begin);}}STDesTroy(&st);
}
Stack.c
#include"Stack.h"//初始化
void STInit(ST* ps)
{ps->arr = NULL;ps->top = ps->capacity = 0;}//销毁
void STDesTroy(ST* ps)
{if(ps->arr)free(ps->arr);ps->arr = NULL;ps->top = ps->capacity = 0;
}//入栈——栈顶
void STPush(ST* ps, STDataType x)
{assert(ps);//判断空间是否足够if (ps->top == ps->capacity){//增容int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;STDataType* tmp = (STDataType*)realloc(ps->arr, newCapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail!");exit(1);}ps->arr = tmp;ps->capacity = newCapacity;}//空间足够ps->arr[ps->top++] = x;
}
//栈是否为空
bool STEmpty(ST* ps)
{assert(ps);return ps->top == 0;
}
//出栈———栈顶
void STPop(ST* ps)
{assert(!STEmpty(ps));ps->top--;
}//取栈顶元素
STDataType STTop(ST* ps)
{assert(!STEmpty(ps));return ps->arr[ps->top - 1];
}//获取栈中有效元素个数
int STSize(ST* ps)
{assert(ps);return ps->top;
}
test.c
#define _CRT_SECURE_NO_WARNINGS 666
#include"Sort.h"void printArr(int* arr, int n)
{for (int i = 0; i < n; i++){printf("%d ", arr[i]);}printf("\n");
}void test01()
{//int a[] = { 5,3,9,6,2,4 };//int a[] = { 5,3,9,6,2,4,7,1,8 };int a[] = { 6,1,2,7,9,3 };//用整个数组的大小/单个数据的大小 = 数组中数据个数int n = sizeof(a) / sizeof(a[0]);printf("排序之前:");printArr(a, n);//InsertSort(a, n);//ShellSort(a, n);//SelectSort(a, n);//QuickSort(a, 0, n - 1);QuickSortNorR(a, 0, n - 1);printf("排序之后:");printArr(a, n);
}// 测试排序的性能对⽐
void TestOP()
{srand(time(0));const int N = 100000;int* a1 = (int*)malloc(sizeof(int) * N);int* a2 = (int*)malloc(sizeof(int) * N);int* a3 = (int*)malloc(sizeof(int) * N);int* a4 = (int*)malloc(sizeof(int) * N);int* a5 = (int*)malloc(sizeof(int) * N);int* a6 = (int*)malloc(sizeof(int) * N);int* a7 = (int*)malloc(sizeof(int) * N);for (int i = 0; i < N; ++i){a1[i] = rand();a2[i] = a1[i];a3[i] = a1[i];a4[i] = a1[i];a5[i] = a1[i];a6[i] = a1[i];a7[i] = a1[i];}//在执行排序之前打印程序执行到此处的时间int begin1 = clock();InsertSort(a1, N);//在执行排序之后打印程序执行到此处的时间int end1 = clock();int begin2 = clock();ShellSort(a2, N);int end2 = clock();int begin3 = clock();SelectSort(a3, N);int end3 = clock();int begin4 = clock();HeapSort(a4, N);int end4 = clock();int begin5 = clock();QuickSort(a4, 0, N - 1);int end5 = clock();int begin6 = clock();//MergeSort(a6, N);int end6 = clock();int begin7 = clock();BubbleSort(a7, N);int end7 = clock();printf("InsertSort:%d\n", end1 - begin1);printf("ShellSort:%d\n", end2 - begin2);printf("SelectSort:%d\n", end3 - begin3);printf("HeapSort:%d\n", end4 - begin4);printf("QuickSort:%d\n", end5 - begin5);printf("MergeSort:%d\n", end6 - begin6);printf("BubbleSort:%d\n", end7 - begin7);free(a1);free(a2);free(a3);free(a4);free(a5);free(a6);free(a7);
}int main()
{test01();//TestOP();return 0;
}
结语

