[算法竞赛]八、排序、排列

排序

排序是最基本的能力要求,在实际竞赛中我们大多用排序函数来解决问题,但是理解各个算法思路和操作是必备的

选择排序

原理:每次从未排序部分选最小(大)元素,放到已排序部分末尾。

复杂度:O(n²)

竞赛中几乎不会使用,仅作为理解排序的例子

复制代码
void selectionSort(int a[], int n) {
    for (int i = 0; i < n - 1; ++i) {
        int minIdx = i;
        for (int j = i + 1; j < n; ++j) {
            if (a[j] < a[minIdx]) minIdx = j;
        }
        swap(a[i], a[minIdx]);
    }
}

插入排序

原理:将当前元素插入前面已排序序列的正确位置。

复杂度O(n)

看情况使用,有的时候插入排序是比较高效的

洛谷P1271

练插入排序模板题,按题意即可

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

int main() {
    int n,m;
    cin>>n>>m;
    vector<int>a(m);
    for (int i=0;i<m;i++)cin>>a[i];
    for (int i=1;i<m;i++) {
        int temp=a[i],j=i-1;
        while (j>=0&&temp<a[j]) {
            a[j+1]=a[j];
            j--;
        }
        a[j+1]=temp;
    }
    for (int i=0;i<m;i++)cout<<a[i]<<" ";
}

注意插入排序可能在本题会超时,该题解仅用作理解插入排序用

冒泡排序

原理:重复遍历数组,相邻元素两两比较,若逆序则交换,每轮将最大元素 "冒泡" 到末尾。可通过flag优化:若某轮无交换,说明已有序,提前终止

复杂度:平均O(n²),最好情况O(n)

竞赛中几乎不会使用,仅作为理解排序的例子

洛谷P1116(橙题)

冒泡交换的次数就是答案

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

int main(){
    int N;
    cin>>N;
    vector<int>a(N);
    for (int i=0;i<N;i++)cin>>a[i];
    int cnt=0;
    for(int i=0;i<N;i++) {
        for(int j=0;j<N-1;j++) {
            if (a[j]>a[j+1]) {
                swap(a[j],a[j+1]);
                cnt++;
            }
        }
    }
    cout<<cnt;
}

归并排序

原理:**分治:**将数组递归分成两半,分别排序,再合并两个有序数组。

复杂度:(nlog n)

适合大量数排序以及求逆序对

详讲一下:

假设我们有一个数组 6 5 4 3 2 1我们想用归并排序,

我们需要将这个数组分别两两分组到最小 然后不断排序并合并

我们先不断给数组分组:

6 5 4 | 3 2 1

6 5 | 4 ||| 3 | 2 1

6 | 5 || 4 ||| 3 | 2 || 1

我们不断分组到每个数字分到不能再分了 接下来开始分别进行排序

我们需要开一个辅助空间来帮助我们进行排序 temp[ ]

我们按照0~based的下标给数组每个元素标好下标,我们从左到右首先看最小的分组 6和5,他们已经小到不能再分了 返回

6 5 || 4 ||| 3 | 2 || 1

接下来我们在0~1位置上进行排序 首先分别启用两个指针,指针a和指针b,分别指向我们原来的下一级的元素 即a指向6,b指向5

我们发现5小,则5先入temp数组 现在temp是temp[5]

我们原来的下一级的元素里5的后面没有元素了,我们再按顺序把左边的6排进去

现在temp是temp[5,6] 然后按照下标把temp内的元素复制到原数组里

现在我们数组为 5 6 || 4 ||| 3 | 2 || 1

现在0~1的位置排好了,我们开始排 2位置 发现2位置已经是最小元素了 返回

现在排0~2位置上的元素 我们还是启用两个指针a,b分别指向原来的下一级元素,即a指向5,b指向4,然后把temp清空我们开始排

我们发现4小,则4先进temp 现在temp为temp[4]

