您的位置:首页 > 理论基础 > 数据结构算法

各种排序算法(内部排序)及其实现

2016-08-26 17:06 363 查看
本文是基于《数据结构(C语言版)(第二版)》(严蔚敏)其排序章节所做的总结。因此具体解释可以去参考

此书。

相关概念

什么是排序

排序是按关键字的非递减或非递增顺序对一组记录重新进行排列的操作。

数学描述:

设n个记录的序列为 {R1, R2, …, Rn},其关键字序列为 {K1, K2, …, Kn}

确定1, 2, …, n的一种排序p1, p2, …, pn,使之满足Kp1 <= Kp2 <= … <= Kpn

即使序列成为按关键字有序的序列:{Rp1, Rp2, …, Rpn}

这样的操作称为排序

排序的稳定性

排序的稳定性是指:当两个或几个记录的关键字相同时,在排序之前和排序之后其相对位置保持不变。

即在原序列中,Ki=Kj,且Ri在Rj之前,而在排序后的序列中,Ri仍在Rj之前,则称这种排序算法是稳定的;否

则称为不稳定的。

内部排序和外部排序

若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。反之,若参加排序的记录数量很大,

整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。

内部排序的分类

内部排序的过程是一个逐步扩大记录的有序序列长度的过程。

排序算法没有绝对的好坏,每一种排序都有各自的优点和缺点,适用于不用的环境下。

我们不仅要掌握各种基本算法本身,更重要的是理解算法的思想,以便能够学习和创造新的算法。

内部排序算法的分类:



注:以下算法及其代码都是排序为非递减序列, 以下代码皆在dev-cpp编译器下测试通过,可以直接复制使用。

注:swap是C++算法库(algorithm)的函数

插入类排序

基本思想:每次将一个待排序的记录,按关键字的大小插入到已排序列的一组记录的适当位置上,直到所有全部

待排序列都插入为止。

直接插入排序

每次将一个待排序的记录,插入到已排序列的一组记录的适当位置上,每次使有序序列增加1个长度。

C语言代码:

void InsertSort(int arr[], int n)
{
for(int i = 1; i < n; ++ i){   // [0, i)为已经排好的有序序列, 每次对arr[i]进行插入
if(arr[i] < arr[i - 1]){   // arr[i]小于有序序列的最大元素, 说明需要插入
int key = arr[i], j;
for(j = i; j > 0 && key < arr[j - 1]; -- j)  // 将大于arr[i]的向后移动一个

位置
arr[j] = arr[j - 1];
arr[j] = key;         // 将arr[i]插入合适到位置
}
}
}


【时间复杂度】O(n^2)

【空间复杂度】O(1)

【算法特点】

稳定排序

可以用于链表和顺序表

适用于初始记录基本有序的情况;当初始无序时、n较大时,移动次数较多,不宜采用

折半插入排序

直接插入排序每次时通过线性查找找到插入位置,而折半插入排序通过折半查找来实现查找位置。

C语言代码:

void BInsertSort(int arr[], int n)
{
for(int i = 1; i < n; ++ i){            // [0, i)为已经排好的有序序列, 每次对arr[i]进行插

入
if(arr[i] < arr[i - 1]){            // arr[i]小于有序序列的最大元素, 说明需要插入
int key = arr[i];

// 折半查找插入的位置
int low = 0, high = i;          // 区间[low, high)
while(low < high){
int mid = (low + high)/2;
if(key < arr[mid])
high = mid;
else
low = mid + 1;
}

for(int j = i; j > low; -- j)  // 将大于arr[i]的向后移动一个位置
arr[j] = arr[j - 1];
arr[low] = key;               // 将arr[i]插入合适到位置
}
}
}


【时间复杂度】O(n^2)

【空间复杂度】O(1)

【算法特点】

稳定排序

由于使用折半查找,只能用于顺序表

适合用于初始记录无序、n较大的情况

shell(希尔)排序

直接插入排序在记录个数较少且序列基本有序时,效率较高。shell排序从“减少记录个数”和“序列基本有序

”两个方面对其进行了改进。

shell排序是一种分组插入的算法。先将记录序列分割成几组,从而减少直接插入的数据量,对每组分别进行直

接插入排序,然后增加每组的记录数量,重新分组,重复若干次。最后对序列进行一次直接插入排序(分组间隔

为1)。

C语言代码:

