IntroSort-内省排序
d0j1a_1701 · · 个人记录
前置知识
插入排序
插入排序(Insertion Sort)是一种简单直观的排序算法。它的工作原理为将待排列元素划分为“已排序”和“未排序”两部分,每次从“未排序的”元素中选择一个插入到“已排序的”元素中的正确位置。
插入排序在序列已部分有序的情况下效率很高。
插入排序的最优时间复杂度为
快速排序
快速排序的工作原理是通过 分治 的方式来将一个数组排序。
快速排序分为三个过程:
将数列划分为两部分(要求保证相对大小关系);
递归到两个子序列中分别进行快速排序;
不用合并,因为此时数列已经完全有序。
和归并排序不同,第一步并不是直接分成前后两个序列,而是在分的过程中要保证相对大小关系。具体来说,第一步要是要把数列分成两个部分,然后保证前一个子数列中的数都小于后一个子数列中的数。为了保证平均时间复杂度,一般是随机选择一个数
之后,维护一前一后两个指针
其实,快速排序没有指定应如何具体实现第一步,不论是选择
第三步中的序列已经分别有序且第一个序列中的数都小于第二个数,所以直接拼接起来就好了。
普通快排的最优与平均时间复杂度为
堆排序
堆排序的工作原理为对所有待排序元素建堆,然后依次取出堆顶元素,就可以得到排好序的序列。
堆排序的时间复杂度为
注:为了缩短代码长度,本文章内堆排序使用STL优先队列实现。
正文
这是朴素的双路快排:
void quickSort(int l, int r) {
if(l >= r) return;
int i = l, j = r;
int pivot = arr[l];
while(i < j) {
while(i < j && arr[j] >= pivot) j--;
while(i < j && arr[i] <= pivot) i++;
if(i < j)
swap(arr[i], arr[j]);
}
swap(arr[l], arr[i]);
quickSort(l, i - 1);
quickSort(i + 1, r);
}
在一些极端情况下,普通的快速排序会退化为
Step 1:基准点的选择
基准点的选择对快速排序的性能有极大的影响。在上文的代码中,始终选择区间的最左侧数据作为基准点。
基准点若选成区间的最大/最小值会导致退化(遍历区间时pivot始终大于或小于其他所有元素,这会导致第6和7行的循环把区间遍历两遍);最好的情况应选择区间内元素的中位数(每次恰好将区间内元素分割成大于、小于基准点的两部分,只需遍历一遍),但这通常无法实现,会导致时间常数大幅增高。
一种常见的方法是使用随机基准点:
void quickSort(int l, int r) {
if(l >= r) return;
int i = l, j = r;
swap(arr[l], arr[rd() % (r - l + 1) + l]);//随机选择基准点
int pivot = arr[l];
while(i < j) {
while(i < j && arr[j] >= pivot) j--;
while(i < j && arr[i] <= pivot) i++;
if(i < j)
swap(arr[i], arr[j]);
}
swap(arr[l], arr[i]);
quickSort(l, i - 1);
quickSort(i + 1, r);
}
随机选择基准点情况下,几乎不可能每次都选中区间的极值。
另一种方法是从区间最左侧,区间中央,区间最右侧取中位数做基准点(三数取中法),这种方法只需少量比较即可获得(不那么准确的)中位数,在序列已有序时获得良好的效果,STL中采用此种方法。
int getPivot(int l, int r) {
int ix = l, iy = r - 1, iz = (l + r - 1) >> 1;
int x = arr[ix], y = arr[iy], z = arr[iz];
if(x > y) {
if(x > z) {
if(y > z) {
return iy;
} else {
return iz;
}
} else {
return ix;
}
} else {
if(x > z) {
return ix;
} else {
if(y > z) {
return iz;
} else {
return iy;
}
}
}
}
void quickSort(int l, int r) {
if(l >= r) return;
int i = l, j = r;
swap(arr[l], arr[getPivot(l,r)]);//三数取中法
int pivot = arr[l];
while(i < j) {
while(i < j && arr[j] >= pivot) j--;
while(i < j && arr[i] <= pivot) i++;
if(i < j)
swap(arr[i], arr[j]);
}
swap(arr[l], arr[i]);
quickSort(l, i - 1);
quickSort(i + 1, r);
}
Step 1.5:
经过优化的快排在遇到大量相同数据时还是会超时,所以可以使用三路快速排序,它将区间分割成大于基准点,等于基准点,小于基准点三部分。但是因为我不会写实现难度大,而且在后续优化后效果不明显,本文章不采用此方法。
Step 2:
当区间长度很小(如
int getPivot(int l, int r) {
int ix = l, iy = r - 1, iz = (l + r - 1) >> 1;
int x = arr[ix], y = arr[iy], z = arr[iz];
if(x > y) {
if(x > z) {
if(y > z) {
return iy;
} else {
return iz;
}
} else {
return ix;
}
} else {
if(x > z) {
return ix;
} else {
if(y > z) {
return iz;
} else {
return iy;
}
}
}
}
void insertSort(int l, int r) {
for(int i = l + 1; i <= r; i++) {
int j = i;
while(j > l) {
if(arr[j] < arr[j - 1]) {
swap(arr[j], arr[j - 1]);
j--;
} else {
break;
}
}
}
}
void quickSort(int l, int r, int deep) {
if(l >= r) return;
if(r - l <= 16) {
insertSort(l,r);
return;
}
int i = l, j = r;
swap(arr[l], arr[getPivot(l, r)]);
int pivot = arr[l];
while(i < j) {
while(i < j && arr[j] >= pivot) j--;
while(i < j && arr[i] <= pivot) i++;
if(i < j)
swap(arr[i], arr[j]);
}
swap(arr[l], arr[i]);
quickSort(l, i - 1, deep + 1);
quickSort(i + 1, r, deep + 1);
}
Step 3:
在递归深度大于
内省排序(英语:Introsort 或 Introspective sort)是快速排序和 堆排序 的结合,由 David Musser 于 1997 年发明。内省排序其实是对快速排序的一种优化,保证了最差时间复杂度为
内省排序将快速排序的最大递归深度限制为
2000 年 6 月,SGI C++ STL 的 stl_algo.h 中 sort()函数的实现采用了内省排序算法。
完整代码(可通过P1177 【模板】快速排序):
#include <algorithm>
#include <iostream>
#include <queue>
using namespace std;
int n, maxdeep;
int arr[100010];
priority_queue<int, vector<int>, greater<int> > pq;
void insertSort(int l, int r) {
for(int i = l + 1; i <= r; i++) {
int j = i;
while(j > l) {
if(arr[j] < arr[j - 1]) {
swap(arr[j], arr[j - 1]);
j--;
} else {
break;
}
}
}
}
int getMaxDeep(int x) {
int k;
for(k = 0; x > 1; x >>= 1) k++;
return k * 2;
}
int getPivot(int l, int r) {
int ix = l, iy = r - 1, iz = (l + r - 1) >> 1;
int x = arr[ix], y = arr[iy], z = arr[iz];
if(x > y) {
if(x > z) {
if(y > z) {
return iy;
} else {
return iz;
}
} else {
return ix;
}
} else {
if(x > z) {
return ix;
} else {
if(y > z) {
return iz;
} else {
return iy;
}
}
}
}
void heapSort(int l, int r) {
for(int i = l; i <= r; i++) pq.push(arr[i]);
for(int i = l; i <= r; i++) {
arr[i] = pq.top();
pq.pop();
}
}
void introSortLoop(int l, int r, int deep) {
if(l >= r) return;
if(r - l <= 16) {
return;//经测试,在排序完成后直接执行一次插入排序更快
}
if(deep >= maxdeep) {
heapSort(l, r);
return;
}
int i = l, j = r;
swap(arr[l], arr[getPivot(l, r)]);
int pivot = arr[l];
while(i < j) {
while(i < j && arr[j] >= pivot) j--;
while(i < j && arr[i] <= pivot) i++;
if(i < j)
swap(arr[i], arr[j]);
}
swap(arr[l], arr[i]);
introSortLoop(l, i - 1, deep + 1);
introSortLoop(i + 1, r, deep + 1);
}
void introSort(int l, int r) {
introSortLoop(l, r - 1, 0);
insertSort(l, r - 1);//经测试,在排序完成后直接执行一次插入排序更快(STL实现)
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n;
maxdeep = getMaxDeep(n);
for(int i = 0; i < n; i++) {
cin >> arr[i];
}
introSort(0, n);
for(int i = 0; i < n; i++) {
cout << arr[i] << ' ';
}
return 0;
}
Q/A:
Q:为什么不直接使用归并排序或堆排序等时间复杂度为
A:快速排序的分治算法在大多数情况下内存访问效率极高,虽然都是
Q:为什么还是没有STL快?
A:STL做了许多特殊优化,比如插入排序有“带边界保护”和“无边界保护”的两个版本。这导致比较次数更少,在数据范围大时造成显著影响。