快速排序的中心思想是将小的数放在左边,大的数放在右边,那什么算小,什么算大呢?
我们可以在数列中找到一个数来作为评判其他数的大小,大于这个数就放在这个数右边,小于这个数就放在这个数的左边,我们把这个数叫主元。
我们可以把第一个数当成主元,然后分别从数列的头和尾判断每个数的大小,如果左边的数小于主元,那我们继续往后寻找,同理,如果右边的数大于主元,我们继续往前寻找。如果发现左边的指针跟右边的指针都停止了寻找,那我们就将目前左边指针指向的数跟右边指针指向的数交换。直到左边的数都小于主元,右边的数都大于主元,此时左边的指针跟右边的指针指向的位置相同了,我们就把主元放进这个位置,那么一次大概的检索就做完了。
具体代码如下:
int Partition(int A[],int left,int right){ //left跟right的初始值为序列首尾下标
int temp = A[left]; //temp是主元
while(left<right){
while(left<right && A[right] > temp) right--;
A[left] = A[right];
while(left<right && A[left] < temp) left++;
A[left] = A[right];
}
A[left] = temp;
return left;
}
当然,这样的结果肯定还是杂乱无章的,上述操作还需要反复进行,所以我们使用递归算法,反复调用上述操作,直到最后的结果是我们想要的有序数列。
void quickSort(int A[],int left,int right){
if(left<right){
//将[left,right]按主元位置一分为二
int pos = Paetition(A,left,right);
quickSort(A,left,pos-1);
quickSort(A,pos+1,right);
}
}
使用上述的两个函数就可以实现对数列的简单快速排序了,但是我们还可以提高该算法的效率。
对于上述算法,我们在面对序列中元素的排列比较随机时效率最高,但是如果序列本身接近有序时,该算法会到达最坏的时间复杂度O(n^2),产生这种糟糕的情况的主要原因在于主元没有把当前区间划分为两个长度接近的子区间。
解决该问题的办法我目前知道两种:
第一种是随机选择主元,我们不总是把第一个数作为主元来使用Partition函数,而是随机选择数列中的一个数作为主元,这样做的好处就是对任意输入数据的期望时间复杂度都能达到O(nlogn),也就是说,不存在一组特定的数据能使这个算法出现最坏情况。
如果我们使用这一种方法,我们首先需要如何去产生一个随机数,当然我们此处的随机数是数组的下标,那么对于一个n个数的数列,我们需要产生的随机数就是[0,n-1]之间。如果用c语言的话,我们需要添加一些头文件,再在主函数里面使用srand((unsigned)time(NULL)),这个语句将生成随机数的种子。然后在需要随机数的地方使用rand()函数即可。我们在这个算法中需要实现的目标是产生[0,n-1]之间的随机数,我们就可以采用rand()%(b-a+1)+a来产生[a,b]中的随机数。下面的示例中就产生了10个[0,9]之间的随机数。
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main(){
srand((unsigned)time(NULL));
for(int i = 0;i < 10; i++){
printf("%d",rand()%10+0);
}
return 0;
}
那么我们现在把随机产生主元跟上面的快速排序算法结合起来使用,便得到了这样一个新的Partition函数:
int randPartition(int A[],int left,int right){
//生成[left.right]中的随机数p
int p = rand()%(right-left+1)+left;
swap(A[p],A[left]);
//下述为Partition()中的操作
int temp = A[left]; //temp是主元
while(left<right){
while(left<right && A[right] > temp) right--;
A[left] = A[right];
while(left<right && A[left] < temp) left++;
A[left] = A[right];
}
A[left] = temp;
return left
}
现在我们来看第二种方法:二分思想加上快速排序
我们每次可以将数列中间的那个数作为主元,就不需要随机选择主元了,在每次递归时选用二分思想来实现代码,具体的代码如下:
void quickSort(int a, int b) {
int mid = arr[(b - a) / 2 + a];
int left=a, right=b;
while (left <= right){
while (arr[right] > mid) right--;
while (arr[left] < mid) left++;
if (left <= right) { //这一句在不同的编译器上效果不同,在洛谷上没有这一句,排序结果是错误的,但是在vs里面是不影响的
swap(arr[left], arr[right]);
left++, right--; //没有这一句在遇到重复元素时程序会陷入死循环
}
}
//此时left>right;
if (right > a) quickSort(a, right); //递归搜索左半部分
if (left < b) quickSort(left,b); //递归搜索右半部分
}
在使用不同的方法去完善算法时,需要注意双指针的大小关系,什么时候需要相等,什么时候left更大一些,都是需要反复斟酌调试的。上面的部分代码跟思路是来自参考书籍跟刷题中看别人的题解了解到的,用我的理解将他们整理在这里。如果有错误的地方也请在评论区告诉我,谢谢大家能看到这里。