排序
基本概述
定义:将一组杂乱无章的数据按一定规律次序排列起来(即将一个无序序列拍成一个有序序列(由小到大))
如果参加排序的数据结点包含多个数据域,我们一般是针对某个域而言
排序方法的分类:
-
按存储介质分为:
- 内部排序:数据量不大、数据在内存、无需内外存储交换数据
- 外部排序:数据量不大、数据在外存(文件排序)
-
按比较器的个数分为:
- 串性排序:单处理机(同一时刻比较一对元素)
- 并行排序:多处理机(同一时刻比较多对元素)
-
按主要操作分为:
- 比较排序:用比较的方法(插入排序、交换排序、选择排序、归并排序)
- 基数排序:不用比较元素的大小,仅仅根据元素本身的取值确定其有序位置
-
按辅助空间可分为:
- 原地排序:辅助空间用量为O(1)的排序方法(所占用的辅助空间与参加排序的数据量大小无关)
- 非原地排序:辅助空间用量超过O(1)的排序方法
-
按稳定性可分为:
- 稳定排序:能使任何数值相等的元素排序以后相对次序不变
- 非稳定排序:不是稳定排序的方法
-
按自然性分为:
- 自然排序:输入数据越有序,排序的速度越快的排序方法
- 非自然排序:不是自然排序的方法
排序的稳定性只对于结构类型数据排序有意义,如学生的信息排序
不是衡量算法优劣的因素
按排序依据原则我们要学习:
- 插入排序:直接插入排序、折半插入排序、希尔排序
- 交换排序:冒泡排序、快速排序
- 选择排序:简单选择排序、堆排序
- 归并排序:2-路归排序
- 基数排序
下面的存储结构均以顺序表存储
#define MAXSIZE 20
typedef struct{int r[MAXSIZE+1]; //r[0]一般作哨兵或缓冲区int length;
}Sqlist;
插入排序
基本思想:每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置,直到对象全部插入为止
- 即边插入边有序,保证子序列中随时都是排好序的
三种插入的基本情况:
- 插在中间
- 插在最前面
- 插在最后面
插入顺序 | 名称 |
---|---|
顺序法定位插入位置 | 直接插入排序 |
二分法定位插入位置 | 二分插入排序 |
缩小增量多遍插入排序 | 希尔排序 |
直接插入排序
算法思路1:
- 用临时变量复制插入元素
- 记录后移,查找插入位置
- 插入到正确位置
void Insertsort1(Sqlist *L){ //假设已经初始化完毕int i,j;for(i=2;i<=L->length;i++){int x=L->r[i];for(j=i-1;j>=1;j--){if(L->r[j]<=x){L->r[j+1]=x;break;}else{L->r[j+1]=L->r[j];}}if(j==0) L->r[1]=x;}
}
算法思路2:
- 将插入位置赋值给0号位置作哨兵
- 记录后移,查找插入位置
- 插入到正确位置
void Insertsort2(Sqlist *L){ //假设已经初始化完毕int i,j;for(i=2;i<=L->length;i++){L->r[0]=L->r[i];for(j=i-1;L->r[0]<L->r[j];j--){L->r[j+1]=L->r[j];}L->r[1]=L->r[0];}
}
时间复杂度结论:
- 原始数据越接近有序,排序速度越快
- 最坏情况下,T=O(n^2)
- 平均情况下,T=O(n^2)
- 要提高查找速度
- 减少元素的比较次数
- 减少元素的移动次数
折半插入排序
void Insertsort3(Sqlist *L){for(int i=2;i<=L->length;i++){L->r[0]=L->r[i];int low=1,high=i-1;while(low<=high){int mid=(low+high)/2;if(L->r[mid]>L->r[0]){high=mid-1;}else if(L->r[mid]<L->r[0]){low=mid+1;}}for(int j=i-1;j>=low;j--) L->r[j+1]=L->r[j];L->r[low]=L->r[0];}
}
-
折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快
-
它所需要的关键码比较次数与待排序对象序列的初始排列无关,当插入第i个位置时,需要经过[log2i]+1次关键码比较才能确定它插入的位置
- 当n较大时,总关键码比较次数比直接插入排序的最坏情况要好很多,但比最好情况要差
- 在对象的初始排列已经按关键码排好序或结晶有序时,直接插入排序比折半插入排序只需不过的关键码比较次数要少
-
时间复杂度O(n^2),空间复杂度O(1)
希尔排序
希尔排序算法的基本出发点:
- 直接插入排序在基本有序时,效率较高
- 在待排序的记录个数较少时,效率较高
基本思想:
先将整个待排记录分割成若干个子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序
特点:
- 定义增长序列Dk:D(M)>D(M-1)>…>D(1)
- 一次移动,移动位置较大,跳跃式的接近排序后的最终位置
- 最后一次只需要少量移动
- 增量序列必须是递减互质的,最后一个必须是1
void Shellinsert(Sqlist* L,int dk){int i,j;for(i=dk+1;i<L->length;i++){if(L->r[i]<L->r[i-dk]){L->r[0]=L->r[i];for(j=i-dk;j>0&&L->r[0]<L->r[j];j=j-dk){L->r[j+dk]=L->r[j];}L->r[j+dk]=L->r[0];}}
}
void shellsort(Sqlist *L,int dlta[],int t){ //取增长序列的前t个for(int k=0;k<t;k++){Shellinsert(L,dlta[k]);}
}
- 希尔排序算法效率与增量序列的取值有关
- 希尔排序是一种不稳定的排序算法
- 空间复杂度O(1)
- 目前尚未解决如何选择最佳增量序列
- 不宜再链式存储结构上实现
交换排序
冒泡排序
基本思想:每趟不断将记录两两比较,并按“前小后大”规则交换
void bubble_sort(Sqlist *L){int i,j,temp;int n=L->length;for(i=1;i<=n-1;i++){for(j=1;j<=n-i;j++){if(L->r[j]>L->r[j+1]){temp=L->r[j+1];L->r[j+1]=L->r[j];L->r[j]=temp;}}}
}
优点:每趟结束,不仅能基础一个最大值到最后面位置,还能同时部分理顺其他元素
在这基础上,一旦某一趟比较时不出现记录交换,说明已经排好序了,可以结束本算法,因此在这基础上我们对该算法进行改进:
void new_bubble_sort(Sqlist *L){ //通过增加标志位来检测是否发生交换int i,j,flag=1,temp;int n=L->length;for(i=1;i<=n-1&& flag==1;i++){flag=0;for(j=1;j<=n-i;j++){if(L->r[j]>L->r[j+1]){flag=1;temp=L->r[j+1];L->r[j+1]=L->r[j];L->r[j]=temp;}}}
}
快速排序
快速排序的思想:在一组数中以某个数为中心将小的扔到它的左边,将比它大的扔到它的右边,并持续这种操作,进而实现排序的目的
具体实现:选定一个中间数作为参考,所有元素与之比较,小的调到其左边,大的调到右边
void Quicksort(Sqlist *L, int low, int high) {if (low >= high) { // 如果区间无效或只有一个元素,直接返回return;}int pivot = L->r[low]; // 选择基准值(这里选择low位置的元素)int left = low, right = high;while (left < right) { // 分区过程// 从右向左找到第一个小于基准值的元素while (left < right && L->r[right] >= pivot) {right--;}if (left < right) {L->r[left] = L->r[right]; // 将该元素移到左边}// 从左向右找到第一个大于基准值的元素while (left < right && L->r[left] <= pivot) {left++;}if (left < right) {L->r[right] = L->r[left]; // 将该元素移到右边}}// 基准值归位L->r[left] = pivot;// 递归排序左右子区间Quicksort(L, low, left - 1);Quicksort(L, left + 1, high);
}
算法分析:
- 时间复杂度:
- 平均时间复杂度为O(n^2)
- 就平均计算时间而言,快速排序算法在所有排序算法中最好
- 空间复杂度:
- 快速排序不是
原地排序
,在程序中使用了递归,需要调用系统栈的支持(即使不用递归也需要用户栈) - 平均情况下:需要O(logn)的栈空间
- 最坏情况下:O(n^2)
- 快速排序不是
- 稳定性:
- 快速排序是一种不稳定的排序方法
- 快速排序不适于对原本有序或基本有序的记录序列进行排序
- 划分元素的选取(中心点)是影响时间性能的关键
- 输入数据次序越乱,所划分元素值的随机性越好,排序速度越快,所以快速排序不是自然排序方法
选择排序
简单选择排序
基本思想:在待排序的数据中依次选取最大(小)值放在其最终位置
void selectsort(Sqlist *L){if (L == NULL || L->length <= 1) {return; }for(int i=1;i<L->length;i++){int k=i;for(int j=i+1;j<=L->length;j++){if(L->r[j]<L->r[k]) k=j;}if(k!=i){int temp=L->r[k];L->r[k]=L->r[i];L->r[i]=temp;}}
}
算法分析:
-
时间复杂度:O(n^2)
- 移动次数:
- 最好情况:0
- 最坏情况:3(n-1) //3是因为交换语句中共有3条
- 比较次数:无论待排序列何种状态,比较次数都相同
- 移动次数:
-
算法稳定性:稳定的
堆排序
堆的定义:若n个元素的序列{a1,a2…an}满足{a(i)≤a(2i) a(i)≤a(2i+1)}或{a(i)≤a(2i) a(i)≤a(2i+1)},则分别称为该序列{a1,a2…an}为小根堆和大根堆
从堆的定义中可以看出,堆序列其实是一颗满足任一非叶子结点均小于(大于)其孩子结点的完全二叉树
堆排序:
若在输出堆顶的最值之后,使得剩余的n-1个元素的序列重新生成一个堆…反复执行,便能得到一个有序序列
关键在于怎样将一个无序序列转化为一个堆
堆的调整:
在输出堆顶元素之后,调整剩余元素为一个新的堆,是为“筛选”过程
- 输出堆顶元素,以编号最后一个元素代替之
- 然后将根结点值与左右子树的根结点进行比较,与其中小者进行交换
- 重复上述操作,直至叶子结点,将得到新的堆
// 堆调整函数
void HeapAdjust(int r[], int s, int m) {int rc = r[s];for (int i = 2 * s; i <= m; i *= 2) {if (i < m && r[i] < r[i + 1]) i++;if (rc >= r[i]) break;r[s] = r[i];s = i;}r[s] = rc;
}
这段代码实现了一个堆调整(Heap Adjust)过程,用于调整一个不满足堆性质的节点,使其重新符合大根堆的定义
建立堆:
由于堆实质上是一个线性表,那么我们可以顺序存储一个堆
- 从第一个非叶子结点n/2开始依次调整,直到根结点
//从最后一个非叶子节点开始
for(int i=n/2;i>=1;i++){HeapAdjust(R,i,n);
}
实质上,堆排序就是利用完全二叉树中父节点和孩子结点之间的内在关系来排序的
完整的堆排序代码:
void swap(int *a, int *b) {int temp = *a;*a = *b;*b = temp;
}
void Heapsort(int r[],int n){for(int i=n/2;i>=1;i--){HeapAdjust(r,i,n);}for(int i=n;i>1;i--){swap(r[1],r[i]);HeapAdjust(r,1,i-1);}
}
算法分析:
-
时间复杂度O(nlog2(n))
-
堆排序的时间主要耗费在建立初始堆和调整建新堆时进行的反复。
-
无论待排序列中的记录是正序还是逆序排列,都不会是堆排序处于“最好”或“最坏”的状态
-
因为该算法中只用到了交换时所产生的辅助空间,因此空间复杂度为O(1)
-
堆排序是一种不稳定的排序方法,不适合n小的时候使用,但对于n较大的文件还是很有效的
归并排序
基本思想:将两个或两个以上有序子序列“归并”为一个有序序列
- 在内部排序中,通常采用的是2-路归并排序
- 即将两个位置相邻的有序子序列归并为1个
// 主递归函数,用于归并排序
void MergeSort(int arr[], int l, int r) {if (l < r) { // 如果数组只有一个元素,无需排序int m = l + (r - l) / 2; // 找到中间点// 递归排序左右两部分MergeSort(arr, l, m);// 合并两个有序子数组 arr[l...m] 和 arr[m+1...r]MergeSort(arr, m + 1, r);// 合并两个已排序的部分Merge(arr, l, m, r);}
}
具体的Merge代码省略
- 时间复杂度:O(nlog2n)
- 空间复杂度:O(n)–需要一个与原始序列同样大小的辅助序列(缺点)
- 稳定性:稳定
基数排序
概念:
基数排序(Radix Sort)是一种非比较型排序算法,通过将整数按位数(个位、十位、百位等)进行分配和收集来实现排序。它特别适合用于整数排序,尤其是当整数的范围较小时。
特点:
-
非比较型排序:不依赖元素之间的比较,而是通过分配和收集的方式完成排序。
-
稳定性:基数排序是一种稳定的排序算法,即相等元素的相对顺序不会改变。
-
适用场景:特别适合整数排序,尤其是当数字的位数较少时。
-
时间复杂度:O(k*⋅(*n+m)),其中:
- m是关键字取值范围
- n 是数组的长度
- k 是关键字个数
-
空间复杂度:O(n+m)