快速排序

对《大话数据结构》P417~P427—快速排序,进行了自己的理解并完善了代码。

一、快排普通版

基本思想:通过一趟排序,将待排记录分割成以枢轴为分界的独立的两部分,一部分的数都比枢轴小,另一部分的数都比枢轴大。这句话也体现了枢轴的作用。再通过递归,对每部分继续排序。

void QSort(SqList *L,int low,int high)//对顺序表L中的子序列L->r[low..high]作快速排序,包括三部分:

1、int Partition(SqList *L,int low,int high)//找到枢轴,并且让枢轴左边的数都小于枢轴,枢轴右边的数都大于枢轴,返回枢轴值的位置

2、QSort(L,low,pivot-1);//递归,对低子表排序

3、QSort(L,pivot+1,high);//递归,对高子表排序

代码和解释如下(VS2012测试通过):

 1 #include <iostream>
 2 using namespace std;
 3 #define MAXSIZE 9//用于要排序数组个数最大值,可根据需要修改
 4 
 5 //排序用的顺序表结构
 6 typedef struct
 7 {
 8     int r[MAXSIZE+1];//定义一个数组,用于存储要排序的数,r[0]作为临时变量
 9     int length;//用于记录顺序表的长度
10 }SqList;
11 
12 //排序表的初始化
13 SqList *InitSqList(SqList *L)
14 {
15     L=new SqList;
16     L->length=MAXSIZE;
17     cout<<"input list"<<endl;
18     for(int i=1;i<=L->length;i++)
19         cin>>L->r[i];
20     return L;
21 }
22 
23 //数组遍历输出
24 void PrintSqList(SqList *L)
25 {
26     for(int i=1;i<=L->length;i++)
27         cout<<L->r[i]<<" ";
28     cout<<endl;
29 }
30 
31 //交换r[i]和r[j]
32 void swap(SqList *L,int i,int j)
33 {
34     int temp=L->r[i];
35     L->r[i]=L->r[j];
36     L->r[j]=temp;
37 }
38 
39 //找到枢轴,并且让枢轴左边的数都小于枢轴,枢轴右边的数都大于枢轴,返回枢轴值的位置
40 int Partition(SqList *L,int low,int high)
41 { 
42     int pivotkey;
43     pivotkey=L->r[low];//用子表的第一个记录作枢轴值
44     while(low<high)//从表的两端交替地向中间扫描
45     { 
46         while(low<high&&L->r[high]>=pivotkey)
47             high--;
48         swap(L,low,high);//将比枢轴值小的数交换到低端
49         //能执行到这一步,说明high位置的值小于枢轴,而low是大于等于枢轴的值,交换之后正好low位置的值小于枢轴,high位置的值大于等于枢轴
50         while(low<high&&L->r[low]<=pivotkey)
51             low++;
52         swap(L,low,high);//将比枢轴值大的数交换到高端
53         //能执行到这一步,说明low位置的值大于枢轴,而high位置的值大于等于枢轴,交换,感觉这一步算法效率不高,50一直在变位置,不知道快排优化版会不会优化这一步
54     }
55     return low;//返回枢轴所在位置
56 }
57 
58 //对顺序表L中的子序列L->r[low..high]作快速排序
59 void QSort(SqList *L,int low,int high)
60 { 
61     int pivot;//枢轴,左边的值都比它小,右边的值都比它大
62     if(low<high)
63     {
64         pivot=Partition(L,low,high);//将L->r[low..high]一分为二,使枢轴左右分别有序,并小于和大于枢轴值,并返回枢轴值pivot
65         PrintSqList(L);cout<<"pivot="<<pivot<<endl;
66         QSort(L,low,pivot-1);//递归,对低子表排序
67         QSort(L,pivot+1,high);//递归,对高子表排序
68     }
69 }
70 
71 //对顺序表L作快速排序
72 void QuickSort(SqList *L)
73 { 
74     QSort(L,1,L->length);
75 }
76 
77 int main()
78 {
79     SqList *p=NULL;
80     p=InitSqList(p);//初始化
81     QuickSort(p);//排序
82     cout<<"after sort"<<endl;
83     PrintSqList(p);//输出
84  }

