蓝桥杯基础--排序模板合集II(快速,归并,桶排序)

目录

1.快速排序

1.1快速排序思想

1.2快速排序模板

1.3例题

2.归并排序

2.1归并排序的思想

2.2模板

2.3例题

3.桶排序

3.1桶排序概念

3.2原理及实现

3.3模板


在掌握了冒泡、选择和插入这三种基础的平方级时间复杂度排序算法后,我们在蓝桥杯等算法竞赛中往往会面临更大的数据规模。当数据量 N 达到十万级别(10的5次方)时,基础排序就会因为超时(TLE)而无法满足需求。

此时,我们需要引入时间复杂度更优的进阶排序算法。本文将深入探讨三种极具代表性的高效排序算法:快速排序、归并排序以及跳出比较排序框架的桶排序。

1.快速排序

1.1快速排序思想

快速排序是计算机科学中最经典、应用最广泛的排序算法之一。它的核心思想是分治法(Divide and Conquer)

快速排序的完整流程可以概括为以下三个步骤:

  1. 选定基准(Pivot):从当前待排序的数组区间中挑选一个元素作为基准值(通常可以选择最右边的元素、最左边的元素或者中间元素)。

  2. 划分区间(Partition):通过双指针扫描,将数组重新排列。使得所有小于基准值的元素都被放到基准的左边,所有大于或等于基准值的元素都被放到基准的右边。这个操作完成后,基准元素就已经落在了它最终排序后应该在的绝对正确的位置上。

  3. 递归求解:对基准元素左边的子区间和右边的子区间,重复上述步骤,直到区间长度为1或0,此时整个数组就天然有序了。

快速排序的平均时间复杂度为 O(N log N),在绝大多数实际应用场景中表现极其优异。

1.2快速排序模板

这里的核心是 partition 函数,它使用了左右双指针向中间逼近的技巧。

复制代码
void Quicksort(int a[], int l, int r)
{
	if(l < r)
	{
		int mid = Partition(a, l, r); // 将数组a[l]~a[r]区间中某个基准数字放到正确的位置并将这个位置返回 
		Quicksort(a, l, mid - 1);
		Quicksort(a, mid + 1, r);
	}
 } 

int partition(int a[], int l, int r)
{
    // 设a[r]为基准元素,partition函数的作用是将数组a[l..r]划分成两部分,使得左边部分的元素都不大于a[r],右边部分的元素都不小于a[r],并返回基准元素a[r]在划分后的数组中的位置。
    int pivot = a[r];
    // 设两个下标i, j分别从l,r开始往中间走
    int i = l, j = r;
    while (i < j) {
        while (i < j && a[i] < pivot) {
            i++;
        }
        // 从上面的循环结束后,i指向第一个不小于pivot的元素
        while (i < j && a[j] >= pivot) {
            j--;
        }
        // 从上面循环结束后,j指向第一个小于pivot的元素
        // 如果i < j,说明a[i]不小于pivot,a[j]小于pivot,需要交换a[i]和a[j]
        if (i < j) {
            swap(a[i], a[j]);
        }
        else{
            swap(a[i], a[r]);
        }
    }
    return i;
}

1.3例题

https://www.lanqiao.cn/problems/3226/learning/?page=1&first_category_id=1&problem_id=3226

深度拓展: 在真正的蓝桥杯赛场上,如果仅仅是要求把一个数组从小到大排序,我们极少会去手写快速排序。因为 C++ 标准模板库(STL)中提供的 sort() 函数,其底层正是基于快速排序(结合了插入排序和堆排序的内省式排序),不仅性能极致,而且一行代码即可调用。

手写快排的核心价值在于解决**"寻找第K大/小的数"**这类问题。利用 Partition 思想,我们可以在 O(N) 的时间复杂度内找到特定排位的元素,而不需要将整个数组完全排序。

直接调用 STL 的参考代码:

