8.1 简介
排序(Sorting)是指将一组数据按照一定规则调换位置,使数据具有某种次序关系。这里排序的域称为键(key),域中的值称为”键值“。
排序过程中,数据的移动方式分为:直接移动、逻辑移动;
直接移动:直接交换存储数据的位置;
逻辑移动:不移动数据的存储位置,仅仅改变指向这些数据的辅助指针位置。
直接移动耗费的时间成本太高,而逻辑移动只需要改变指针的位置就可以达到目的。
8.1.1 排序分类
按照存储器的不同:
内部排序:排序数据量较小,可以完全在内存中进行排序;
常用的有:冒泡排序法、选择排序法、插入排序法、合并排序法、快速排序法、堆排序法、希尔排序法、基数排序法。
外部排序:排序的数据量无法在内存中进行排序,而必须使用到辅助存储器。
常用的有:直接合并排序法、k-路合并法、多相合并法。
8.1.2 排序算法分析
1.算法稳定性:指的是数据排序后两个相同的键值记录仍然保持原来的次序。
2.时间复杂度(Time Complexity):当数据量相当大时所花费的时间就相当重要。排序算法的时间复杂度可以分为最好情况(best case)、最坏情况(worst case)和平均情况(average case)。
3.空间复杂度(space complexity):算法在执行过程中需要付出的额外存储空间,例如所挑选的排序法必须借助递归的方式来进行,那么递归过程中会使用到的堆栈就是这个排序法必须付出的额外空间。
排序法所使用到的额外空间越少,它的空间复杂度就越佳,比如冒泡排序法仅用到一个额外的空间。在所有的排序算法中,这样的空间复杂度就是最好的。
8.2 内部排序法
内部排序法按照算法的时间复杂度和空间复杂度及键值整理如下图所示:
其中简单排序法有:冒泡排序、选择排序、插入排序、希尔排序;
高等排序法有:快速排序法、堆排序法、基数排序法。
8.2.1 冒泡排序法
思想:又称为交换排序法,比较方式是从第一个元素开始,比较相邻元素大小,若大小顺序有误,则对调后在进行下一个元素的比较。如此扫描一遍可确保最后一个元素位于正确的位置。在进行二次。。。扫描,直到完成所有元素的排序关系为止。
对于n个元素,最坏情况需要比较n(n-1)/2次;时间复杂度为O(n^2);
最好的情况是扫描一遍,发现没有交换的动作则表示已经排序完成,所以只需要做n-1次比较,时间复杂度为O(n)。
优点:是稳定排序方法,因为相邻对比并对调,不会影响其他元素的排序;
只需要一个额外空间,空间复杂度最佳;
适用于小数据量或者部分数据已经排过序的数组。
/***************************************************
冒泡排序法
by CC
2016.3.21.19.46
****************************************************/
#include<iostream>
#include<iomanip>
using namespace std;
int main()
{
int data[6]={2,3,7,1,9,5};//定义一个6维度的数组
cout<<"冒泡排序法求解:\n原始数据为:"<<endl;
for(int i=0;i<6;i++)
cout<<setw(5)<<data[i];
cout<<endl;
//开始扫描,冒泡排序,对于n个元素,需要扫描n-1次
for(int i=5;i>0;i--)
{
for(int j=0;j<i;j++)
{
if (data[j]<data[j+1])
{
int temp=0;
temp=data[j];
data[j]=data[j+1];
data[j+1]=temp;
}
}
//输出每次扫描后的结果
cout<<"第"<<6-i<<"次扫描排序后的结果为:";
for(int k=0;k<6;k++)
cout<<setw(5)<<data[k];
cout<<endl;
}
system("pause");
return 0;
}
由上面的代码可以看到,不管数据是否已经排序完成都固定执行n(n-1)/2次。也可以在循环条件中加入flags=0;从而可以判断是否参与了交换的操作,如果没有交换操作代表排序结束,可以break跳出循环。
8.2.2 选择排序法
算法思想:
1)遍历数组,将最大/最小值放在首位置;
2)在此遍历数组,将最大/最小放在第二位置,此时首位置不参与比较了;
3)以此类推,可以升序或者降序排列。
无论是最坏情况、最佳情况还是平均情况都需要找到最大值(或者最小值),因此其比较次数为:(n-1)+(n-2)+…3+2+1=n(n-1)/2次,时间复杂度是O(n^2);
由于选择排序是以最大或者最小值直接与最前方未排序的键值交换,数据排列顺序可能会被改变,是不稳定的排序法;
只需要一个额外空间,所以空间复杂度最佳;
此排序算法只适用于小数据量或者有部分数据已经排序过得数组。
C++代码实现为:
/***************************************************
选择排序法
by CC
2016.3.21.20.18
按照从小到大的顺序排列
****************************************************/
#include<iostream>
#include<iomanip>
using namespace std;
void showdata(int *,int N);//显示数组数据
void sortdata(int *,int N);//排列数组函数
int main()
{
int n=8;
int arr[8]={8,7,6,5,4,3,2,1};
cout<<"原始数据为:"<<endl;
showdata(arr,n);
sortdata(arr,n);
system("pause");
return 0;
}
void showdata(int arr[],int N)
{
for(int i=0;i<N;i++)
cout<<setw(5)<<arr[i];
cout<<endl;
}
void sortdata(int arr[],int N)
{
//扫描第N-1次
for(int i=0;i<N-1;i++)
{
for(int j=i+1;j<N;j++)
{
if (arr[i]>arr[j])
{
int temp=0;
temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
}
cout<<"第"<<i<<"次的扫描排序结果为:";
for(int k=0;k<N;k++)
{
cout<<setw(5)<<arr[k];
}
cout<<endl;
}
}
8.2.3 插入排序法(Insert Sort)
插入排序法是将数组中的元素与排序好的元素一一比较,然后将该元素插入到适当的地方。
特性:1)最坏及平均情况都需要比较n(n-1)/2次;
2)插入排序是稳定排序法;
3)只需要一个额外的空间,空间复杂度最佳;
4)此排序法适用于大部分数据已经排好序或者已经排好序的数组新增元素进行排序;
5)插入排序法会造成大量的数据移动,所以建议在链表结构上使用。
插入排序法的c++实现:
/***********************************************************
插入排序法
by CC 2016.3.22.13.44
***********************************************************/
#include "iostream"
#include "iomanip"
#define size 8
using namespace std;
//申明三个函数,分别是输入数据、显示数据、插入排序函数
void inputarr(int *,int n);
void showarr(int *,int n);
void insertarr(int *,int n);
int main()
{
int arr[size];
inputarr(arr,size);
cout<<"数据集的内容为:"<<endl;
showarr(arr,size);
cout<<endl;
insertarr(arr,size);
system("pause");
return 0;
}
//定义输入的八位数组
void inputarr(int arr[],int n)
{
//int arr[size];
cout<<"please input dataset for sorting:"<<endl;
for(int i=0;i<n;i++)
{cout<<"请输入第"<<i<<"个元素:"<<endl;
cin>>arr[i];}
}
//定义显示数组内容的函数
void showarr(int *arr,int n)
{
//cout<<"数据集的内容为:";
for(int j=0;j<n;j++)
cout<<setw(5)<<arr[j];
}
//定义插入排序的函数
void insertarr(int *arr,int n)
{
//定义扫描次数,8个元素就应该扫描7次
for(int i=0;i<size;i++) //这里i可以从1开始,从0都不需要比对了。
{
int tmp=arr[i];
int j=i-1;
while(j>=0&&tmp<arr[j])
{
arr[j+1]=arr[j];
j--;
}
arr[j+1]=tmp;
cout<<"第"<<i<<"次扫描排序之后为:";
showarr(arr,size);
cout<<endl;
}
}
8.2.4 希尔排序法
希尔排序法是D.L.Shell在1957年7月发明的一种排序法,所以该算法直接以这个发明者的名字命名。其排序法的原理类似于插入排序法,但它可以减少数据移动的次数。希尔排序的原则是将数据划分为特定间隔的几个自己,然后以插入发分别给这些子集进行排序。
特性:
1)任何情况下时间复杂度是O(n^3/2);
2)希尔排序法和插入排序法都是稳定排序法;
3)只需要一个额外空间,空间复杂度最佳;
4)此排序法适用于数据大部分都已排序完成的情况。
/*****************************************************
希尔(shell)排序法
by CC 2016.3.22.15.21
类似于插入排序,只是步长不是1,而是变化的
***************************************************/
#include "iostream"
#include "iomanip"
using namespace std;
#define SIZE 5
//申明函数
void inputdata(int *,int size);
void showdata(int *,int size);
void shellsort(int *,int size);//声明希尔排序
int main()
{
int arr[SIZE];
inputdata(arr,SIZE);
showdata(arr,SIZE);
shellsort(arr,SIZE);
return 0;
}
//定义输入数据函数
void inputdata(int arr[],int size)
{
cout<<"please input the data cell for sorting:"<<endl;
for(int i=0;i<size;i++)
{
cout<<"请输入第"<<i<<"个元素:"<<endl;
cin>>arr[i];}
}
//定义显示数据的函数
void showdata(int arr[],int size)
{
for(int j=0;j<size;j++)
cout<<setw(5)<<arr[j];
}
//希尔排序函数
void shellsort(int arr[],int size)
{
int step=size/2;//定义步长
int k=0;
while(step!=0)
{
for(int i=step;i<size;i++)
{
int temp=arr[i];
int j=i-step;
//插入排序法
while(temp<arr[j]&&j>=0)
{
arr[j+step]=arr[j];
j=j-step;
}
arr[j+step]=temp;
}
cout<<"第"<<++k<<"次排序结果为:";
showdata(arr,size);
step=step/2;
}
}
8.2.5 合并排序法(Merge Sort)
合并排序法是将N个元素的待排序集合看成子集有1个元素的集合,算法步骤为:
1)将N个长度为1的文件合并成N/2个已排好序且长度为2的文件;
2)将N/2个长度为2的文件合并成N/4个长度为4的文件;
3)。。。
4)以此类推,最终变成一个长度为N的1个文件。
特性:
1)合并排序法对于n个数据一般需要约log2n次处理;每次处理的时间复杂度为O(n),所以合并排序法的最佳情况、最差情况和平均情况复杂度为O(nlog2n);
2)由于在排序过程中需要一个与文件大小相同的额外空间,故其空间复杂度为O(n),是一个稳定的排序方式;
3)合并排序法适合较大的外部文件排序。