总体思想
先找到一个枢轴,让他作为分水岭。通过一趟排序将待排序的记录分割成独立的两部分,前面一部分都比枢轴小,后面一部分逗比枢轴大,然后又分别对这两部分记录继续进行递归的排序,达到整个序列有序的目的。
核心分离算法
如何根据枢轴将无序序列分成两个独立的部分是快速排序的关键,具体做法是:
- 采用两个辅助变量,一个指向待排序序列的第一个元素(low),另一个指向最后一个元素(high),这里的low和high都是待排序序列的下标或者称之为元素在序列中的位置;
- 然后先从high开始,比较后面的元素和枢轴的大小,如果比枢轴大,则不移动元素,将high–,使得high往前移动;直到high指向的元素比枢轴小,high停止往前移动,交换枢轴和high指向元素的位置;
- 从low开始,比较前面的元素和枢轴的大小,如果比枢轴小,则不移动元素,将low++,使得low往后移动;直到low指向的元素比枢轴大,low停止往前移动,交换枢轴和low指向元素的位置;
- 进行下一次循环,又从high开始进行比较,直到low不小于high为止,退出循环,此时的序列则按照枢轴一分为二,达到我们的目的。
传统的快速排序代码
template <typename T>
void SqList<T>::QSOrt(int low, int high)
{
int pivot_Position;
if (low < high)
{
pivot_Position = Partition(low,high);
QSOrt(low, pivot_Position - 1);
QSOrt(pivot_Position + 1, high);
}
}
template <typename T>
int SqList<T>::Partition(int low, int high)
{
T pivot_Var = r[low];
while (low < high)
{
while (r[high] >= pivot_Var && low < high )
{
high--;
}
swapElement(low, high);
while (r[low] <= pivot_Var && low < high)
{
low++;
}
swapElement(low, high);
}
return low;
}
枢轴的选取
一般采取“三个元素取中间”的做法,即从要排序的序列中,取出左、中、右三个元素,然后选取他们三个中的中间值作为比较的枢轴。一般来说,传统的快速排序是将中间值和low交换,使得在开始排序之前low位置处存放的就是枢轴。方便后面的比较和交换操作。但这里需要改进!
避免不必要的交换
上面提到了枢轴如果一开始存放到low处,后面再进行和序列中其他元素交换需要改进,毕竟交换操作太频繁是很低效的。所以能避免交换当然要避免,根据快速排序的思想,在发现后面的元素比枢轴小或者前面的元素比枢轴大的时候,需要交换枢轴和相应元素,其实这里只需要进行覆盖就可以,分析如下:
- 从high开始往前走,和枢轴比较的时候,low位置存放的是枢轴(一开始枢轴就放在low处),如果遇到比枢轴小的元素,进行交换,也就是low处存放原来high处的元素,而high处存放low处的元素;
- 从low开始往后走,和枢轴比较的时候,high位置存放的是枢轴(经过前面的操作,high存放的就是枢轴),如果遇到比枢轴大的元素,进行交换,也就是low处存放原来high处的元素,而high处存放low处的元素;
- 这样看来,其他元素实际上在一趟排序过程中只交换了一次,是合理的,但是枢轴这个元素则交换了很多次,但最终枢轴应该只存放在它应该处于的位置,而这个位置就是最后循环结束以后,low代表的位置。因为经过high–和low++以后,从两边向中间靠拢的过程中,就能确定选取的枢轴应该在这个序列中的位置。这也正是快速排序算法精髓的所在。所以可以在需要交换枢轴和其他元素的时候,直接将枢轴原来所处位置的元素覆盖,赋值成需要交换的非枢轴元素,这样序列中会有两个一样的元素,但是枢轴不见了,所以在循环开始之前,需要利用一个辅助变量将枢轴缓存起来,直到其他元素排序完毕(循环结束)以后,再将缓存的枢轴存放到low指向的位置,即可完成排序。
- 对于其他元素而言,由于在第一次覆盖的时候,就是覆盖枢轴,以后每一次覆盖都是覆盖传统的交换方法中枢轴应该所在的位置,所以不会发生其它元素被覆盖丢失的情况,在整个过程中,会有两个相同的元素(其中一个就在原来枢轴的位置,是另一个的拷贝)存在,随着high–和low++这个相同的元素是会变化的。
对于小规模的序列
快速排序涉及到了递归操作,在小规模的序列排序的时候,性能还不如简单的直接插入排序,所以在一定规模范围内,采用直接插入排序,当大规模序列的时候,递归带来的性能牺牲可以忽略不计。所以需要设定一个阈值,当大于阈值的时候采用快速排序,当小于阈值的时候需要使用直接插入排序,避免杀鸡用牛刀。
对于递归的考虑
如果待排序的序列划分极为不平衡,根据传统的排序算法,递归的深度可能达到n而不是 logn ,所以如果能减少递归对于空间和时间的节约是非常重要的。在递归处理独立的两部分子序列的时候,我们可以将其改为如下代码:
template <typename T>
void SqList<T>::QSOrt(int low, int high)
{
int pivot_Position;
while (low < high)
{
pivot_Position = Partition(low,high);
QSOrt(low, pivot_Position - 1);
//QSOrt(pivot_Position + 1, high);
low = pivot_Position + 1;
}
}
将if改成while循环之后,可以减少一半的递归操作。因为在原来第一次递归以后,变量low就变得毫无用处了,所以将pivot_Position+1赋值给low,再循环后,再来一次Partition(low,high)其效果和QSort(pivot_Position+1,high)是一样的,但采用的是迭代/循环,不是递归,所以可以缩减堆栈深度,从而提高了整体性能。
改进后的快速排序代码
#include <iostream>
using namespace std;
#define MAXSIZE 100
typedef int Status;
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
template <typename T>
class SqList
{
public:
T r[MAXSIZE+1];
int length;
public:
SqList(T * var,int len);
SqList();
void printList();
Status swapElement(int i, int j);
void InsertSort();
void QuickSort();
int Partition(int low,int high);
void QSOrt(int low,int high);
};
template <typename T>
SqList<T>::SqList(T * var,int len)
{
length = len/sizeof(T);
memcpy(&(r[1]), var, len);
}
template <typename T>
SqList<T>::SqList()
{
memset(this, 0, sizeof(SqList));
}
template <typename T>
Status SqList<T>::swapElement(int i,int j)
{
T tmp =this->r[i];
this->r[i] = this->r[j];
this->r[j] = tmp;
return OK;
}
template <typename T>
void SqList<T>::printList()
{
for (int i = 1; i <= length; i++)
{
cout << this->r[i] << "\t";
}
cout << endl;
}
/************************************************************************/
/* 直接插入排序 */
/************************************************************************/
template <typename T>
void SqList<T>::InsertSort()
{
int i = 0, j = 0;
for (i = 2; i <= length; i++)//外层循环表示从无序表中依次将其元素插入到有序表中
//默认第一个元素是有序表,所以从第二个元素开始直到第n个元素结束,
//这些元素都是无序表中的元素,需要依次把它们插入到前面的有序表中
//一次外层循环表示处理好无序表中的一个元素
{
if (r[i] < r[i - 1])//第i个元素是无序表中的第一个元素
//第i-1个元素是有序表中的最后一个元素,也是有序表中最大的元素
//如果无序表中的第一个元素(当前元素)比有序表中的最大元素小
//表示需要将当前元素往前插,至于插到什么位置,由内层循环决定
{
r[0] = r[i];//利用哨兵将当前元素的值缓存下来
for (j = i - 1; r[j] > r[0]; j--)//内层循环将比当前元素的值(哨兵的值)大的元素后移挪出空位
{
r[j + 1] = r[j];//移动比哨兵大的元素
}
r[j + 1] = r[0];//将哨兵插入空位,该空位以前的元素要么为空要么逗比他小,否则不会结束循环
}
}//遍历到最后一个元素的时候排序完毕
}
/************************************************************************/
/*快速排序 */
/************************************************************************/
/*保持和其他排序算法接口一致*/
template <typename T>
void SqList<T>::QuickSort()
{
QSOrt(1,length);
}
#define MAXCNT 7
/*核心的快排算法*/
template <typename T>
void SqList<T>::QSOrt(int low, int high)
{
int pivot_Position;//记录枢轴的位置
if (high - low > MAXCNT)//避免杀鸡用牛刀,整个序列大于8(MAXCNt+1)个用快排
{
while (low < high)//减少递归的深度
{
pivot_Position = Partition(low, high);//获取枢轴的位置
QSOrt(low, pivot_Position - 1);//递归处理独立的两部分
//第一次循环的时候处理前半部分
//第二次处理后半部分
//QSOrt(pivot_Position + 1, high);
low = pivot_Position + 1;//更新待处理序列的起始位置
}
}
else{//小于8个元素用直接插入排序
InsertSort();
}
}
/*交换无序表中子表的元素,使得枢轴移动到该到的位置,并返回其位置
此时前半部分全部比他小
后半部分全部比他大*/
template <typename T>
int SqList<T>::Partition(int low, int high)
{
T pivot_Var;//记录枢轴的值
int m = (low + high) / 2;//找到中间元素的下标
/*三个if语句实现取出左、中、右三个元素的中间值并存放到r[low]*/
if (r[low] > r[high])//保证r[low]比r[high]小
swapElement(low, high);
if (r[m] > r[high])//保证r[m]比r[high]小,至此r[high]肯定是三个元素中最大的
swapElement(m, high);
if (r[m] > r[low])//如果r[m]比r[low]大,说明三个元素的中间值是r[m],进行交换操作
swapElement(low, m);
pivot_Var = r[low];//得到比较用的枢轴
r[0] = pivot_Var;//缓存枢轴到哨兵
while (low < high)
{
/*从high开始往前进行划分得到子序列*/
while (r[high] >= pivot_Var && low < high )
{
high--;
}
//swapElement(low, high);
r[low] = r[high];//避免频繁的交换操作
/*从low开始向后进行划分得到前半部分的子序列*/
while (r[low] <= pivot_Var && low < high)
{
low++;
}
//swapElement(low, high);
r[high] = r[low];//避免交换操作
}
r[low] = r[0];//此时的low就是该趟排序中,枢轴应该位于的位置,恢复枢轴
return low;//返回数轴的位置
}
int main(void)
{
int myList[9] = {90,10,50,80,30,70,40,60,20};
SqList<int> list(myList,sizeof(myList));
cout << "before sort:"<< endl;
list.printList();
list.QuickSort();
cout << "after sort:" << endl;
list.printList();
cout<<"Hello!"<<endl;
system("pause");
return 0;
}