快速排序的基本思想
快速排序也是利用分治的思想。
如果需要排序的数据的下标是从s到t,在下标s到t之间的任意一个数据作为分区点p,遍历s到t之间的数据,将小于分区点的数据放到分区点p的左边,将大于分区点的数据放到分区点的右边,分区点放在中间,经过这一操作后。数据被分为3个部分,s到p-1之间为小于分区点的数据,p+1到t之间为大于分区点的数据,p为分区点。根据分治递归的处理思想,可以递归排序s到p-1的区间和p+1到t的区间,直到区间缩小为1,说明排序完成,所有数据都已经为有序。
文字还是没有图直观:分区点的择为最末尾的一个元素
首先还是看看分区的操作,一个直观的对比
快速排序的代码实现
先还是从分区函数的实现开始吧
结合图再来看看分区函数的代码实现
/**
* 分区函数,在快速排序中使用的是原地分区,实现非常的巧妙
*
* <p>函数需要返回分区点
*
* @param data 原始数据
* @param s 开始位置
* @param t 结束位置
* @return 分区点的位置
*/
private int partition(T[] data, int s, int t) {
// 1,指定分区点为当前的最后一个元素
int point = t;
int i = s;
for (int j = s; j <= t - 1; j++) {
// i < j ? -1
// 如果数据小于分区点,执行互换操作
if (((Comparable) data[j]).compareTo(data[point]) < 0) {
// 交换数据
swap(data, i, j);
// i向后移动一位,指向分区点
i += 1;
}
}
// 当结束后,将分区点的位置与i互换,即可完成
swap(data, i, point);
// System.out.println(Arrays.toString(data));
return i;
}
/**
* 进行数据交换操作
*
* @param data 原始数据
* @param src 原始位置
* @param target 目标位置
*/
private void swap(T[] data, int src, int target) {
T dataTmp = data[src];
data[src] = data[target];
data[target] = dataTmp;
}
完整的快速排序的实现:
/**
* 快速排序的代码实现
*
* @author liujun
* @version 0.0.1
* @date 2019/10/28
*/
public class QuickSort<T> implements SortInf<T> {
@Override
public void sort(T[] data) {
if (data == null || data.length <= 1) {
return;
}
if (!(data[0] instanceof Comparable)) {
throw new IllegalArgumentException("data not implement compator interface");
}
// 调用快速排序
this.quickSort(data, 0, data.length - 1);
}
private void quickSort(T[] data, int s, int t) {
// 当前需操作元素仅一个,则退出
if (t - s < 1) {
return;
}
int point = this.partition(data, s, t);
quickSort(data, s, point - 1);
quickSort(data, point + 1, t);
}
/**
* 分区函数,在快速排序中使用的是原地分区,实现非常的巧妙
*
* <p>函数需要返回分区点
*
* @param data 原始数据
* @param s 开始位置
* @param t 结束位置
* @return 分区点的位置
*/
private int partition(T[] data, int s, int t) {
// 1,指定分区点为当前的最后一个元素
int point = t;
int i = s;
for (int j = s; j <= t - 1; j++) {
// i < j ? -1
// 如果数据小于分区点,执行互换操作
if (((Comparable) data[j]).compareTo(data[point]) < 0) {
// 交换数据
swap(data, i, j);
// i向后移动一位,指向分区点
i += 1;
}
}
// 当结束后,将分区点的位置与i互换,即可完成
swap(data, i, point);
// System.out.println(Arrays.toString(data));
return i;
}
/**
* 进行数据交换操作
*
* @param data 原始数据
* @param src 原始位置
* @param target 目标位置
*/
private void swap(T[] data, int src, int target) {
T dataTmp = data[src];
data[src] = data[target];
data[target] = dataTmp;
}
}
分析快速排序的时间复杂度
最好情况时间复杂度
快速排序也是使用递归来实现的,在分析归并排序时,总结的公式同样适用,在最好情况下,每次都能将数据均匀的分成两个相同的区间,那快速排序与归并排序的时间复杂度是相同的
所以快排的最好情况时间复杂度就是O(nlogn)
t(1)=C 只需要常量级的执行时间,使用C表示
t(n)=2*t(n/2)+n n>1
平均情况下的时间复杂度
假设每次分区都将数据分成9:1的两个区间,继续套用公式来求解时间复杂度:
就会变成
t(1)=C t=1时,只需常量级的时间,表示为C
t(n)=t(9n/10)+t(n/10)+n n>1求解时间复杂度
t(n)=t(9n/10)+t(n/10)+n
(过程我还不能推导出来,目前空余)
只知道结果是n*logn
针对快速排序时间退化成O(n^2)的优化方法
优化方式1:三数取中
从待排序的数组中的头部、中间、还有尾部三个位置各取一个数,然后在这三个数中求出中位数,这种方法肯定比单纯的取固定值的方法要好,当要排序的数据很大时,三数取中可能就不够了,可能就会采用5数取中,或者更大的10数取中。
先还是来说三数取中法吧
首先对前两个数A和B进行比较大小,如果A大于B,则将A与换。
这时开始处理数C,数C处理在这里存在三种情况:
情况1:C小于A,那么A就是中位数
情况2:C大于A,小于B,那么C就是中位数
情况3:C大于B,那么B就是中位数。
代码实现:
/**
* 快速排序的代码实现,使用三数取中法进行分区点的选择操作
*
* @author liujun
* @version 0.0.1
* @date 2019/10/28
*/
public class QuickSortThirdMod<T> implements SortInf<T> {
@Override
public void sort(T[] data) {
if (data == null || data.length <= 1) {
return;
}
if (!(data[0] instanceof Comparable)) {
throw new IllegalArgumentException("data not implement compator interface");
}
// 调用快速排序
this.quickSort(data, 0, data.length - 1);
}
private void quickSort(T[] data, int s, int t) {
// 当前需操作元素仅一个,则退出
if (t - s < 1) {
return;
}
int point = this.partition(data, s, t);
quickSort(data, s, point - 1);
quickSort(data, point + 1, t);
}
/**
* 分区函数,在快速排序中使用的是原地分区,实现非常的巧妙
*
* <p>函数需要返回分区点
*
* @param data 原始数据
* @param s 开始位置
* @param t 结束位置
* @return 分区点的位置
*/
private int partition(T[] data, int s, int t) {
// 1,分区函数采用三数取中法
int countPoint = this.dataMid(data, s, t);
// 求得中位数后将,最后一个位置与中位数互换即可
swap(data, countPoint, t);
// 当前的分区点
int midPoint = t;
int i = s;
for (int j = s; j <= t - 1; j++) {
// i < j ? -1
// 如果数据小于分区点,执行互换操作
if (((Comparable) data[j]).compareTo(data[midPoint]) < 0) {
// 交换数据
swap(data, i, j);
// i向后移动一位,指向分区点
i += 1;
}
/// System.out.println("分区数据:" + Arrays.toString(data));
}
// 当结束后,将分区点的位置与i互换,即可完成
swap(data, i, midPoint);
// System.out.println("分区点:" + midPoint);
// System.out.println("分区完成后" + Arrays.toString(data));
return i;
}
/**
* 三数取中法
*
* <p>对三个数进行比较,然后交换次序
*
* @param data 数据
* @param s 开始索引
* @param t 结束索引
* @return 当前的中位数
*/
public int dataMid(T[] data, int s, int t) {
// 求两个数的中位数与(s+t)/2效果一致,但在接近最大数时,将不能运算,所以使用t+((s-t)>>1)
int midIndex = t + ((s - t) >> 1);
// 首先保证前两个数是有序的
if (((Comparable) data[s]).compareTo(data[midIndex]) > 0) {
swap(data, s, midIndex);
}
// 分为多种情况,情况1,比最小的数字小
if (((Comparable) data[t]).compareTo(data[s]) <= 0) {
return s;
}
// 情况2,大于最小,小于最大
else if (((Comparable) data[t]).compareTo(data[s]) > 0
&& ((Comparable) data[t]).compareTo(data[midIndex]) <= 0) {
return t;
}
// 情况3:大于最大的数,返回最大数
else {
return midIndex;
}
}
/**
* 进行数据交换操作
*
* @param data 原始数据
* @param src 原始位置
* @param target 目标位置
*/
private void swap(T[] data, int src, int target) {
T dataTmp = data[src];
data[src] = data[target];
data[target] = dataTmp;
}
}
优化方式2:随机
就是每次在要排序的数据区间中,随机选择一个点作为分区点,因为是随机挑选的,并不能保证每次挑选的分区点都比较合适,但从概率上来说,也不会每次挑选的分区点都很差。从而,退化到O(n^2)的情况非常的少
代码实现:
/**
* 快速排序的代码实现
*
* <p>使用随机分区点的方法
*
* @author liujun
* @version 0.0.1
* @date 2019/10/28
*/
public class QuickSortRandomPoint<T> implements SortInf<T> {
@Override
public void sort(T[] data) {
if (data == null || data.length <= 1) {
return;
}
if (!(data[0] instanceof Comparable)) {
throw new IllegalArgumentException("data not implement compator interface");
}
// 调用快速排序
this.quickSort(data, 0, data.length - 1);
}
private void quickSort(T[] data, int s, int t) {
// 当前需操作元素仅一个,则退出
if (t - s < 1) {
return;
}
int point = this.partition(data, s, t);
quickSort(data, s, point - 1);
quickSort(data, point + 1, t);
}
/**
* 分区函数,在快速排序中使用的是原地分区,实现非常的巧妙
*
* <p>函数需要返回分区点
*
* @param data 原始数据
* @param s 开始位置
* @param t 结束位置
* @return 分区点的位置
*/
private int partition(T[] data, int s, int t) {
// 1,采用随机的方法采用分区点,进行分区
int countPoint = ThreadLocalRandom.current().nextInt(s, t);
// 求得中位数后将,最后一个位置与中位数互换即可
swap(data, countPoint, t);
// 当前的分区点
int midPoint = t;
int i = s;
for (int j = s; j <= t; j++) {
// i < j ? -1
// 如果数据小于分区点,执行互换操作
if (((Comparable) data[j]).compareTo(data[midPoint]) < 0) {
// 交换数据
swap(data, i, j);
// i向后移动一位,指向分区点
i += 1;
}
}
// 当结束后,将分区点的位置与i互换,即可完成
swap(data, i, midPoint);
return i;
}
/**
* 进行数据交换操作
*
* @param data 原始数据
* @param src 原始位置
* @param target 目标位置
*/
private void swap(T[] data, int src, int target) {
T dataTmp = data[src];
data[src] = data[target];
data[target] = dataTmp;
}
}
快速排序的总结
算法名称 | 最好情况时间复杂度 | 最好情况的原始数据 | 最坏情况时间复杂度 | 最坏情况的原始数据 | 平均情况时间复杂度 | 是否基于比较 | 空间复杂度 | 是否稳定排序算法 |
---|---|---|---|---|---|---|---|---|
快速排序 | O(nlogn) | 每次分区都分成大小相同的两个区间 | O(n^2) | 原始数据已经有序 | O(nlogn) | 是 | O(n)原地排序算法 | 非移定的原地排序算法 |