排序算法是各种笔试,面试最常考到的一类题目,提到排序,一定会要求提供一种高效的方法,所以就不得不说一下快速排序了。快嘛!
写出快速排序一定要先理解什么是枢纽元(pivot),枢纽元就是每次执行快排需要参照的那个元素。
最常见的选择pivot的方法是选择第一个元素。此外还有最后一个元素,随机选择,中值法等等。
比如 20,34,4,53,43,42,6,67,193 选择20为pivot经过一趟快速排序后会达到:
6,4,20,53,43,42,34,67,193 过程可参见动画http://www.jcc.jx.cn/xinwen3/news/kj/flash/2004/0426/1306.htm
由此我们也可以看到快速排序每次的目的就是找到枢纽元的准确位置,
即 小于枢纽元的数 | 枢纽元 | 大于等于枢纽元的数
所以只要你能每趟排序达到这样的效果,就可以条条大陆通罗马了。
实现快速的排序的难点在于,
(1) 如何停止递归,即什么时候return?
比较容易记忆的方式是:在 start>=end 时 return,很明显这时表示待排序的序列只有一个或0个元素。不需要做任何处理,所以返回。
(2) i,j越界问题。
编写程序时,只要单独考虑到以下三条特例情况,如果不出问题通常可以满足越界要求了:
1. 2个元素的排序,
2. i++到end时,
3. j--到为-1时。
(3) 如何停止i++,j--操作,即什么时候算是完成一趟快速排序操作?
通常i++,j--外面要包一层循环,问题也就变成了,如何停止这个循环。
比较常见的做法是:
do{
}
while(i<j)
当然也可用:
for(; ;)
{
if (i < j)
{...}
else
break;
}
(4) 如果序列有相等元素怎么办?
一种常见方法是,对于j--操作要求判断条件为 <= 而不是<
具体对于这4个难点的解决参见下面的代码片段:
//注意,该函数是以最后一个元素p[end]为pivot。
QuickSort(int p[], int start, int end)
{
if (start >= end) //(1)此时无需排序,所以返回。(2).1 这还保证了,如果只有2个元素时,j=end-1 =1>0
return;
int i = start, j = end - 1;
int tmp;
do
{
while(p[i]<p[end]){i++;}
while(p[j]>=p[end]){j--;} //(4) 解决相等元素问题。
if (i<j)
swap(p[i], p[j]);
else
swap(p[i], p[end]);//确定pivot的准确位置。
}
while(i < j);
//(3)至此完成了一趟快速排序得到:小于pivot的数 | pivot | 大于等于pivot的数
QuickSort(p, start, i-1); //对小于pivot的数进行快速排序。
QuickSort(p, i+1, end); //对大于等于pivot的数进行快速排序。
//(2).2 如果i到end时,即end前的所有元素都小于pivot,会发现,i+1>end了,即递归调用时会立刻返回,所以不会发生p[i]操作越界的现象,因为根本就没机会往下走。(2).3同理。
}
下面给一段可执行代码:
#include <iostream>
using namespace std;
void QuickSort(int p[], int start, int end)
{
if (start >= end)
{
return;
}
int i=0, j=0, tmp=0;
i = start;
j = end-1;
do
{
// i++是不会越界的,因为随着i++,p[i]最后会指向最后一个元素即等于p[end], 这时p[i]<p[end]不再成立,i将停止增加。
while(p[i] < p[end])
{
i++;
}
//吸纳网友回复,j--的确存在越界的情况,即j--会最后为-1,但p[-1] 是个未知内存地址,初值应该是-2147483648(见后面论据),所以会有p[j] < p[end],终止循环,除了下面1种例外。
希望大家也能随我更深入的看一下这个问题,严格讲会有一种情况出现bug:p[end] = -2147483648 如:{20,34,4,53,43,42,6,67,-2147483648}, 这时p[j]>=p[end]将一直成立,我在自己电脑win7+vs2010测试: -2147483637仍然能过,所以我猜测系统给未初始化的地址初值是-2147483648(2^31 = 2147483648 = -2147483648)。 很隐蔽的bug!! 当然越界总归不好,另外,两层循环也显的有些臃肿,做了一个更新版QuickSort,见下面。
while(p[j] >= p[end])
{
j--;
}
if (i<j)
{
tmp = p[i];
p[i] = p[j];
p[j] = tmp;
}
else
{
tmp = p[i];
p[i] = p[end];
p[end] = tmp;
}
}
while(i<j);
QuickSort(p, start, i-1);
QuickSort(p, i+1, end);
}
void PrintArrary(int data[], int size)
{
for (int i=0; i<size; ++i)
{
cout <<data[i]<<" ";
}
cout<<endl;
}
int main(int argc, const char** argv)
{
int array[]= {20,34,4,53,43,42,6,67,193};
int size = sizeof(array)/sizeof(int);
QuickSort(array, 0, size - 1);
PrintArrary(array, size);
return 0;
}
更新版QuickSort:
void QuickSort(int a[], int low, int high)
{
if (low >= high)
{
return;
}
int i = low;
int j = high;
int pivot = a[low];
while(i <= j)
{
if (a[i] <= pivot)
{
i++;
}
else if (a[j] > pivot)
{
j--;
}
else
{// swap a[i], a[j]
int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
i++;
j--;
}
}
// swap a[low] , a[j]
a[low] = a[j];
a[j] = pivot;
j--;
QuickSort(a, low, j);
QuickSort(a, i, high);
}
但我们要警惕,快速排序算法曾被认为,理论上快速高效,但实际中却不可能正确编写的算法。
为什么这么说,这最主要源自枢纽元(pivot)的选择方法不唯一。
比如上面列举的以第一个或最后一个元素为pivot的方法就不是一个好的方法,存在隐患。
快速排序的平均时间复杂度是 O(nlogn) 但如果序列是预排序的或是反序的,采用这种pivot方式的快速排序花费的时间将变为二次的。即O(n2)但预排序(或大部分预排序)又是一种很常见的数据提供方式。
所以更为安全的办法是采用三数中值法选取pivot:最左边的数,最右边的数,中间数三个位置的数的中值为pivot
20,34,4,53,43,42,6,67,193 left=20, right=193, middle=43, 20<43<193所以选择43为pivot。
将pivot与right互换,然后便可以按照上面提供的end作为pivot的算法进行快速排序了。
20,34,4,53,193,42,6,67|43 i从第一个数开始,j从倒数第二个数开始。
i j