排序中相关问题说明:
排序的分类
在内排序中,主要进行两种操作:比较和移动。比较指关键字之间的比较,这是要做排序最起码的操作。移动指记录从一个位置移动到另一个位置。排序算法的方法有很多,如果按照排序过程中依据的主要操作来进行分类,可以分为插入排序、交换排序、选择排序、归并排序和分配排序五大类;而如果按照算法的复杂度来进行分类的话,可以分为三类:
- 简单的排序算法 O(n^2);
- 先进的排序算法 O(nlog n);
- 基数排序 O(d*n) 。
(注:n是排序元素个数,d是数字位数)
对于每一类算法,除了掌握算法本身的思想外,更重要的是需要了解该算法在进行排序时所依据的原则,以利于发展和创造更优秀的算法。
插入排序
在一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入此数后,数据序列仍然有序。
插入操作的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据,由于复杂度较高,一般而言插入排序算法适用于少量数据的排序。
- 直接插入排序
伪代码描述
第 i 趟直接插入排序的操作是,在含有 i-1 个记录的有序序列 [1,2,…,i-1] 中,插入一个记录 i 之后,变成了个含有 i 个记录的有序序列 [1,2,…,i];同时,采用顺序查找算法依次查找记录 i 的合适位置,并执行记录插入操作。
C 语言描述:
/*==========================================================
函数功能:直接插入排序
函数输入:数组首地址*a,数组长度n
函数输出:无
==========================================================*/
void InsertSort(int *a, int n)
{
int i,j;
int temp; //temp 作为哨兵,一个额外存储空间
for(i=1;i<n;++i)
{
if(a[i]<a[i-1])
{
temp=a[i]; //哨兵
for(j=i-1;temp<a[j] && j>=0;--j)
a[j+1]=a[j];
a[j+1]=temp; //L.r[j+1]即 L.r[j-1]
}
}
}
算法效率:O(n*n)
最主要的循环次数花费在 [1,i-1] 的有序序列中查找记录 i 的位置。
顺序查找采用逐个比较的方法来遍历查找,因此会存在两个极端情况。
当原始序列是完全递增有序时,将其重排序为递增有序。
当原始序列是完全递减有序时,将其重排序为递增有序。
- 希尔排序
希尔排序算法:
/*====================================================
函数功能:希尔排序
函数输入:数组首地址*arr,数组长度n
函数输出:无
====================================================*/
void ShellSort(int *arr,int n)
{
int dlta[3]={5,3,1};//按增量序列dlta[0..t-1]对数组作希尔排序
for(int k=0;k<3;++k)
ShellInsert(arr,n,dlta[k]);
}
希尔排序特点:反复调用一趟希尔排序算法,来实现不同增量下的排序,用于 n 较大,分割成几个小的直接插入排序,不稳定,O(n log n)。
/*==========================================================
函数功能:一趟希尔排序算法
函数输入:数组首地址*arr,数组长度n,间隔dk
函数输出:无
===========================================================*/
void shellInsert(int *arr,int n,int dk)
{
int i,j,k,temp;
for(k=0;k<dk;k++)
{
for(i=k+dk;i<n;i=i+dk) //从第二个元素开始插入排序
{
if(arr[i]<arr[i-dk]) //插入数据小于前面一个数据
{
temp=arr[i]; //存储插入的数据
//移位直到找到插入数据的顺序位置
for(j=i-dk;j>=0&&temp<arr[j];j=j-dk)
arr[j+dk]=arr[j];
arr[j+dk]=temp;
}
}
}
}
希尔排序是一种有效地改进排序算法,但其中的步长参数 k 的选取,一直是一个难以解决的问题。但一般而言,认为希尔排序的时间复杂度最好是能够达到 O(n*logn)。
交换排序
根据序列中的两个关键字的值比较结果来对换这两个记录在序列中的位置
- 冒泡排序
/*==========================================================
函数功能:直接插入排序
函数输入:数组首地址*a,数组长度n
函数输出:无
==========================================================*/
void InsertSort(int *a, int n)
{
int i,j;
int temp; //temp 作为哨兵,一个额外存储空间
for(i=1;i<n;++i)
{
if(a[i]<a[i-1])
{
temp=a[i]; //哨兵
for(j=i-1;temp<a[j] && j>=0;--j)
a[j+1]=a[j];
a[j+1]=temp; //L.r[j+1]即 L.r[j-1]
}
}
}
/*====================================================
函数功能:希尔排序
函数输入:数组首地址*arr,数组长度n
函数输出:无
====================================================*/
void ShellSort(int *arr,int n)
{
int dlta[3]={5,3,1};//按增量序列dlta[0..t-1]对数组作希尔排序
for(int k=0;k<3;++k)
ShellInsert(arr,n,dlta[k]);
}
/*==========================================================
函数功能:一趟希尔排序算法
函数输入:数组首地址*arr,数组长度n,间隔dk
函数输出:无
===========================================================*/
void shellInsert(int *arr,int n,int dk)
{
int i,j,k,temp;
for(k=0;k<dk;k++)
{
for(i=k+dk;i<n;i=i+dk) //从第二个元素开始
{
if(arr[i]<arr[i-dk]) //插入数据小于前面一个数据
{
temp=arr[i]; //存储插入的数据
//移位直到找到插入数据的顺序位置
for(j=i-dk;j>=0&&temp<arr[j];j=j-dk)
arr[j+dk]=arr[j];
arr[j+dk]=temp;
}
}
}
}
/*=======================================================
函数功能:冒泡排序
函数输入:数组首地址*a,数组长度n
函数输出:无
=======================================================*/
void BubbleSort(int *a,int n)
{
int i,j,temp;
int change=0; //检测一个排序中是否发生交换,若无交换,则序列有序,仅需一趟排序
for(i=n-1; i>=0&&change==0; i--) //for 循环,先判断条件,再看是否执行
{
change=1; //位置没有变
for(j=0; j<i; j++) //一次排序
{
if(a[j]>a[j+1])
{
change=0;
temp=a[j];
a[j]=a[j+1];
a[j+1]=temp;
}
}
}
}
i 指向当前准备排序的最大值 i 的位置,j 从 0 到 i-1 直到找到最大值与 i 位置比较。
冒泡排序特点:速度慢,移动次数多。
算法效率:O(n^2)
- 快速排序
为了实现这个目标,需要选择一个特殊的记录,称为枢轴(pivot)。枢轴的作用就是区分两个部分,如果能够找到一种方法,使得枢轴处于一个合适的位置,也就是说处于枢轴之前的所有记录都小于它,处于枢轴之后的记录都大于它,那么就达到了快速排序的目标。
首先需要选择一个枢轴数,一般而言选择整个序列的第一个数为枢轴。为了能够将枢轴移到一个合适的位置,保证枢轴前的数都小于枢轴,而枢轴后的数都大于枢轴,按如下步骤进行枢轴的移动:
(1)设置 low、high 两个指针,分别指向序列的第一个记录和最后一个记录。此时 low 指向了序列的第一个记录,也就是枢纽的位置。在图中,黑圈圈住的记录代表指针 low 指向的记录;虚线圈圈住的记录代表指针 high 指向的记录。
(2)比较 low、high两个指针所指记录的大小,若记录 low 小于记录 high,因此不需要交换记录 low 和记录 high;若记录 low 大于记录 high,则交换记录 low 和记录 high。下图中,记录 low 的值{49}大于记录 high 的值{04},因此交换记录 low 和记录 high 的记录值,记录 low 的值变为{04},记录 high 的值变为{49}。
(3)在交换了 low 和 high 的记录值之后,需要移动指针。移动指针 low 到记录{38}。
(4)此时记录的 low 为{38},记录 high 为{49},此时记录 low 小于记录 high,不需要交换两者的位置。
(5)再次移动指针 low 到记录{65},并再次与枢轴进行比较。此时 low 值大于记录 high 的值,交换指针 low 和 指针 high 所指向的记录,第三趟交换完毕。
(6)此时,指针 low 所指的记录变为了枢轴{49}。移动指针 high,使其指向记录{55}。将指针 low 所指向的枢轴与指针 high 所指向的记录相比较,枢轴值更小,因此不交换记录。
(7)继续移动指针 high,选取记录与枢轴进行比较,直到第五趟指针 high 指向记录{27}之后,枢轴才会与指针 high 所指向的记录交换位置。
(8)重复上述算法,直到指针 low 与指针 high 相等。
分别从两头与枢轴进行比较
只采用一趟快速排序算法,是无法对整个序列进行排序的,因此,需要继续调用快速排序算法,对枢轴之前的序列和枢轴之后的序列分别进行排序。
这种在算法的内部调用自身算法的思路,就是利用分治与递归的思路解决实际问题的典型思路。
快速排序的算法:
/*=====================================================
函数功能:快速排序
函数输入:数组首地址*a,数组长度n,初始low指针位置,初始high指针位置
函数输出:无
=====================================================*/
void QSort(int *arr,int n,int low,int high)
{
//对数组arr[n]作快速排序,递归调用
int pivotloc;
if(low<high)
{
pivotloc=Partition_1(arr,low,high);
QSort(arr,n,low,pivotloc-1);
QSort(arr,n,pivotloc+1,high);
}
}
/*=======================================================
函数功能:保持所有排序算法的输入输出参数一致
函数输入:数组首地址*a,数组长度n
函数输出:无
=======================================================*/
void QuickSort(int *arr,int n)
{
//递归函数设置初值
QSort(arr,n,0,n-1);
}
/*=============================================================
函数功能:一趟快速排序,利用枢轴移位排序。
前后两路查找,一趟快速排序
函数输入:数组首地址*a,初始low指针位置,初始high指针位置
函数输出:无
==============================================================*/
int Partition_1(int arr[],int low,int high)
{ //前后两路查找
int pivotloc=arr[low],temp; //将 low 位置指针空出,以第一个元素为枢轴数
while(low<high)
{
while(low<high && arr[high]>=pivotloc)
--high; //一直循环,直到从后面找到第一个小的值
temp=arr[low];
arr[low]=arr[high];
arr[high]=temp; //high 位置空出
while(low<high && arr[low]<=pivotloc)
++low;
temp=arr[low];
arr[low]=arr[high];
arr[high]=temp; //high 位置空出
}
return low;
}
快速排序的特点:反复调用一趟快速排序算法对数组 arr[n] 做快速排序,不稳定。
算法效率: O(nlogn)
但是初始记录的序列基本有序时,由于每一次比较都会涉及枢轴的交换,因此快速排序会蜕化为冒泡排序,复杂度恶化为 O(n^2)。
选择排序
- 简单选择排序
步骤:
第一步:在 1~n 个数中找出最小的数,然后与第一个交换,第一个数排好
第二步:在 2~n 个数中找出最小的数,然后与第二个交换,前两个数排好
…
第 n-1 步:在 n-1~n 个数中找出最小的数,然后与第 n-1 个交换,排序结束
相对于冒泡排序来说,简单选择排序效率高一些,因为冒泡排序在每一轮的每一次比较后,如果发现前面的数比后面的大,就要立即进行数据交换,而选择排序则每一轮最多进行一次数据交换。
C 语言程序:
/*=============================================================
函数功能:选择排序
函数输入:数组首地址*a,数组长度n
函数输出:无
==============================================================*/
void SelectionSort (int *a, int n)
{//选择排序
int i,j,temp;
for(i=0;i<n-1;i++)
for(j=i;j<n;j++)
{
if(a[j]<a[i])
{
temp=a[j];
a[j]=a[i];
a[i]=temp;
}
}
}
i 是开始比较的初始位置,i∈[0,n-1]。
选择排序特点:算法不稳定。
算法效率:O(n*n)。
简单选择排序算法的主要操作都耗费在记录的比较操作中,等价于第一趟是在所有记录中寻找最小的记录并取出,第二趟在剩下的 n-1 个记录中寻找最小的记录并取出,直到最后一个记录。因此,无论初始记录的排序是否基本有序,简单选择排序算法的比较次数都为 n(n-1) / 2,因此其时间复杂度也为 O(n^2),属于简单的排序算法的一种。
- 堆排序
堆的概念
堆排序思路
堆排序的两个关键问题:
- 一个无序序列中所有记录如何排成一个堆?
- 在输出了堆顶的记录之后,如何将输出了一个记录后序列中所剩下的记录再次排成一个堆?
2
3
4
2
3
4
5
6
程序实现:
/*=============================================================
函数功能:堆排序
函数输入:数组首地址*H,数组长度N
函数输出:无
==============================================================*/
void HeapSort(int *H,int N)
{
int i,temp;
for(i=N/2;i>=0;--i)
{
MaxHeap(H,i,N); //建立最大堆,找出了最大元素
}
for(i=N-1;i>=0;--i) //最大值分别放于尾部
{
temp=H[0];
H[0]=H[i];
H[i]=temp; //互换,最大值给数组最后元素
MaxHeap(H,0,i-1); //重新建立最大堆
}
/*=============================================================
函数功能:建立最大堆
函数输入:数组首地址*H,元素在数组中的位置为n,数组长度N
函数输出:无
==============================================================*/
void MaxHeap(int *H,int n,int N)
{//建立最大堆
int l=2*n+1; //数组从0开始(l=2*n+1,r=2*(n+1))和从1开始(l=2*n,r=2*n+1)的左右孩子标志不一样
int r=2*n+2;
int max; //l,r,max均表示数组坐标
int temp;//表示元素,交换时候用
if (l<N&&H[l]>H[n]) max=l; //左节点大
else max=n; //根大
if(r<N&&H[r]>H[max]) max=r; //右节点大
if(max!=n) //交换
{
temp=H[n];
H[n]=H[max];
H[max]=temp;
MaxHeap(H,max,N);//保证最大堆
}
}
在保证最大堆代码中叶子结点的根结点虽看不出用途,不过层数再递增后可以保证下方仍保持最大根。
一般而言,对于记录较少的序列排序时,并不建议使用堆排序。但是当记录数很大的序列进行排序时,堆排序会比较有效,因为主要时间耗费在建立初始堆和调整新堆时反复进行的“筛选”上。堆排序在最坏的情况下,时间复杂度也在 O(nlogn),相比快速排序在最差的情况下时间复杂度会恶化至 O(n^2),堆排序在最差情况下也有较好时间复杂度是其最大的优先。
归并排序
归并:将两个或两个以上的有序序列合起来,生成一个新序列,并且保证这个新序列有序。无论采用顺序结构还是链表结构,都可以在 O(m+n)的时间量级上实现。
归并排序就是重复调用归并算法,首先将单个记录视为一个有序序列,然后不断将相邻的两个有序序列合并得到新的有序序列,如此反复,最后得到一个整体有序的序列。这种算法称为 2-路归并排序。
代码实现:
/*=============================================================
函数功能:归并排序
函数输入:待排序数组首地址*arr,arr数组长度N
函数输出:无
==============================================================*/
void TMSort(int *arr,int N)
{
int t=1;
int *arr2=(int*)malloc(N*sizeof(int));
while(t<N)
{
MSort(arr,arr2,N,t); //(原数组,排序后数组,总长度,子数组长度)
t*=2; //子数组长度*2
MSort(arr2,arr,N,t);
t*=2;
}
free(arr2);
}
/*=============================================================
函数功能:一趟归并排序算法
函数输入:待排序数组首地址*SR,存放排序数组首地址*TR,数组长度n,一趟归并子系列长度t
函数输出:无
==============================================================*/
void MSort(int *SR,int *TR,int n,int t)
{
int i=0,j;
while(n-i>=2*t) //将相邻的两个长度为t的各自有序的子序列合并成一个长度为2t的子序列
{
Merge(SR,TR,i,i+t-1,i+2*t-1);
i=i+2*t;
}
if(n-i>t) //若最后剩下的元素个数大于一个子序列的长度t时
Merge(SR,TR,i,i+t-1,n-1);
else //n-i <= t时,相当于只是把X[i..n-1]序列中的数据赋值给Y[i..n-1]
for( j=i; j<n; ++j )
TR[j]=SR[j];
}
/*=============================================================
函数功能:合并算法,将两个有序序列合并为一个有序序列
函数输入:待排序数组首地址*SR,存放排序数组首地址*TR,m分SR为两个有序数组,SR长度n,i是SR数组的第一个数据地址
函数输出:无
==============================================================*/
void Merge(int *SR,int *TR,int i,int m,int n)
{//将有序的SR[i...m]和SR[m+1...n]归并为有序的TR[i...n]
int j,k;
for(j=m+1,k=i;i<=m&&j<=n;++k)
{
if(SR[i]<SR[j]) //选择最小的赋值
TR[k]=SR[i++];
else
TR[k]=SR[j++];
}
while(i<=m) //剩余的插入
TR[k++]=SR[i++];
while(j<=n) //剩余的插入
TR[k++]=SR[j++];
}
归并排序程序分析如下。参加排序的初始序列分成长度为 1 的子序列使用 MSort 函数进行第一趟排序1,得到 n/2 个长度为 2 的各自有序的子序列(若 n 为奇数,还会存在一个最后元素的子序列),再一次调用 MSort 函数进行第二趟排序,得到 n/4 个长度为 4 的各自有序的子序列,第 i 趟排序就是两两归并长度为 2^(i-1) 的子序列得到 n/(2^i) 长度为 2^i 的子序列,直到最后只剩一个长度为 n 的子序列。
由此看出,一共需要 log n 趟排序,每一趟排序的时间复杂度是 O(n),由此可知该算法的总的时间复杂度是 O(nlogn),但是该算法需要 O(n) 的辅助空间。
归并排序特点:反复调用一趟归并排序算法,属于稳定排序。
算法效率:复杂度 O(nlogn)。
归并排序的最好、最坏和平均时间复杂度都是 O(nlogn),而空间复杂度是 O(n),因此,归并排序算法比较占用内存,但效率较高。
另外,归并排序还有非递归的实现形式。
分配排序
分配排序是一种基于空间换时间的排序方法。其基本思想是,排序过程无须比较关键字,而是通过用额外的空间进行“分配”和“收集”来实现排序,当额外空间占用较大时,分配排序的时间复杂度可达到线性阶 O(n)。简言之,就是分配排序利用空间换时间,因此其性能与基于比较的排序有数量级的提高。
- 桶排序
桶排序将待排序序列划分成若干个区间,每个区间即视为一个桶;然后基于某一种映射函数,将待排序序列中的记录全部映射到对应的桶中,最后每个桶中的所有记录进行比较排序(如高效的快速排序),并按照顺序依次输出,即可得到最终的有序序列。
桶排序特点:均匀分布效果好,类型为整数,按照元素位数,分配到不同桶里面。
- 基数排序
数值排序的例子是按照最次为关键字(个位数)进行搜集和分配,再按照最主要关键字(十位数)进行搜集和分配,最终完成基数排序;这种情况我们称为最低位优先(Least Significantion Digit first,LSD)方法。
从最主要关键字开始基数排序的情况称为最高为优先(Most Significantion Digit first,MSD)。使用 MSD 方法时,必须从最主要关键字出发,首先把序列分成若干个子序列,每个子序列中的元素的最主要关键字相同;随后对每个子序列进行第二次 MSD 排序,直到子序列中仅剩一个元素为止。
一般而言,在关键字较少的时候,使用 LSD 会有很好的性能,而在关键字较多的时候,使用 MSD 会有较好的性能。
与桶排序相比,桶排序按照元素位数分桶,每个桶要进行其他排序,使其成为有序桶。
设:最大数组位数为 MaxBit,则进行 MaxBit 次收集和分配。0~9 共 10 个桶,依次由低位到高位填入(分配,桶排序),收集。
基数排序特点:从低位到高位一次排序,属于稳定排序,排序速度快。
测试代码:
/************************排序算法测试*****************************/
#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
#define length 11 //定义排序长度
void InsertSort(int *a,int n); //直接插入排序
void ShellSort(int *a,int n); //希尔排序
void BubbleSort(int *a, int n);//冒泡排序
void QuickSort(int *a, int n);//快速排序
void SelectionSort(int *a, int n);//选择排序
void HeapSort(int *a,int n);//堆排序
void TMSort(int *a,int n); //2路—归并排序(Two_Merging Sort)
void BucketSort(int *a,int n);//桶排序
void RadixSort(int *a,int n); //基数排序
//各种排序算法代码,为了方便理解,输入元素采用数组,实际应用中可根据需要设置为结构体
void main() {
int arr[length]= {15,124,2,19,14,321,415,1,62,58,55};
int method;
int i;
printf("排序前数组:\n");
for(i=0; i<length; i++)
printf("%3d ",arr[i]);
printf("\n");
printf("**************** 请选择排序方式 ********************\n");
printf("插入排序 1:直接插入排序 2:希尔排序\n");
printf("交换排序 3:起泡排序 4:快速排序\n");
printf("选择排序 5:简单选择排序 6:堆排序\n");
printf("归并排序 7:二路归并排序 \n");
printf("分配排序 8:桶排序 9:基数排序\n");
printf("*********************************************************\n");
scanf("%d",&method);
switch(method) {
case 1:
InsertSort(arr,length);
break; //插入排序
case 2:
ShellSort(arr,length);
break; //希尔排序
case 3:
BubbleSort(arr,length);
break; //冒泡排序
case 4:
QuickSort(arr,length);
break; //快速排序
case 5:
SelectionSort(arr,length);
break; //一般选择排序
case 6:
HeapSort(arr,length);
break; //堆排序
case 7:
TMSort(arr,length);
break; //2路—归并排序
case 8:
BucketSort(arr,length);
break; //桶排序
case 9:
RadixSort(arr,length);
break; //基数排序
default:
InsertSort(arr,length); //如果选择错误,默认直接插
} 入排序
printf("排序后数组:\n");
for(i=0; i<length; i++)
printf("%3d ",arr[i]);
printf("\n");
}
各种排序比较
各种排序算法的性能比较