sort_谈谈快排
在排序届中,快排 无论论其空间复杂度还是时间复杂度,它的优势都位居前列,这也是很多人在提起sort时,第一个想起的就是快速排序
但实际快排也有属于它的痛点,首先附上一段最简单的快排代码:
void QuickSortRecurse(vector< int > &vec, int start, int end) {
if (start >= end) return;
//[start ... end]左闭右闭
int tmp_val = vec[start];
int left_index = start, right_index = end;
while (left_index < right_index) { // 保证左边的小于,右边的大于等于
while (left_index < right_index && vec[right_index] >= tmp_val) //注意边界条件
--right_index; //右面找个最小
vec[left_index++] = vec[right_index]; //右面给左面,左面++
while (left_index < right_index && vec[left_index] < tmp_val)
++left_index;
vec[right_index--] = vec[left_index];
}
vec[left_index] = tmp_val; //结束后left_index=right_index
QuickSortRecurse(vec, start, left_index - 1);
QuickSortRecurse(vec, left_index + 1, end);
}
void QuickSortInternal(vector< int > &vec) {
QuickSortRecurse(vec, 0, vec.size() - 1);
}
熟悉快排思想的人应该很容易看懂,这里便不做初级介绍。
属于快排的痛点确实值得仔细分析,下面我们一一列举
快排的缺点
1.数据局限性
对快排有一些了解的人可能会清楚,快排的时间复杂度是不稳定的,在特定数据的情况下,可能最差会差到O(n^2)!这样不仅失去快排优势,甚至比排序小白都知道的冒泡排序效率还差
Q:是什么样的数据会让其如此落魄呢?
A:基本有序的情况下
我们举一个极端的例子:{1, 2, 3, 4, 5, 6, 7}
假设第一次以最左边的值1为临界值,那么第一次遍历下来,所有的数据都比1大,没有人交换位置。
那么在继续下次的递归时,left的索引就变成区间[0,-1](自然不成立),right区间就变成[1,6]
显而易见的,这样明显会进行n次递归,而每次递归都要进行n次的遍历,答案由此可见。
2.对于很小或部分有序的情况下,快排不如插排好
3.数据量大的情况下可能导致递归过深
改进方式
1.选取基准时下心思
经过证实,随机选取基准值会比选左右两边为基准值更高效,而三数取中(即左边界、中间值、右边界)取中值和随机取值同样高效,甚至几乎会更高效,同时也会更简单,所以这里更加推荐三数取中的方式。
2.基准值相同的值不再纳入下次递归
举例{1, 2, 2, 2, 2, 2, 3},在第一次排好序后,如果第一次的基准值为2,下次递归区间若能排除2,将会提升很大的效率,但这将需要多出一个变量来记录右侧遍历到的“待填数值的位置的索引”,同时还要一个变量来记录基准值的数量。
3.采用多种排序的组合
正如小标题所说的那样,C++的标准库中的sort算法正是采用了三种排序来提升效率,分别为:快速排序 , 插入排序 和 堆排序。
这样做的好处是显而易见的:
第一阶段:首先通过快速排序来分段,一定程度上减少快排的缺点;
第二阶段:当数据量小时,优先选择插入排序;当递归层次深时,优先选择堆排序。
经过以上的优化,快排的效率将会比默认情况快出很多。

浙公网安备 33010602011771号