前言
本文介绍排序算法中的快速排序,快速排序是比较常用的一种排序算法,也是面试中经常会问到的一种排序算法,简称快排,是我们要介绍的第一种时间复杂度为O(nlogn)的排序算法。
核心思想
快速排序(Quick Sort)使用分治法策略,也是一种交换式排序方法。
基本思想:选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序流程:
(1) 从数列中挑出一个基准值。
(2) 将所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边);在这个分区退出之后,该基准就处于数列的中间位置。
(3) 递归地把"基准值前面的子数列"和"基准值后面的子数列"进行排序。
核心思想
假设有数组 {4、5、8、1、7、2、6、3},我们要将它按从小到大排序。按照快速排序的思想,我们先选择一个基准元素,进行排序:
-
选取4为我们的基准元素,并设置基准元素的位置为index,设置两个指针left和right,分别指向最左和最右两个元素:
-
把right指针所指向的元素和基准元素 arr[index] 做比较,
如果 arr[right] > arr[index] ,right 左移;
否则,交换 arr[right] 和 arr[index]。
3和4比较,3比4小,将3填入index中,原来3的位置成为了新的index,同时left右移一位
-
切换left指针进行比较,如果left指向的元素小于基准元素,则left指针向右移动,如果元素大于基准元素,则把left指向的元素填入index中:
5和4比较,5比4大,将5填入index中,原来5的位置成为了新的index,同时right左移一位
-
接下来,我们再切换到right指针进行比较,6和4比较,6比4大,right指针左移一位
-
随着left右移,right左移,最终left和right重合
-
此时,我们将基准元素填入index中,这时,基准元素左边的都比基准元素小,右边的都比基准元素大,这一轮交换结束。
基准元素4将序列分成了两部分,左边小于4,右边大于4,第二轮则是对拆分后的两部分进行比较
-
此时,我们有两个序列需要比较,分别是3、2、1和7、8、6、5,重新选择左边序列的基准元素为3,右边序列的基准元素为7
-
第二轮排序结束后,结果如下所示
-
此时,3、4、7为前两轮的基准元素,是有序的,7的右边只有8一个元素也是有序的,因此,第三轮,我们只需要对1、2和5、6这两个序列进行排序:
10. 第三轮排序结果如下所示,已经升序有序
核心代码
/*
* 快速排序
*
* 参数说明:
* a -- 待排序的数组
* l -- 数组的左边界(例如,从起始位置开始排序,则l=0)
* r -- 数组的右边界(例如,排序截至到数组末尾,则r=a.length-1)
*/
void quick_sort(int a[], int l, int r)
{
if (l < r)
{
int i,j,x;
i = l;
j = r;
x = a[i];
while (i < j)
{
while(i < j && a[j] > x)
j--; // 从右向左找第一个小于x的数
if(i < j)
a[i++] = a[j];
while(i < j && a[i] < x)
i++; // 从左向右找第一个大于x的数
if(i < j)
a[j--] = a[i];
}
a[i] = x;
quick_sort(a, l, i-1); /* 递归调用 */
quick_sort(a, i+1, r); /* 递归调用 */
}
}
此处我们使用了递归
性能
时间复杂度
快速排序的时间复杂度在最坏情况下是O(n2),平均的时间复杂度是O(nlogn)。
假设被排序的数列中有n个数。遍历一次的时间复杂度是O(n),需要遍历多少次呢?至少log(n+1)次,最多n次。
为什么最少是log(n+1)次?快速排序是采用的分治法进行遍历的,我们将它看作一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的定义,它的深度至少是log(n+1)。因此,快速排序的遍历次数最少是log(n+1)次。
为什么最多是n次?这个应该非常简单,还是将快速排序看作一棵二叉树,它的深度最大是n。因此,快读排序的遍历次数最多是n次。
算法稳定性
快速排序是不稳定的算法,它不满足稳定算法的定义。
稳定算法的定义如下:
假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!