由于最近在帮一个学长整理考研数据结构的真题部分,很有感悟,所以今天终于下定决心写博客了,由于是第一次写如果有误请多多包涵quq,并且能指出来的话感激不尽。
大一第一次看快速排序的时候也是看的一脸懵逼,只是大概懂了怎么回事,现在把它好好整理出来(参考资料结合自己),希望能对像当时的我一样头大的人一点帮助(虽然网上已经有其他各种版本的讲解。。),并且我会尽可能以通俗易懂的语言来讲述。同时对于考研的考点我也会做标注,以供自己和复习的同学使用,如果不需要的请无视它们就好了(*/ω\*)
如果想要学习本排序方法请阅读全文,如果只是想要代码请直接拉到最后方位置。
1.基本原理
快速排序最早是由图灵奖得主Tony.Hoare于1962年提出的一种划分交换排序,它是从冒泡排序改进而得到的一种“交换”排序方法,通过一趟排序将记录分割成独立的两部分。它采用了一种分治策略。
它的基本思想(以从小到大排序为例)是: ←考点
(1)从数列中取出一个数作为基准数(书上也称它为枢轴);
(2)把数列中比基准数小的数都放在它的左边,比基准数大的数都放在它的右边;
(3)再对被放好的左右两堆数重复步骤(2)进行划分,直到各区间只有一个数,此时整个数列有序。
我们把将待排序的一堆数划分为两堆数的过程成为一次划分或一趟快速排序。 ←考点
2.基本操作
一趟快速排序的基本操作是:(为了方便阅读和理解我划分了步骤)
1.假如待排数列为(,,…,,),在其中任选一个数Ri作为基准数(取题目中要求排序的位置的第一个,比如排第2-5个数,则取i=2),将它的值保存为临时变量val(以防后面比较的时候丢失)。
2.然后凡是比基准值小的都移动到它的前面,凡是比它大的都移动到它的后面,那么在一趟排序后,数列就被分成了两部分:R[s…i-1]和R[i+1…t]。既然要分成两部分那我们自然要有能比较的标准,因此设两个指针low和high来指定要比较的数,采取左右开弓的策略:low指向数列的第一个位置,high指向数列的最后一个位置。
3.首先将high指向的值(假设为R[high].key)与基准数val进行比较,如果R[high]≥val,那么high减一(即向左移动一位),否则将high指向的数移动到low指针指向的位置(作用是把比val小的移动到它的左边);一直到了high指针指向的数移动在low指针后,再看low指针,如果R[low].key≤val,则low加一(即向右移动一位)。
4.重复上述两个方向的大小比较,直到这两个指针指向的位置重合。
3.过程演示
如果你对上面的文字描述还有疑问,那么可以看看下面的画图演示XDD,以下为排序全部数列。则基准值为r[0]。
一趟快速排序:
接下来再对分好的左右两堆数进行排序,整个过程可通过递归过程进行。若待排的数列中只有一个数字了,则说明它已经有序,不再需要排序;否则对该数列进行一次递归快速排序。
快速排序总过程:
4.代码实现
一趟快速排序算法如下:
1int FindPos(int *a,int low,int high)
2{
3 int val=a[low];//把low指针指向的数存入val中
4 while(low<high)
5 {
6 //两个指针还未重叠且high指向的数字比基准值大
7 while(low<high&&a[high]>=val)
8 {--high;}//high指向减一
9 a[low]=a[high];//否则high指向的值移动到low指向的位置处
10 //当high指向的数移动到low处后再进行low指针的判断
11 while(low<high&&a[low]<=val)
12 {++low;}
13 a[high]=a[low];
14 }//终止while循环后low和high值相等
15 a[low]=val;//相当于把val的值填入空缺地方
16 return low;//high也行
17}
快速排序的函数如下:
1void QuickSort(int *R,int low,int high)
2{
3 int pos;//pos为基准值的位置
4 if(low<high)//数组长度大于一
5 {
6 pos=FindPos(R,low,high);//对待排数列进行一次划分,并返回基准值的位置
7 QuickSort(R,low,pos-1);//对基准值左边的数列进行递归排序
8 QuickSort(R,pos+1,high);//对基准值右边的数列进行递归排序
9 }
10}
完整代码实现快速排序:
1#include<stdio.h>
2void QuickSort(int *,int,int);
3int FindPos(int *,int,int);
4int main(void)
5{
6 //设定为为已知长度数列排序
7 int r[8]={49,38,65,48,74,13,27,52};
8 int i;
9 QuickSort(r,0,7);//第二个参数表示要排序位置的第一个元素的下标,第三个参数表示最后一个元素的下标,可以按照题目要求修改
10 for(i=0;i<8;i++)
11 printf("%d ",r[i]);
12 return 0;
13}
14
15void QuickSort(int *r,int low,int high)
16{
17 int pos;
18 if(low<high)//数组长度大于一
19 {
20 pos=FindPos(a,low,high);
21 QuickSort(a,low,pos-1);
22 QuickSort(a,pos+1,high);
23 }
24}
25
26int FindPos(int *r,int low,int high)
27{
28 int val=r[low];
29 while(low<high)
30 {
31 while(low<high&&r[high]>=val)
32 --high;
33 r[low]=r[high];
34
35 while(low<high&&r[low]<=val)
36 ++low;
37 r[high]=r[low];
38 }//终止while循环后low和high值相等
39 r[low]=val;
40 return low;//high也行
41}
运行结果:
5.时间、空间复杂度分析
快速排序的一次划分从两头交替搜索,直到low和high重合,时间复杂度是O(n),整个算法的时间复杂度与划分的趟数有关,也就是说,快速排序的时间性能取决于快速排序递归的深度。如下图所示,可以用递归树来描述递归过程的执行情况(一趟快速排序)。其中根就是以“49”为基准值的划分,左子树为所有值比它小的,右子树为所有比它大的。此划分过程比较均匀(一边三一边四),递归树是平衡的,此时性能比较好。
在最优情况下,每次划分都比较均匀,如果排序n个数字,其递归树的深度为lbn(lb是以2为底的对数),即仅需要递归lbn次。这样整个算法的时间复杂度为O(nlbn)。 ←考点
最坏情况是,每次所选的基准值都是当前序列中最大或者最小的元素,这使得每次划分所得的两堆数中有一堆是空的,另一堆为原来的长度减一。这样就需要n趟划分,整个算法的时间复杂度为O()。 ←考点
快速排序的平均时间复杂度也是O(nlbn),因此,该排序方法被认为是目前最好的一种内部排序。
从空间复杂度来看,尽管快排需要一个元素(val)的辅助空间,由于它是递归的,每层递归调用时的指针和参数均要用栈来存放,存储开销在理想的情况下为O(lbn);在最坏的情况下为O(n)。
另外,在快速排序过程中,如果碰到有相同的元素,那么他们的相对位置是会发生改变的(可以自己试试看),因此,快速排序是不稳定的排序方法。 ←考点
最后,基准值的选取是影响算法性能的关键,输入数据的次序随机性越好,排序的性能也就越好。
第一次写博客还真的有好多不足。。前前后后弄了快6个小时。。没有写过是一方面,但更重要的没有很好的掌握它,导致写的时候思路断断续续的,浪费了很多的时间。以后一定要多花时间写博客,整理知识点什么的。(*・ω< )