排序操作的学习
一、排序的简述
- 排序是计算机内经常进行的一种操作,其目的是将一组**“无序”的记录序列调整为“有序”**的记录序列。分内部排序和外部排序,若整个排序过程不需要访问外存便能完成,则称此类排序问题为内部排序。反之,若参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,则称此类排序问题为外部排序。内部排序的过程是一个逐步扩大记录的有序序列长度的过程。
- 主要分类:

- 比较和非比较的区别:常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序**。在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。在冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)。比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)。非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。
二、主要排序的C语言实现
1.冒泡排序(Bubble Sort)
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一 次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
//冒泡排序
void BubbleSort(int a[], int n)
{
int i, j;
for(i=0; i<n; i++){
bool flag=false; //表示本趟冒泡是否发生交换的标志
for(j=1; j<n-i; j++){ //j的起始位置为1,终止位置为n-i
if(a[j]<a[j-1]){
Swap(a[j-1], a[j]);
flag=true;
}
}
if(flag==false) //未交换,说明已经有序,停止排序
return;
}
}
最佳情况:T(n) = O(n) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)
2.选择排序(Selection Sort)
表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
//选择排序,以升序为例
void SelectSort(int a[],int n){
int i,j,min;
for(i=0;i<n-1;i++){
min=i;
for(j=i+1;j<n;j++){
if(a[j]<a[min)
min=j;
}
if(min!=i)
Swap(a[i],a[min]);
}
}
void Swap(int a,int b){
int temp;
temp=a;
a=b;
b=temp;
}
最佳情况:T(n) = O(n2) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(n2)
3.插入排序(Insertion Sort)
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
//直接插入法一
void InsertSort1(int a[], int n)
{
int i, j;
for(i=1; i<n; i++)
if(a[i] < a[i-1])
{
int temp = a[i]; //保存要比较的值
for(j=i-1; j>=0 && a[j]>temp; j--) //从后向前查找待插入位置
a[j+1] = a[j]; //挪位
a[j+1] = temp; //复制到插入位置
}
}
//直接插入法二:用数据交换代替法一的数据后移(比较对象只考虑两个元素)
void InsertSort2(int a[], int n)
{
for(int i=1; i<n; i++)
for(int j=i-1; j>=0 && a[j]>a[j+1]; j--)
Swap(a[j], a[j+1]);
}
void Swap(int a,int b){
int temp;
temp=a;
a=b;
b=temp;
}
最佳情况:T(n) = O(n) 最坏情况:T(n) = O(n2) 平均情况:T(n) = O(n2)
4.希尔排序(Shell Sort)
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破O(n2)的第一批算法之一。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。希尔排序是把记录按下表的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
基本步骤:在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
//希尔排序法一
void shellSort1(int a[],int n){
int i,j,gap,temp;
for(gap = n/2;gap>0;gap/=2){
for(i=gap;i<n;i+=gap){
temp = a[i];
for(j = i-gap;j>=0&&a[j]>temp;j-=gap){
a[j+gap] = a[j];
}
a[j+gap] = temp;
}
}
}
//希尔排序法二,和上面的直接插入排序类似,用数据交换代替法一的数据后移(比较对象只考虑两个元素)
void ShellSort2(int a[], int n)
{
int i, j, gap;
//分组
for(gap=n/2; gap>0; gap/=2)
//直接插入排序
for(i=gap; i<n; i++)
for(j=i-gap; j>=0 && a[j]>a[j+gap]; j-=gap)
Swap(a[j], a[j+gap]);
}
最佳情况:T(n) = O(nlog2 n) 最坏情况:T(n) = O(nlog2 n) 平均情况:T(n) =O(nlog2n)
5.归并排序(Merge Sort)
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(n log n)的时间复杂度。代价是需要额外的内存空间。归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
//归并排序思想
ElemType *b = (ElemType *) malloc ((n+1)*sizeof(ElemType)); //带排序表有n个记录
void Merge(ElemType a[],int low,int mid,int high){
//表a中两段a[low,...,mid]和a[mid+1,...,high]各自有序,将其合并成一个有序表
for(int k=low;k<=high;k++)
b[k]=a[k]; //将a中元素复制到辅助数组b中
for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
if(b[i]<=b[j]) //比较b的左右两段中的元素
a[k]=b[i++]; //将较小值复制到a中
else
a[k]=b[j++];
}
while(i<=mid) //若左半部分表未检测完,复制
a[k++]=b[i++];
while(j<=high) //若右半部分表未检测完,复制
a[k++]=b[j++];
}
void MergeSort(ElemType a[],int low,int high){
if(low<high){
int mid=(low+high)/2; //从中间划分2个子序列
MergeSort(a,low,mid); //对左侧子序列进行递归排序
MergeSort(a,mid+1,high); //对右侧子序列进行递归排序
Merge(a,low,mid,high); //归并
}
}
-------------------------------------------------------------------------------------------
//递归排序的完整实现
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//合并两个有序序列(即归并)
// A: 待合并的序列(含两个子序列,排在一起)
// Temp: 辅助空间
// L: 左边序列起点下标
// R: 右边序列起点下标,
// RightEnd: 右边序列终点下标
void Merge(int A[], int Temp[], int L, int R, int RightEnd){
int LeftEnd = R-1;
int p=L,i;
int num=RightEnd-L+1;
//先合并最短序列的长度的个数个元素
while(L<=LeftEnd&&R<=RightEnd){
if(A[L]<=A[R])
Temp[p++]=A[L++];
else
Temp[p++]=A[R++];
}
//判断如果是左侧序列还有剩余
while(L<=LeftEnd)
Temp[p++]=A[L++];
//判断如果是右侧序列还有剩余
while(R<=RightEnd)
Temp[p++]=A[R++];
// 将辅助空间中的值拷贝到原列表中,完成排序
for(i=0;i<num;i++,RightEnd--)
A[RightEnd]=Temp[RightEnd];
}
//递归拆分,递归归并
void m_sort(int* arr, int* temp, int L, int right_end){
int center;
if(L<right_end){
center = (L+right_end)/2;
m_sort(arr,temp,L,center);
m_sort(arr,temp,center+1,right_end);
Merge(arr,temp,L,center+1,right_end);
}
}
//归并排序
void merge_Sort(int* arr,int length){
int *temp=(int* )malloc(length*sizeof(int)); //申请辅助空间
if(temp==NULL){
return;
}
m_sort(arr,temp,0,length-1);
free(temp);
temp=NULL;
}
//打印元素
void printArr(int *arr,int length){
for(int i=0;i<length;i++){
printf("%d\n",arr[i]);
}
}
int main(){
int arr[10]={3,5,2,10,8,9,6,4,7,1};
int length = sizeof(arr)/sizeof(int);
length = 10;
merge_Sort(arr,length);
printArr(arr,length);
return 0;
}
最佳情况:T(n) = O(n) 最差情况:T(n) = O(nlogn) 平均情况:T(n) = O(nlogn)
6.快速排序(Quick Sort)
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
//快速排序
void QuickSort(int a[],int left,int right){
if(left<right){
int i=left,j=right;
int base=a[left]; //基准
while(i<j){
while(i<j&&a[j]>=base) //从右往左找小于base的元素
j--;
if(i<j)
a[i++]=a[j];
while(i<j&&a[i]<base) //从左往右找大于base的值
i++;
if(i<j)
a[j--]=a[i];
}
a[i]=base; //将基准base填入最后的坑中
QuickSort(a,left,i-1); //递归调用,分治
QuickSort(a,i+1,right);
}
}
最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(nlogn)
最佳情况:T(n) = O(nlogn) 最差情况:T(n) = O(n2) 平均情况:T(n) = O(nlogn)
三、总结
稳定性
稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
不稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
选择排序算法准则
1.待排序的记录数目n的大小;
2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
3.关键字的结构及其分布情况;
4.对排序稳定性的要求。