自己动手写快速排序
一、初探—朴素的快速排序算法
void quick_sort(int a[], int l, int u)
{
if (l >= u) return;
int m = l;
for (int i=l+1; i<=u; i++) {
if (a[i] < a[l])
swap(a, i, ++m);
}
swap(a, l, m);
quick_sort(a, l, m-1);
quick_sort(a, m+1, u);
}
这个
朴素的快速排序有个缺陷就是在一些极端情况如所有元素都相等时(或者元素本身有序,如1,2,3,4,5等),朴素算法时间复杂度为O(N^2)。
二、改进—双向划分快速排序算法
一种改进方法就是采用双向划分,使用两个变量i和j,i从左往右扫描,移过小元素,遇到大元素停止;j从右往左扫描,移过大元素,遇到小元素停止。然后测试i和j是否交叉,如果交叉则停止,否则交换i与j对应的元素值。注意,如果数组中有相同的元素,则遇到相同的元素时,我们停止扫描,并交换i和j的元素值。虽然这样交换次数增加了,但是却将所有元素相同的最坏情况由O(N^2)变成了差不多O(NlgN)的情况。比如数组A={2,2,2,2,2}, 则使用朴素快速排序方法,每次都是划分n个元素为1组1个和和1组n-1个,时间复杂度为O(N^2),而使用双向划分后,第一次划分的位置是2,基本可以平衡划分两部分。代码如下:
void quick_sort_2(int a[], int l, int u)
{
if (l >= u) return;
int i = l;
int j = u+1;
int t = a[l];//选择最左边元素作为枢纽元
while (1) {
do {
i++;
} while (a[i] < t && i <= u); //注意i<=u这个判断条件,不能越界。
do {
j--;
} while (a[j] > t);
if (i > j) break;
swap(a, i, j);
}
swap(a, l, j); //注意这里是交换l和j,而不是l和i,因为i与j交叉后,a[i...u]都大于等于枢纽元t,而枢纽元又在最左边,所以不能与i交换。只能与j交换。
quick_sort_2(a, l, j-1);
quick_sort_2(a, j+1, u);
}
虽然双向划分解决了所有元素相同的问题,但是对于一个已经排好序的数组如1,2,3,4,5,该方法还是会达到O(N^2)的复杂度。此外,双向划分还要注意的一点是代码中循环的写法,如果写成while(a[i]<t) {i++;}等形式,因为当左右划分的两个值都等于枢纽元时,会导致死循环。
三、继续改进—随机化枢纽元
为了解决上述问题,可以进一步改进,通过 随机选取枢纽元的方式可以在一定程度改进性能。当然,常用的方法还有 三数取中等策略。此外,在数据基本有序的情况下,使用插入排序可以得到很好的性能,而且在排序很小的子数组时,插入排序比快速排序更快。通过使用随机化枢纽元和插入排序来综合改进快速排序,代码如下:/*快速排序修改版*/
void quick_sort_3(int a[], int l, int u)
{
if (u-l < 7)
return insert_sort(a, l, u); //元素数目小于7调用插入排序
swap(a, l, randint(l, u)); //随机选取枢纽元
int i = l; int j = u+1;
int t = a[l];
while (1) {
do { i++; } while (a[i] < t && i <= u);
do { j--; } while (a[j] > t);
if (i > j) break;
swap(a, i, j);
}
swap(a, l, j);
quick_sort_3(a, l, j-1);
quick_sort_3(a, j+1, u);
}
/*返回[l,u]范围的随机数*/
int randint(int l, int u)
{
srand(time(NULL));
return rand()%(u-l+1) + l;
}
/*插入排序实现*/
void insert_sort(int a[], int l, int u)
{
cout << "insert_sort" << endl;
for (int i=l; i<u; i++) {
for (int j=i+1; j>l && a[j-1]>a[j]; j--)
swap(a, j, j-1);
}
}
四、再探—采用三数取中方法
除了随机化枢纽元,还可以采用三数取中的方法,三数取中指的就是从数组A[left... right]中选择左中右三个值进行排序,并使用中值作为枢纽元。如数组A={1, 3, 5, 2, 4},则我们对A[0]、A[2]、A[4]进行排序,选择中值A[4]作为枢纽元,数组在三数取中排序后变成A={1,3,4 ,2,5}。我们选择排序后的A[2]=4作为枢纽元,并将其交换到right-1的位置,也就是位置3,从而最终数组变成A={1, 3, 2, 4, 5},然后就可以从i=1和j=2开始双向划分进行快速排序了。
/*三数取中算法*/
int median3(int a[], int left, int right)
{
int center = (left + right) / 2;
/*三数排序*/
if( a[left] > a[center] )
swap(a, left, center);
if( a[left] > a[right] )
swap(a, left, right);
if( a[center] > a[right] )
swap(a, center, right);
/* assert: a[left] <= a[center] <= a[right] */
swap(a, center, right-1); //交换枢纽元到位置right-1
return a[right-1]; //返回枢纽元
}
使用三数取中的快速排序如下:
void quick_sort_4(int a[], int l, int u)
{
if (u - l < 7) return insert_sort(a, l, u);
int pivot = median3(a, l, u); //得到枢纽元
int i = l, j = u - 1;
while (1) {
while (a[++i] < pivot)
;
while (a[--j] > pivot)
;
if (i > j)
break;
swap(a, i, j);
}
swap(a, i, u-1); //注意,这里是i与枢纽元位置u-1交换,因为这里是选择u-1作为枢纽元,与前面选择l作为枢纽元不同
quick_sort_4(a, l, i-1);
quick_sort_4(a, i+1, u);
}
五、号外—非递归写快速排序
非递归写快速排序着实比较少见,不过练练手总是好的。需要用到栈,注意压栈的顺序。这里把划分函数提取出来,便于多次调用,代码如下:
void quick_sort_5(int a[], int l, int u)
{
if (l >= u) return ;
stack<int> s; //栈存储划分位置
int left = l, right = u;
int p = partition(a, left, right);
if (p-1 > left) { //左半部分两个边界值入栈
s.push(p-1);
s.push(left);
}
if (p+1 < right) { //右半部分两个边界值入栈
s.push(right);
s.push(p+1);
}
while (!s.empty()) { //栈不为空,则循环划分过程
left = s.top();
s.pop();
right = s.top();
s.pop();
p = partition(a, left, right);
if (p-1 > left) {
s.push(p-1);
s.push(left);
}
if (p+1 < right) {
s.push(right);
s.push(p+1);
}
}
}
/*划分函数,返回枢纽元的位置*/
int partition(int a[], int l, int u)
{
int m = l;
for (int i=l+1; i<=u; i++) {
if (a[i] < a[l])
swap(a, i, ++m);
}
swap(a, l, m);
return m;
}