void ShellSort(int arr[], int n)
{
for(int gap = n/2; gap; gap /= 2){        // gap为分组间隔
for(int k = 0; k < gap; ++ k){        // 第k个分组,  下面将每个分组进行直接插入排

序

for(int i = k + gap; i < n; i += gap){  // arr[i]为每个分组的元素
int key = arr[i], j;
for(j = i; j - gap >= k && key < arr[j - gap]; j -= gap)  // 将分

组内大于arr[i]的向后移动一个位置
arr[j] = arr[j - gap];
arr[j] = key;                 // 将arr[i]插入合适到位置
}

}
}
}


【时间复杂度】与分组方法有关,平均O(n^1.3)

【空间复杂度】O(1)

【算法特点】

不稳定排序

只能用于线性表

分组方法的不同,效率可能不同,但最后一次的分组间隔必须为1

适合用于初始记录无序、n较大的情况

交换类排序

基本思想:两两比较待排序记录的关键字,若不满足次序要求则进行交换,直到整个序列全部满足要求为止。

冒泡排序

通过比较相邻的关键字,逆序则交换,每一次使关键字最小的记录像气泡一样“上浮”,使关键字大的记录如同

石头一样“下沉”。

C语言代码:

void BubbleSort(int arr[], int n)
{
for(int i = 1; i < n; ++ i){   // [0, n-i)表示未排序列
int flag = 0;              // 交换标记, 如果未发生交换说明序列已经有序
for(int j = 0; j < n - i; ++ j){
if(arr[j] > arr[j + 1]){
swap(arr[j + 1], arr[j]);
flag = 1;
}
}
if(!flag)
break;
}
}


【时间复杂度】平均O(n^2),最好O(n),最坏O(n^2)

【空间复杂度】O(1)

【算法特点】

稳定排序

可用于链表和顺序表

不适合用于初始记录无序、n较大的情况。

交换次数较多,平均效率低于直接插入排序。

快速排序

快速排序是由冒泡排序改进的。

快速排序每次选定一个关键字作为基准值,将小于和大于这个基准值的记录放在序列两端,从而将序列分成两个

子序列,然后对两个子序列进行相同操作,直到每个子序列只有一个记录时排序完成。这个过程是递归的,因此

通常使用递归实现。

快速排序有几种实现方法。这下面只是其中的一种。

C语言代码:

// 分割序列[low, high)  (用于快速排序) ,返回分割位置
int Partition(int arr[], int low, int high)
{
int key = arr[low], i = low, j = high - 1;
while(i < j){
while(i < j && arr[j] >= key) -- j;
arr[i] = arr[j];

while(i < j && arr[i] <= key) ++ i;
arr[j] = arr[i];
}

arr[i] = key;
return i;
}

// 快速排序
void QuickSort(int arr[], int n)
{
if(n > 1){
int p = Partition(arr, 0, n);
QuickSort(arr, p);
QuickSort(arr + p + 1, n - p - 1);
}
}


【时间复杂度】平均O(nlogn), 最好O(n),最差O(n^2/2)

【空间复杂度】O(nlogn)

【算法特点】

不稳定排序

只能用于顺序表(其他的实现方法可以用于链表)

适合用于初始记录无序、n较大的情况。

n较大时,平均情况下快速排序是所有内部排序算法最快的算法之一

选择类排序

基本思想:每次从待排序的记录中选择关键字最小的记录,按顺序放在已排序的记录最后,直到全部排完为止。

直接(简单)选择排序

每次从待排序的记录中选择关键字最小的记录,放在已排序的记录最后。

C语言代码:

void SelectSort(int arr[], int n)
{
for(int i = 0; i < n; ++ i){         // 无序区间为[i, n)
int minI = i;
for(int j = i +1; j < n; ++ j){  // 找出无序区间最小的
if(arr[j] < arr[minI]){
minI = j;
}
}

if(minI != i){
swap(arr[minI], arr[i]);    // 将最小的加入有序序列
}
}
}


【时间复杂度】平均O(n^2), 最好O(3n-1),最差O(n^2/2)

【空间复杂度】O(1)

【算法特点】

不稳定排序(可以改写成稳定形式)

可用于顺序表和链表

移动次数较少,当每个记录所占的空间较大时,比直接插入排序快。

堆排序

堆排序是一种树形选择排序,在排序过程中将待排序记录当做完全二叉树的顺序存储结构,每次利用二叉树选择

