曾经在程序员杂志上看到快速排序的作者,Hoare,曾经的图灵奖获得者啊,牛光闪闪的。不过当时,对快速排序什么的,印象不算深刻,毕竟没好好学。记得当时杂志上说到的是,快速排序,应该是目前最快的内部排序算法(虽然独立到语言上,C++的sort会比调用快速排序快)。现在就进入快速排序的美好复习吧。
与归并排序类似,快排也用分治模式。主要是三个步骤:
1)分解:将数组A[p....r]划分为2个子数组A[p....q-1]和A[q+1....r],使前一个每个元素都小于A[q],后一个数组,每个元素都大于A[q](q在划分过程中计算)
2)解决:递归调用快速排序,对2个子数组进行排序
3)合并:因为2个子数组是就地排序,所以合并不用操作,数组已排序
看到这个合并,就想到啊,和归并比,一个从小到大,一个从大到小,差距就是这么大,快排么得合并开销,一下就省了很多啊,说明,方向很重要啊,如同那句,同样一个B,S与N的差别,大家都懂的。
快速排序的实现代码如下:
//===============================================================
// Name : Qsort.cpp
// Author : xia
// Copyright : NUAA
// Description : 快速排序的实现
//===============================================================
#include <iostream>
#include <vector>
#include <fstream>
#include <algorithm>
#include <ctime>
using namespace std;
const int MAX = 1000 ;
void WriteToFile(vector<int> v)
{//将v写入文件,纯看排序结果是否正确,也可以写个test()
int i;
ofstream result("Qsort.txt");
if (result.fail())
{
cout << " open data error " << endl;
exit(EXIT_FAILURE);
}
for (i=0 ; i<v.size() ; i++)
{
result << v[i] << " " ;
}
result.close();
}
int Partion(vector<int> &A,int p ,int r)
{//数组划分
int x=A[r];//x都感觉没用
int i=p-1;
for (int j=p ; j<r ;j++)
{
if ( A[j] <= x )
{
i++;
swap(A[i],A[j]);
}
}
swap(A[i+1],A[r]);
return i+1;
}
void Qsort(vector<int> &A, int p ,int r)
{//递归快排
if (p < r)
{
int q = Partion(A,p,r);
Qsort(A,p,q-1);
Qsort(A,q+1,r);
}
}
int main(int argc, char **argv)
{
vector<int> v;
int i;
for (i=0 ; i< MAX ;i++)
v.push_back(i);
random_shuffle(v.begin(),v.end());//打乱
Qsort(v,0,v.size()-1);
WriteToFile(v);
return 0;
}
说到代码,很惭愧的,
http://www.cnblogs.com/chinazhangjie/archive/2010/12/09/1901491.html张杰同学的c++模板类实现,(这里也得感谢Tanky Woo,看了他的总结,得以看很多的链接),果然我写的只是C的扩充啊,像用vector只是防止溢出,也就是数组来使用,一些操作也只是作为一个扩充。
继续说正题,这里生成随机数没有用常规的C语言的srand()和rand()(参考http://blog.sina.com.cn/s/blog_42af30e4010002qo.html)由于以下两个原因:
1)做格式化时,结果常常是扭曲的,所以得不到正确的随机数(如某些数的出现频率要高于其它数)
2)rand()只支持整型数;不能用它来产生随机字符,浮点数,字符串或数据库中的记录
所以采用了STL函数random_shuffle(),先随机生成0到MAX-1的随机数,用random_shuffle()打乱,再进行排序。
另外, 其实Hoare老师用的快排并不是如上代码所示,也就是说,最原始的快速排序,是这样滴:
int HoarePartion(vector<int> &A, int p , int r)
{
int x=A[p];
int i=p-1;
int j=r+1;
while (1)
{
while (A[--j] > x)
;
while (A[++i] < x)
;
if (i<j)
swap(A[i],A[j]);
else
return j;
}
}
void Qsort(vector<int> &A, int p ,int r)
{//递归快排
if (p < r)
{
int q = HoarePartion(A,p,r);
Qsort(A,p,q);
Qsort(A,q+1,r);
}
}
也可以参考: http://tayoto.blog.hexun.com/25048556_d.html ,区别只是我的代码直接while里面用A[--j],可读性不高,因为着实不喜欢do-while结构。
对于最原始的快排,严蔚敏老师的《数据结构》是这样实现的:
int Partion(vector<int> &v ,int low ,int high)
{//对vector进行划分,返回枢轴下标
int pivotkey;
pivotkey = v[low] ;
while ( low < high )
{
while (low < high && v[high] >= pivotkey)
high -- ;
v[low] = v[high];
while (low < high && v[low] <= pivotkey )
low ++ ;
v[high] = v[low];
}
v[low] = pivotkey ;
return low ;
}
void quickSort(vector<int> &number ,int left ,int right)
{
if ( left < right )
{
int i = Partion(number , left, right) ;
quickSort(number, left, i-1); // 对左边进行递归
quickSort(number, i+1, right); // 对右边进行递归
}
}
当然,区别都只是在划分的过程,毕竟分治,才是快排的精髓嘛,不过这俩大同小异。
快排的运行时间,显然与划分是否对称有关,要是直接划分出来,是一个最不均衡的二叉树,那就够喝一壶的了,跟插入排序似的。下面网址有说法,是快排隐藏的二叉排序树思想,其实可以参考,虽然只是个人理解http://bbs.chinaunix.net/viewthread.php?tid=1011316。其实说到二叉,堆排序不也是吗?区别只是堆排序显式的建堆,也就构成了一笔不小的开销,如果考虑隐藏排序二叉树的话,倒是可以理解为毛快排快于堆排。
由于快排平均情况下效果显然很良好,那么怎么得到平均情况就是个值得思考的问题,所以书上给出了,在划分的时候,随机获取一个数作为枢轴,而不是用我们的A[low]。于是我们得到了快排的随机化版本如下:
int Partion(vector<int> &A,int p ,int r)
{//数组划分
int x=A[r];
int i=p-1;
for (int j=p ; j<r ;j++)
{
if ( A[j] <= x )
{
i++;
swap(A[i],A[j]);
}
}
swap(A[i+1],A[r]);
return i+1;
}
int RandomPartion(vector<int> &A,int p ,int r)
{//在A[p]到A[r]中随机划分
int i= p + rand()%(r-p+1); //i<-RANDOM(p,r)
swap(A[r],A[i]);
return Partion(A,p,r);
}
void RandomQsort(vector<int> &A, int p ,int r)
{//递归快排
if (p < r)
{
int q = RandomPartion(A,p,r);
RandomQsort(A,p,q-1);
RandomQsort(A,q+1,r);
}
}
与常规快排的区别,就是在划分的时候,获取一个随机数下标,再用其数组中的值作为枢轴,当然,这样就充分考虑平均性能了。
还有一种改进RANDOM-QUICKSORT的方法,就是根据从子数组更仔细地选择的(而不是随机选择的元素)作为枢轴来划分。常用的做法是三数取中。可以参考:
http://blog.csdn.net/zhanglei8893/article/details/6266915
本章最后还提到个很蛋疼的Stooge排序,实现如下:
void StoogeSort(vector<int> &A, int i ,int j)
{//递归快排
if (A[i] > A[j])
swap(A[i],A[j]);
if (i+1 >=j)
return;
int k = (j-i+1)/3;
StoogeSort(A,i,j-k);//前2/3
StoogeSort(A,i+k,j);//后2/3
StoogeSort(A,i,j-k);//又前2/3
// StoogeSort(A,i,i+k-1);// 如果采用1/3排不出来啊
}
对于数组A[i...j],STOOGE-SORT算法将这个数组划分成均等的3份,分别用A, B, C表示。第8、9行从宏观上来看它进行了两趟,结果是最大的1/3到了C,最小的1/3到了B,从宏观上来看,整个数组的三个大块就有序了,再进行递归,整个数组就有序了。第8和第9行,可以看做一个冒泡过程。
不过从运行时间的测试来讲,很不给力(具体数据就不列了)。STOOGE-SORT最坏情况下的运行时间的递归式 T(n) = 2T(2n/3)+Θ(1) 由主定律可以求得T(n)=n^2.71,相比插入排序、快速排序的Θ(n^2)和 堆排序、合并排序的Θ(nlgn),不给力啊。参考自:http://blog.csdn.net/zhanglei8893/article/details/6235294。
本章最后, 练习7-4还提出个尾递归的概念,起因是QuickSort的第二次递归调用不是必须的,可以用迭代控制结构来替代。如:
QUICKSORT'(A, p, r)
1 while p < r
2 do ▸ Partition and sort left subarray.
3 q ← PARTITION(A, p, r)
4 QUICKSORT'(A, p, q - 1)
5 p ← q + 1
具体 有效性的证明可以参考: http://blog.csdn.net/zhanglei8893/article/details/6236792,需要说明的是,当数组正序时,其递归深度和栈深度都为 Θ(n)。突然记笔记到这,发现《算法导论》的练习题,很给力啊,好书,真不是盖的。啃起来不是那么容易,不过得继续了
菜鸟goes on ~~~下面解决flex读取和修改配置文件的问题。