快速排序的具体实现
// 快速排序,使得array[st,ed]是有序的
public static void quickSort(int[] array,int st,int ed){
if (st >= ed){
return;
}
// 以array[st]为基准,使用双指针不停交换array[st->ed]之间的元素
// 直到找到位置sortedPos,将基准放在这个位置上
// 其左边的元素都小于基准,其右边的元素都大于基准
int sortedPos = findSortedPosition(array,st,ed);
// 分治,处理sortedPos前后两个子数组
// 由于sortedPos后面的子数组的所有元素大于前一个子数组中的所有元素
// 所以分别使两个子数组变得有序之后,就达到了全局有序
quickSort(array, st, sortedPos-1);
quickSort(array, sortedPos+1, ed);
}
// 寻找一个基准array[pivot],使用双指针不停交换array[st->ed]之间的元素
// 双指针相遇时,将基准与array[left]交换
// 使得基准之前的所有元素小于基准,其后的所有元素大于基准
public static int findSortedPosition(int[] array,int st,int ed){
// 维护[st,left]之间的元素都小于或等于array[pivot]
// 维护[right,ed]之间的元素都大于或等于array[pivot]
int left = st;
int right = ed;
int pivot = st;
while (left != right){
while (left != right && array[right] >= array[pivot]){
right--;
}
while (left != right && array[left] <= array[pivot]){
left++;
}
if (left != right){
swap(array, left, right);
}
}
swap(array, pivot, right);
return left;
}
如何理解findSortedPosition这个函数?我用一个表格来解释这个过程。
标为红色的是基准,标为蓝色的是左指针维护的区域,标为绿色的是右指针维护的区域。
右指针的维护的区域为:区域内的元素都大于基准
左指针维护的区域为:区域内的元素都小于基准
我们不难得出,左指针和右指针移动的规律:由于要维护上述两个区域的性质,左指针遇到非法元素(比基准大的元素)会停下来,右指针遇到非法元素(比基准小的元素)会停下来。而且左、右指针相遇时也会停下来。
我们让右指针先移动,最终右指针遇到左指针时,左右指针都会停止在同一格上。
这时候,他们所在的这一格的数值,究竟比基准大还是比基准小?由于最后一轮中,右指针是先移动的,无论它是遇到了非法元素,还是遇到左指针停了下来,最后一轮右指针所指的一定是非法元素——比基准小的元素!
这时候,我们将基准元素与array[right]交换,就得到了最终结果——基准元素左边的元素都比它小,基准元素右边的元素都比它大。
4 | 9 | 7 | 1 | 5 | 3 | 7 | 2 | 初始时,left=st,right=ed |
---|---|---|---|---|---|---|---|---|
4 | 9 | 7 | 1 | 5 | 3 | 7 | 2 | 先移动右指针,第一个元素就是非法元素(比基准元素小的元素) |
4 | 9 | 7 | 1 | 5 | 3 | 7 | 2 | 再移动左指针,第一个元素就是非法元素(比基准元素大的元素) |
4 | 2 | 7 | 1 | 5 | 3 | 7 | 9 | 交换左右指针所指的元素,保证左、右区域的合法性 |
4 | 2 | 7 | 1 | 5 | 3 | 7 | 9 | 先移动右指针,右指针遇到非法元素3停下来了 |
4 | 2 | 7 | 1 | 5 | 3 | 7 | 9 | 再移动左指针,左指针遇到非法元素7停下来了 |
4 | 2 | 3 | 1 | 5 | 7 | 7 | 9 | 交换左右指针所指的元素,保证左、右区域的合法性 |
4 | 2 | 3 | 1 | 5 | 7 | 7 | 9 | 先移动右指针,右指针遇到非法元素1停下来了 |
4 | 2 | 3 | 1 | 5 | 7 | 7 | 9 | 左右指针相遇,而且右指针指向比基准元素4小的元素 |
1 | 2 | 3 | 4 | 5 | 7 | 7 | 9 | 将基准元素与array[right]交换,最终基准元素左侧的元素都小于基准元素,右侧的元素都大于基准元素。 |
如果让左指针先移动会怎么样?基于上述分析,我们知道左指针一定会向右移动并最终指向非法元素——比基准元素大的元素,这时候将基准与其交换,坏了,array中的第一个元素比基准元素大了。
平均时间复杂度 | O(nlogn) |
---|---|
最坏时间复杂度 | O(n^2) |
稳定度 | 不稳定:举个例子,[5,9,9,2],第一次划分的时候,首先9与2交换, |
就破坏了两个9的相对位置 | |
空间复杂度 | O(nlogn):快排并没有开辟空间,但是使用了递归,递归会开辟栈帧, |
递归算法的空间复杂度 = 每次递归的空间复杂度*递归深度 | |
适用场景 | n大的时候好 |
排序十万个随机数 | 耗时15ms |
排序一千个随机数 | 耗时1ms |
排序一百万个随机数 | 耗时94ms |
快速排序的时间复杂度分析——为什么平均时间复杂度是O(nlogn)?什么情况下退化成O(n^2)?
- 用注释分析一下平均时间复杂度为什么是O(nlogn)
public static void quickSort(int[] array,int st,int ed){
if (st >= ed){
return;
}
// 划分的时间复杂度是O(n),因为需要比较每个元素,可能要交换一些元素
int sortedPos = findSortedPosition(array,st,ed);
// 采用分治的思路,每调用一次quickSort进行分治,实际上数据规模是成倍减小的。
// 拿最好的情况来说,sortedPos正好是array正中间的位置
// 那么这部分最好的时间复杂度是O(log2n)
// 总而言之,治理嵌套了划分,两部分的时间复杂度相乘得到O(nlogn)
quickSort(array, st, sortedPos-1);
quickSort(array, sortedPos+1, ed);
}
- 那什么时候退化成O(n^2)呢?答案是array已经排好序、或者逆序排序的情况下。递归二叉树画出来应该是一棵斜树。
因为在排好序的情况下,由于基准元素是第一个元素,那么经过一轮时间复杂度为O(n)的比较之后,发现基准元素依然只能在原地不动。而且这种划分还会进行n次,为什么要进行n次呢?原本明明是logn啊,这是因为每次划分,左边部分元素数量为0,右边部分的元素数量为n-1,每调用一次quickSort数据规模只减了1,所以"治理"的时间复杂度也退化成O(n)了,两部分相乘,最坏情况下的时间复杂度就是O(n^2)。
逆序的情况跟排好序的情况类似。
如何避免最坏情况的发生?换句话说,怎么优化快速排序?
目前一种比较好的优化方法是三数取中。具体来说,在进行划分的时候,我们取array[st]、array[ed]、array[mid]这三个值的中间元素作为基准,其中mid = st + (ed-st)/2;
这样做有什么好处呢?考虑一下最好情况,我们希望基准正好是array[st->ed]这部分的中间值,取这三个数再取中间值实际上是一种贪心的思路,希望取到的值既不是最大也不是最小,可以避免最坏情况。
增加了三数取中优化的代码如下:
public static void quickSort(int[] array,int st,int ed){
if (st >= ed){
return;
}
int sortedPos = findSortedPosition(array,st,ed);
quickSort(array, st, sortedPos-1);
quickSort(array, sortedPos+1, ed);
}
public static int findSortedPosition(int[] array,int st,int ed){
// 三数取中
makeLowMid(array, st, ed);
int left = st;
int right = ed;
int pivot = st;
while (left != right){
while (left != right && array[right] >= array[pivot]){
right--;
}
while (left != right && array[left] <= array[pivot]){
left++;
}
if (left != right){
swap(array, left, right);
}
}
swap(array, pivot, right);
return left;
}
// 保证array[low]是array[low]、array[mid]、array[high]里的中间值
public static void makeLowMid(int[] array,int low, int high){
int mid = low + ((high-low)>>1);
// 保证array[high]大于array[mid]
if (array[mid] > array[high]){
swap(array, mid, high);
}
// 保证array[high]是三个数中最大的
if (array[low] > array[high]){
swap(array, low, high);
}
// 保证array[mid] < array[low]
if(array[mid] > array[low]){
swap(array, low, mid);
}
// 这样一来,array[high]是最大值,array[low]是中间值,array[mid]是最小值
}