运行结果:

1、以50为枢轴,50左边都小于50,50右边都大于50。

2、对50左边序列递归。

50左边序列10 20 40 30,再一分为二。

10 20归位。

30 40归位。

3、对50右边序列递归。

50右边序列70 80 60 90,再一分为二。

60 70归位。

80 90归位。

时间复杂度:

二、快排优化版

有4个地方可以优化(代码中有注明):

1、优化第一次选取的枢轴

pivotkey=L->r[low];存在潜在的性能瓶颈。r[1]如果是最大或者最小数,就彻底变成斜树了。比如上一个例子,50是中间数,递归数是平衡的(有点完全二叉树的意思,但不是完全二叉树)。如果排序n个数,递归树的深度就是log2n(向下取整)+1。所以r[1]在整个序列的位置很重要,r[1]不能太大或者太小。书上介绍了三数取中。在左端,中间,右端,三个数中间选中间数,作为第一个枢轴。保证递归树相对平衡,不要变成斜树。

2、优化不必要的交换

这是最难理解的一步优化,把交换改成替换。我在看普通版的快排时候也考虑到这个问题,还在代码中注释了这个疑问,但是当时不知道怎么优化。

high指到小于枢轴的数时,把high位置的数赋值给当前low位置的数。

low指到大于枢轴的数时,把low位置的数赋值给当前high位置的数。

为什么赋值给当前的数,它原来的数不会丢,对第一步,它的数在哨兵。第二步开始,被替换的数,是上一步已经替换给其它位置的数,所以永远不会丢失掉数。

3、快排用到了递归操作,在大量数据排序时,性能好。但如果数组只有几个数,还不如用直接插入排序(简单排序中性能最好)。增加判断,当high-low不大于7,就用直接插入排序。

4、优化递归

递归对性能有一定影响。QSort函数在尾部有两次递归操作。如果待排序的序列划分不平衡,递归深度接近于n(斜树),而不是平衡时的log2n(向下取整)

