介绍排序
排序:根据元素关键字的大小或其他规则来对序列中的元素进行调整,以确保它们的相对顺序符合所需的排序顺序(升序或降序)。
排序的稳定性:如果关键字key1=key2,在排序后记录1和记录2的位置没有发生变化,那么这种排序方法就是稳定的,相反,如果排序后记录1和2的顺序和排序前发生了变化,则此排序方法不稳定。
内排序:在排序的过程中,待排序的所有记录放置在内存中。
外排序:当数据无法一次性全部加载到内存中时,将大文件分割成适当大小的块,然后在内存中对这些块进行排序,最后将排好序的块合并成一个有序的文件。这个过程通常需要从外部存储(磁盘)读取和写入大量数据。
内排序又分为:插入排序、交换排序、选择排序、递归排序
排序准备
此篇文章的所有排序操作均是对以下顺序表进行排序操作。
结构如下:
typedef struct{
int a[MAXSIZE+1];//加了一个哨兵位置
int length;//表长(元素个数)
}SqList;
交换函数:
void swap(SqList *L,int i,int j){
int temp;
temp=L->a[i];
L->a[i]=L->a[j];
L->a[j]=temp;
}
冒泡排序
传统冒泡排序
如果不知道冒泡排序的同学,详细介绍请看下面文章:
写文章-CSDN创作中心https://mp.csdn.net/mp_blog/creation/editor/134959929
冒泡排序优化
看了以上文章你应该知道,这种排序方法不管在什么情况下都会进行n-1趟排列。但是当一个序列只有前几个是无序的,后几个都是满足你规定的顺序,那这样做就显得很不聪,根本没有必要做那么多趟排序。
具体优化见下面代码:
void BubbleSort(SqList *L){
int i,j;
bool flag=true;//标记变量
for(i=0;i<L->length -1&&flag;i++){
flag=false;
for(j=0;j<L->length -1-i;j++){//每一趟最后一个元素都已经确定为最大的,所以每次不用比较最后的元素,这里减掉趟数
if(L->a[j]>L->a[j+1]){
swap(L,j,j+1);
flag=true;//如果存在元素交换则将标记变量赋值为真。当不存在交换,就说明此序列已经有序,flag=false,结束循环
}
}
}
}
进行优化后,只有序列为逆序的情况下才会进行n-1次,其他情况均小于n-1次。
分析
(以下对优化后的算法进行分析)
当序列有序时,进行n-1次比较,只进行一趟排序;
当序列逆序时,每一趟进行(n-i)次比较,总共进行次比较,进行n-1趟排序;
时间复杂度:O()
简单选择排序
简单选择排序:在未排序的部分中选择最小的元素,然后将其与未排序部分的第一个元素交换位置。这个过程不断重复,每次选择未排序部分的最小元素,直到整个序列有序。
代码如下:
void SelectSort(SqList *L){
int i,j;
int min;
for(i=0;i<L->length;i++){
min=i;
for(j=i+1;j<L->length;j++){
if(L->a[min]>L->a [j]){
min=j;
}
}
if(i!=min);
swap(L,min,i);
}
}
分析
优点:交换的次数少。
不论什么情况,每一趟进行(n-i)次比较,总共进行次比较。
最少交换0次,最多交换n-1次。
时间复杂度:O()
直接插如排序
将数组分为已排序和未排序两部分,初始时已排序部分只有一个元素。算法从未排序部分逐个取出元素,插入到已排序部分的正确位置,通过逐步构建有序序列实现整体排序。在每一步,当前元素与已排序部分的元素逐个比较,找到其正确插入位置,确保已排序部分仍然有序。这一过程重复进行直到整个数组有序。
代码如下:
void InsertSort(SqList *L){
int i,j,temp;
for(i=1;i<L->length ;i++){// 循环从第二个元素开始,因为第一个元素默认是有序的
j=i-1;
if(L->a [i]<L->a [j]){
temp=L->a[i];
while(j>=0&&temp<L->a[j]){
L->a[j+1]=L->a[j];
j--;
}
L->a[j+1]=temp;// 将 temp 插入到找到的合适位置,j+1 是因为在 while 循环中 j 被减了一次
}
}
}
分析
序列是有序是需要比较(n-1)次,移动零次
序列为逆序时,
对于每个元素,都需要移动到已排序部分的最前面。因此,第二个元素需要移动一次,第三个元素需要移动两次,第四个元素需要移动三次,以此类推。
总的移动次数是前 n 个整数的累加和:
需要移动:
时间复杂度:O()
希尔排序
希尔排序:希尔排序是一种基于插入排序的排序算法,它的特点在于通过预处理使得原数组中某些较远的元素先有序,然后逐步缩小间隔,最终实现整体有序。
思想:希尔排序的核心思想是通过每一轮的插入排序,减小数组中元素的距离,使得原本较远的元素有机会在前几轮中先有序。
算法步骤:
- 选择一个增量序列(间隔序列):通常使用长度/2。
- 对每个增量序列进行排序:对于每个增量,将数组分成若干个子序列,每个子序列包含相邻间隔为增量的元素。对每个子序列应用插入排序。
- 减小增量:每一次减小增量/2。重复上述过程,缩小增量,直到增量为1。最终一次使用增量为1的插入排序,完成整个排序过程。
分析
堆排序
大顶堆:完全二叉树每个结点的值都大于或等于其左右孩子结点的值。
小顶堆:完全二叉树每个结点的值都小于或等于其左右孩子结点的值。
在堆排序算法中,第一步是构建一个初始的大顶堆,然后将堆顶元素(最大值)与堆中最后一个元素交换,接着对剩余的堆进行调整,确保仍然保持大顶堆的性质。这样的交换和调整步骤会重复进行,直到整个序列有序。
归并排序
归并排序的初始阶段将原始序列中的每个元素看作一个有序队列(长度为1的队列)。随后,逐步将相邻的有序队列两两合并,并对合并后的队列进行排序。然后,将这些排好序的队列再次两两合并,不断重复这一步骤,直到合并后的队列长度达到原始序列的长度,此时排序完成。这个过程保证了每次合并都是在有序的基础上进行,最终得到整个序列的有序结果。
快速排序
基本思想:
- 选择基准值:从待排序的序列中选择一个元素作为基准值。(为了方便选择第一个元素作为基准值)
- 划分序列:将比基准值小的放在基准值的左边,将大于基准值的放在基准值的右边。一趟排序下来基准值左边的值均小于右边的值。再次递归对基准值左边和右边的序列进行以上操作,直到当低指针low大于等于高指针high时,表示当前子序列的长度为1或0,即已经有序,此时递归结束。
代码如下:
//快排关键函数
int Partition(SqList *L,int low,int high){
int pivotkey=L->a [low];//将第一个值确定为基准值
while(low<high){
while(low<high&&L->a[high]>pivotkey){
high--;
}
swap(L,low,high);
while(low<high&&L->a[low]<pivotkey){
low++;
}
swap(L,low,high);
}
return low;//返回基准值的位置
}
//快排递归函数
void QSort(SqList *L,int low,int high){
int pivot;
if (low < high) {//递归条件,直到当低指针low大于等于高指针high时,表示当前子序列的长度为1或0,即已经有序
pivot = Partition(L, low, high);
QSort(L, low, pivot - 1);//对基准值左侧进行排序
QSort(L, pivot + 1, high);//对基准值右侧进行排序
}
}
//快排函数
void QuickSort(SqList *L){
QSort(L,0,L->length-1 );
}
快速排序优化
优化基准值
三数取中
选择中间位置的元素作为基准值,通过比较三个位置的元素,确保基准值在三者中间,有助于提高基准值的选择的准确性,减小最坏情况的概率,从而提高整体排序性能。
代码如下:
int mid=(low+high)/2;
if(L->a [low]>L->a [high])
swap(L,low,high);
if(L->a [mid]>L->a [high])
swap(L,mid,high);
if(L->a [mid]>L->a [low])
swap(L,low,mid);
int pivotkey=L->a [low];
结合插入排序优化性能
当子序列的规模较小时,切换到插入排序,因为对于小规模数据,插入排序更为高效。插入排序在小规模数据上有较好的性能,而快速排序在大规模数据上表现更好,因此这种切换可以提高整体性能。
代码如下:
#define THRESHOLD 7 //设置一个阈值,当序列长度小于该阈值时使用插入排序,反之,则使用快速排序
void QSort(SqList *L,int low,int high){
int pivot;
if (high-low>THRESHOLD) {
pivot = Partition(L, low, high);
QSort(L, low, pivot - 1);//对基准值左侧进行排序
QSort(L, pivot + 1, high);//对基准值右侧进行排序
}
else
InsertSort(L);
}
优化递归
代码如下:
#define THRESHOLD 3
void QSort(SqList *L,int low,int high){
int pivot;
if (high-low>THRESHOLD) {
//优化递归----------------------------------------------------------------------------------
while(low<high){
pivot = Partition(L, low, high);
QSort(L, low, pivot - 1);
low=pivot+1;
}
//-----------------------------------------------------------------------------------------
}
else
InsertSort(L);
}
减少交换
将交换操作改变为替换操作(简单的赋值语句:L->a [low]=L->a [high];),减少交换次数优化性能。
代码如下:
while(low<high){
while(low<high&&L->a[high]>pivotkey){
high--;
}
L->a [low]=L->a [high];//减少交换次数
while(low<high&&L->a[low]<pivotkey){
low++;
}
L->a [high]=L->a [low] ;//减少交换次数
}
L->a [low]=temp;
优化后的完整代码如下:
//交换操作函数
void swap(SqList *L,int i,int j){
int temp;
temp=L->a[i];
L->a[i]=L->a[j];
L->a[j]=temp;
}
//快排关键函数
int Partition(SqList *L,int low,int high){
//三数取中优化基准值 ------------------------------------------------------
int mid=(low+high)/2;
if(L->a [low]>L->a [high])
swap(L,low,high);
if(L->a [mid]>L->a [high])
swap(L,mid,high);
if(L->a [mid]>L->a [low])
swap(L,low,mid);
//-------------------------------------------------------------------------
int pivotkey=L->a [low];//将第一个值确定为基准值
int temp=L->a[low];
while(low<high){
while(low<high&&L->a[high]>pivotkey){
high--;
}
L->a [low]=L->a [high];//减少交换次数
while(low<high&&L->a[low]<pivotkey){
low++;
}
L->a [high]=L->a [low] ;//减少交换次数
}
L->a [low]=temp;
return low;//返回基准值的位置
}
//快排递归函数
#define THRESHOLD 3 //设置一个阈值,当序列长度小于该阈值时使用插入排序,反之,则使用快速排序
void QSort(SqList *L,int low,int high){
int pivot;
if (high-low>THRESHOLD) {
//优化递归 -------------------------------------------------------------------------------
while(low<high){
pivot = Partition(L, low, high);
QSort(L, low, pivot - 1);
low=pivot+1;
}
//---------------------------------------------------------------------------------------
}
else
InsertSort(L);
}
//快排函数
void QuickSort(SqList *L){
QSort(L,0,L->length-1 );
}