关键字最小的记录。

详细介绍:http://www.cnblogs.com/mengdd/archive/2012/11/30/2796845.html

C语言代码:

// [s + 1, m) 已经是堆, 将[s, m)调整为堆
void HeapAdjust(int arr[], int s, int m)
{
int i = s, key = arr[s];
while(2*i + 1 < m){    // 2*i+1、2*i+2分别为i的左右结点位置

int j = 2*i + 1;   // 左结点
if(j + 1 < m && arr[j] < arr[j + 1])  // 选择左右结点值中大的
++ j;

if(key >= arr[j])  //顺序满足则结束循环
break;

arr[i] = arr[j];   // 结点值上移
i = j;
}
arr[i] = key;   // 插入
}

// 堆排序
void HeapSort(int arr[], int n)
{
// 构造堆
for(int i = n/2; i >= 0; -- i){
HeapAdjust(arr, i, n);
}

for(int i = n - 1; i > 0; -- i){  // [i, n)为有序序列
swap(arr[0], arr[i]);         // 将最大值加入有序序列
HeapAdjust(arr, 0, i);        // 调整堆
}
}


【时间复杂度】O(nlogn)

【空间复杂度】O(1)

【算法特点】

不稳定排序

只能用于顺序表

适用于n较大的情况;初始构造堆比较次数较多,不适合n较小的情况

堆排序最坏时间复杂度为O(nlogn),相比于快速排序排序O(n^2)是一个优点。

归并类排序

基本思想:将两个或两个以上的有序表合成一个有序表的过程。

归并排序(2-路归并)

每次合并两个有序表,直到合并所有的记录。

C语言代码:

//将a的[low, mid), [mid, high) 两部分归并
void Merge(int arr[], int low, int mid, int high)
{
int t[high - low];
int i = low, j = mid, k = 0;

while(i < mid && j < high){
if(arr[i] <= arr[j])
t[k ++] = arr[i ++];
else
t[k ++] = arr[j ++];
}

while(i < mid)  t[k ++] = arr[i ++];
while(j < high) t[k ++] = arr[j ++];

for(i = low, k = 0; i < high; i ++){
arr[i] = t[k ++];
}
}

// 归并排序 ,非递归形式
void MergeSort(int arr[], int n)
{
for(int k = 1; k < n; k *= 2){
for(int i = 0; i < n; i += k*2){
if(i + k*2 <= n)
Merge(arr, i, i + k, i + 2*k);
else if(i + k <= n)
Merge(arr, i, i + k, n);
}
}
}

// 归并排序, 递归形式
void MergeSort_d(int arr[], int n)
{
if(n > 1){
int mid = n/2;
MergeSort_d(arr, mid);
MergeSort_d(arr + mid, n - mid);
Merge(arr, 0, mid, n);
}
}


【时间复杂度】O(nlogn)

【空间复杂度】O(n)

【算法特点】

稳定排序

可用于顺序表和链表

分配类排序

基本思想:前面各种排序都是基于关键字的比较进行排序,而基数排序则不需要。它根据关键字中各位的值,对

待排序记录进行若干趟“分配”与“收集”来实现排序。

基数排序

基数排序属于“分配式排序”,基数排序法又称“桶子法”或binsort,顾名思义,它是透过键值的部份资讯,

将要排序的元素分配至某些“桶”中,藉以达到排序的作用。

基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序

方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

详细解释:http://wenku.baidu.com/view/20c1dbdd172ded630b1cb638.html

C语言代码:

void RadixSort(int arr[], int n)
{
// 取得最大数
int maxNum = arr[0];
for(int i = 1; i < n; ++ i){
if(arr[i] > maxNum){
maxNum = arr[i];
}
}

// 取得最大数的位数
int maxBitCnt = 0;
while(maxNum){
++ maxBitCnt;
maxNum /= 10;
}

// 进行maxBitCnt次的分配与收集
for(int power = 1, i = 0; i < maxBitCnt; ++ i){

// 根据特定位的数进行分配
int bucket[10]
, count[10] = {0};
for(int j = 0; j < n; ++ j){
int k = arr[j] / power % 10;
bucket[k][count[k]++] = arr[j];
}
power *= 10;

// 收集
for(int m = 0, k = 0; k < 10; ++ k){
for(int j = 0; j < count[k]; ++ j){
arr[m ++] = bucket[k][j];
}
}
}
}