代码和解释如下(VS2012测试通过):

  1 #include <iostream>
  2 using namespace std;
  3 #define MAXSIZE 9//用于要排序数组个数最大值,可根据需要修改
  4 #define MAX_LENGTH_INSERT_SORT 7//用于快速排序时判断是否选用插入排序阙值
  5 
  6 //排序用的顺序表结构
  7 typedef struct
  8 {
  9     int r[MAXSIZE+1];//定义一个数组,用于存储要排序的数,r[0]作为临时变量
 10     int length;//用于记录顺序表的长度
 11 }SqList;
 12 
 13 //排序表的初始化
 14 SqList *InitSqList(SqList *L)
 15 {
 16     L=new SqList;
 17     L->length=MAXSIZE;
 18     cout<<"input list"<<endl;
 19     for(int i=1;i<=L->length;i++)
 20         cin>>L->r[i];
 21     return L;
 22 }
 23 
 24 //数组遍历输出
 25 void PrintSqList(SqList *L)
 26 {
 27     for(int i=1;i<=L->length;i++)
 28         cout<<L->r[i]<<" ";
 29     cout<<endl;
 30 }
 31 
 32 //交换r[i]和r[j]
 33 void swap(SqList *L,int i,int j)
 34 {
 35     int temp=L->r[i];
 36     L->r[i]=L->r[j];
 37     L->r[j]=temp;
 38 }
 39 
 40 //直接插入排序
 41 void InsertSort(SqList *L)
 42 {
 43     for(int i=2;i<=L->length;i++)
 44     {
 45         if(L->r[i]<L->r[i-1])//如果r[i]比排在它前面的数小
 46         {
 47             int j;
 48             L->r[0]=L->r[i];//把r[i]移动到哨兵位置
 49             for(j=i-1;L->r[j]>L->r[0];j--)//从r[i-1]开始,与哨兵比较,如果比它大
 50                 L->r[j+1]=L->r[j];//把该元素依次右移一位,直至r[j]比哨兵小,结束循环
 51             //因为有j--,结束循环后,r[j]是比r[0]小的
 52             L->r[j+1]=L->r[0];//所以把哨兵插入到r[j+1]
 53             cout<<"after one sort"<<endl;
 54             PrintSqList(L);
 55         }
 56     }
 57 }
 58 
 59 //找到枢轴,并且让枢轴左边的数都小于枢轴,枢轴右边的数都大于枢轴,返回枢轴值的位置
 60 int Partition1(SqList *L,int low,int high)
 61 { 
 62     int pivotkey;
 63     //优化1,三数取中,3个if之后,左端是中间值
 64     int m = low + (high - low) / 2; //计算数组中间的元素的下标
 65     if (L->r[low]>L->r[high])            
 66         swap(L,low,high);//交换左端与右端数据,保证左端较小
 67     if (L->r[m]>L->r[high])
 68         swap(L,high,m);//交换中间与右端数据,保证中间较小
 69     if (L->r[m]>L->r[low])
 70         swap(L,m,low);//交换中间与左端数据,保证左端较大,即左端是中间值
 71     
 72     pivotkey=L->r[low];//用左中右,三个数中的中间值作枢轴记录
 73     //优化2,优化不必要的交换,把交换改为替换,这一步挺神奇的
 74     L->r[0]=pivotkey;//将枢轴关键字备份到L->r[0]
 75     while(low<high) //从表的两端交替地向中间扫描
 76     { 
 77          while(low<high&&L->r[high]>=pivotkey)
 78             high--;
 79          L->r[low]=L->r[high];
 80          while(low<high&&L->r[low]<=pivotkey)
 81             low++;
 82          L->r[high]=L->r[low];
 83     }
 84     L->r[low]=L->r[0];
 85     return low;//返回枢轴所在位置
 86 }
 87 
 88 //优化后的快排
 89 void QSort1(SqList *L,int low,int high)
 90 { 
 91     int pivot;
 92     if((high-low)>MAX_LENGTH_INSERT_SORT)//优化3,当high-low不大于7时,用直接插入排序
 93     {
 94         while(low<high)
 95         {
 96             pivot=Partition1(L,low,high);//将L->r[low..high]一分为二,使枢轴左右分别有序,并小于和大于枢轴值,并返回枢轴值pivot
 97             QSort1(L,low,pivot-1);//递归,对低子表排序
 98             low=pivot+1;//优化4,优化递归操作,
 99             //第一次递归后,变量low就没有用处了,将pivot+1赋值给low,下次循环的Partition1(L,low,high)等价于QSort1(L,pivot+1,high)
100             //因采用迭代而不是递归,可以缩小堆栈深度,提高整理性能
101         }
102     }
103     else
104         InsertSort(L);
105 }
106 
107 //对顺序表L作优化后的快速排序
108 void QuickSort1(SqList *L)
109 { 
110     QSort1(L,1,L->length);
111 }
112 
113 int main()
114 {
115     SqList *p=NULL;
116     p=InitSqList(p);//初始化
117     QuickSort1(p);//排序
118     cout<<"after sort"<<endl;
119     PrintSqList(p);//输出
120  }

运行结果:

只有第一步用了快排,后面调用了直接排序。

而且在选择枢轴的过程中,已经把序列修改成80 70 30 40 10 20 60 50 90。选80作为第一次的枢轴。high移动到第8位,用第8位的50替换第1位的80。low一次性挪动到第8位。把哨兵的值给第8位。

再递归,但是这时候对于50 70 30 40 10 20 60,调用的是直接排序。

posted @ 2016-04-25 20:53  Pearl_zju  阅读(361)  评论(0编辑  收藏  举报