复制代码
#include <iostream>
#include <algorithm> // 引入 sort 函数
using namespace std;

const int N = 1e5 + 9;
int a[N];

int main() {
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);

    int n; 
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> a[i];

    // 直接调用 STL,传入起始和结束迭代器(地址)
    sort(a + 1, a + n + 1);

    for(int i = 1; i <= n; i++) cout << a[i] << " \n"[i == n];

    return 0;
}

2.归并排序

2.1归并排序的思想

归并排序同样使用了分治思想,但它的侧重点与快速排序截然不同。 快速排序是"先划分,再递归",而归并排序是"先递归拆分,再合并"。

它将大数组无脑地从中间劈成两半,一直拆分到每个子数组只有一个元素(此时可以认为这个单元素数组是有序的)。然后,利用一个核心操作:将两个已经有序的数组,合并成一个更大的有序数组。不断向上合并,最终得到完整的有序序列。

归并排序的最大优势在于它是稳定排序(相等的元素排序后相对位置不变),且在任何情况下时间复杂度都是严格的 O(N log N)。代价是需要一个额外的 O(N) 规模的辅助数组来暂存合并过程的数据。

2.2模板

在合并(Merge)步骤中,我们需要非常严谨地处理四个指针状态分支,这是归并排序最容易写错的地方。

复制代码
void MergeSort(int a[], int l, int r)
{
    if (l == r) {
        return;
    }
    int mid = l + (r - l) / 2;
    MergeSort(a, l, mid);
    MergeSort(a, mid + 1, r);
    // 排序完成后,a[i, mid]和a[mid+1, r]都是有序的了
    // 将a[l..mid]和a[mid+1..r]合并成一个有序的数组
    int p1 = l, pr = mid + 1, pb = l;
    while(p1 <= mid || pr <= r)
    {
        if(p1 > mid) {
            // 说明左边的数组已经合并完了,直接将右边的数组剩余部分合并到b中
            b[pb++] = a[pr++];
        }
        else if(pr > r) {
            // 说明右边的数组已经合并完了,直接将左边的数组剩余部分合并到b中
            b[pb++] = a[p1++];
        }
        else if(a[p1] < a[pr]) {
            // 说明a[p1]应该排在a[pr]前面
            b[pb++] = a[p1++];
        }
        else {
            // 说明a[pr]应该排在a[p1]前面
            b[pb++] = a[pr++];
        }
    }
    // 将b数组中的元素复制回a数组中
    for (int i = l; i <= r; i++) {
        a[i] = b[i];
    }
}

2.3例题

https://www.lanqiao.cn/problems/3226/learning/?page=1&first_category_id=1&problem_id=3226

深度拓展: 在蓝桥杯中,归并排序有一个极其经典的独门绝技------求逆序对。在上述模板的"分支4"(即右边元素小于左边元素)被触发时,意味着左半边剩下的所有元素都比当前右边的这个元素大,这就构成了一批逆序对。利用归并排序,我们可以在 O(N log N) 的极速下统计出几十万规模数组的逆序对总数。

完整参考代码:

复制代码
#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 9;
int a[N], b[N];

void Mergesort(int a[], int l, int r)
{
    if(l == r) return;
    int mid = (l + r) / 2;

    Mergesort(a, l, mid);
    Mergesort(a, mid + 1, r);

    int pl = l, pr = mid + 1, pb = l;
    while(pl <= mid || pr <= r)
    {
        if(pl > mid) b[pb++] = a[pr++];
        else if(pr > r) b[pb++] = a[pl++];
        else if(a[pl] < a[pr]) b[pb++] = a[pl++];
        else b[pb++] = a[pr++];
    }
    for(int i = 1; i <= r; i++) a[i] = b[i];
}

int main()
{
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  int n; cin >> n;
  for(int i = 1; i <= n; i++) cin >> a[i];

  Mergesort(a, 1, n);

  for(int i = 1; i <= n; i++) cout << a[i] << " \n"[i == n];

  return 0;
}

