快速排序
快速排序是目前内部排序中性能较好的算法,面试必问,必须掌握.
快速排序是在冒泡排序算法上优化而来,最好可以先掌握冒泡排序,然后才能较快掌握快排为什么快,以及在特定条件下为什么会慢.
快排与归并两中算法的解决思路相似.均采用分治法
.但是实现细节又稍有不同(两者都有交替处理的步骤).最好可以提前掌握归并排序.
步骤拆解
目的:将原始序列按照升序排序.
- 从待排序的原始序列中选取一个枢纽点
pivot
- 从原始序列的两端(最左边
key = 0
,以及最右边key = len - 1
) 交替与pivot
元素值比较,
从pivot
的右侧序列中剔除比pivot
小的元素,移动到pivot
左侧.
从pivot
的左侧序列中剔除比pivot
大的元素,移动到pivot
右侧.
直到pivot
左侧序列中元素的最大值小于pivot
右侧序列中的最小值. - 对
pivot
两侧序列重复进行上述两个步骤.直到每个序列只剩下一个元素.此时,完成整个序列排序.
落实到代码上
假设原始序列为{8 , 20 , 5 , 6 , 16 , 7 }
step.1 从待排序的原始序列中选取一个枢纽点pivot
,一般取序列的第一个元素. int pivot = arr[0]
,即pivot = 8
;
step.2 从原始序列的双端交替与pivot
元素值比较.
设原始序列的最左端,最右端元素索引分别为left
,right
.
首先,从右向左开始逐个与pivot
比较,找到pivot
右边第一个小于pivot
的元素,移动到pivot
左侧.
(这个移动到pivot
左侧操作有玄机,如果是第一次比较,相当于把该元素放置在左侧序列的最左边,也就是key=0
处,等待第二次循环时,
把第二次找出来的元素从左数key=1
处即可,即紧紧挨着上一次循环找到的小于pivot
的元素.
此时第一次与第二次找到的小于pivot
的元素之间谁大谁小还不确定,需要递归到最底层才能确定).
如此交替进行置换.直到pivot
左侧序列中元素均小于pivot
右侧序列中元素,(即left/right相遇)时,一趟排序完成
(注意是一趟排序完成,不是一次排序完成.相当于冒泡排序中将一个元素浮动到了顶端)
after right to left -> 7 , 20 , 5 , 6 , 16 , 8
after left to right -> 7 , 8 , 5 , 6 , 16 , 20
第一次循环之后, left = 1, right= 5 , 此时 pivot 右侧仍然存储比它小的元素,继续交替比较
after right to left -> 7 , 6 , 5 , 8 , 16 , 20
after left to right -> 7 , 6 , 5 , 8 , 16 , 20
第二次循环之后, left = 3, right= 3 , 此时 pivot 左侧元素 均 小于右侧 元素,本趟排序完成.
step.3 递归对pivot
左右两边的子序列重复上述操作.pivot
本身无需再次参与比较.
代码实例
#include <stdio.h>
#define MAX_SIZE 10
int wait_sort[MAX_SIZE] = {11,15,20,13,17,65,27,49,99,18};
void show(int * s,int length)
{
int i ;
for(i=0;i<length;i++){
printf(" %d ",s[i]);
}
printf("\n");
}
void swap(int * i,int * j)
{
int temp;
temp = *i;
*i = *j;
*j = temp;
}
void quick_sort(int a[],int left,int right)
{
// 快速错误
if(left >= right ){
return ;
}
int i = left;
int j = right;
int pivot = a[left];
while(left < right)
{
// 从右边找小值
while(left < right && pivot < a[right]){
right--;
}
swap(&a[left],&a[right]);
// 从左边找大值
while(left < right && pivot > a[left]){
left++;
}
swap(&a[left],&a[right]);
}
// 递归调用 pivot 不再参与
quick_sort(a,i,left - 1);
quick_sort(a,left + 1,j);
}
int main(void)
{
quick_sort(wait_sort,0,MAX_SIZE - 1);
show(wait_sort,MAX_SIZE);
return 0;
}
算法分析
快速排序的一次划分算法从两头交替搜索,直到low和high重合,因此其时间复杂度是O(n)
;即内循环的时间复杂度为O(n)
,外循环为需要进行多少趟排序.
所以整个快速排序算法的时间复杂度与划分的趟数有关。
- 理想的情况: 每次划分所选择的中间数恰好将当前序列几乎等分,经过
logn
(以2为底,n的对数)趟划分,便可得到长度为1的子表。此时算法的时间复杂度为O(n * logn)
- 最坏的情况: 每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中有一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过
n
趟划分,使得整个排序算法的时间复杂度为O(n^2)
。
为改善最坏情况下的时间性能,可采用其他方法选取中间数。通常采用“三者值取中”方法,
即比较H->r[low].key
、H->r[high].key
与H->r[(low+high)/2].key
,取三者中关键字为中值的元素为中间数。
快速排序的平均时间复杂度也是O(n * logn)
. 因此,该排序方法被认为是目前最好的一种内部排序方法。
从空间性能上看,尽管快速排序只需要一个元素的辅助空间,但快速排序需要一个栈空间来实现递归。
最好的情况下,即快速排序的每一趟排序都将元素序列均匀地分割成长度相近的两个子表,所需栈的最大深度为log2(n+1)
;
但最坏的情况下,栈的最大深度为n
。这样,快速排序的空间复杂度为O(logn)
。