一、原理
- 从区间中取一个数据作为基准值,按照基准值将区间划分为左右两部分,其中左半部分的数据 < 基准值,右半部分的数据>基准值;
- 按照快排的思想排左半部分;
- 按快排的思想排右半部分;
类似于二叉树前序遍历的框架:
public static void quickSort(int[] arr,int left, int right){
if(right-left > 1){
//按基准值对[left,right)区间进行分割
int key = partion2(arr,left,right);
//递归基准值左半侧和右半侧
quickSort(arr,left,key);
quickSort(arr,key+1,right);
}
}
二、如何进行划分?
下面我会讲三种方法来进行划分。
- 交换的方法(每个划分都会用到,所以写在最前)
public static void swap(int[]arr,int left, int right){
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
- 方法一:进行数据划分
标注:key=arr[length-1]
1、设置两个索引 begin 和 end;
2、 begin 从前往后找,找比 key 大的数,找到后停止;
3、end 从后往前找,找比 key 小的数,找到后停止;
4、begin 位置上的元素与 end 位置上的元素进行交换;
5、最后再将 key 与 begin 位置数据进行交换(如果所指元素位置就是 key , 就不需要交换了)
public static int partion(int[]arr,int left, int right){
int begin = left;
int end = right-1;
int mid = getIndexOfMiddle(arr,left,right);//优化(后面会讲到)
swap(arr,mid,right);
int key = arr[end];
while (begin<end){
//begin
while (begin<end && arr[begin]<=key){
begin++;
}
//end
while (begin<end && arr[end]>=key){
end--;
}
if(begin<end){
swap(arr,begin,end);
}
}
if(begin!=right-1){
swap(arr,begin,key);
}
return begin;
}
- 方法二:“挖坑法”
标注:key=arr[length-1]
1、设置两个索引 begin 和 end;
2、 begin 从前往后找,找比 key 大的数,找到后停止;【8的位置】
3、begin 去填坑 【8 → 5】,end向前走一步;
4、begin的位置则为新的坑【8】;
5、end 从后往前走,找到比 key 小的元素,找到后填坑;
6、找到的位置又为新的坑,再从 begin 开始向后找,以此类推;
7、用key填最后一个坑。
public static int partion2(int[]arr,int left, int right){
int begin = left;
int end = right-1;
int mid = getIndexOfMiddle(arr,left,right);//优化(后面会讲到)
swap(arr,mid,right);
int key = arr[end];
while (begin<end){
//begin从前往后找,找大于Key的
while (begin<end && arr[begin] <= key){
begin++;
}
//找到大于key的,用该元素填end位置的坑
if(begin<end){
arr[end] = arr[begin];
end--;
}
//end从后往前找,找比Key小的
while (begin<end && arr[end] >= key){
end--;
}
//找到了,用该元素去填begin位置的坑
if(begin<end){
arr[begin] = arr[end];
begin++;
}
}
//用key填最后一个坑
arr[begin] = key;
return begin;
}
- 方法三:前后索引
标注:key=arr[length-1]
此方法结合代码来分析,首先先看代码,如下所示
public static int partion3(int[]arr,int left, int right){
int cur = left;
int pre = cur-1;
int mid = getIndexOfMiddle(arr,left,right);//优化(后面会讲到)
swap(arr,mid,right);
int key = arr[right-1];
while (cur<right){
if(arr[cur] < key && pre++!=cur){
swap(arr,cur,pre);
}
cur++;
}
if(pre++!=cur){
swap(arr,pre,right-1);
}
return pre;
}
1、定义两个索引:cur 和 pre ;
2、从3开始,arr[cur] < key (3 < 5满足),但是pre++!=cur(不满足) ,pre(pre++)、 cur都在3的位置上;不进入if语句。
3、cur++: cur 到8的位置上,再次进行while循环;
4、arr[cur] < key (8 不小于 5 ----不满足),直接cur++(cur到2的位置上);
5、此时 arr[cur] < key (2 < 5满足),pre++!=cur(pre在3的位置上)此条件也满足,进行交换后cur++(此时cur在6的位置上);
6、一直循环,直到跳出循环位置;
7、循环结束后,pre+1与key值进行交换即可。
注意: pre 与 cur 一直是一前一后的关系,一段时间后,二者之间有距离,且二者之间的元素都大于key值。
四、最优情况与最差情况
- 最优:
如果每次获取到的基准值都能够将区间划分成左右两半部分,类似于一棵平衡二叉树:O(N1ogN);
- 最差:
每次划分之后,数据都在基准值的一侧(每次拿到的基准值刚好是区间中的极值),类似于一棵单支树:O(N²);
我们要尽量避免最差情况情况出现,因此我们可以采取三数取中的方式进行优化,使得平均时间复杂度为O(N1ogN)
public static int getIndexOfMiddle(int[] arr,int left,int right){
int mid = left+(right-left)>>1;
if(arr[left] < arr[right-1]){ //a<c
if(arr[mid] < arr[left]){ // b<a
return left;
}else if(arr[mid] > arr[right-1]){//b>c
return right-1;
}else{
return mid;
}
}else{ //a>c
if(arr[mid]> arr[left]){ //b>a
return left;
}else if(arr[mid]< arr[left]){
return right-1;
}else {
return mid;
}
}
}
采用三数取中优化之后,每次拿到极值的概率就降低,认为快排最终看的是平均复杂度:O(NlogN)
五、应用场景及优化
【应用场景】:数据量大此较随机(数据杂乱)
数据量大,将来递归深度可能比较深,每次递归都是一次函数调用,每次都需要再栈中压入一个栈帧;
(栈帧:函数在运行期间要保存的中间结果-比如:函数中的局部变量参数返回值信息)
栈是有大小的,所以可能会导致栈溢出,优化递归过深可能会导致栈溢出的问题,所以我们采取插入排序优化(插入排序在这里就不写了):
public static void quickSort(int[] arr,int left, int right){
if(right-left < 16){
insertSort(arr,left,right);
}else{
//按基准值对[left,right)区间进行分割
int key = partion2(arr,left,right);
//递归基准值左半侧和右半侧
quickSort(arr,left,key);
quickSort(arr,key+1,right);
}
}
right-left < 16:没有让递归到区间只剩一个数据时退出,是因为递归到一定程度,区间中的数据实际慢慢的变少;
采取插入排序优化的这种方式只能将递归导致栈溢出的概率降低,不能杜绝;如果想要杜绝此问题,可采取循环的方式(可借助栈完成):
①、栈的特性:后进先出;
②、递归:先调用的后退出,后调用的先退出;
public static void quickSort2(int[] arr) {
Stack<Integer> stack = new Stack<>();
//相当于right left
stack.push(arr.length);
stack.push(0);
while (!stack.empty()){
int left = stack.pop();
int right = stack.pop();
if(right- left > 1){
int key =partion(arr,left,right);
//[key+1,right)
stack.push(right);
stack.push(key+1);
//[left,key)
stack.push(key);
stack.push(left);
}
}
}