快速排序及其优化--(理论+java实现)
是一种交换排序
简单快排
算法思想:
快速排序的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分:分割点左边都是比它小的数,右边都是比它大的数。
然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
详见--https://cuijiahua.com/blog/2017/12/algorithm_4.html
里面提供了详细的动态求解过程
时间复杂度分析
平均情况 | 最好情况 | 最坏情况 | 稳定性 |
不稳定 |
- 当数据有序时,以第一个关键字为基准分为两个子序列,前一个子序列为空,此时执行效率最差。
- 当数据随机分布时,以第一个关键字为基准分为两个子序列,两个子序列的元素个数接近相等,此时执行效率最好。
所以,数据越随机分布时,快速排序性能越好;数据越接近有序,快速排序性能越差
稳定性分析
在快速排序中,相等元素可能会因为分区而交换顺序,所以它是不稳定的算法。
代码
public void quickSort(int[] num){
int left = 0;
int right = num.length - 1;
quickSortCore(num, left, right);
}
private void SortCore(int[] num, int left, int right) {
//左下标一定小于右下标 否则越界
//递归结束条件
if (left >= right){
return;
}
//对数组进行分割,取出下次分割的基准符号
int base = division(num, left, right);
//对基准标号左侧的一组数据进行分割
SortCore(num, 0, base - 1);
//右侧
SortCore(num, base + 1, right);
}
private int division(int[] num, int left, int right) {
//以最左边的数(left)为基准
int base = num[left];
while (left < right){
// 从序列右端开始,向左遍历,直到找到小于base的数
while (left < right && num[right] >= base){
right--;
}
//找到了比base小的,则将其值赋给left所指向的值
num[left] = num[right];
// 从序列左端开始,向右遍历,直到找到大于base的数
while (left < right && num[left] <= base){
left++;
}
//找到了比base小的,则将其值赋给right所指向的值
num[right] = num[left];
}
//循环结束left和right指向位置 将该位置换位base的值
num[left] = base;
//此时left位置左侧的值都比base小 右侧的值都比base大
//即分割点为当前left指向的位置
return left;
}
优化点一:切换到插入排序
对于小数组而言, 快速排序比插入排序要慢, 所以在排序小数组时应该切换到插入排序。
只要把SortCore函数中的
//递归结束条件
if (left >= right){
return;
}
修改为
if(right <= left + M) { Insertion.sort(a,low, high) return; } // Insertion表示一个插入排序类
就可以了,这样的话,这条语句就具有了两个功能:
- 在适当时候终止递归
- 当数组长度小于M的时候(high-low <= M), 不进行快排,而进行插排
优化点二:基准元素选取的随机化
在上面所有的快速排序的例子中,我们都是固定选取基准元素,这种操作做了一个假设性的前提:数组元素的分布是随机的。而如果数组不是随机的,而是有一定顺序的,甚至在最坏的情况下:完全正序或完全逆序, 这个时候麻烦就来了: 快排所消耗的时间大大延长,完全达不到快排应有的效果。
在division函数中的添加一行
我们利用随机数从原始数组中随机找一个基准元素,然后将它与数组的头元素进行替换,这样我们选出的基准元素就是随机的
swap(num, left + (int)(Math.random() * (right - left + 1)), right);
新的division函数如下
private int division(int[] num, int left, int right) {
//以最左边的数(left)为基准
swap(num, left + (int)(Math.random() * (right - left + 1)), right);
int base = num[left];
while (left < right){
// 从序列右端开始,向左遍历,直到找到小于base的数
while (left < right && num[right] >= base){
right--;
}
//找到了比base小的,则将其值赋给left所指向的值
num[left] = num[right];
// 从序列左端开始,向右遍历,直到找到大于base的数
while (left < right && num[left] <= base){
left++;
}
//找到了比base小的,则将其值赋给right所指向的值
num[right] = num[left];
}
//循环结束left和right指向位置 将该位置换位base的值
num[left] = base;
//此时left位置左侧的值都比base小 右侧的值都比base大
//即分割点为当前left指向的位置
return left;
}
private void swap(int[] num, int index, int right) {
int temp = num[index];
num[index] = num[right];
num[right] = temp;
}
优化点三:基准元素三数取中法
一般认为, 当取得的基准元素是数组元素的中位数的时候,排序效果是最好的,但是要筛选出待排序数组的中位数的成本太高, 所以只能从待排序数组中选取一部分元素出来再取中位数, 经大量实验显示: 当筛选数组的长度为3时候,排序效果是比较好的, 所以由此发展出了三数取中法:
三数取中法: 分别取出数组的最左端元素,最右端元素和中间元素, 在这三个数中取出中位数,作为基准元素。
在division函数中修改
int base = selectMiddleOfThree(num, left, right);
private static int selectMiddleOfThree(int[] num, int left, int right) {
int middle = left + (right - left) / 2;
if (num[left] > num[right]){
swap(num, left, right);
}
if (num[middle] > num[right]){
swap(num, middle, right);
}
if (num[middle] > num[left]){
swap(num, middle, left);
}
return num[left];//此时num[left]的值是三个数的中位数将其返回
}