排序算法
直接插入排序
时间复杂度:\(O(n^2)\)
空间复杂度:\(O(1)\)
稳定性:√
代码
void sort(int* a, int l, int r)
{
int temp, j;
for (int i = l + 1; i <= r; i++)
{
temp = a[i];
for (j = i - 1; j >= l && temp < a[j]; j--) a[j + 1] = a[j]; //比较并且后移
a[j + 1] = temp; //插入
}
}
过程模拟
数组\(a[n]=\{8 ,6, 3, 2, 4, 1\}\)
第0轮:\(\{\}\),\(\{8 ,6, 3, 2, 4, 1\}\)
第1轮:\(\{8\}\),\(\{6, 3, 2, 4, 1\}\)
第2轮:\(\{6,8\}\),\(\{3, 2, 4, 1\}\)
第3轮:\(\{3,6,8\}\),\(\{2, 4, 1\}\)
第4轮:\(\{2,3,6,8\}\),\(\{4, 1\}\)
第5轮:\(\{2,3,4,6,8\}\),\(\{1\}\)
第6轮:\(\{1,2,3,4,6,8\}\),\(\{\}\)
折半插入排序
时间复杂度:\(O(n^2)\)
空间复杂度:\(O(1)\)
稳定性:√
代码
void sort(int* a, int L, int R)
{
int temp, j, l, r;
for (int i = L + 1; i <= R; i++)
{
temp = a[i];
if (temp >= a[i - 1]) continue;
l = L - 1, r = i;
while (l + 1 < r)
{
int mid = l + r >> 1;
if (a[mid] >= temp) r = mid; //最大化:找到有序区第一个>=temp的数字
else l = mid;
}
for (j = i - 1; j >= r; j--) a[j + 1] = a[j];
a[r] = temp;
}
}
希尔排序
时间复杂度:跟增量\(d\)有关
空间复杂度:\(O(1)\)
稳定性:×
代码
void sort(int* a, int L, int R)
{
int d = (R - L + 1)/2, i, j, temp;
while (d)
{
for (i = L + d; i <= R; i++)
{
//对这个d对应的每一组进行插入排序(步长为d)
temp = a[i];
for (j = i - d; j >= L && temp < a[j]; j -= d) a[j + d] = a[j];
a[j + d] = temp; //插入
}
d = d / 2;//或者其他
}
}
冒泡排序
时间复杂度:\(O(n^2)\)
空间复杂度:\(O(1)\)
稳定性:√
代码
(小优化:这一轮没交换过,说明已经有序)
void sort(int* a, int L, int R)
{
int n = R - L + 1;
bool flag;
for (int i = n; i > 1; i--)
{
flag = true;
for (int j = L; j < L + i - 1; j++)
if (a[j] > a[j + 1]) swap(a[j], a[j + 1]), flag = false;
if (flag) break;
}
}
快速排序 第\(k\)小的数
快速排序P1177 【模板】排序 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
主要利用分治思想,时间复杂度\(O(n \log n)\)
- 令指针\(i,j\)指向数列的区间外侧,数列的中值记为\(x\)
- 将数列中\(\le x\)的数放在左段,\(\geq x\)的数放在右段
- 对于左右两段,再递归以上两个过程,直到每段只有一个数,即全部有序
void quicksort(int l,int r){
if(l == r) return;
int i = l-1, j = r+1, x = a[(l+r)/2];
while(i<j){
do i ++; while(a[i] < x);
do j --; while(a[j] > x);
if(i<j) swap(a[i],a[j]);
}
quicksort(l,j);
quicksort(j+1,r);
}
- 退出while循环时,\(i=j\)或者\(i=j+1\)
- 稳定性:相同元素的顺序可能会交换,是不稳定的
快速排序应用--第\(k\)小的数P1923 【深基9.例4】求第 k 小的数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
每次取一半递归即可
int n,k,a[N];
int qnth_element(int l,int r){
if(l == r) return a[l];
int i = l-1, j = r+1, x = a[(l+r)/2];
while(i<j){
do i ++; while(a[i] < x);
do j --; while(a[j] > x);
if(i<j) swap(a[i],a[j]);
}
if(k<=j) return qnth_element(l,j);
else return qnth_element(j+1,r);
}
时间复杂度\(O(n)\)
归并排序 逆序对
归并排序P1177 【模板】排序 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
主要利用分治思想,时间复杂度\(O(n\log n)\)
- 对数列不断等长拆分,直到一个数的长度
- 回溯时,按升序合并左右两段
- 重复以上两个过程,直到递归结束
合并过程:
- \(i,j\)分别指向\(a\)的左右段起点,\(k\)指向\(b\)的起点
- 枚举\(a\)数组,如果\(a_{left} \le a_{right}\),把\(a_{left}\)放入\(b\)数组,否则,把\(a_{{right}}\)放入\(b\)数组
- 把左段或右段剩余的数放入\(b\)数组
- 把\(b\)数组的当前段复制回\(a\)数组
int n,a[N],b[N];
void msort(int l,int r){
if(l >= r) return;
//拆分
int mid = (l+r)/2;
msort(l,mid);
msort(mid+1,r);
//合并
int i = l,j = mid+1,k = l;
while(i<=mid && j<=r){
if(a[i]<=a[j]) b[k++] = a[i++];
else b[k++] = a[j++];
}
while(i<=mid) b[k++] = a[i++];
while(j<=r) b[k++] = a[j++];
for(int i = l ; i <= r ; i ++) a[i] = b[i];
}
总结:
| 快速排序 | 归并排序 | |
|---|---|---|
| 分治 | 先交换,后拆分 | 先拆分后交换 |
| 稳定性 | 不稳定 | 稳定 |
逆序对P1908 逆序对 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
对于给定的一段正整数序列,逆序对就是序列中\(a_i>a_j\)且\(i<j\)的有序对
要求序列\(\{a_n\}\)的逆序对数,只需要在归并排序的合并过程中,统计从右段取数的次数
int n,a[N],b[N];
long long res;
void msort(int l,int r){
if(l >= r) return;
//拆分
int mid = (l+r)/2;
msort(l,mid);
msort(mid+1,r);
//合并
int i = l,j = mid+1,k = l;
while(i<=mid && j<=r){
if(a[i]<=a[j]) b[k++] = a[i++];
else b[k++] = a[j++],res += mid-i+1;
}
while(i<=mid) b[k++] = a[i++];
while(j<=r) b[k++] = a[j++];
for(int i = l ; i <= r ; i ++) a[i] = b[i];
}
说明:
在每次合并时,当从右段取数\(a_j\),那么\(a_j\)就比\(a[i,mid]\)中任意一个数都要小,共构成\(mid-i+1\)个逆序
堆 堆排序
堆P3378 【模板】堆 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
- 堆:是一颗完全二叉树
- 小根堆:父节点的值\(\le\)其子节点的值
- 大根堆:父节点的值\(\ge\)其子节点的值
对节点使用左右孩子编号法:
- 节点\(i\)的左孩子是\(2i\)
- 节点\(i\)的右孩子是\(2i+1\)
- 节点\(i\)的父节点是\(\lfloor \frac{i}{2} \rfloor\)
堆(完全二叉树)可以用一维数组存储
堆的操作
堆的插入:把新元素从堆尾插入,再逐层上浮到合适位置
int a[N],size;
//上浮
void up(int u){
if(u/2 && a[u/2] > a[u])
swap(a[u],a[u/2]),up(u/2);
}
//压入
void push(int x){
a[++size] = x;
up(cnt);
}
时间复杂度\(O(\log n)\)
堆的删除:把尾元素移到根上,再逐层下沉到合适位置
//下沉
void down(int u){
int v = u;
if(u*2 <= size && a[u*2] < a[v]) v = u*2;
if(u*2+1 <= size && a[u*2+1] < a[v]) v = u*2+1;
if(u!=v) swap(a[u],a[v]),down(v);
}
//删除
void pop(){
a[1] = a[size--];
down(1);
}
时间复杂度\(O(\log n)\)
堆排序P1177 【模板】排序 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
建立小根堆,每次取堆顶元素并且删除,时间复杂度\(O(n\log n)\)
int a[N],size;
//上浮
void up(int u){
if(u/2 && a[u/2] > a[u])
swap(a[u],a[u/2]),up(u/2);
}
//压入
void push(int x){
a[++size] = x;
up(cnt);
}
//下沉
void down(int u){
int v = u;
if(u*2 <= size && a[u*2] < a[v]) v = u*2;
if(u*2+1 <= size && a[u*2+1] < a[v]) v = u*2+1;
if(u!=v) swap(a[u],a[v]),down(v);
}
//删除
void pop(){
a[1] = a[size--];
down(1);
}
int main(){
int n; cin >> n;
int m;
for(int i = 1 ; i <= n ; i ++) cin >> m,push(m);
for(int i = 1 ; i <= n ; i ++) cout << a[1] << " ",pop();
}
对顶堆 第\(k\)大的数
对顶堆
可以动态维护一个序列上第\(k\)大的数,\(k\)值会发生变化。比写 线段树 或 BST 简单
对顶堆由一个大根堆与一个小根堆组成,小根堆维护前\(k\)大的数(包含第\(k\)个),大根堆维护比第\(k\)大数小的数
插入:若插入的元素\(\geq\)小根堆堆顶元素,则将其插入小根堆,否则插入大根堆
维护:当小根堆的大小\(>k\)时,不断将小根堆堆顶元素取出并插入大根堆,直到小根堆的大小\(=k\)
当小根堆的大小\(<k\)时,不断将大根堆堆顶元素取出并插入小根堆,直到小根堆的大小\(=k\)
查询第\(k\)大元素:小根堆堆顶元素
删除第\(k\)大元素:删除小根堆堆顶元素
//大根堆
priority_queue<int> a;
//小根堆
priority_queue<int,vector<int>,greater<int>> b;
for(int i = 1 ; i <= n ; i ++){
//插入
int x; cin >> x;
if(b.empty() || x > b.top()) b.push(x);
else a.push(x);
//调整
while(b.size() > k) a.push(b.top()),b.pop();
while(b.size() < k) b.push(a.top()),a.pop();
//取值,删除
cout << b.top() << " ";
b.pop();
}
插入、删除后,小根堆的大小与期望的\(k\)值最多相差\(1\),故每次维护最多只需要对大根堆与小根堆中的元素进行一次调整,这些操作的时间复杂度都是\(O(\log n)\)
习题:
[P7072 CSP-J2020] 直播获奖 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
每次更新\(k = \max(1, i\times w / 100)\)
#include<iostream>
#include<cmath>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
int main() {
int n, w, k;
cin >> n >> w;
//大根堆
priority_queue<int> a;
//小根堆
priority_queue<int, vector<int>, greater<int>> b;
for (int i = 1; i <= n; i++) {
//插入
int x; cin >> x;
if (b.empty() || x >= b.top()) b.push(x);
else a.push(x);
k = max(1, i * w / 100);
//调整
while (b.size() > k) a.push(b.top()), b.pop();
while (b.size() < k) b.push(a.top()), a.pop();
//取值
cout << b.top() << " ";
}
return 0;
}
P1168 中位数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
取\(k=\frac{i}{2}+1\),当\(i\)为奇数时输出即可
#include<iostream>
#include<cmath>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;
int main() {
int n,k;
cin >> n;
//大根堆
priority_queue<int> a;
//小根堆
priority_queue<int, vector<int>, greater<int>> b;
for (int i = 1; i <= n; i++) {
//插入
int x; cin >> x;
if (b.empty() || x >= b.top()) b.push(x);
else a.push(x);
k = i / 2 + 1;
//调整
while (b.size() > k) a.push(b.top()), b.pop();
while (b.size() < k) b.push(a.top()), a.pop();
//取值
if (i % 2) cout << b.top() << endl;
}
return 0;
}
习题:中位数 距离和的最小值
【例1】P10452 货仓选址 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
建模:求\(x\),使得\(|A_1-x|+|A_2-x|+···+|A_n-x|\)取得最小值
思路:将\(A_1\)~\(A_n\)排序,设货仓建立在\(x\)坐标处,且\(\sum A_i<x=P\),\(\sum A_i >x=Q\)
- 假设\(P<Q\),若\(x\)向右移动一个单位,那么总的距离和就会减少\(Q-P\)
- 假设\(P>Q\),若\(x\)向左移动一个单位,那么总的距离和就会减少\(P-Q\)
综上,当且仅当\(P=Q\)时,\(|A_1-x|+|A_2-x|+···+|A_n-x|\)取得最小值,此时\(x\)为\(\{A_n\}\)的中位数
当\(n\)为奇数时,建在\(A_{\lfloor \frac{n+1}{2} \rfloor}\)处
当\(n\)为偶数时,建在\([A_{\lfloor \frac{n}{2} \rfloor},A_{\lfloor \frac{n}{2} \rfloor + 1}]\)任意一个位置都可以
#include<bits/stdc++.h>
using namespace std;
const int N = 100100;
int n,a[N];
int main(){
cin >> n;
for(int i = 1 ; i <= n ; i ++) cin >> a[i];
sort(a+1,a+n+1);
int ans = 0;
for(int i = 1 ; i <= n ; i ++) ans += fabs(a[i] - a[(n+1)/2]);
cout << ans;
}
【例2】Eastern Exhibition - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
建模:求点\(P(x,y)\),使得\(d_M(A_1,P)+d_M(A_2,P) + ···+d_M(A_n,P)\)取得最小值
思路:分别在\(x\)方向和\(y\)方向使用【例1】的算法即可
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1010;
int n,a[N],b[N];
int main(){
int t; cin>>t;
while(t--){
cin>>n;
for(int i=0; i<n; i++) cin>>a[i]>>b[i];
sort(a,a+n); sort(b,b+n);
int x=a[n/2]-a[(n-1)/2]+1;
int y=b[n/2]-b[(n-1)/2]+1;
cout<<1LL*x*y<<'\n';
}
return 0;
}

浙公网安备 33010602011771号