- gf24240 的博客
一个极好的排序算法
- @ 2026-8-4 17:40:28
前言
相信你们已经见过不少好的排序算法,例如 Bogo 排序、Miracle 排序等。但是你见过好的排序,同时真的可以 AC 题目吗?这就是极好的排序算法。
取随机数 + 取毫秒
下面的算法会用到。
#include <random>
#include <chrono>
int getms()
{ // 得到当前毫秒
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()
);
return ms.count();
}
int randint(int l, int r)
{ // 得到 [l, r] 的随机整数
static std::mt19937 gen(static_cast<unsigned>(
std::chrono::steady_clock::now().time_since_epoch().count()
));
std::uniform_int_distribution<int> dist(l, r);
return dist(gen);
}
一些好的排序算法
Bogo 排序
Bogo 排序,俗称猴子排序。步骤:
- 检查数组是否有序。
- 如果有序,退出。
- 如果无序,继续进行。
- 随机打乱数组。
代码可参考:
#include <iostream>
#define int long long
using namespace std;
int getms();
int randint(int l, int r);
const int N = 1e5 + 5;
int n, a[N];
bool check()
{ // 检查是否有序
for (int i = 1; i < n; ++i)
if (a[i] > a[i + 1])return 0;
return 1;
}
void randa()
{ // 随机打乱数组
for (int i = 1; i <= n; ++i)
swap(a[i], a[randint(1, n)]);
}
void bsort()
{ // 核心代码
while (!check())
randa();
}
signed main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
bsort();
for (int i = 1; i <= n; ++i)
cout << a[i] << ' ';
return 0;
}
时间复杂度:
- 最优:。第一次检查就是好的。
- 期望:。 个数就有 个排列,乘上每次检查就是了。
- 最差:。一直都没有好。
空间复杂度:
- 。这里的空间复杂度是指除开原数组之外的空间。就像归并排序的空间是 。
Miracle 排序
Miracle 排序,俗称奇迹排序。步骤:
- 检查数组是否有序。
- 如果有序,退出。
- 如果无序,继续进行。
- 等待宇宙射线/量子涨落/神迹把数组变成有序。
代码可参考:
#include <iostream>
#define int long long
using namespace std;
int getms();
int randint(int l, int r);
const int N = 1e5 + 5;
int n, a[N];
bool check()
{ // 检查
for (int i = 1; i < n; ++i)
if (a[i] > a[i + 1])return 0;
return 1;
}
void wait() { return ; } // 等待
void Msort() // M 大写是因为和归并排序冲突了
{ // 核心代码
while (!check())
wait();
}
signed main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
Msort();
for (int i = 1; i <= n; ++i)
cout << a[i] << ' ';
return 0;
}
时间复杂度:
- 最优:。原来就有序,检查一次就好了。
- 期望/最差:。一直等。
空间复杂度:
- 。没有多余的数组开销。
DeepSleep 排序
DeepSleep 排序,俗称睡眠排序。步骤:
- 为每个数字建立线程。
- 线程休眠 毫秒。
- 每个线程结束输出当前数字。
- 直到所有线程结束。
朴素 DeepSleep 排序:
#include <iostream>
#include <thread>
#include <chrono>
#include <vector>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, m, a[N], b[N];
void deep_sleep(int num)
{
// 休眠 num * 10 毫秒,数字越大醒得越晚
this_thread::sleep_for(chrono::milliseconds(num * 10));
b[++m] = num; // 新开一个数组,避免占用原数组
}
void DSort()
{ // 核心代码
vector <thread> threads;
// 1. 为每个数字创建线程
for (int i = 1; i <= n; ++i)
threads.emplace_back(deep_sleep, a[i]);
// 2. 等待所有线程结束(join)
for (auto &t : threads)
t.join();
}
signed main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
DSort();
for (int i = 1; i <= m; ++i)
cout << b[i] << ' ';
return 0;
}
时间复杂度:
- , 为单位时间。
空间复杂度:
- 。新开数组避免占用原来的数组。
但是,这样做有一个致命的问题!(怎么一股 AI 味)
就是:
- 不能处理非正数和小数。
- 数字大()时等 秒,就是 天。
就是这样了。所以我们要引入优化。
- 对于不能处理非正数:这个简单,我们在处理之前对所有数进行平移,具体就是每个数都加上一个数使得每个数都变为正数。这样不会改变数据之间的大小关系。
- 对于数字大:可以使用一些数据结构常用的方法:离散化。具体的:给每个数字一个排名,用这个排名代替原来的数字。这样就将原来有 的数据映射到 (数组长度上界)。
优化睡眠排序:
#include <algorithm>
#include <iostream>
#include <thread>
#include <chrono>
#include <vector>
#include <mutex>
#include <set>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, m, o, a[N], b[N], c[N];
mutex mtx;
void deep_sleep(int num)
{
// 休眠 num * 10 毫秒,数字越大醒得越晚
this_thread::sleep_for(chrono::milliseconds(num * 10));
lock_guard<mutex> lock(mtx);
c[++o] = num; // 新开一个数组,避免占用原数组
}
void DSort()
{ // 核心代码
vector <thread> threads;
// 1. 为每个数字创建线程
for (int i = 1; i <= n; ++i)
threads.emplace_back(deep_sleep, a[i]);
// 2. 等待所有线程结束(join)
for (auto &t : threads)
t.join();
}
void ls()
{ // 离散化
set <int> s; // 使用 set 创建排名
for (int i = 1; i <= n; ++i) s.insert(a[i]);
for (int it : s) b[++m] = it;
for (int i = 1; i <= n; ++i)
a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
}
signed main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
ls(); // 离散化(过程自动平移)
DSort();
for (int i = 1; i <= n; ++i)
cout << b[c[i]] << ' '; // 记得还原到原数组
return 0;
}
时间复杂度:
- , 为单位时间。
空间复杂度:
- 。实际上常数更大。
其实吧,set 内部红黑树就能自动排序。而且你别想在洛谷上交这份代码,满屏 RE 够你受的。
Stalin 排序
不必多说了吧,,,😱
步骤:维护一个当前最大值,没有超过的直接删除。
代码可参考:
#include <iostream>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, a[N], m, b[N];
void ssort()
{
int mmx = a[1]; // 当前最大值
for (int i = 1; i <= n; ++i)
{
if (a[i] >= mmx)
{
b[++m] = a[i];
mmx = a[i];
}
}
}
signed main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
ssort();
for (int i = 1; i <= m; ++i)
cout << b[i] << ' '; // SXXX 排序不一定会得到 n 个数字的
return 0;
}
时间复杂度:
- 。😱吓哭了最强排序,,,。
空间复杂度:
- 。保留的数据。如果要直接在原数组删除,空间可以达到 ,但时间会变成 。
不过这样也太血腥暴力了。为何不多保留一些人呢?我们还是可以维护 表示留下的人。每个人进来时,二分查找最后一个比自己小的,然后代替他。如果没有,则直接加到 的末尾。
代码可参考:
#include <algorithm>
#include <iostream>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, a[N], m, b[N];
void ssort()
{
for (int i = 1; i <= n; ++i)
{
if (a[i] > b[m]) b[++m] = a[i]; // 没有更小的了,加入末尾
else b[lower_bound(b + 1, b + m + 1, a[i]) - b] = a[i]; // 找到最后一个比自己小的
}
}
signed main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
ssort();
for (int i = 1; i <= m; ++i)
cout << b[i] << ' '; // SXXX 排序不一定会得到 n 个数字的
return 0;
}
时间复杂度:
- 。遍历 ,二分 。
空间复杂度:
- 。
等等,,这不就是 LIS 的 贪心 + 二分, 实现吗??
正文
前置知识:插入排序
用扑克牌比喻。假设你现在有一些从小到大排好序的牌。
- 现在你拿到了一张新的牌,记为「新牌」。
- 从后往前,直到当前牌比新牌小。
- 将新牌插入到当前牌的后面。
这就是插入排序了。代码:
#include <iostream>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int n, a[N];
signed main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
// 插入排序
for (int i = 2; i <= n; i++)
{
int key = a[i], j = i - 1;
while (j >= 1 && a[j] > key)
{ // 每次执行 = 消除 1 个逆序对
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
for (int i = 1; i <= n; ++i)
cout << a[i] << ' ';
return 0;
}
可以发现,这和冒泡排序、归并排序一样可以求逆序对 插入排序的时间复杂度,很大程度上是决定于逆序对的数量。所以如果能减少逆序对数量,那就能优化插入排序!(又有一股 AI 味了)
附加知识:希尔排序
可跳过。
根据上面的 ↑ 内容,通过减少逆序对,就能对插入排序优化。这就是希尔排序。步骤:
- 先大步长地比较和交换,消除远距离的逆序对。
- 逐步缩小步长,直到步长为 1(此时就是普通插入排序)。
- 经过前面的“预处理”,逆序对数量已经大幅减少,最后的插入排序就非常快。
部分代码(DeepSeek 提供):
void shell_sort(int a[], int n) {
// 步长从 n/2 开始,逐步减半
for (int gap = n / 2; gap > 0; gap /= 2) {
// 对每个子序列做插入排序
for (int i = gap; i < n; ++i) {
int key = a[i];
int j = i;
// 比较相隔 gap 的元素
while (j >= gap && a[j - gap] > key) {
a[j] = a[j - gap];
j -= gap;
}
a[j] = key;
}
}
}
但是我这里毕竟不是为了将希尔排序,所以不详细将了。
随机打乱 + 插入排序
这就是本篇 blog 的主要内容了。
随机打乱
一个由 Bogo 猴子排序引出的想法:每次都打乱所有数字也太慢了,不如每次随机交换数字,而且交换前判断这样做是否能使数组更有序而不是更乱。代码:
#include <iostream>
#include <random>
#include <chrono>
#define int long long
using namespace std;
int getms();
int randint(int l, int r);
const int N = 1e5 + 5;
int n, a[N], st;
signed main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
int st = getms(); // 开始打乱时间
while (getms() - st < 990)
{ // 持续打乱 990ms,避免超时
int i = randint(1, n - 1);
int j = randint(i + 1, n);
if (a[j] < a[i])
swap(a[i], a[j]);
}
for (int i = 1; i <= n; ++i)
cout << a[i] << ' ';
return 0;
}
交上洛谷之后:TLE ✕ 3。那缩短时间呢,改成打乱 900ms 呢?WA ✕ 3。于是就要引出插入排序了。
混合
如果逆序对较少,插入排序是很高效的。经过随机打乱,肯定已经解决了部分(至少是小部分)逆序对。因此,打乱后再做插入排序。这样就好了。
代码:
#include <iostream>
#include <random>
#include <chrono>
#define int long long
using namespace std;
int getms();
int randint(int l, int r);
const int N = 1e5 + 5;
int n, a[N], st;
signed main()
{
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
// 随机减少逆序对
int st = getms();
while (getms() - st < 900)
{
int i = randint(1, n - 1);
int j = randint(i + 1, n);
if (a[j] < a[i])
swap(a[i], a[j]);
}
// 插入排序
for (int i = 2; i <= n; i++)
{
int key = a[i], j = i - 1;
while (j >= 1 && a[j] > key)
{ // 每次执行 = 消除1个逆序对
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
for (int i = 1; i <= n; ++i)
cout << a[i] << ' ';
return 0;
}
交上洛谷,竟然 AC 了!我到底在惊讶什么,都做过插入排序了肯定 AC 啊。但是,纯插入排序是不能 AC 的。
时间复杂度:
- 最优:。如果数组本来就有序,则无序做插入排序。只需要等 毫秒。
- 期望/最差:。插入排序是 的。而且这个随机打乱其实就是“随机版冒泡排序”,理论上也是 。
空间复杂度:。
可以优化:如果数组本来就有序,或者打乱的过程中接近有序,那么不需要再打乱了。称打乱失败为随机取数后这两个数有序。那么可以设定打乱失败次数超过一个阙值,则提前结束打乱。代码:
#include <iostream>
#include <random>
#include <chrono>
#define int long long
using namespace std;
int getms();
int randint(int l, int r);
const int N = 1e5 + 5;
int n, a[N], st, m, cnt;
signed main()
{
cin >> n;
m = n; // 最多可以忍受的打乱失败次数
for (int i = 1; i <= n; ++i)
cin >> a[i];
// 随机减少逆序对
int st = getms();
while (getms() - st < 900)
{
int i = randint(1, n - 1);
int j = randint(i + 1, n);
if (a[j] < a[i])
{
cnt = 0;
swap(a[i], a[j]);
}
else
{
++cnt;
if (cnt > m) break;
}
}
// 插入排序
for (int i = 2; i <= n; i++)
{
int key = a[i], j = i - 1;
while (j >= 1 && a[j] > key)
{ // 每次执行 = 消除1个逆序对
a[j + 1] = a[j];
j--;
}
a[j + 1] = key;
}
for (int i = 1; i <= n; ++i)
cout << a[i] << ' ';
return 0;
}
这样,可以得到一点优化。当然,打乱和插入排序可以做一些权衡。500ms~600ms 是比较好的选择。900ms 中,可能前一部分是很有效的,后一部分时数组已经接近有序,此时做插入排序就很快。
然而经过测试,发现一个问题:这个随机化过程中,如果 会炸。特判一下就好了。