排序分类
排序分为内部排序和外部排序两种
内部排序
内部排序通常为常见的七种算法,本实验要求掌握选择排序、冒泡排序、合并排序、快速排序、插入排序、堆排序、希尔排序
算法一:插入排序
插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入
算法步骤:
- 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置 (如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面 )
算法二:希尔排序
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位
希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序
算法步骤:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1
- 按增量序列个数k,对序列进行k趟排序
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序,仅增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度
算法三:选择排序
选择排序(Selectionsort)也是一种简单直观的排序算法。
算法步骤:
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾
- 重复第二步,直到所有元素均排序完毕
算法四:冒泡排序
冒泡排序(BubbleSort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端
算法步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这步做完后,最后的元素会是最大的数
- 针对所有的元素重复以上的步骤,除了最后一个
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
算法五:归并排序
归并排序(Mergesort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(DivideandConquer)的一个非常典型的应用。
算法步骤:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针达到序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
算法六:快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个项目要Ο(nlogn)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(nlogn)算法更快,因为它的内部循环(innerloop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divideandconquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
算法步骤:
- 从数列中挑出一个元素,称为“基准”(pivot)
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会退出,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去
算法七:堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序的平均时间复杂度为Ο(nlogn)
算法步骤:
- 创建一个堆H[0…n-1]
- 把堆首(最大值)和堆尾互换
- 把堆的尺寸缩小1,并调用shift_down(0),目的是把新数组顶端数据调整到相应位置
- 重复步骤2,直到堆的尺寸为1
算法分析:
关于时间复杂度:
- 平方阶(O(n2))排序
各类简单排序:直接插入、直接选择和冒泡排序; - 线性对数阶(O(nlog2n))排序
快速排序、堆排序和归并排序; - O(n1+§))排序,§是介于0和1之间的常数
希尔排序
关于稳定性:
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序
不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序
外部排序
外部排序则主要用分治思想,最常用的算法是多路归并排序,即将原文件分解成多个能够一次性装入内存的部分,分别把每一部分调入内存完成排序。然后,对已经排序的子文件进行归并排序。一般来说外排序分为两个步骤:预处理和合并排序。即首先根据内存的大小,将有n个记录的磁盘文件分批读入内存,采用有效的内存排序方法进行排序,将其预处理为若干个有序的子文件,这些有序子文件就是初始顺串,然后采用合并的方法将这些初始顺串逐趟合并成一个有序文件
外部排序最常用的算法是 n 路归并排序,即将原文件分解成多个能够一次性装入内存的部分,分别把每一部分调入内存完成排序。然后,对已经排序的子文件进行n路归并排序。在内部归并过程中利用败者树将 k 个归并段中选取最小记录比较的次数降为(向上取整) log2 次使总比较次数为(向上取整),与 k 无关
七种内部排序实现
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <time.h>
/*插入排序*/
//时间:O(N^2)
//空间:T(1)
//稳定性:稳定
void InsertSort(float* a, int n)
{
assert(a);
int i;
for (i = 0; i < n - 1; ++i)
{
int end = i;
float tmp = a[end + 1];
while (end >= 0 && a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
a[end + 1] = tmp;
}
}
/*希尔排序*/
//时间:O(N^1.3 - N^2)
//空间:T(1)
//稳定性:不稳定
void ShellSort(float* a, int n)
{
assert(a);
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
float tmp = a[end + gap];
while (end >= 0 && a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
a[end + gap] = tmp;
}
}
}
void Swap(float* a, float* b)
{
float tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
/*选择排序*/
//时间:O(N^2)
//空间:T(1)
//稳定性:不稳定
void SelectSort(float* a, int n)
{
assert(a);
int begin = 0, end = n - 1;
while (begin < end)
{
int i, min;
min = begin;
for (i = begin; i <= end; ++i)
{
if (a[i] < a[min])
min = i;
}
Swap(&a[begin], &a[min]);
begin++;
}
}
/*堆排序*/
//时间:O(N * logN)
//空间:T(1)
//稳定性:不稳定
void ShiftDown(float* a, int n, int root)
{
assert(a);
int parent = root;
int child = 2 * parent + 1;
//当前节点是否有孩子
while (child < n)
{
//是否有右孩子,有则进行比较
if (child + 1 < n && a[child + 1] > a[child])
++child;
//孩子是否大于父亲,大则进行交换
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
//更新下一次调整的位置
parent = child;
child = 2 * parent + 1;
}
else
{
//以父亲为根的子树已经是一个大堆,结束调整
break;
}
}
}
void HeapSort(float* a, int n)
{
assert(a);
int i;
for (i = (n - 2) / 2; i >= 0; i--)
{
ShiftDown(a, n, i);
}
while (n > 1)
{
Swap(&a[0], &a[n - 1]);
ShiftDown(a, n - 1, 0);
n--;
}
}
/*冒泡排序*/
//时间:O(N^2)
//空间:T(1)
//稳定性:稳定
void BubbleSort(float* a, int n)
{
assert(a);
for (int i = n; i > 0; i--)
{
int flag = 0;
for (int j = 0; j < i - 1; j++)
{
//标记一次的冒泡过程是否发生元素交换
flag = 1;
if (a[j] > a[j + 1])
Swap(&a[j], &a[j + 1]);
}
if (flag == 0)//如果没有发生元素交换,提前结束
break;
}
}
/*快速排序*/
//时间:O(N * logN)
//空间:T(logN) 空间可以复用,最大的递归调用链
//稳定性:不稳定
//三数取中
int getMid(float* a, int left, int right)
{
assert(a);
int mid = left + (right - left) / 2;
while (left <right)
{
if (a[mid] > a[left])
{
if (a[mid] < a[right])
return mid;
else
{
if (a[left] > a[right])
return left;
else
return right;
}
}
else
{
if (a[left] < a[right])
return left;
else
{
if (a[mid] > a[right])
return mid;
else
return right;
}
}
}
}
int PartSort(float* a, int left, int right)
{
assert(a);
int mid = getMid(a, left, right);
Swap(&a[mid], &a[left]);
float key = a[left];
int start = left;
while (left < right)
{
while (left < right && a[right] >= key)
--right;
while (left < right && a[right] <= key)
++left;
Swap(&a[left], &a[right]);
}
Swap(&a[start], &a[left]);
return left;
}
//递归写法,非递归写法要用栈
void QuickSort(float* a, int left, int right)
{
assert(a);
if (left >= right)
return;
else if (right - left + 1 < 5)
{
InsertSort(a + left, right - left + 1);
}
else
{
int mid = PartSort(a, left, right);
QuickSort(a, left, mid - 1);
QuickSort(a, mid + 1, right);
}
}
/*合并排序*/
//时间:O(N * logN)
//空间:T(N)
//稳定性:稳定
void _MergeSort(float* a, int left, int right, float* tmp)
{
assert(a);
//区间只剩一个元素,无需分解和归并
if (left >= right)
return;
//分解
int mid = (right - left) / 2 + left;
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
//合并
int begin1 = left, end1 = mid, begin2 = mid + 1, end2 = right;
int tmpindex = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[tmpindex++] = a[begin1++];
}
else
{
tmp[tmpindex++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[tmpindex++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[tmpindex++] = a[begin2++];
}
}
void MergeSort(float* a, int n)
{
assert(a);
float *tmp = (float*)malloc(n * sizeof(int));
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
void ArrayPrint(float* a, int n)
{
assert(a);
for (int i = 0; i < n; i++)
{
printf("%0.2f ", a[i]);
}
printf("\n");
}
//计算时间
void testTime()
{
int i = 20;
double time1 = 0, time2 = 0, time3 = 0, time4 = 0, time5 = 0, time6 = 0, time7 = 0;
int num;
scanf("%d", &num);
while (i--)
{
float* b = (float*)malloc(num * sizeof(float));
for (int i = 0; i < num; i++)
{
//生成随机数
int random_num1 = rand();
int random_num2 = rand() % 100;
float random = random_num1 + 0.01 * random_num2;
b[i] = random;
//printf("%.2f ", b[i]);
}
float* b1 = (float*)malloc(num * sizeof(float));
float* b2 = (float*)malloc(num * sizeof(float));
float* b3 = (float*)malloc(num * sizeof(float));
float* b4 = (float*)malloc(num * sizeof(float));
float* b5 = (float*)malloc(num * sizeof(float));
float* b6 = (float*)malloc(num * sizeof(float));
float* b7 = (float*)malloc(num * sizeof(float));
memcpy(b1, b, sizeof(float)* num);
memcpy(b2, b, sizeof(float)* num);
memcpy(b3, b, sizeof(float)* num);
memcpy(b4, b, sizeof(float)* num);
memcpy(b5, b, sizeof(float)* num);
memcpy(b6, b, sizeof(float)* num);
memcpy(b7, b, sizeof(float)* num);
clock_t start1, end1, start2, end2, start3, end3, start4, end4, start5, end5, start6, end6, start7, end7;
start1 = clock();
InsertSort(b1, num);
end1 = clock();
time1 += (((double)end1 - (double)start1) / CLOCKS_PER_SEC);
start2 = clock();
ShellSort(b2, num);
end2 = clock();
time2 += (((double)end2 - (double)start2) / CLOCKS_PER_SEC);
start3 = clock();
SelectSort(b3, num);
end3 = clock();
time3 += (((double)end3 - (double)start3) / CLOCKS_PER_SEC);
start4 = clock();
HeapSort(b4, num);
end4 = clock();
time4 += (((double)end4 - (double)start4) / CLOCKS_PER_SEC);
start5 = clock();
BubbleSort(b5, num);
end5 = clock();
time5 += (((double)end5 - (double)start5) / CLOCKS_PER_SEC);
start6 = clock();
QuickSort(b6, 0, num);
end6 = clock();
time6 += (((double)end6 - (double)start6) / CLOCKS_PER_SEC);
start7 = clock();
MergeSort(b7, num);
end7 = clock();
time7 += (((double)end7 - (double)start7) / CLOCKS_PER_SEC);
}
//计算20组数据平均用时,并转化为微秒
double ave1 = time1 * 50000;
double ave2 = time2 * 50000;
double ave3 = time3 * 50000;
double ave4 = time4 * 50000;
double ave5 = time5 * 50000;
double ave6 = time6 * 50000;
double ave7 = time7 * 50000;
printf("插入排序运行时间:%.1lf us\n", ave1);
printf("希尔排序运行时间:%.1lf us\n", ave2);
printf("选择排序运行时间:%.1lf us\n", ave3);
printf("堆排序运行时间:%.1lf us\n", ave4);
printf("冒泡排序运行时间:%.1lf us\n", ave5);
printf("快速排序运行时间:%.1lf us\n", ave6);
printf("合并排序运行时间:%.1lf us\n", ave7);}
int main()
{
testTime();
return 0;
}
时间复杂度分析
如图为在一定规模下,比较不同算法的运行时间(单位:us)
将实际运行时间的数据录入到表中,得出如下表格,曲线图展示不同排序算法运行效率
从曲线图上可以看出,快速排序与堆排序和希尔排序、归并排序运行效率最快,四条曲线几乎重叠在一起,而插入排序与选择排序次之,最慢的是冒泡排序。当规模级别到200000,运行时明显感到运行缓慢,相对低效,而快速排序在数据量越大时,优势越明显