4的后面已经没有元素了,我们依次把a的元素放进去 现在temp为temp[4,5,6],再把temp复制到原数组里,现在原数组为4 5 6 ||| 3 | 2 || 1

我们现在同理再来排右半部分元素,道理与上面相同 那么现在我们排好的数组为 4 5 6 ||| 1 2 3

我们现在已经排好了0~2位置和3~5位置的元素,返回,

现在排0~5位置上的元素

我们仍然启用a,b指针,a指向4,b指向1,temp清空

1小,b指针后移,1入列 temp[1]

2小,b指针后移,2入列 temp[1,2]

3小,b指针后移,3入列temp[1,2,3]

3后面没有了,依次放左边的a 即temp[1,2,3,4,5,6]

temp复制到原数组,原数组就排好序了

大家可以自己动手模拟一下,就能很好理解归并排序,理解后我们做一道归并排序模板题

洛谷P1908

求逆序对,这里我们用归并排序尝试解决

逆序对很好理解 1 2 3 4 5的逆序对是0对, 2 1 3 4 5 的逆序对是1对 ,2 3 1 4 5逆序对是 2 对

就是统计每个数前面有几个比他大的数 加起来就是逆序对总数

复制代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int a[1000001],tmp[1000001];

ll merge(ll l,ll mid,ll r) {
    ll cnt=0;
    ll i=l,j=mid+1,k=l;
    while (i<=mid&&j<=r) {
        if (a[i]<=a[j])tmp[k++]=a[i++];
        else {
            tmp[k++]=a[j++];
            cnt+= mid-i+1;
        }
    }
    while (i<=mid)tmp[k++]=a[i++];
    while (j<=r)tmp[k++]=a[j++];
    for (int i=l;i<=r;++i)a[i]=tmp[i];
    return cnt;
}

ll mergesort(ll l,ll r) {
    ll cnt=0;
    if (l<r) {
        ll mid=(l+r)/2;
        cnt+= mergesort(l,mid);
        cnt+= mergesort(mid+1,r);
        cnt+= merge(l,mid,r);
    }
    return cnt;
}

int main() {
    int n;
    cin>>n;
    for (int i=0;i<n;i++) cin>>a[i];
    cout<<mergesort(0,n-1);
}

快速排序

原理:**分治:**选一个基准值(pivot),将小于pivot的放左边,大于的放右边,递归排序左右子数组。

复杂度:平均O(nlog n)

是比赛里非常常用的排序算法

这里简单模拟一下,思路有些类似归并

比如有一个数组a[6,5,4,3,2,1],我们这里每次取中间元素作为基准元素,定义本次排序左边界为l=0,右边界为r=5然后递归排序

第一次我们选mid=4,然后先把4放在末尾,这样方便我们进行遍历操作

现在数组为651324,我们定义一个已经比4小的元素指针i 定义一个遍历用指针j,让j指针指向首元素,i指针指向l-1的位置,

我们规定在j指针遍历的时候每次交换小于等于4的数

6比4大跳过,下一个元素,5比4大跳过,下一个元素

1比4小,我们交换6和1即swap(a[i+1],a[j]),数组现在为156324,i指针在a[0]

3比4小,交换3和5,数组现在为136524,i指针在a[1]

2比4小,交换2和6,数组现在为132564,i指针在a[2]

4小于等于4,交换4和5,数组现在为132465,i指针在a[3]

返回i+1

再排序l到mid-1和mid+1到r范围的数

即排序数组1324和排序数组65

重复上述过程即为快速排序

复制代码
int partition(int a[], int l, int r) {
    int mid=(l+r)/2;
    swap(a[mid],a[r]);
    int pivot=a[r],i=l-1;
    for(int j=l;j<r;j++) {
        if(a[j]<=pivot) swap(a[i],a[j]);
    }
    swap(a[i+1],a[r]);
    return i+1;
}