3.桶排序

3.1桶排序概念

前文提到的所有排序(冒泡、插入、快排、归并)都属于基于比较的排序 ,数学上已经证明,基于比较的排序时间复杂度下限就是 O(N log N)。 如果我们想要突破这个极限,达到令人惊叹的 O(N) 线性时间复杂度,就需要借助非比较排序算法,桶排序就是其中的代表。

桶排序的核心在于:它利用了数据的"值"本身的特征,通过建立数据到空间下标的映射关系来实现排序。它本质上是用极致的空间换取时间

3.2原理及实现

桶排序的步骤:

  1. 将值域分为若干段,每段对应一个桶
  2. 将待排序元素放入对应的桶中
  3. 将个桶内的元素进行排序
  4. 将桶中的元素依次取出

3.3模板

模板一:计数排序(特殊的桶排序) 当数据范围比较集中且全为整数时(例如对学生考试成绩 0-100分 排序),我们可以让"每一个数值对应一个单独的桶"。这种极限情况也就是著名的计数排序。数组的下标直接就代表了数值,数组里面存的是该数值出现的次数。

复制代码
#include<bits/stdc++.h>
using namespace std;

const int MAXN = 5e5 + 7;
int n;
int bucket[MAXN]; // 一个值对应一个桶,桶里存储这个值出现的次数
int main() {
    cin >> n;
    for (int i = 0; i < n; i++) {
        int x;
        cin >> x;
        bucket[x]++; // 将这个值放入对应的桶中,桶里存储这个值出现的次数
    }
    for (int i = 0; i < MAXN; i++) {
        for(int j = 1; j <= bucket[i]; j++) { // 将桶里的值输出出来,输出次数为这个值出现的次数
            cout << i << " ";
        }
    }
    cout << endl;
    return 0;
}

模板二:通用桶排序(利用 Vector 管理区间) 当数据跨度很大或者包含浮点数时,我们不能为一个值单独开一个数组下标,这时就需要利用 vector 作为容器,让一个桶管理一个区间的值。

复制代码
vector<int> bucket[MAXN];
int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        int x;
        cin >> x;
        bucket[x / 1000].push_back(x); // 将这个值放入对应的桶中,桶里存储这个值出现的次数
    }
    for(int i = 0; i < MAXN; i++)
    {
        // 对每个桶进行排序,方法随意
        // sort(bucket[i].begin(), bucket[i].end());
    }
    for(int i = 0; i < MAXN; i++)
    {
        for(auto item : bucket[i]) { // 将桶里的值输出出来,输出次数为这个值出现的次数
            cout << item << " ";
        }
    }
    cout << endl;
    return 0;
}

总结: 快速排序追求极致的比较效率,归并排序保障绝对的稳定且是处理逆序对的利器,而桶排序则在特定数据特征下用空间打开了 O(N) 排序的大门。掌握它们,足以应对蓝桥杯绝大多数的排序拓展题。

本章完。

相关推荐
月落归舟2 小时前
排序算法---(四)
算法·排序算法
Jordannnnnnnn2 小时前
追赶29,28
c++
童话ing2 小时前
【LeetCode】239.滑动窗口最大值
数据结构·算法·leetcode·golang
:mnong2 小时前
油藏数值模型ReservoirSim 系统设计分析
c++
计算机安禾2 小时前
【数据结构与算法】第13篇:栈(三):中缀表达式转后缀表达式及计算
c语言·开发语言·数据结构·c++·算法·链表
another heaven2 小时前
【软考 IDEF系列方法:从概念到核心差异】
数据结构
章鱼丸-2 小时前
DAY40 训练与测试规范写法
人工智能·算法·机器学习
简单~2 小时前
C++ 函数模板完全指南
c++·函数模板
·心猿意码·2 小时前
C++ 线程安全单例模式的底层源码级解析
c++·单例模式