一.基本思想
1.在快速排序中,采取的主要的"拆"的思想
2.每一次将数组拆为两个部分,令中间元素为pivot,pivot左边的所有元素都会小于pivot,而pivot的右边的所有元素会大
于等于pivot的值
3.再拆出来的部分,分别对其左右两部分通过递归的方式,继续对它进行拆分
4.直到最左侧的索low<high条件不成立,此时的数组有序
二.Eg:假如数组arr={15,18,13},low=0,high=arr.length-1=2,利用拆的思想,将它拆为两部分
图解上述例子
1. 先确定pivot的值=arr[low],并且需要明确我们的low和high的指针不仅仅是充当界限,并且low的索引也是比pivot元素要小的元素要插入的位置,high则是比pivot大于等于的元素要插入的位置
2. 将右侧指针high指向的比pivot要小的元素,换到左侧位置中
3. 找到左侧比pivot元素要大或等于pivot元素的位置
4. 将左侧大于等于pivot的元素换到右侧位置处
5. 继续上述步骤,从右侧找到一个比pivot要小的元素
6. 将右侧指针所指的比pivot要小的元素换到左侧中,即arr[low]=arr[high]
,但是由于此时的low=high,所以这一步并不会实际改变什么
7. 从左侧查找一个大于等于pivot的元素,并将其换到右侧去
8. 此时low<high的条件已经不满足了,所以我们退出整个步骤,并且在这个时候,我们可以发现,low所指的位置,其实就是我们pivot要实际存储的位置,所以在结束整个循环后,我们令arr[low]=pivot
,此时的数组就满足pivot左侧的元素都小于pivot,pivot右侧的元素都大于或等于pivot
9. 由于在该例中只有3个元素,在进行一次拆分后,数组就已经有序了,但是在实际运用中,可能有多个元素,所以在我们每次拆分完成之后,还需要对pivot左右两侧再次进行拆分操作,所以我们在每次拆分完成后,需要返回 pivot的索引位置,即return low
三.代码
package com.cdc.algorithm.sort;
import java.util.Arrays;
/**
* @author cdc
* @email c925638766@163.com
* @date 2022/6/1 21:13
*/
public class QuickSortDemo {
public static void main(String[] args) {
int[] arr = {49, 38, 65, 97, 76, 13, 27, 49};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
/**
* 快速排序主要采取的方式是将数组中的元素,不断的分为两个部分,并且以一个元素pivot为边界
* pivot左边的元素都比pivot要小
* pivot右边的元素都比pivot要大
* 由于要将其划分为左右两个部分,所以左侧索引left和右侧索引right是必不可少的
* 当left<right满足时,需要不断的将已经分好的两部分再次进行上述操作
*
* @param arr 数组
* @param low 数组的左侧索引部分
* @param high 数组的右侧索引部分
*/
public static void quickSort(int[] arr, int low, int high) {
if (low < high) {
//将完整的数组拆分为以pivot元素为边界的两部分,并对pivot左右两部分的元素继续递归进行拆分操作
int pivotPos = partition(arr, low, high);
//对pivot左侧元素进行递归拆分
quickSort(arr, low, pivotPos - 1);
//对pivot右侧元素进行递归拆分
quickSort(arr, pivotPos + 1, high);
}
}
/**
* 将给定的数组拆分为两个部分,以pivot为界,pivot左侧都比pivot小,pivot右侧都比pivot大于或等于
*
* @param arr 需要进行拆分的数组
* @param low 数组的左侧索引
* @param high 数组的右侧索引,high可取
* @return pivot的下标
*/
public static int partition(int[] arr, int low, int high) {
//以数组的最左侧元素为边界,将数组拆分为两部分
int pivot = arr[low];
while (low < high) {
//先从右侧开始,确保右侧的元素都比pivot大于或等于,直到找到一个比pivot元素要小的元素
while (low < high && arr[high] >= pivot) {
high--;
}
//将右侧中比pivot元素要小的元素,放到pivot的左侧
arr[low] = arr[high];
//从左侧开始,要确保pivot左侧的元素都是小于它的,所以需要找到一个大于等于pivot的元素,然后将它进行交换位置
while (low < high && arr[low] < pivot) {
low++;
}
//将左侧大于等于pivot的元素,放到pivot的右侧
arr[high] = arr[low];
}
/*
将pivot元素放到它应该放到的位置,以49,27,65为例,
在上面的循环中
第一步从右侧开始查找的操作中low=0,high=2,pivot=49
先从右边查找比pivot小的元素也就是27,此时的high=1,
arr[low]=arr[high],即数组变为27,27,65
第二步从左侧开始查找的操作中,low=0,high=1,pivot=49
找到左侧大于或等于pivot的元素,49>27,low++=1
low=high,所以退出循环,由于low=high,所以下面的赋值操作可以忽略不计
low=high,退出整个训话,并且我们可以发现,pivot它应该插入的元素是与low所指的位置是一致的
所以我们令arr[low]=pivot,数组变为:27,49,65
*/
arr[low] = pivot;
//返回pivot元素的位置
return low;
}
}
四.性能分析
(一)时间复杂度(以代码中的数组{49, 38, 65, 97, 76, 13, 27, 49}为例)
- 在第一次QuickSort的排序后,将数组分为了两个部分,以第一个元素49为轴,分为左右两个部分。此时待排序的数组元素为:
0~2与4~7
。对这两个部分分别进行拆分即调用Partition()
函数,这两个区间内,待排序的数组长度小于n,所以对这两个区间调用Partition()
函数所使用的时间也肯定不会超过O(n)
这个时间
- 在第二次QuickSort排序后,又将49的左侧分为了以27为轴的左右两部分;右侧以76为轴的左右两部分。此时待排序的数组元素为:
0~0、2~2、4~5、7~7
。在这个待排序的元素中,它的长度也不会超过n,所以对它们进行Partition()
的调用所使用的时间也肯定不会超过O(n)
- 在第三次QuickSort中,对27的左侧即13与右侧即38进行了拆分
对76的左侧,由于只有两个元素,并且轴是第一个元素,也就是49,所以49会被排序,但是65并不会被排序,所以65会被单独分为一个待排序的位置;
对76的右侧,只有一个97,所以会对97进行排序。综上,在第三次QuickSort结束后,剩余的待排序只有5~5
。与上面所得到的结论一样,在这个区间内调用Partiton()
所使用的时间肯定不会超过O(n)
- 第四次QuickSort中,对65进行排序,此时数组全部变为有序状态。
- 在每一次的QuickSort过程中,它的时间都不会超过
O(n)
,所以在k次QuickSort即递归调用的次数中,它的时间不会超过O(n*k)
(二) 空间复杂度
- 由于在每一次的递归调用过程中,都需要创建一个单独的栈用于保存变量信息,并且在每一次创建过程中,都只需要创建
low、pivot、high以及对arr的引用
,所以每次调用过程中,所花费的空间复杂度为O(1)
- 那么k次的递归调用,就会花费
O(K*1)=O(k)
的空间 - 所以不管是时间复杂度,还是空间复杂度,都与它的递归调用次数息息相关
(三)递归次数
- 在第一次QuickSort后,我们将原数组分为了这样的两个部分,我们如果将它们表示为二叉树的形式,就是这样的一个表示形式
- 在第二次QuickSort后,我们又将49的左右两份又拆分开,并将它们表示为二叉树的形式,就形成了下面的表示形式
- 在第三次的QuickSort后,我们又将27的左右两边以及76的左右两边拆开,并将它们用二叉树的形式表示,就得到了下面的表示形式
- 在第四次QuickSort后,只需要将剩下的65进行拆分,并将它以二叉树的形式表示,就得到了下面的表示形式
- 在将n个元素经历了k次的递归调用后,得到了上图的二叉树形式,树的高度实际上就是递归调用的次数。而对于n个结点能得到的最小高度为:(log2n)+1;最大高度为:n
- 因此对于快速排序算法而言,它的最好时间复杂度为:O(n*log2n);最坏时间复杂度为:O(n*n)=O(n2),它的平均复杂度为:O(n*log2n)
- 对于它的空间复杂度而言,最好空间复杂度为:O(log2n);最坏时间复杂度为:O(n)