void quickSort(int a[], int l, int r) {
    if(l<r) {
        int pivot=partition(a,l,r);
        quickSort(a,l,pivot-1);
        quickSort(a,pivot+1,r);
    }
}

堆排序

堆是一种树形结构,树的根是堆顶,堆顶的元素永远是最优的元素

堆的应用场景基本上只有堆排序和优先队列

如果是一个下标从1开始的数组,那么每个元素的父节点是i/2,设共有n个节点 ,如果2i>n,那么节点没有孩子

堆的基本思想是每个节点和自己的父节点比较(i/2)如果大/小于父节点,那么就交换

堆排序的思想就是堆排序后每次把堆顶的元素弹出,然后把堆顶元素和下标的最后一个元素交换,再进行堆排序

堆排序的过程我在此简单模拟一下

比如我们一个数组654321,我们想小顶堆排序

6是我们的堆顶,现在堆的size为1,下一个元素5,我们把5加入堆,现在堆的size为2

5比6小就交换,现在堆顶是5,数组为564321

5是我们的堆顶,现在堆的size为2,下一个是4,四入堆,堆size3

4也比5小,交换,堆顶是4,数组为456321

4是堆顶,size为3,下个为3,3入堆,堆size 4

3的父亲节点是5, 3比5小,交换,数组为435621,4是3的父亲节点,3比4小,交换,数组为345621

2和1同样的操作

最后就能得到123456

最后给一个大顶堆的模板作为参考

复制代码
void heapify(int a[], int n, int i) {
    int largest = i;
    int l = 2 * i + 1, r = 2 * i + 2;
    if (l < n && a[l] > a[largest]) largest = l;
    if (r < n && a[r] > a[largest]) largest = r;
    if (largest != i) {
        swap(a[i], a[largest]);
        heapify(a, n, largest);
    }
}

void heapSort(int a[], int n) {
    // 建堆
    for (int i = n / 2 - 1; i >= 0; --i) heapify(a, n, i);
    // 排序
    for (int i = n - 1; i > 0; --i) {
        swap(a[0], a[i]);
        heapify(a, i, 0);
    }
}

洛谷P3378

堆排序模板题,可以用来练练手,后续可以用优先队列给秒了,优先队列的底层逻辑就是堆

计数排序

原理:统计每个元素的出现次数,根据统计结果将元素放回原数组

不大常用,了解思想即可

很好理解

比如有个数组655443221

开一个辅助数组,然后遍历原数组,原数组遍历到哪个数组,辅助数组对应下标就加一

遍历完后辅助数组应该是1 2 1 2 2 1,即1个1,2个2,1个3......

然后再一次输出即可122344556

复制代码
void countingSort(int a[], int n) {
    int minVal = *min_element(a, a + n);
    int maxVal = *max_element(a, a + n);
    memset(cnt, 0, sizeof(cnt));
    for (int i = 0; i < n; ++i) cnt[a[i] - minVal]++;
    int idx = 0;
    for (int i = 0; i <= maxVal - minVal; ++i) {
        while (cnt[i]--) a[idx++] = i + minVal;
    }
}

基数排序

原理:按排序(从低位到高位),每一位用计数排序作为子过程。

也非常好理解

比如有个数组[94,65,32,53,12,6]

先计数排序每个数的个位

0:0 1:0 2:12,32 3:53 4:94 5:65 6:6

然后依次输出[12,32,53,94,65,6]

再看每个数的十位

0:6 1:12 2:0 3:32 ..................

最后再按十位依次输出即可[6,12,32,53,65,94]

复制代码
int getMax(int a[], int n) {
    int maxVal = a[0];
    for (int i = 1; i < n; ++i) if (a[i] > maxVal) maxVal = a[i];
    return maxVal;
}

