个人专栏:
《数据结构世界》
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
基本思想:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
假设tmp为我们要插入的牌,我们往[0, end] 这个有序区间中比较插入 (这里排升序)
代码实现:
上述是插入一次数据的过程,那如果我要对一个数组进行排序呢?很简单,先取数组的第二个元素进行插入排序,再取第三个……依此类推,循环上述过程,即可完成对数组的排序。
先简单写个打印数组函数
从上述分析可以看出,如果要排序的数据越接近有序,则使用直接插入排序的时间效率越高。
void PrintArray(int* a, int n)
{
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[i + 1];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
上述分析给予我们启发,如果我让要排序的数据先接近有序,再用直接插入排序,那不就皆大欢喜了吗?
而就是一位叫希尔的大佬,付诸了行动,希尔排序主要分为两步:
那么,什么叫做预排序呢?简单来说,分为两步:
举个例子:
假设 gap=3 ,那么我们就可以分成红蓝绿3组,每组间距为3。那么我们对每组进行插入排序,就会变成下图的情况。这样做可以节省时间,比如9原本到后面要9次,现在只用3次即可,因为每次都跨越gap步
有了前面直接插入排序的经验,我们就很容易实现一组的插入排序(比如红色),只用将间距从1变成gap即可。
那么我们要实现对每一组都排序,只要在外层套一层循环即可。因为有gap组,所以循环gap次。
先测试一下预排序,运行结果:
预排序功能基本实现,但是,其实我们还可以简化一下代码,减少一层循环,只需要小小的改动一下。
大家可以仔细思考其中的差异。其实,思想转变很简单,原本是红绿蓝一组一组排,现在是多组并排,但是时间效率是相等的。
完成了预排序的实现,我们再来看看gap的取值。
同时,我们发现gap越大,预排序速度越快,但越不接近有序;gap越小,预排序速度越慢,但越接近有序。
所以,gap的取值应该不断变化,n很大时,gap也大,预排序快速,而后面不断排序的过程中,为保证接近有序,gap应该逐步减小,最后gap为1,就是直接插入排序。
实现代码如下:
这里gap=gap/3 + 1中的+1,保证了最后一次gap一定为1,也就是循环中先预排序,最后一次直接插入排序。单个代码实现双重含义,简洁明了。
运行结果:
所以总结一下,希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有记录分成gap个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后,gap不断变小,重复上述分组和排序的工作。当到达gap=1时,所有记录在统一组内排好序。
void ShellSort(int* a, 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 = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
既然说希尔排序是直接插入排序的升级版,那能比它快多少呢?那我们就来实际测试一下(release版本)。
运行结果:
这样看来,是不是希尔排序比起直接插入排序优化了非常多啊?
时间复杂度分析:
《数据结构(C语言版)》 — 严蔚敏
《数据结构-用面相对象方法与C++描述》— 殷人昆
因为我们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:O(n1.25)到O(1.6 * n1.25)来算
基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
这里我们对前面的思想进行一点小小的优化,一次分别选出最小和最大的元素,分别放在起始和末尾。
这里因为频繁使用交换,所以独立写了一个交换函数。
上述只是一次遍历选择,而要排序完整个数组,只需在外层套上一层循环即可。
注意,要把mini和maxi放进循环中,因为begin和end会不断更新
运行结果:
但是,我们居然发现,排序结果居然不对!这是为什么呢?
其实,有一种特殊情况我们并没有考虑到,请看下图。
如果maxi和begin重叠,那么就会出问题,因为在maxi交换前,它的值已经被替换了。那我们应该怎么改呢?只需要更新一下maxi即可。
正确的运行结果:
时间复杂度分析:
void Swap(int* e1, int* e2)
{
int tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin, maxi = end;
for (int i = begin; i <= end; i++)
{
if (a[mini] > a[i])
{
mini = i;
}
if (a[maxi] < a[i])
{
maxi = i;
}
}
Swap(&a[begin], &a[mini]);
if (maxi == begin)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
关于堆排序,在往期的【数据结构】【版本2.0】【树形深渊】——二叉树入侵 有详细讲解,这里简单归类一下,不再讲具体实现。
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是
通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
时间复杂度分析:
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
//升序 -- 建大堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
既然堆排序是选择排序中的强者,希尔排序又是插入排序的精英,谁能更胜一筹呢?直接插入排序与直接选择排序相比,谁又能做第二把交椅呢?请看下面测试(release版本)。
数据量为10w时:
数据量为100w时
经过上述代码测试,我们可以轻易发现希尔排序和堆排序在伯仲之间,而在O(N2)的排序算法中插入排序还是有不错的适应性的,但是选择排序就效率太低了,上不了台面。
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
冒泡排序是最简单的排序,而且方便理解,具有教学意义,也是我们学的第一个排序算法。早在往期的指针章节 不允许你还不了解指针的那些事(二)已经详细讲解,这里也是简单归类一下。
冒泡排序的核心思想就是:两两相邻的元素进行比较
运行结果:
时间复杂度分析:
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n; i++)
{
bool exchange = false;
for (int j = 0; j < n - i - 1; j++)
{
if (a[j] > a[j + 1])
{
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
exchange = true;
}
}
if (exchange == false)
{
break;
}
}
}
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
这里我们可以用分治的思想,来递归实现。
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort3(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,所以快速排序叫做二叉树结构的交换排序方法。
而实现按基准值切割左右子序列,这里有三种实现版本:
这个方法是最初hoare实现快速排序的版本。
这里有几个值得注意的点:
相遇无非两种情况:L遇R,R遇L
所以,这样就证明了左边做key,右边先走,就保障了相遇的值一定小于等于key。
//Hoare版本
int PartSort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])
{
right--;
}
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
但是由于hoare版本有很多需要考虑周全的地方,所以又流传出了一种简化的版本,比较便于理解。
主要思路:
有没有发现,挖坑法是hoare的改版,能避免我们去讨论一些细节,更加易于理解。
//挖坑法
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int hole = left;
while (left < right)
{
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
这种方法和前两种方法不同,是一种全新的思路,比较抽象,但也是最简洁的一种写法。
主要思路:
分析:
当cur找到比key小的值,prev向后一格(指向比key大的值),再与cur交换,相当于翻滚式的将比key大的值向右推,同时把小的换到左边
最后,cur越界时,prev所在的值(经过前面的交换)肯定比key小,所以与key交换,则完成了左边全比key小,右边全比key大的子序列分割。
这里代码实现时,加上了额外判断,使prev和cur相等时不进行交换
//前后指针法
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
上述三种思路只是实现方式的不同,但是实际快速排序的效率是相等的。
运行结果:
时间复杂度分析:
那么,我们如何尽可能避免最坏情况的发生呢?这里就可以用一种方法进行优化——三数取中法
int GetMidIndex(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[right] < a[left])
{
return left;
}
else
{
return right;
}
}
else//a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[right] > a[left])
{
return left;
}
else
{
return right;
}
}
}
当然,递归还有相应的缺陷,就是当数据量太大时,有栈溢出的风险。所以我们应该掌握一下非递归实现的方法——用栈模拟递归
因为栈是动态开辟在堆区的,而堆区空间远远比栈区大,基本没有溢出风险。
因为C语言没有对应的库,所以我们将之前写好的栈拷贝过来。
主要思路:
运行结果:
void QuickSortNonR(int* a, int begin, int end)
{
ST st;
STInit(&st);
STPush(&st, end);
STPush(&st, begin);
while (!STEmpty(&st))
{
int left = STTop(&st);
STPop(&st);
int right = STTop(&st);
STPop(&st);
int keyi = PartSort3(a, left, right);
if (keyi + 1 < right)
{
STPush(&st, right);
STPush(&st, keyi + 1);
}
if (left < keyi - 1)
{
STPush(&st, keyi - 1);
STPush(&st, left);
}
}
STDestroy(&st);
}
既然冒泡排序和插入排序时间复杂度最坏都为O(N2),最好都为O(N),谁能更胜一筹呢?还有刚刚实现的快速排序,比起希尔排序,堆排序,是否能力压群雄呢?请看下面测试(release版本)。
数据量为10w时:
我们可以发现,同为O(N2)量级的插入排序,选择排序和冒泡排序中,插入排序对数据适应性最强,综合最优;而冒泡排序虽然在数据随机无序比选择排序略低,但是在数据接近有序或有序的情况下,也有较强的适应性;但是选择排序却对数据没有任何适应性。
数据量为100w时:
因为冒泡排序和选择排序太慢了,所以这里它们将退出舞台。
我们发现,在100w数据量下,插入排序已经非常吃力,在数据无序随机的情况下,快速排序确实更优一些;而在数据有序的情况下,插入排序以及其进阶的希尔排序,对数据有序或接近有序,有着天然的极强适应性(因为插入排序的思想,便是插入已经有序的数组)
数据量为1000w时:
这时插入排序也更不上时代的步伐了,必须退居二线。
数据量为1亿时:
让我们再疯狂一点吧!
这时,我们居然发现,快速排序效率居然急剧下降!!!这是,为什么呢?
其实,快速排序还有一个致命的弱点——无法快速处理含有大量相同元素的数据,而我们产生的随机数是有范围的(0—3w),所以总数据量非常大的情况下,数据中就会产生大量相同元素。那怎么解决呢?请看下面优化。
分析:当数据中含有大量重复元素时,我们的三数取中法就会失效(因为取出的三数极有可能相等)。这样,我们又会面临最坏情况,快速排序将会退化成冒泡排序。
优化思路:
看图分析:
这样,经过上图的分析,是不是很清晰地理解了该优化算法的运作?其实,我们可以发现,该方法是hoare版本(左右指针法)和前后指针法的结合体,取二者之精华。
进一步理解,left 和 right 最后锁定了值全为key的区间。
递归的子区间划分:[begin, left - 1] [left, right] [right + 1, end]
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int left = begin;
int right = end;
int cur = left + 1;
int mid = GetMidIndex(a, left, right);
Swap(&a[left], &a[mid]);
int key = a[left];
while (cur <= right)
{
if (a[cur] < key)
{
Swap(&a[cur++], &a[left++]);
}
else if (a[cur] > key)
{
Swap(&a[cur], &a[right--]);
}
else
{
cur++;
}
}
QuickSort(a, begin, left - 1);
QuickSort(a, right + 1, end);
}
最后,我们再更改一下三数取中法,让其不再固定取中,而是随机取中。
再来测试1亿的数据量:
我们发现,快速排序又遥遥领先啦!
基本思想:归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序首先需要动态开辟n个元素大小的tmp空间,因为空间只用开一次,所以不能在本函数内进行递归,那么就写一个子函数进行递归调用。
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
归并排序递归的思路,和二叉树后序遍历规则十分相似,也是利用二叉树结构的排序。
运行结果:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin == end)
{
return;
}
int mid = (begin + end) / 2;
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
_MergeSort(a, begin1, end1, tmp);
_MergeSort(a, begin2, end2, tmp);
int i = 0;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + begin, tmp, sizeof(int) * i);
}
大体思路:
gap=1时
一组归并拷贝完,则起始位置+=gap,进行第二组……直到将所有元素,两两归并,拷贝回原数组
gap=2时
gap=4时
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
int j = 0;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
memcpy(a + i, tmp, sizeof(int) * j);
}
gap *= 2;
}
free(tmp);
}
但是我们发现,上述代码只有在2的次方数个数据才有用,其他个数时(比如10个)就会程序崩溃或者随机值,这是为什么呢?其实这是因为数组访问越界,让我们打印出边界值来分析一下。
经过分析,这里一共有三种情况的越界。
所以,针对以上情况,我们要进行修正。
正确的运行结果:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
if (end1 >= n || begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
int j = 0;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
memcpy(a + i, tmp, sizeof(int) * j);
}
gap *= 2;
}
free(tmp);
}
其实我们发现,当区间长度比较小时,可以不用归并排序,而用其他排序则能更快完成数据的排序。而从基本排序里选择,插入排序便是我们的首选。这种优化方式称为小区间优化
有人可能会疑惑,觉得区间长度怎么才小于10,如果再大一点,不是排序的更快速吗?
其实,小于10的不采用归并递归,减少了最后三层的递归调用。(如下图)
测试1亿的数据量:
归并排序是不是快了不少?
归并排序主要思考的是解决硬盘中的外排序问题。
之前我们讲的所有排序都可以解决在内存中的排序问题,这种称为内排序。而归并排序不仅能用于内排序,还能用于外排序。
那么,为什么要使用外排序呢?让我们来思考一个问题:如果要对100亿个整数进行排序,应该怎么办?(要知道100亿整数,就是400亿字节,换算过来大约是40G,而你的内存显然没有那么大)
这个时候,我们就只能在硬盘中进行数据排序。而在硬盘中有一个致命的缺陷,那就是文件只能顺序读取,不支持下标随机访问。这才导致之前的种种排序算法都无能为力,而只有归并排序的非递归可以做到(按顺序从小到大依次归并)。
那么稳定性有什么意义呢?
假设考试要选前三名,但是有4个分数相同的,那么先交卷的排名就高。所以,这时候就需要稳定的排序算法来完成分数的排序,保证排序完,先后交卷的顺序不变。
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(N2) | O(N) | O(N2) | O(1) | 稳定 |
直接选择排序 | O(N2) | O(N2) | O(N2) | O(1) | 不稳定 |
直接插入排序 | O(N2) | O(N) | O(N2) | O(1) | 稳定 |
希尔排序 | O(N) ~ O(N*log2N) | O(N1.3) | O(N2) | O(1) | 不稳定 |
堆排序 | O(N*log2N) | O(N*log2N) | O(N*log2N) | O(1) | 不稳定 |
归并排序 | O(N*log2N) | O(N*log2N) | O(N*log2N) | O(N) | 稳定 |
快速排序 | O(N*log2N) | O(N*log2N) | O(N2) | O(log2N) ~ O(N) | 不稳定 |
看到这里了还不给博主扣个:
⛳️ 点赞☀️收藏 ⭐️ 关注!
❤️
拜托拜托这个真的很重要!
你们的点赞就是博主更新最大的动力!
有问题可以评论或者私信呢秒回哦。
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"sort.h"
void TestInsertSort()
{
int a[] = { 5,9,8,4,6,3,1,5,6,4,8 };
int sz = sizeof(a) / sizeof(a[0]);
PrintArray(a, sz);
InsertSort(a, sz);
PrintArray(a, sz);
}
void TestShellSort()
{
int a[] = { 5,9,8,4,6,3,1,5,6,4,8 };
int sz = sizeof(a) / sizeof(a[0]);
PrintArray(a, sz);
ShellSort(a, sz);
PrintArray(a, sz);
}
void TestSelectSort()
{
int a[] = { 5,9,8,4,6,3,1,5,6,4,8 };
int sz = sizeof(a) / sizeof(a[0]);
PrintArray(a, sz);
SelectSort(a, sz);
PrintArray(a, sz);
}
void TestHeapSort()
{
int a[] = { 5,9,8,4,6,3,1,5,6,4,8 };
int sz = sizeof(a) / sizeof(a[0]);
PrintArray(a, sz);
HeapSort(a, sz);
PrintArray(a, sz);
}
void TestBubbleSort()
{
int a[] = { 5,9,8,4,6,3,1,5,6,4,8 };
int sz = sizeof(a) / sizeof(a[0]);
PrintArray(a, sz);
BubbleSort(a, sz);
PrintArray(a, sz);
}
void TestQuickSort()
{
int a[] = { 5,9,8,4,6,3,1,5,6,4,8 };
int sz = sizeof(a) / sizeof(a[0]);
PrintArray(a, sz);
QuickSort(a, 0, sz-1);
//QuickSortNonR(a, 0, sz - 1);
PrintArray(a, sz);
}
void TestMergeSort()
{
int a[] = { 5,9,8,4,6,3,1,5,6,4,8 };
int sz = sizeof(a) / sizeof(a[0]);
PrintArray(a, sz);
MergeSort(a, sz);
//MergeSortNonR(a, sz);
PrintArray(a, sz);
}
void TestOP()
{
srand((unsigned int)time(NULL));
const int N = 100000000;
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();
//BubbleSort(a5, N);
int end5 = clock();
int begin6 = clock();
QuickSort(a6, 0, N-1);
int end6 = clock();
int begin7 = clock();
MergeSort(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("BubbleSort:%d\n", end5 - begin5);
printf("QuickSort:%d\n", end6 - begin6);
printf("MergeSort:%d\n", end7 - begin7);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
free(a7);
}
int main()
{
srand((unsigned int)time(NULL));
//TestInsertSort();
//TestShellSort();
//TestSelectSort();
//TestHeapSort();
//TestBubbleSort();
//TestQuickSort();
//TestMergeSort();
TestOP();
return 0;
}