Introsort(内观排序)
2013-09-28 23:59 by v.la, 51 阅读, 0 评论, 收藏, 编辑
.NET 4.5 这个版本的Array.Sort更改了STL的内观排序算法,那相对于快速排序内观排序到底有什么优化过的呢?
根据维基百科所说,这个排序算法首先从快速排序开始,当递归深度超过一定深度(深度为排序元素数量的对数值)后转为堆排序。
采用这个方法,Introsort既能在常规数据集上实现快速排序的高性能,又能在最坏情况下仍保持 O(N log N) 的时间复杂度。
由于这两种算法都属于比较排序算法,所以Introsort也是一个比较排序算法。
按我的理解可以说是快速排序+插入排序+堆排序的混合方式;
优化过的快速排序:
private static void IntroSort<T>(T[] array, int low, int height,
int depthLimit, IComparer<T> comparer, bool desc = false) {
while (height > low) {
int partitionSize = height - low + 1;
//判断区间长度少于等于16时候不再使用快速排序提升效率
if (partitionSize <= IntrosortSizeThreshold) {
if (partitionSize == 1) {
return;
}
if (partitionSize == 2) {
//长度为2的时候直接左右尝试交换
if (desc) {
SwapIfLessthan(array, low, height, comparer);
} else {
SwapIfGreater(array, low, height, comparer);
}
return;
}
if (partitionSize == 3) {
//长度为3时候,三数取中分割法
if (desc) {
SwapMed3ByLessthan(array, low, height, height - 1, comparer);
} else {
SwapMed3(array, low, height, height - 1, comparer);
}
return;
}
//使用插入排序算法
InsertionSort(array, low, height, desc);
return;
}
if (depthLimit == 0) {
HeapSort(array, low, height, desc);
return;
}
depthLimit--;
//pivotpos划分后的基准记录的位置
//对R[low..high]做划分
int pivotpos = PickPivotAndPartition(array, low, height, comparer, desc);
//对右区间递归排序
IntroSort(array, pivotpos + 1, height, depthLimit, comparer, desc);
//对左区间递归排序 因为有个while所以不需要递归,相当于QuickSort(array,low,pivotpos-1);
height = pivotpos - 1;
}
}
1.利用基于三中值分区的中枢值来做快排
//分治法:三数取中分割法
private static int PickPivotAndPartition<T>(T[] keys, int lo, int hi,
IComparer<T> comparer, bool desc) {
//用区间中位记录作为基准
int median = GetMedian(lo, hi);
//采取keys[lo],keys[median],keys[hi]三者之中的那个第二大的元素为主元时
//便能尽最大限度保证快速排序算法不会出现O(N^2)的最坏情况
if (desc) {
SwapMed3ByLessthan(keys, lo, hi, median, comparer);
} else {
SwapMed3(keys, lo, hi, median, comparer);
}
//基准
T pivot = keys[median];
//注意:hi-1是因为上面的三数取中算法已经做了低位和高位比较,
//所以这里获取hi-1(前一个比较),下面的高位实际是高位前一个位置
//尝试中间和高位交换
Swap(keys, median, hi - 1);
int left = lo;
//这里意义是为了下面--right使用,
//如果数组是5开始递减应该是3开始,
//因为Swap(keys, median, hi - 1);
//已经做了比较减少一位
int right = hi - 1;
//从区间两端交替向中间扫描,直至left=right为止
while (left < right) {
if (desc) {
//left左边的元素大于pivot,right右边的元素都小于pivot
//线性时间的原地划分,只扫描数组一次就能完成
//使用++left为了跳过第一位,因为上面已经坐了三数取中
//从左向右扫描实,左边的元素比基准大,遇到小于pivot时候停止
while (comparer.Compare(keys[++left], pivot) > 0) ;
//从右向左扫描,右边的元素比基准小,遇到大于pivot时候停止
while (comparer.Compare(keys[--right], pivot) < 0) ;
} else {
//left左边的元素小于pivot,right右边的元素都大于pivot
//线性时间的原地划分,只扫描数组一次就能完成
//使用++left为了跳过第一位,因为上面已经坐了三数取中
//从左向右扫描实,左边的元素比基准小,遇到大于pivot时候停止
while (comparer.Compare(keys[++left], pivot) < 0) ;
//从右向左扫描,右边的元素比基准大,遇到小于pivot时候停止
while (comparer.Compare(keys[--right], pivot) > 0) ;
}
//左右碰撞退出扫描,准备下次递归
if (left >= right) break;
//进行交换
Swap(keys, left, right);
}
//pivot 上面做了Swap(keys, median, hi - 1);
//中位和高位比较,所以这里需要基准位置left和高位(hi - 1 同上)交换,
//因为上面循环的--right已经跳过比较。
Swap(keys, left, hi - 1);
//基准记录已被最后定位
return left;
}
//分治法:三数取中分割法 private static int PickPivotAndPartition<T>(T[] keys, int lo, int hi, IComparer<T> comparer, bool desc) { //用区间中位记录作为基准 int median = GetMedian(lo, hi); //采取keys[lo],keys[median],keys[hi]三者之中的那个第二大的元素为主元时 //便能尽最大限度保证快速排序算法不会出现O(N^2)的最坏情况 if (desc) { SwapMed3ByLessthan(keys, lo, hi, median, comparer); } else { SwapMed3(keys, lo, hi, median, comparer); } //基准 T pivot = keys[median]; //注意:hi-1是因为上面的三数取中算法已经做了低位和高位比较, //所以这里获取hi-1(前一个比较),下面的高位实际是高位前一个位置 //尝试中间和高位交换 Swap(keys, median, hi - 1); int left = lo; //这里意义是为了下面--right使用, //如果数组是5开始递减应该是3开始, //因为Swap(keys, median, hi - 1); //已经做了比较减少一位 int right = hi - 1; //从区间两端交替向中间扫描,直至left=right为止 while (left < right) { if (desc) { //left左边的元素大于pivot,right右边的元素都小于pivot //线性时间的原地划分,只扫描数组一次就能完成 //使用++left为了跳过第一位,因为上面已经坐了三数取中 //从左向右扫描实,左边的元素比基准大,遇到小于pivot时候停止 while (comparer.Compare(keys[++left], pivot) > 0) ; //从右向左扫描,右边的元素比基准小,遇到大于pivot时候停止 while (comparer.Compare(keys[--right], pivot) < 0) ; } else { //left左边的元素小于pivot,right右边的元素都大于pivot //线性时间的原地划分,只扫描数组一次就能完成 //使用++left为了跳过第一位,因为上面已经坐了三数取中 //从左向右扫描实,左边的元素比基准小,遇到大于pivot时候停止 while (comparer.Compare(keys[++left], pivot) < 0) ; //从右向左扫描,右边的元素比基准大,遇到小于pivot时候停止 while (comparer.Compare(keys[--right], pivot) > 0) ; } //左右碰撞退出扫描,准备下次递归 if (left >= right) break; //进行交换 Swap(keys, left, right); } //pivot 上面做了Swap(keys, median, hi - 1); //中位和高位比较,所以这里需要基准位置left和高位(hi - 1 同上)交换, //因为上面循环的--right已经跳过比较。 Swap(keys, left, hi - 1); //基准记录已被最后定位 return left; }
2.设定一个使用切分时数组长度的最小值,如果小于这个值,就使用插入排序(这个最小值根据经验给定,一般设定为16)
//判断区间长度少于等于16时候不再使用快读排序提升效率
if (partitionSize <= IntrosortSizeThreshold) {
if (partitionSize == 1) {
return;
}
if (partitionSize == 2) {
//长度为2的时候直接左右尝试交换
if (desc) {
SwapIfLessthan(array, low, height, comparer);
} else {
SwapIfGreater(array, low, height, comparer);
}
return;
}
if (partitionSize == 3) {
//长度为3时候,三数取中分割法
if (desc) {
SwapMed3ByLessthan(array, low, height, height - 1, comparer);
} else {
SwapMed3(array, low, height, height - 1, comparer);
}
return;
}
//使用插入排序算法
InsertionSort(array, low, height, desc);
return;
}
//判断区间长度少于等于16时候不再使用快读排序提升效率 if (partitionSize <= IntrosortSizeThreshold) { if (partitionSize == 1) { return; } if (partitionSize == 2) { //长度为2的时候直接左右尝试交换 if (desc) { SwapIfLessthan(array, low, height, comparer); } else { SwapIfGreater(array, low, height, comparer); } return; } if (partitionSize == 3) { //长度为3时候,三数取中分割法 if (desc) { SwapMed3ByLessthan(array, low, height, height - 1, comparer); } else { SwapMed3(array, low, height, height - 1, comparer); } return; } //使用插入排序算法 InsertionSort(array, low, height, desc); return; }
3.监视快排的递归深度,以确保高效的处理。如果快排递归深度超过log(n)级,那么内观排序切换到堆排序
if (depthLimit == 0) { HeapSort(array, low, height, desc); return; } depthLimit--;
if (depthLimit == 0) { HeapSort(array, low, height, desc); return; } depthLimit--;