在学数据结构中排序这一章节的时候,有一道有关快速排序的作业题描述如下:
按下述要求编写快速排序的非递归算法:
定义一个栈(或队列),把整个序列的上、下界入栈(或队列)。当栈(或队列)非空时进行如下操作:
(1)取栈顶(或队头)元素作为序列的上、下界,在区间的头部、中间、尾部取关键字居中的元素作为中枢元素,进行一趟快速排序;
(2)在一趟排序过程中,如果子表已有序(没有发生元素交换),则该子序列排序结束,否则先对划分出的长度较短的子表进行排序,且将另一子表的上、下界入栈(或队列)保存;
(3)若待排序区间中数据元素数小于等于3,则不再进行分割,而是直接进行比较完成排序。
完成算法实现后,用大数据量进行测试,同原有快速排序算法在时间上进行对比分析。
这道题本质上是在优化快速排序。
1.用栈代替递归
函数的递归本身就利用了堆栈,用栈改写递归算法为非递归是常用思路。我们可以将序列的上下界入栈,排序时再取栈顶元素并出栈,本题中要求先对划分出的长度较短的子表进行排序,那么我们就先入栈较长的子表,将较短的子表后入栈以便下次循环中优先出栈。(部分)代码如下:
SeqStack<int> s;
while( !s.isEmpty()) //栈非空且未排好序{//得到某分区的左右边界int r;s.getTop(r);s.pop();int l;s.getTop(l);s.pop();boundary = Part(a, l, r, Flag); //boundary为枢轴元素所在位置if( boundary - l < r - boundary) ///如果左分区比右分区短{if( boundary + 1 < r ) //判断右分区是否存在{s.push(boundary + 1);s.push(r);}if( boundary - 1 > l ) //判断左分区是否存在{//将左分区端点后入栈以便优先排序s.push(l);s.push(boundary - 1);}}else //如果右分区比左分区短{if( boundary - 1 > l ) //判断左分区是否存在{s.push(l);s.push(boundary - 1);}if( boundary + 1 < r ) //判断右分区是否存在{///将右分区端点后入栈以便优先排序s.push(boundary + 1);s.push(r);}}}
2.中枢元素的取法
一般书本上的快速排序是取第一个待排序的元素作为中枢元素,而为了优化快速排序算法,如果能够尽量将中枢元素取在整个待排序数组的中位数附近,那么两个子表的长度就会接近,这样一来就减少了时间复杂度,优化了快速排序算法,本题中采取的方法是:在区间的头部、中间、尾部取关键字居中的元素作为中枢元素。其实我们也可以平均取五个点、七个点来找居中的元素。(头中尾取中值)代码如下:
int Mid(int first, int mid, int last)
//求头,中,尾中关键字居中的元素
{if ((first>=mid&&first<=last)||(first<=mid&&first>=last))return first;else if ((mid>=first&&mid<=last)||(mid<=first&&mid>=last))return mid;elsereturn last;
}
注意,要在Partition函数中交换首元素和中枢元素的位置:
int pivot = Mid(low, (low+high)/2, high);//选择区间的头部、中间、尾部取关键字居中的元素作为中枢元素Swap(elem[pivot], elem[low]); //保持代码的统一性
3.在一趟排序中,如果子表已有序,则该子序列排序结束
这一点便于理解,代码实现上可以定义一个bool类型的标记,来判断Partition中是否发生过元素交换,如果没有交换bool赋true。
4.待排区间中元素数<=3,则不再进行分割,则直接比较排序
快速排序是适用于大数据量的排序方式,在数据量比较小的时候,使用插入排序或者选择排序较好,所以很多实用的排序算法使用快排+插排的方式排序。本题中要求:若待排序区间中数据元素数小于等于3,则不再进行分割,而是直接进行比较完成排序,代码如下:
void JustSort(int a[], int left, int right)
//数组元素小于等于3时的直接比较排序
{if(right-left==1) //序列只有两个元素时{if(a[left] > a[right]){Swap(a[left], a[right]);}}else //三个元素时{if(a[left] > a[left+1])Swap(a[left], a[left+1]);if(a[left+1] > a[right])Swap(a[left+1], a[right]);if(a[left] > a[left+1])Swap(a[left], a[left+1]);}
}
应用以上优化方法后,用50000个随机元素的数组进行排序,与书本上的递归快排进行时间比较,如下:
可见有一定的优化效果。
全部代码如下:
#ifndef __ExQUICKSORT_H__
#define __ExQUICKSORT_H__
#include "SeqStack.h"int Mid(int first, int mid, int last)
///求头,中,尾中关键字居中的元素
{if ((first>=mid&&first<=last)||(first<=mid&&first>=last))return first;else if ((mid>=first&&mid<=last)||(mid<=first&&mid>=last))return mid;elsereturn last;
}int Part(int elem[], int low, int high, int &flag) ///参数flag用以判断是否已经有序,以便结束排序
//原快速排序算法中的划分部分,写成函数方便循环调用
{int pivot = Mid(low, (low+high)/2, high);//选择区间的头部、中间、尾部取关键字居中的元素作为中枢元素Swap(elem[pivot], elem[low]); /// 交换枢轴元素和首元素的位置,保持代码的统一性int e = elem[low]; // 取枢轴元素int i = low, j = high;while (i < j){while (i < j && elem[j] >= e) // 使j右边的元素不小于枢轴元素j--;if (i < j){elem[i++] = elem[j];flag = 1;}while (i < j && elem[i] <= e) // 使i左边的元素不大于枢轴元素i++;if (i < j){elem[j--] = elem[i];flag = 1;}}elem[i] = e;return i; //返回枢轴元素位置
}void JustSort(int a[], int left, int right)
///数组元素小于等于3时的直接比较排序
{if(right-left==1) //序列只有两个元素时{if(a[left] > a[right]){Swap(a[left], a[right]);}}else //三个元素时{if(a[left] > a[left+1])Swap(a[left], a[left+1]);if(a[left+1] > a[right])Swap(a[left+1], a[right]);if(a[left] > a[left+1])Swap(a[left], a[left+1]);}
}template <class ElemType>
void ExQuickSort(ElemType a[], int left, int right)
// 操作结果:对数组elem[low .. high]中的元素进行快速排序
{int Flag = 0; ///标记,用来判断是否已经排好序(未发生过交换)SeqStack<int> s;if( left<right ){if(right-left < 3) //如果序列小于等于3个元素{JustSort(a, left, right);return ;}int boundary = Part(a, left, right, Flag); //划分后的中枢所在位置if(Flag == 0)return ;if( boundary - left < right - boundary) ///如果左分区比右分区短{if( boundary + 1 < right ) //判断右分区是否存在{s.push(boundary + 1);s.push(right);}if( boundary - 1 > left ) //判断左分区是否存在{///将左分区端点后入栈以便优先排序s.push(left);s.push(boundary - 1);}}else ///如果右分区比左分区短{if( boundary - 1 > left ) //判断左分区是否存在{s.push(left);s.push(boundary - 1);}if( boundary + 1 < right ) //判断右分区是否存在{///将右分区端点后入栈以便优先排序s.push(boundary + 1);s.push(right);}}while( !s.isEmpty()) //栈非空且未排好序{//得到某分区的左右边界int r;s.getTop(r);s.pop();int l;s.getTop(l);s.pop();boundary = Part(a, l, r, Flag); //boundary为枢轴元素所在位置if(Flag == 0) ///如果已经有序,结束排序。return ;if(right-left < 3) //如果序列小于等于3个元素{JustSort(a, left, right);return ;}if( boundary - l < r - boundary) ///如果左分区比右分区短{if( boundary + 1 < r ) //判断右分区是否存在{s.push(boundary + 1);s.push(r);}if( boundary - 1 > l ) //判断左分区是否存在{///将左分区端点后入栈以便优先排序s.push(l);s.push(boundary - 1);}}else ///如果右分区比左分区短{if( boundary - 1 > l ) //判断左分区是否存在{s.push(l);s.push(boundary - 1);}if( boundary + 1 < r ) //判断右分区是否存在{///将右分区端点后入栈以便优先排序s.push(boundary + 1);s.push(r);}}}}
}#endif // __ExQUICKSORT_H__