排序算法

直接插入排序

时间复杂度:\(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)\)

  1. 令指针\(i,j\)指向数列的区间外侧,数列的中值记为\(x\)
  2. 将数列中\(\le x\)的数放在左段,\(\geq x\)的数放在右段
  3. 对于左右两段,再递归以上两个过程,直到每段只有一个数,即全部有序
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)\)

  1. 对数列不断等长拆分,直到一个数的长度
  2. 回溯时,按升序合并左右两段
  3. 重复以上两个过程,直到递归结束

合并过程:

  1. \(i,j\)分别指向\(a\)的左右段起点,\(k\)指向\(b\)的起点
  2. 枚举\(a\)数组,如果\(a_{left} \le a_{right}\),把\(a_{left}\)放入\(b\)数组,否则,把\(a_{{right}}\)放入\(b\)数组
  3. 把左段或右段剩余的数放入\(b\)数组
  4. \(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\)大元素:删除小根堆堆顶元素

image-20250131113610808
//大根堆
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\)

  1. 假设\(P<Q\),若\(x\)向右移动一个单位,那么总的距离和就会减少\(Q-P\)
  2. 假设\(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;
}
posted @ 2025-06-20 22:14  _P_D_X  阅读(15)  评论(0)    收藏  举报