void countSort(int a[], int n, int exp) {
    int output[n], cnt[10] = {0};
    for (int i = 0; i < n; ++i) cnt[(a[i] / exp) % 10]++;
    for (int i = 1; i < 10; ++i) cnt[i] += cnt[i - 1];
    for (int i = n - 1; i >= 0; --i) { // 从后往前,保证稳定性
        output[cnt[(a[i] / exp) % 10] - 1] = a[i];
        cnt[(a[i] / exp) % 10]--;
    }
    for (int i = 0; i < n; ++i) a[i] = output[i];
}

void radixSort(int a[], int n) {
    int maxVal = getMax(a, n);
    for (int exp = 1; maxVal / exp > 0; exp *= 10) countSort(a, n, exp);
}

桶排序

原理:将元素分到多个"桶"中,每个桶内单独排序(如插入排序),最后合并所有桶。

比如有个从1到100的序列,我依次分成0~20 21~40 41~60 61~80 81~100

然后分别对这五组进行排序 最后汇总

这个就是桶排序,每个桶内可以用不同的排序算法

STL

函数:sort()

sort(数组首地址,数组末地址,)默认这么写就从小到大排

sort(数组首地址,数组末地址,比较函数)排序用的比较函数也可以自定义

结构体排序

结构体排序时通常需要我们自拟排序算法

洛谷P1093
复制代码
#include<bits/stdc++.h>
using namespace std;
int N;
struct student{
    int id;
    int total;
    int chinese;
};

bool cmp(student a,student b) {
    if (a.total==b.total) {
        if (a.chinese==b.chinese) {
            return a.id<b.id;
        }
        return a.chinese>b.chinese;
    }
    return a.total>b.total;
}

int main(){
    cin>>N;
    vector<student> students(N+1);
    for(int i=1;i<=N;i++) {
        int a,b,c;
        students[i].id=i;
        cin>>a>>b>>c;
        students[i].total=a+b+c;
        students[i].chinese=a;
    }

    sort(students.begin()+1,students.end(),cmp);
    for(int i=1;i<=5;i++) {
        cout<<students[i].id<<" "<<students[i].total<<endl;
    }

}

排列

在stl中我们主要使用next_permutation()

next_permutation()主要是按照字典序进行的,所以在排序前一定要保证序列是当前序列按字典序排序的最小序列

这个函数主要用两种使用方法

一种是返回布尔值,即返回当前序列还有没有下一个全排列

复制代码
bool next_permutation(a.begin(),a.end())

一种则是对当前元素进行全排列

比如我们想全排列abc的排列组合

复制代码
int main() {
    string s;
    s="bca";
    sort(s.begin(),s.end());
    do {
        cout<<s<<endl;
    }while (next_permutation(s.begin(),s.end()));
    return 0;
}

但有些题目的排列可能需要我们自拟排列函数

分情况而论

相关推荐
xiaoye-duck1 小时前
《算法题讲解指南:优选算法-分治-归并》--49.计算右侧小于当前元素的个数,50.翻转对
c++·算法
im_AMBER1 小时前
Leetcode 137 组合 | 电话号码的字母组合
开发语言·算法·leetcode·深度优先·剪枝
Alex艾力的IT数字空间1 小时前
OCR 原理:从像素到文本的智能转换
数据结构·人工智能·python·神经网络·算法·cnn·ocr
自然常数e1 小时前
文件 操作
c语言·数据结构·visual studio
仟濹2 小时前
【算法打卡day19(2026-03-11 周三)算法:打家劫舍-DP,双指针,二分查找,滑动窗口,方向控制,前缀和 】8个题
算法·leetcode·二分查找·动态规划
未来之窗软件服务2 小时前
自己写算法(十)js加密UUID保护解密——东方仙盟化神期
java·javascript·算法·代码加密·东方仙盟算法
样例过了就是过了2 小时前
LeetCode热题100 腐烂的橘子
数据结构·c++·算法·leetcode·bfs
mango_mangojuice2 小时前
C++学习笔记(list)3.6
c++·笔记·学习
X在敲AI代码2 小时前
D32次 第2题 因子化简
开发语言·c++