基础排序算法

引入

在生活中,经常要对亿些东西进行排序。比如,考完试后,老师会对成绩的高低将试卷进行排序;玩扑克牌时,按点数排序手牌......多亏了排序,才能将杂乱无章的东西整理清楚,便于查询统计与利用

排序算法有很多,本帖将介绍几种使用于不同场合的简单排序算法

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. 构建最大堆:将无序数组调整为最大堆。最大堆的性质是每个节点的值都大于或等于其左右孩子节点的值,即根节点的值最大。
  2. 排序过程
    • 将堆顶元素(最大值)与堆的最后一个元素交换,这样最大值就被移到了数组的末尾,并从未排序的部分分离出来。
    • 缩小堆的范围,排除已经排好序的元素。
    • 对剩下的堆进行堆调整,使其保持最大堆性质。
    • 重复上述步骤,直到堆的大小为 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;
}
代码解析
  1. heapify 函数:用于维护堆的性质。它从指定的节点开始,递归地调整子树使之满足最大堆的性质。
  2. heapSort 函数 :首先通过 heapify 构建最大堆,然后逐步将堆顶元素(最大值)移到数组的末尾,并对剩下的未排序部分进行堆调整。
  3. 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,最后进行一次标准的插入排序。

  1. 分组:通过一个间隔序列(gap sequence)将数组分成若干个子序列,每个子序列进行插入排序。
  2. 插入排序 :对每个子序列进行插入排序,不同于传统插入排序的是,这里比较的是间隔为 gap 的元素。
  3. 缩小间隔 :逐步减小 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]
  1. 第一次排序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]
  2. 第二次排序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

相关推荐
java小吕布3 小时前
Java中的排序算法:探索与比较
java·后端·算法·排序算法
Neteen1 天前
七大经典基于比较排序算法【Java实现】
java·算法·排序算法
誓约酱1 天前
(动画版)排序算法 -希尔排序
数据结构·c++·算法·排序算法
pianmian11 天前
排序算法.
算法·排序算法
誓约酱1 天前
(动画版)排序算法 -选择排序
数据结构·算法·排序算法
妈妈说名字太长显傻1 天前
【数据结构】交换排序——冒泡排序 和 快速排序
数据结构·算法·排序算法
爱跑步的一个人1 天前
STL-常用排序算法
开发语言·c++·排序算法
三小尛1 天前
快速排序(C语言)
数据结构·算法·排序算法
椅子哥1 天前
数据结构--排序算法
java·数据结构·算法·排序算法