1.什么是快速排序?
在之前我们已经见识到了一些基础的排序算法,比如冒泡排序,选择排序,插入排序,堆排序,希尔排序了,对于以上的几种排序我们知道希尔排序和堆排序的时间复杂度更小一些,他们的算法优先性对于其他算法还是有很大的差别,而今天我们会介绍一个排序速度如其名的排序--快速排序。
快速排序由C. A. R. Hoare在1962年提出,它是一种基于二叉树结构的交换排序算法,它采用了一种分治的策略,通常称其为分治法。该方法的基本思想是:先从数列中取出一个数作为基准数,然后将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边,再对左右区间重复第二步,直到各区间只有一个数。快速排序是一种不稳定的排序方法,但是他的效率非常快,比肩希尔和堆排序也略胜一筹。
2.快速排序的思想
核心思想:
1.在待排序的元素任取一个元素作为基准(通常选第一个元素,称为基准元素)
2.将待排序的元素进行分块,比基准元素大的元素移动到基准元素的右侧,比基准元素小的移动到作左侧,从而一趟排序过程,就可以锁定基准元素的最终位置
3.对左右两个分块重复以上步骤直到所有元素都是有序的(递归实现)。
可以看到每次排序后都会实现key的左边比key小,右边比key大,这也相当于确定了一个数的位置,之后在递归进行排序。
要想了解整个排序过程的实现,首先我们必须明白单趟排序的实现:
这里注意:左边小人开始出发时是从key处开始的,如果右边停止左边出发时,左边找不到比key大的直接相遇了,也是直接交换。 这里先出发的顺序可以是左,也可以是右边。
左边为key,右边先走,相遇值比key小
右边为key,左边先走,相遇值比key大
之后就是调用递归其他区间:
这里为什莫相遇的值一定比key小,仔细发现我们可以看到最重要的是key的位置是随左右两边的值的顺序有关,相遇的位置才是key,而再相遇之前,右边一直在再找比key小的,比key大的都跳过了,遇到小的才停下来,这就注定相遇位置一定比key小。
算法实现:
int partsort(int *a,int left,int right) { //定义key值,默认是从最左边,右边也行 int key = left; while (left < right) { //先从右边开始找小于key的 while(left < right&&a[right]>=a[key]) { right--; } //小于的时候在左边开始找大于key的 while (a[left] <= a[key]) { left--; } //不在两个循环内说明可以交换了 swap(&a[left], &a[right]); } //推出循环相等时也要交换 swap(&a[key],&a[right]); return key;//返回key的位置以便区间递归排序 }
之后便是分治思想,子问题的实现,利用递归实现对key的两边区间的的进行排序。
3.快速排序的实现
快速排序的实现方法从最早hoare最早研究出的一种到现在已经有三种实现方法了,这三种方法他们的实现的原理基本一致,但是在某些算法上提出了改良
一,Hoare法(左右指针法)
霍尔大佬自己编写的第一个快速排序的算法,也是最经典的。
void swap(int* a, int* b) { int tmp = *a; *a = *b; *b = tmp; } //实现从left->ringht区间的排序 int partsort(int *a,int left,int right) { //定义key值,默认是从最左边,右边也行 int key = left; while (left < right) { //先从右边开始找小于key的 while(left < right&&a[right]>=a[key]) { right--; } //小于的时候在左边开始找大于key的 while (a[left] <= a[key]) { left--; } //不在两个循环内说明可以交换了 swap(&a[left], &a[right]); } //推出循环相等时也要交换 swap(&a[key],&a[right]); return key;//返回key的位置以便区间递归排序 } void quicksort(int* a,int begin,int end) { if (begin >= end) { return; } int keyi = partsort(a, begin, end); //排一次,分为三个区间 [begin,key-1] key [key+1,end] quicksort(a, begin, keyi-1); quicksort( a, keyi+1, end); }
但是要注意的是在实现一趟排序时的算法,也就时partsort算法的部分,是有两个坑的:
一,为了防止左边与右边与key相同时就停止,之后两个交换后,right不再--,left不再++,使得死循环,这里再判断与key的大小关系时,右边比key大于等于的就继续前进,左边小于等于继续前进。
二,为了防止在循环体中的两个循环中对数组的正常访问,需要加上left<right这一条件,防止因为key本身太小比如等于0,使得right减到越界访问。
二,挖坑法
无论是挖坑法。还是下面的前后指针法,其他方法都是对一趟排序的算法的改善,也就是对partsort这一部分改变,但之后递归排序的思想是一样的。
该方法的思路就如同它的名字一样,将key处的值拿走留下一个空,但实际上我们只是这样想象,对于数组,值是还在的,然开始左边比key小的填到新坑,之后右边比key大的天道新坑,之后再从左边开始,知道相遇,此时该坑的位置就是key的位置。
代码实现:
int partsort2(int* a, int left, int right) { int key = a[left]; int Hole = left; while (left < right) { //找小的 while (left < right&&a[right] >= key) { right--; } //小的就换到坑的位置,此时该座标为新坑 a[Hole] = a[right]; Hole = right; //找大的 while (left < right&&a[left] <= key) { left++; } //大的就换到坑的位置,此时该座标为新坑 a[Hole] = a[left]; Hole = left; } //相遇 a[Hole] = key; }
三,前后指针法
顾名思义,给了两个可以想象成指针的标识,其中cur用来探路找小,cur位置的从自身开始,遇到比key小的此时,prev++,即prev出发一步,之后交换prev与cur的位置的值,cur继续前进。
可以看到:
1.最开始cur与prev是相邻的、
2.当遇到大key的时候,cur继续走,两者之间的数都是比key大的,当cur开始出发遇到比key小的值时,会和++prev交换位置,此时大的数就被往右边推,而小于的都被换到左边(我们可以看到cur与prev之间都是比key大的,而之后cur一遇到小的,prev此时指向大的,两者换位。实现左小右大)。
3.当cur走完时,此时prev的位置就是key的位置。
代码实现:
int partsort3(int* a, int left, int right) { int key = left; int prev = left; int cur = left + 1; while (cur <= right) { //cur与prev相等时也要交换 if (a[cur] <a[key]&& ++prev!=cur) { swap(&a[prev], &a[cur]); } cur++; } swap(&a[prev], &a[key]); key = prev; return key; }
这三种方法中,后两种方法都是对与一次排序方法想出的新思路,他们的时间复杂度是一样的,效率是一样的,不同点在于他们如何将左边排小于key的,右边排大于key的,且时间复杂度都为O(N),之后整个排序化简为他的子问题进行递归即可。
4.快速排序的非递归实现
在完成上述的快速排序我们知道在排序中因为是对区间的子区间进行单趟排序,循环直到区间是一个数,我么们是利用递归来处理,可是这也引发了一个问题:数据足够大,递归的深度太深,就会出现栈溢出的问题,此时排序就崩了,因此递归有它的好处,也存在他的隐患。
了解的排序算法的本质,我们知道它是将区间一次次拆分,对每个区间进行单趟排序,直到区间是00 ,11,这样表示一个数就完成。不用递归实现的话,还有一种方法,利用栈先进后出的特点实现区间的处理。
我们先将左右端的下标入栈,代表该区间,之后在栈不为空的情况下,保留栈顶元素,在出栈,将两个代表区间的数表示出来后,进行一趟(part sort)排序,返回key值,分割出左右区间,在入栈,分别将左右区间入栈,开始循环,栈为空时,即完成排序。
int partsort1(int* a, int left, int right) { //定义key值,默认是从最左边,右边也行 int key = left; while (left < right) { //先从右边开始找小于key的 while (left < right && a[right] >= a[key]) { right--; } //小于的时候在左边开始找大于key的 while (left < right && a[left] <= a[key]) { left++; } //不在两个循环内说明可以交换了 swap(&a[left], &a[right]); } //推出循环相等时也要交换 swap(&a[key], &a[right]); return left;//返回key的位置以便区间递归排序 } //初始栈,给五个元素空间 void Stackinit(ST* p) { (p)->a = (DATAtype*)malloc(sizeof(DATAtype) * 5); (p)->capacity = 5; (p)->top = 0;//记录栈中数组位置 } //销毁栈 void Stackdetroy(ST* p) { free(p->a); (p)->a = NULL; (p)->capacity = 0; (p)->top = 0; } //入栈 void chckcapacity(ST* p) { //若容量不够,则扩容 if ((p)->top == (p)->capacity) { DATAtype*tmp= (DATAtype*)realloc((p)->a,sizeof(DATAtype) * (p)->capacity * 2); if (tmp == NULL) { printf("realloc fail"); return; } (p)->a = tmp; (p)->capacity = (p)->capacity * 2; printf("容量不够,已自动扩容,当前容量:%d\n", (p)->capacity); } } //入栈 void Stackpush(ST* p, DATAtype x) { //检查容量 chckcapacity(p); p->a[(p)->top] = x; p->top++; } //出栈 void Stackpop(ST*p) { p->top--; //assert((p)->top); assert(p); //检查是否过量出栈 } //返回大小 int Stacksize(ST* p) { assert(p); return (p)->top; } //判空 bool Stackempety(ST* p) { assert(p); if ((p)->top == 0) { return true; } else { return false; } } int Stacktop(ST* p) { assert(p); return p->a[p->top-1]; } void stackprintf(ST* p) { assert(p); while ((p)->top>0) { printf("%d->", (p)->a[(p)->top-1]); (p)->top--; } } bool isValid(char* a) { ST p; Stackinit(&p); int size = strlen(a); for (int i = 0; i < size; i++) { if (a[i] == '(' || a[i] == '[' || a[i] == '{') { Stackpush(&p, a[i]); } if (a[i] == ')' || a[i] == '}' || a[i] == ']') { if ((Stacktop(&p)=='('&&a[i]==')')|| (Stacktop(&p) == '{' && a[i] == '}')|| (Stacktop(&p) == '[' && a[i] == ']')) { Stackpop(&p); } } } printf("%d", Stacksize(&p)); if (Stacksize(&p) == 0) { return true; } return false; }
5.面对oj的优化
虽然上属于述实现了快速排序,但对于不稳定的排序算法,还是有极端情况。
例如oj题,说给出了专门针对快速排序的测试用例,如所有数相同,或者有多个数相同为key的情况,就会大大增加比较的次数,是的时间增加。
所以对于一般快速排序,首先对于key的选择,我们不选最左边,而是选择随机的中间值,左边值,右边值的那个中间值,也叫三数取中。
其次为了面对多个数相同为key的情况,我们还利用三路划分--即将所给的数组先进行划分,将所有与key值相同的集中到中间,所有比key小的放在数组左边,比key大的放在右边变。
利用三路划分再返回left和right的值,此时排序就只排序左边和右边的即可。
void swap(int* a, int* b) { int tmp = *a; *a = *b; *b = tmp; } int Getmidle(int *a, int left, int right) { int mid = left + (rand() % (right - left));//随机取中 if (a[left] < a[mid]) { if (a[mid] < a[right]) { return mid; } else if (a[left] < a[right]) { return right; } else { return left; } }else { if (a[mid] > a[right]) { return mid; } else if (a[left] > a[right]) { return right; } else { return left; } } } void quicksort(int* a,int begin,int end) { if (begin >= end) { return; } int left = begin; int right = end; int cur = left+1; int mid = Getmidle(a, left, right); swap(&a[left], &a[mid]); int key = a[left]; //三路划分 <key =key >key while (cur <= right) { if (a[cur] < key) { swap(&a[cur], &a[left]); left++; cur++; } else if(a[cur] >key) { swap(&a[cur], &a[right]); right--; } else { ++cur; } } quicksort(a, begin, left-1); quicksort(a, right+1, end); }
优良化后的快速排序对于多呈复制也没有问题,我们给出随机出种子,排序,给出大小,返回数组,即可通过。