【时间复杂度】O(d(n + rd)), d为关键字个数,rd为关键字取值个数

【空间复杂度】O(n + rd)

【算法特点】

稳定排序

可用于顺序表和链表

时间复杂度可突破基本关键字比较排序的上限O(nlogn),达到O(n)

使用条件严格:需要知道各级关键字的主次关系及其取值范围

总结

各种排序算法的比较

排序方法最好最坏平均空间复杂度稳定性
直接插入排序O(n)O(n^2)O(n^2)O(1)稳定
折半插入排序O(nlogn)O(n^2)O(n^2)O(1)稳定
shell排序O(n^1.3)O(1)不稳定
冒泡排序O(n)O(n^2)O(n^2)O(1)稳定
简单选择排序O(n^2)O(n^2)O(n^2)O(1)稳定
快速排序O(nlogn)O(n^2)O(nlogn)O(nlogn)不稳定
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
基数排序O(d(n+rd))O(d(n+rd))O(d(n+rd))O(n+rd)稳定
注:最好、最坏、平均均指时间复杂度

从表中可以看出,就算法的平均复杂度而言,直接插入排序、折半插入排序、冒泡排序和简单选择排序的速度较慢,其他的排序算法速度较快。

就算法的实现而言,速度较慢的算法一般实现比较简单,称为简单的排序算法;速度较快的算法可以看做简单排序算法的改进,称为先进的排序算法,这些算法一般实现较为复杂。

总的来说,各种排序算法各有优缺点,没有绝对最好的。在使用时根据不同情况进行选择,也可以组合使用。一般选择排序算法综合考虑以下因素:

待排序记录的个数n

记录本身所占空间大小

关键字的结构及初始状态

稳定性要求

存储结构

根据这些因素和表格的比较,有以下一些结论。

当n较小时,n和nlogn差别不大,可以选择简单的排序算法。当关键字基本有序时,可选择直接插入排序或者冒泡排序,尤其是直接插入排序性能最佳。

当n较大时,应该选择先进的排序算法。平均而言,快速排序是最好的。但关键字基本有序时,快速排序复杂度达到了最差。n较大时,选用的原则:

当关键字基本随机分布,稳定性不要求时,可选择快速排序

当关键字基本有序,稳定性不要求时,可选择堆排序

当关键字基本有序,要求稳定且内存足够时,可选择归并排序

可以组合使用简单的排序算法和先进的排序算法。如:n较大时,先将待排序序列分成若干组,分别进行直接插入排序,然后使用归并排序将这些分组合并。又如:在快速排序中,当划分子序列的长度小于某个长度时,可以转而使用直接插入排序算法。

基数排序的实际复杂度可以写成O(d*n)。因此它适合用于n较大而关键字较小的序列。若关键字很大,而大多数记录的“最高位关键字”均不同,可以先根据“最高位关键字”的不同分组,然后进行直接插入排序。但基数排序的使用条件较严格:需要知道各级关键字的主次关系及其取值范围,只适用于整数和字符这类带有明显特征的关键字,当关键字的取值范围无穷大时,不能使用此排序。

从稳定性来比较,基数排序是稳定的排序,所有时间复杂度为O(n^2)的简单排序也是稳定的,然而快速排序、堆排序和shell排序等较好的排序都是不稳定的。

一般来说,如果排序过程中的“比较”是在“相邻的记录关键字”间进行的,则排序方法是稳定的。稳定性是由算法本身决定的,不稳定的算法,总能找到不稳定的例子来。反之,稳定的算法可能有不稳的描述形式。

大多数情况下排序是按关键字的主关键字进行的,则所用的排序算法是否稳定无关紧要,若排序安记录的次关键字进行,则必须采用稳定的排序方法。

上面的排序方法都是使用顺序表实现的。当n较大时,为避免移动记录花费时间过多,可以使用链表。如直接插入排序。

算法测试

以下是利用以上的排序算法进行的测试结果

【测试1】100次100个数的排序测试,总时间如下:



【测试2】100次1000个数的排序测试,平均时间如下:



【测试3】100次10000个数的排序测试,平均时间如下:



由此可见,在n较小时各种排序算法差别较小;但当n较大时,快速排序、堆排序、合并排序、基数排序表现的较

突出,直接插入排序、冒泡排序、直接选择排序则较慢尤其是冒泡排序(原因应该是比较和交换的次数太多了)

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息