思路
在表中随机选一个元素作为基准,将表划分成两个子表,其中左子表元素小于等于基准,右子表大于等于基准,基准归位。然后对子表再进行划分,直到某个子表的元素小于2。此时表中元素已有序。
实现
void quick_sort(RceType R[], int s, int t)
{
int i, pivot;
pivot = (s + t) / 2;
if (s < t)
{
if (pivot != s)
swpa(R, s, pivot);
i = partition(R, s, t);
quick_sort(R, s, i-1);
quick_sort(R, i+1, t);
}
}
int partition(RecType R[], int low, int high)
{
int i = low, j = high;
RecType tmp;
while (i < j)
{
while (j > i && R[j].key >= tmp.key)
j--;
R[i] = R[j];
while (i < j && R[i].key <= tmp.key)
i++;
R[j] = R[i];
}
R[i] = tmp;
return i;
}
算法分析
时间复杂度
快速排序的算法执行过程可用对应的递归树进行描述
-
最好:元素序列尽可能的无序,此时每趟划分的左右子表的长度都尽可能的相近,此时排序过程的递归树的高度最小,可以近似看成一棵完全2叉树,所以算法划分时间复杂度为
O ( l o g 2 n ) O(log_2{n}) O(log2n)
而每趟划分的时间复杂度为
O ( n ) O(n) O(n)
所以算法的最好时间复杂度为
O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
因此最好时间复杂度达到基于比较的排序算法的最好时间复杂度 -
最坏:与插入类排序或冒泡排序相反,当元素序列呈正序或反序时,算法执行过程近似的描述成一棵单支数,此时树的高度最大,所以算法划分时间复杂度为
O ( n ) O(n) O(n)
每趟划分的时间复杂度不变,所以算法的最坏时间复杂度为
O ( n 2 ) O(n^2) O(n2) -
平均:接近最好时间复杂度
O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
空间复杂度
每次划分的临时变量与问题规模无关,但划分次数与问题规模有关——快速排序的过程可以描述成一棵递归树,而树高与问题规n模相关,所以空间复杂度为
O
(
l
o
g
2
n
)
O(log_2{n})
O(log2n)
稳定性
不稳定,举一反例即可证明
应用场景
因为快速排序是根据基准元素进行子表的划分,并且一趟划分以后基准元素归位,所以基准元素与左、右子表的大小关系就确定了。因此快速排序可以应用于求序列中的前k个最大或最小的数。
思路
-
直接插入排序 --> 使元素有序 --> 输出前k个元素
时间复杂度为
O ( n 2 ) O(n^2) O(n2) -
冒泡排序 --> 冒出前k个最小元素 --> 结束算法
时间复杂度为
O ( k n ) O(kn) O(kn) -
快速排序 --> 划分 --> 如果基准为第k个元素 --> 输出
时间复杂度为
O ( n ) O ( n ) O(n)O(n) O(n)O(n)
对应实现算法void solution(int R[], int s, int t, int k) { if ((k-1 > t) || (k-1 < 0)) return ; else if (s < t) { int i, pivot; pivot = (s + t) / 2; if (s != pivot) swap(R, s, pivot); i = partition(R, s, t); if (i == k-1) { print_num(R, i); return ; } else if (k-1 < i) solution(R, s, i-1, k); else solution(R, i+1, t, k); } else if (s == t) print_num(R, s); }