文章目录
排序的种类
根据在排序过程中待排序的记录是否全部被放置在内存中,排序分为内排序和外排序。
内排序:待排序的所有记录全部被放置在内存中。
外排序:由于排序记录个数太多,不能放置在内存中,排序过程中需要在内外存之间多次交换数据才能进行。
根据在排序过程中待排序的记录是否进行比较,可以分为比较排序和非比较排序。
比较排序:冒泡排序、选择排序、插入排序、归并排序、堆排序、快速排序等。
非比较排序:基数排序、桶排序、计数排序等。
影响排序算法的三个指标
我们通常所说的排序算法是内部排序算法,对于内排序,主要受三个方面影响。分别是:时间性能、辅助空间(空间性)、算法的复杂性。
1.指标一——时间性能
在内排序中,主要进行两种操作:比较和移动。比较是关键字之间的比较,移动指记录从一个位置移到另一个位置。因此,排序算法的时间开销是衡量其好坏的重要标志。
2.指标二——辅助空间
辅助空间指除了存放待排序所占用的存储空间之外,执行算法所需要的其他存储空间,这是衡量算法好坏的另一个标准。
3.指标三——算法的复杂性
这里的算法复杂性,并不是时间复杂度。按照算法的复杂度分为两大类,简单算法和改进算法。
简单算法:冒泡、选择、插入
改进算法:希尔、堆、归并、快排
结构体和其它函数
结构体定义
typedef int ElemType;
#define MAXSIZE 10
#define N 10
typedef struct {
ElemType data[MAXSIZE];//存储的数据元素
int length;// 线性表当前长度
}SqList;//线性表
交换函数
/*交换线性表L中下标为i和j的元素*/
void Swap(SqList &L,int i,int j) {
int temp=L.data[i];
L.data[i]=L.data[j];
L.data[j]=temp;
}
初始化线性表
/* 初始化顺序线性表 */
bool InitList(SqList &L)
{
L.length=MAXSIZE;
return true;
}
输出线性表中的元素
/*输出线性表中的元素*/
void PrintList(const SqList &L){
for(int i=0;i<L.length;i++)
cout<<L.data[i]<<" ";
cout<<endl;
}
一、冒泡排序
冒泡排序(Bubble Sort),一种交换排序,基本思想是:两两比较相邻记录的关键字,如果逆序则交换,直到没有逆序的记录为止。
对存放原始数据的数组,按从前往后的方向进行多次扫描,每次扫描成为一趟,当发现两个数据的大小与位序不符时,即进行交换,每一趟排序过后,都会将最大的数放到最后。每一趟比较的次数都逐级减少,因为每一次比较,都将每趟中的最大数沉到了底下。
特点:升序排序中每一轮比较会把最大的数下沉到最底,所以相互比较的次数每一轮都会比前一轮少一次。
把N个数通过N-1趟(轮)排序,比较升序排序中,大的下沉,降序排序中,小的下沉。
冒泡排序初级版
/*
冒泡排序初级版
不算是标准的冒泡排序算法,因为不满足
"两两比较相邻记录的" 的冒泡排序思想。
算法思路:
让每一个数据元素和后一个数据元素
进行比较,如果大则进行交换,在第一趟
比较,最后一个元素一定是最大值。
*/
void EasyBubbleSort(SqList &L){
for(int i=0;i<L.length-1;i++)//n个数比较 n-1趟
for(int j=0;j<L.length-i;j++)//比较n-i次
if(L.data[j]>L.data[j+1])
Swap(L,j,j+1) ;//交换 L->data[i]与 L->data[j]的值
}
普通冒泡排序
/*冒泡排序*/
void BubbleSort(SqList &L) {
for(int i=0;i<L.length-1;i++)
for(int j=L.length-1;j>=i;j--)//将L.data[i] 元素归位
if(L.data[j]>L.data[j+1])//相邻的练个元素反序时
Swap(L,j,j+1);//将 L->data[i]与 L->data[j]的值 交换
}
改良冒泡排序
/*
冒泡排序改进
假如序列{2,1,3,4}
如果比较第一二次之后,在之前
的冒泡排序还会一直比下去,从i=1一直
执行到i=3,数量越大,则时间耗得越长,
为了避免这种情况,设计了一个标志位。
在每一次交换过后,置为1,循环判断如果为1则执行
否则跳过本趟比较。
*/
void BubbleSortImprove(SqList &L){
bool flag= true;
for(int i=0;i<L.length-1&&flag;i++){
flag=false;
for(int j=L.length-1;j>=i;j--){
if(L.data[j-1]>L.data[j]){
Swap(L,j-1,j);
flag=true;//如果有数据交换,则flag为true
}
}
}
}
冒泡排序时间复杂度总的排序为O(n^2)
二、简单选择排序
算法思想:通过n-i次关键字之间的比较,从n-i+1个记录中选出关键字最小的记录,并且和第i(1<=i<=n)个记录进行交换。
这种采用简单比较方法选出关键字最小的元素就是简单选择排序名称的由来。
/*简单选择排序*/
void SelectSort(SqList &L){
for(int i=0;i<L.length;i++){
int min=i;//将当前的下标定义为最小值下标
for(int j=i+1;j<L.length;j++) {
if(L.data[min]>L.data[j]){//如果当前的值比下标为min中的还小
min=j;//把这个下标给min
}
}
if(i!=min) //如果min与当前i值不想等,说明找到最小值
Swap(L,i,min);
}
}
简单选择排序与冒泡排序的时间复杂度都为O(n^2),但简单选择排序的性能还是稍微比冒泡要好一些。
三、插入排序
直接插入排序(借助数组第一个元素当做缓冲器)
算法思想:将一个记录插入到已经排好序的有序表中,从而得到一个新的,记录数增1的有序表。
/*
直接插入排序:
第一种写法, 数组的第一个元素不存储,起到一个缓冲器的作用
线性表中的第一个元素在这里起到哨兵的作用
当前元素如果比后一个元素小,那么将当前元素设置为哨兵,
再把当前元素之前的所有元素与哨兵进行比较,如果比哨兵小,
那么哨兵为这个元素,反之,则进行交换。
所以,最后一次比较,哨兵是最后一轮的最小元素。
*/
void InsertSort(SqList &L) {
int i,j;
for( i=2;i<L.length;i++){
if(L.data[i]<L.data[i-1]){
L.data[0]=L.data[i];//设置哨兵,哨兵为每轮比较的最小元素
for( j=i-1;L.data[j]>L.data[0];j--){
L.data[j+1]=L.data[j];//记录后移 这里的j+1其实是i
}
L.data[j+1]=L.data[0];//交换元素,把哨兵插入到正确的位置
}
}
}
这种写法如果排序记录是随机的,那么平均比较和移动次数≈(n2)/4,时间复杂度为O(n2),但直接插入排序比冒泡和选择排序的性能要稍微好一些。
算法思想:将无序区的开头元素a[i](1<=i<=n-1)插入到有序区a[0…i-1]中的适当的位置,使得a[0…i]变成新的有序区。
直接插入排序(直接进行交换)
/*
直接插入排序
第二种写法,不采用数组第一个元素为缓冲器,
默认第一个元素是有序的,直接进行交换
*/
void InsertSort2(SqList &L) {
for(int i=1;i<L.length;i++){
int key=L.data[i];
int j=i-1;
while(j>=0&&L.data[j]>key){//定位,当前一个元素比key大
L.data[j+1]=L.data[j];//插入,大记录后移,小记录前移
j--;
}
//如果j<0,则说明依次与前面进行比较比完了,此时是data[0]=key;
//如果data[j]<key,则不比较data[j],key的值不变。
L.data[j+1]=key;
}
}
说明:第二种写法每趟产生的有序区并不一定是全局有序区,也就是说有序区的元素并不一定放在最终的位置上,当一个元素在整个排序结束前就已经放在其最终位置上成为归位。
折半插入排序
算法思想:将无序区的开头元素a[i](1<=i<=n-1)插入到有序区a[0…i-1],采用顺序比较的方法,因为有序区的元素是有序的,这里借助了折半查找的方法在a[0…i-1]中定位找到插入位置,再进行元素移动,也叫二分插入排序。
/*
折半插入排序
利用二分查找定位,再进行插入排序
*/
void BinInsertSort(SqList &L){
int i,j,low,mid,high;
for( i=1;i<L.length;i++){
if(L.data[i]<L.data[i-1]){
int tmp=L.data[i];
low=0;
high=i-1;
while(low<=high){//在L.data[low...high] 中查找插入的位置
mid=(low+high) /2;//取中间位置
if(tmp<L.data[mid])
high=mid-1;//插入点在左半区间
else
low=mid+1;//插入点在右半区间
}
for(j=i-1;j>=high+1;j--) {
L.data[j+1]=L.data[j];
}
L.data[high+1]=tmp;
}
}
}
折半排序和直接插入排序相比,移动元素的性能没有改善,仅仅减少了关键字的比较次数,就平均性能而言,折半查找优于顺序查找,所以折半插入排序也优于直接插入排序,折半插入排序的空间复杂度为O(1)。
四、快速排序
由冒泡排序改进而得,基本思想:在待排序的n个元素中任取一个元素(通常取第一个元素)作为基准,把该元素放入适当位置后,数据序列被此元素划分成两部分。比该元素小的放在前面,比该元素大的放在后面,并把该元素放在这两个元素的中间(成为元素归位),这个过程称为一趟快速排序(也叫一趟划分)。
之后对产生的两个部分分别重复上面的过程,直到每部分内只有一个元素或者为空为止。简单来讲,就是每趟划分使表中的第一个元素放入合适的位置,将表一分为二,对子表进行递归方式继续这种划分,直到划分的子表的长度为1或0为止。
/*
快排
*/
void QuickSort(SqList &L,int l,int r){
//判断合法性 递归结束
if(l>=r) return ;
int key=L.data[l],i=l-1,j=r+1;
while(i<j){
//保证左边的<key,右边的>key
do i++;while(L.data[i]<key) ;
do j--;while(L.data[j]>key);
if(i<j)Swap(L,i,j);
}
QuickSort(L,l,j);
QuickSort(L,j+1,r);
}
五、归并排序
基本思想:
利用归并的思想实现的排序方法,假设初始序列含有n个记录,那么就可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,就可以得到⌈n/2⌉(⌈x⌉表示不小于x的最小整数)个长度为2或1的有序子序列,再两两归并,如此重复,直到得到一个长度为n的有序序列为止。
/*
归并排序
*/
int temp[MAXSIZE];
void MergeSort(SqList &L, int l,int r){
if(l>=r)return ;
//划分区间
int mid=(l+r)/2;
MergeSort(L,l,mid);
MergeSort(L,mid+1,r);
int k=0,i=l,j=mid+1;
//把两边区间最小的数存入temp中
while(i<=mid&&j<=r) {
if(L.data[i]<=L.data[j])
temp[k++]=L.data[i++];
else
temp[k++]=L.data[j++];
}
//把两边任一剩下的数存入temp中
while(i<=mid)
temp[k++]=L.data[i++];
while(j<=r)
temp[k++]=L.data[j++];
//从左边开始把temp中的数放回至a中
for(i=l,j=0;i<=r;i++,j++)
L.data[i]=temp[j];
}
七大算法性能总结
排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 复杂性 | ||
---|---|---|---|---|---|---|
平均情况 | 最坏情况 | 最好情况 | ||||
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 | 简单 |
简单选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 | 简单 |
直接插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 | 简单 |
折半插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 | 简单 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(logn) | 不稳定 | 较复杂 |
二路归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 | 较复杂 |