思想:
选取序列中的一个数作为基准值,按照排序将序列分为两个子序列,左子序列的值都小于基准值,右子序列的值的大于基准值,再对左右子序列做相同操作,直到数据有序为止。
一. Hoare法:
怎么找到基准值使得左边全小于基准右边全大于基准呢?
就是让每个序列的第一个数为基准然后,定义right为该序列最右边,left为该序列最左边,从right开始找比基准小的数,找到后再开始从left开始找比基准大的数,都找到后交换他们,直到right与left相遇再将基准值放到相遇位置。(为什么不能从左边开始找呢?因为如果从左边开始找那么当left与right相遇的时候他们的值是大于key的那么就把大的值放到了基准的左边,所以不能从左边开始找)
按照这个思想那么我们接下来到排完序是这样的:
从这里我们就可以看出Hoare法的快速排序类似于树,都是左边结束之后返回去做右边的递归思路,重点就在于这个基准的确定每次都在变化,那么我们就先来写确定基准的代码:
public int partition(int[] array,int start,int end){
int i = start;//事先存储好start的下标
int key = array[start];//存基准值
while(start<end){
//第一个while循环里的start<end是如果start=end说明该序列就一个元素了那么一定是有序的
//下面while循环里的start<end是因为如果end前的元素都大于key的话若没有这个条件end会一直--到-1那就越界了
//为什么end对应的元素要>=key而不是>key呢?若是>key那么如果key的值与end对应元素的值相等那么程序会进入死循环
while(start<end&&array[end]>=key){
end--;
}
while(start<end&&array[start]<=key){
start++;
}
swap(array,start,end);
}
swap(array,i,start);
return start;
}
(代码用start和end代表图示中的left和right)
这个代码就为我们找到了每一次的基准,但是其中有很多细节需要去理解。
最后我们写出Hoare法的快速排序:
public void quickSort(int[] array,int left,int right){
while(left>=right){
return;
}
int poive = partition(array,left,right);
quickSort(array,left,poive-1);
quickSort(array,poive+1,right);
}
二.挖坑法:
挖坑法思想很简单,就是先定一个坑位key,然后用right从最后找到比key的值小的值放到key所在的位置,这个时候这个找到比key的值小的位置就成为了新的坑位,再用left从左边开始找比key大的值,放到坑位中,直到right与left相遇后将一开始的key值放到相遇位置,这个时候就会发现左边全部是比key小的元素,右边就是比key大的元素,重复此步骤直到序列有序。
其实挖坑法的代码与hoare法没有本质上的区别,就是找元素过程中数值变换有一点不同,所以挖坑法与hoare法的代码只有找基准代码的区别我们先把一样的代码写下来:
public void quickSort(int[] array,int left,int right){
while(left>=right){
return;
}
int poive = partition(array,left,right);
quickSort(array,left,poive-1);
quickSort(array,poive+1,right);
}
再通过上图和描述写partition代码:
public int partition(int[] array,int start,int end){
int key = array[start];
while(start<end){
while(start<end && array[end]>=key){
end--;
}
array[start] = array[end];
while(start<end && array[start]<=key){
start++;
}
array[end] = array[start];
}
array[start] = key;
return start;
}
三.双指针法
双指针法就是定义两个指针,快慢指针,给慢的指针加限定条件,只有满足才能往后走,以达到我们的目的。
public int partition(int[] array,int start,int end){
int prev = start;
int cur = start+1;
while(cur<=end){
if(array[cur]<array[start] && array[++prev]!=array[cur]){
swap(array,cur,prev);
}
cur++;
}
swap(array,prev,start);
return prev;
}
看这段代码,我们定义了cur为start+1,prev为start这两个指针,cur固定每次都会向前走一步,但是prev只有但array[cur]<array[start]时才会向前进,两个指针一直向前走直到满足array[cur]<array[start]并且prev与cur不重叠的时候才进行交换,如图所示:
当走到这一步的时候是进入判断array[cur]<array[start]不满足不执行&&后面的代码这个时候prev就没有向前走,cur向前走一步
再下一步 array[cur]<array[start]满足并且prev与cur不重叠故将prev与cur的值交换
接着直到结束再将start的值与prev的值交换
最后返回我们的基准prev,也满足了左边比基准小右边比基准大。
快速排序的时间复杂度:O(n*logn) (理想情况下—>每次基准都在中间平均分配) O(n^2)->顺序或逆序
空间复杂度:最好->O(logn) 最坏->O(n)
稳定性:不稳定
我们可以想想这三个方法的快速排序,都是用递归的思想去完成的,如果数据量大的话,极有可能会栈溢出,为了能解决这个问题我们将对快速排序进行优化。
我们可以发现若是对于一个有序的序列若我们的基准一开始都为第一个数的话,就会造成最大时间复杂度,最大空间复杂度的情况,因为这样递归的次数是最多的,我们理想的情况是将越靠近中间值的数作为基准越好。
对选取基准做优化:
三数取中法:
public int midNumIndex(int[] array,int left,int right){
int mid = (left+right)/2;
if(array[left]>array[right]){
if(array[mid]<array[right]){
return right;
}else if(array[mid]>array[left]){
return left;
}else{
return mid;
}
}else{
if(array[mid]>array[right]){
return right;
}else if(array[mid]<array[left]){
return left;
}else{
return mid;
}
}
}
public void quickSort(int[] array,int left,int right){
while(left>=right){
return;
}
int index = midNumIndex(array,left,right);
swap(array,left,index);
int poive = partition(array,left,right);
quickSort(array,left,poive-1);
quickSort(array,poive+1,right);
}
感受以下三数比较取中间的代码,在quickSort里将本来作为基准的left与三数取中之后的index进行交换,再开始快速排序,这样可以避免极端数据导致的使用大空间。