引入
在生活中,经常要对亿些东西进行排序。比如,考完试后,老师会对成绩的高低将试卷进行排序;玩扑克牌时,按点数排序手牌......多亏了排序,才能将杂乱无章的东西整理清楚,便于查询统计与利用
排序算法有很多,本帖将介绍几种使用于不同场合的简单排序算法
G o ! Go! Go!
选择排序:
选择排序是比较排序,首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。
因为这种算法有两重循环,所以时间复杂度为 O ( n 2 ) O(n^2) O(n2),由于需要一个数组来存储数字,所以空间复杂度为 O ( n ) O(n) O(n)
模板:
for(int i=1;i<=n;i++)//选择排序
for(int j=i+1;j<=n;j++)
if(a[j]<a[i])
swap(a[j],a[i]);
冒泡排序:
冒泡排序是比较排序,其实跟选择排序没啥区别。但是,冒泡排序是把最大项放从左往右疯狂移动的
所以,时间复杂度也是 O ( n 2 ) O(n^2) O(n2),空间复杂度也是 O ( n ) O(n) O(n)
模板:
for(int i=1;i<=n;i++)//冒泡排序
for(int j=1;j<=i-1;j++)
if(a[j]>a[j+1])
swap(a[j],a[j+1]);
插入排序:
插入排序是比较排序,就是把数分为有序部分与无序部分,我们只需要把无序的数插入到有序中就行了
所以,时间复杂度也还是 O ( n 2 ) O(n^2) O(n2),空间复杂度也还是 O ( n ) O(n) O(n)
模板:
for(int i=1,now;i<n;i++){//插入排序
now=a[i],j;
for(j=i-1;j>=0;j--){
if(a[j]>now) a[j+1]=a[j];
else break;
}
a[j+1]=now;
}
归并排序:
归并排序是比较排序,每次将数组分解为单个元素,此时单个元素必为有序数组,并不断递归
归并排序是稳定的,时间复杂度只有 O ( n log n ) O(n\log n) O(nlogn),空间复杂度也是 O ( n ) O(n) O(n)
模板:
void guibing(int l,int r){//归并排序
if(l==r) return;
int o=l,p=r,mid=(l+r)/2,cnt=l;
guibing(l,mid),guibing(mid+1,r);
int v=mid+1;
while(l<=mid&&v<=r){
if(a[l]>a[v]) b[cnt++]=a[v++];
else b[cnt++]=a[l++];
}
while(l<=mid) b[cnt++]=a[l++];
while(v<=r) b[cnt++]=a[v++];
for(int i=o;i<=p;i++) a[i]=b[i];
}
快速排序:
更详细请看This
快速排序,简称"快排",是比较排序。它的数据量可大了,插入排序、选择排序、冒泡排序都无法胜任。快排说起来也挺简单的,算法大致过程就是,找到一个"哨兵数",将序列中比哨兵数小的数字都放在哨兵
数的左边,而大的则放在右边,就这样不停的分割,直到集合不能分割为止
怎么选哨兵数呢?随便选,可能是第一个,也可能是中间那个,也可以随机选择。
虽然在极端情况下,快速排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2),但是如果随机哨兵数,则很难出现这种情况。事实上,随机化快排的时间复杂度为 O ( n log n ) O(n \log n) O(nlogn),空间复杂度为 O ( n ) O(n) O(n),而且不需要额外的辅助空间所以是一种最为实用的排序算法。其实,快排也利用了分治的思想(把大问题分解成小问题)
实现方法
void qsort(int a[],int l,int r){
int i=l,j=r,flag=a[(l+r)/2],tmp;
do{
while(a[i]<flag) i++;
while(a[j]>flag) j--;
if(i<=j){
swap(a[i],a[j]);
i++,j--;
}
}while(i<=j);
if(l<j) qsort(a,l,j);
if(l<r) qsort(a,i,r);
}
但是,这样比较繁琐。所以,有一个函数可以直接帮你解决,所需要用的头文件如下。
#include<algorithm>
当然你也能用万能头。
这里面包含了一个名叫sort
的东西,它用法如下。
sort(a+1,a+n+1);
对数组 a 1 a_1 a1 到 a n a_n an 进行从小到大进行排序
但是,如果是从大到小呢?
那我们就可以用到 cmp
自定义函数,如果是从大到小,则为
bool cmp(int a,int b){return a>b?true:false;}
int main(){
sort(a+1,a+n+1,cmp);
}
桶排序
桶排序是非比较排序,它的大体思路就是得到无序数组的取值范围,根据取值范围"创建"对应数量的"桶",遍历数组,把每个元素放到对应的"桶"中,按照顺序遍历"桶"中的每个元素,依次放入数组中,即可完成数组的排序
桶排的时间复杂度为 O ( n + k ) O(n+k) O(n+k),空间复杂度为 O ( n + k ) O(n+k) O(n+k)
注: k k k 是数据的范围
模板:
for(int i=1,a;i<=n;i++){
cin>>a;
tong[a]++;
}
堆排序
堆排序(Heap Sort)是一种基于堆(Heap)这种数据结构的排序算法,堆是一种特殊的完全二叉树,分为最大堆和最小堆。在堆排序中,通常使用最大堆来实现升序排序。堆排序的时间复杂度为 O ( n log n ) O(n \log n) O(nlogn),是一个不稳定的原地排序算法。
堆排序的基本思想
- 构建最大堆:将无序数组调整为最大堆。最大堆的性质是每个节点的值都大于或等于其左右孩子节点的值,即根节点的值最大。
- 排序过程 :
- 将堆顶元素(最大值)与堆的最后一个元素交换,这样最大值就被移到了数组的末尾,并从未排序的部分分离出来。
- 缩小堆的范围,排除已经排好序的元素。
- 对剩下的堆进行堆调整,使其保持最大堆性质。
- 重复上述步骤,直到堆的大小为
1
,排序完成。
实现步骤
假设数组的下标从 0
开始,那么对于一个节点 (i)
:
- 父节点 :
((i-1)/2)
- 左孩子节点 :
(2*i + 1)
- 右孩子节点 :
(2*i + 2)
C++ 代码实现
以下是堆排序的 C++ 代码实现:
cpp
#include <iostream>
#include <vector>
using namespace std;
// 调整堆,使得以 index 为根的子树满足最大堆性质
void heapify(vector<int>& arr, int n, int index) {
int largest = index; // 初始化为根节点
int left = 2 * index + 1; // 左孩子节点
int right = 2 * index + 2; // 右孩子节点
// 如果左孩子存在且大于当前节点
if (left < n && arr[left] > arr[largest])
largest = left;
// 如果右孩子存在且大于当前节点
if (right < n && arr[right] > arr[largest])
largest = right;
// 如果 largest 不是根节点,交换并递归调用
if (largest != index) {
swap(arr[index], arr[largest]);
heapify(arr, n, largest);
}
}
// 主函数:堆排序
void heapSort(vector<int>& arr) {
int n = arr.size();
// 构建最大堆
for (int i = n / 2 - 1; i >= 0; i--)
heapify(arr, n, i);
// 逐步将最大值放到数组的末尾
for (int i = n - 1; i > 0; i--) {
swap(arr[0], arr[i]); // 将当前最大值(堆顶)与数组末尾元素交换
heapify(arr, i, 0); // 重新调整堆的结构
}
}
// 测试代码
int main() {
vector<int> arr = {12, 11, 13, 5, 6, 7};
heapSort(arr);
cout << "排序后的数组: ";
for (int val : arr)
cout << val << " ";
cout << endl;
return 0;
}
代码解析
- heapify 函数:用于维护堆的性质。它从指定的节点开始,递归地调整子树使之满足最大堆的性质。
- heapSort 函数 :首先通过
heapify
构建最大堆,然后逐步将堆顶元素(最大值)移到数组的末尾,并对剩下的未排序部分进行堆调整。 - main 函数 :定义数组并调用
heapSort
进行排序,输出排序结果。
时间复杂度
- 构建堆的时间复杂度 : O ( n ) O(n) O(n)。
- 排序过程的时间复杂度 : O ( n log n ) O(n \log n) O(nlogn),因为每次删除堆顶元素并进行调整的时间复杂度是 O ( log n ) O(\log n) O(logn)。
因此,堆排序的总时间复杂度是 O ( n log n ) O(n \log n) O(nlogn)。
希尔排序(Shell Sort)介绍
希尔排序是一种基于插入排序的排序算法,通过对数组进行分组并逐渐减小组间距,改善了插入排序的效率。它通过提前移动远距离的元素,使得数据逐步接近有序,从而减少后续排序的交换次数。
希尔排序的基本思想
希尔排序的基本思想是:先将待排序数组分成若干组,分别对每组进行插入排序,然后逐渐缩小这些组的间隔,直到间隔为 1,最后进行一次标准的插入排序。
- 分组:通过一个间隔序列(gap sequence)将数组分成若干个子序列,每个子序列进行插入排序。
- 插入排序 :对每个子序列进行插入排序,不同于传统插入排序的是,这里比较的是间隔为
gap
的元素。 - 缩小间隔 :逐步减小
gap
的值,直到gap = 1
,此时执行标准的插入排序。
希尔排序的增量序列(Gap Sequence)
间隔序列(gap sequence)对希尔排序的性能有重要影响。常见的增量序列包括:
- 希尔增量序列
- Hibbard增量序列
- Sedgewick增量序列
通常情况下,选择较好的增量序列可以显著提高排序效率。
希尔排序的工作原理
假设数组 A = [ a 1 , a 2 , a 3 , ... , a n ] A = [a_1, a_2, a_3, \dots, a_n] A=[a1,a2,a3,...,an],我们选择一个增量序列(例如: gap = n / 2 , n / 4 , 1 \text{gap} = n/2, n/4, 1 gap=n/2,n/4,1)进行排序。
示例: A = [ 12 , 34 , 54 , 2 , 3 ] A = [12, 34, 54, 2, 3] A=[12,34,54,2,3]
-
第一次排序 (
gap
=2
):- 将数组分为两组: A 1 = [ 12 , 54 , 3 ] , A 2 = [ 34 , 2 ] A_1 = [12, 54, 3], A_2 = [34, 2] A1=[12,54,3],A2=[34,2]
- 对每组进行插入排序:
- A 1 = [ 3 , 12 , 54 ] A_1 = [3, 12, 54] A1=[3,12,54]
- A 2 = [ 2 , 34 ] A_2 = [2, 34] A2=[2,34]
- 合并后的数组: [ 3 , 12 , 54 , 2 , 34 ] [3, 12, 54, 2, 34] [3,12,54,2,34]
-
第二次排序 (
gap
=1
):- 对整个数组进行插入排序:
- 最终得到: 2 , 3 , 12 , 34 , 54 ] 2, 3, 12, 34, 54] 2,3,12,34,54]
C++实现代码
下面是基于希尔增量序列的希尔排序实现:
cpp
#include <iostream>
#include <vector>
using namespace std;
// 希尔排序算法
void shellSort(vector<int>& arr) {
int n = arr.size();
// 初始间隔为 n/2,然后逐步减小
for (int gap = n / 2; gap > 0; gap /= 2) {
// 对每个间隔值执行插入排序
for (int i = gap; i < n; i++) {
int temp = arr[i];
int j = i;
// 插入排序:将 arr[i] 插入到正确的位置
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
}
}
// 测试代码
int main() {
vector<int> arr = {12, 34, 54, 2, 3};
cout << "排序前的数组:";
for (int val : arr) {
cout << val << " ";
}
cout << endl;
shellSort(arr);
cout << "排序后的数组:";
for (int val : arr) {
cout << val << " ";
}
cout << endl;
return 0;
}
时间复杂度分析
-
最坏情况时间复杂度 :在最坏的情况下,如果使用最简单的间隔序列 gap = n / 2 , n / 4 , ... , 1 \text{gap} = n/2, n/4, \dots, 1 gap=n/2,n/4,...,1,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
-
最好情况时间复杂度 :如果数组已经接近有序,希尔排序的时间复杂度可以接近 O ( n ) O(n) O(n)。
-
平均时间复杂度 :依赖于增量序列的选择,通常情况下,希尔排序的时间复杂度介于 O ( n log n ) O(n \log n) O(nlogn) 和 O ( n 2 ) O(n^2) O(n2) 之间。
优缺点总结
优点:
- 比插入排序快:希尔排序相对于直接插入排序有显著的性能提升,尤其是对于较大的数组。
- 原地排序 :无需额外的内存空间,空间复杂度为 O ( 1 ) O(1) O(1)。
- 适应性强:可以适应各种规模的数组,尤其在数组规模较大时表现较好。
缺点:
- 不稳定:希尔排序是一个不稳定的排序算法,相同的元素可能会被交换位置。
- 时间复杂度不稳定:时间复杂度受增量序列选择的影响,选择不当可能导致较差的性能。
小结
希尔排序是一种高效的插入排序算法,通过减少元素间的间隔来提高排序效率。它比插入排序更适合处理较大的数据集,尤其当使用较优的增量序列时,能显著提高排序的性能。然而,希尔排序并不稳定,其最坏时间复杂度也可能达到 O ( n 2 ) O(n^2) O(n2)。
资料来源
oi-wiki
,Chat-gpt