目录
1. 算法介绍⭐⭐⭐⭐⭐
1.1 图示解析
2. 执行流程和代码实现
2.1 挖坑法⭐⭐⭐⭐
2.2 Hoare法⭐⭐⭐⭐
2.3 前后指针法(了解即可)
2.4 非递归实现快速排序(了解即可)
4. 性能分析
5. 算法改进
5.1 三数选中法
5.2 最后几行数据采用直接插入排序
快速排序也是“交换”类的排序,运用了分治的思想,它通过多次划分操作实现排序。以升序为例,其执行流程可以概括为:每一趟选择当前所有子序列中的一个关键字(通常是第一个)作为基准值,将子序列中比基准值小的移动到基准值的前边,比基准值大的移到基准值后边;当本趟所有子序列都被基准值以上述规则划分完毕后,会得到新的一组更短的子序列,它们成为下一趟划分的初始序列集。
(1)从待排序区间选择一个数,作为基准值(mid);
(2)Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
(3)采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序,或者小区间的长度 == 0,代表没有数据。
小伙伴们,自行放大图片看一下,很细滴!!!
/*
下面性能部分细讲
时间复杂度:
最好情况是:O(NlogN) 均匀分割待排序列,尽量满二叉树
最坏情况是:O(N^2),有序或者逆序,单分支的树
空间复杂度:
最好情况是:O(logN) 树的高度,把持着mid变量
最坏情况是:O(N),有序或者逆序,单分支的树
稳定性:不稳定
*/
//这里为了统一其他排序算法的接口,所以只传一个参数——数组
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
//划分
private static void quick(int[] array, int start, int end) {
//这里为什么是>=,因为防止end到了start后面,导致越届
/*
s e
mid
例如:1 2
再递归进去,end == -1,start == 0,所以必须交 =
*/
if (start >= end) {
return;
}
int mid = partition1(array, start, end);
//递归左边
quick(array, start, mid - 1);
//递归右边
quick(array, mid + 1, end);
}
//第一种:快速排序,挖坑法,无优化版本
//每一趟的操作,交换,填坑
private static int partition1(int[] array, int left, int right) {
int tmp = array[left];//保存最左位置的值
//开始找坑
//多次左右走
while (left < right) {
//先右往左走,找一个值比tmp小的,一次循环或者说一次向右走
// 这里必须要是 >= ,不然当下标0和下标array.length的话,就会陷入死循环,这两个下标的值一直在交换
//提问:为什么不需要再次++,去进入循环呢?
while (left < right && array[right] >= tmp)
right--;
array[left] = array[right];
//从左往右走,找一个值比tmp大的,一次循环或者说一次向左走
while (left < right && array[left] <= tmp)
left++;
array[right] = array[left];
}
array[left] = tmp;
return left;
}
public static void main(String[] args) {
int[] a = {10,10,9,8,7,6,5,4,3,2,1};
Sort.quickSort2(a);
for (int x : a) {
System.out.print(x + " ");
}
}
思路:设置两个下标指针i和j,j向左走,找到比基准值(pivot)小的值先停下来,不交换,i向右走,找到比基准值(pivot)大的值先停下来,然后交换array[i] 和 array[j],当i和j相遇后,交换array[i] 和 array[left]。
/*
下面性能部分细讲
时间复杂度:
最好情况是:O(NlogN) 均匀分割待排序列,尽量满二叉树
最坏情况是:O(N^2),有序或者逆序,单分支的树
空间复杂度:
最好情况是:O(logN) 树的高度,把持着mid变量
最坏情况是:O(N),有序或者逆序,单分支的树
稳定性:不稳定
*/
//这里为了统一其他排序算法的接口,所以只传一个参数——数组
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
//划分
private static void quick(int[] array, int start, int end) {
//这里为什么是>=,因为防止end到了start后面,导致越届
/*
s e
mid
例如:1 2
再递归进去,end == -1,start == 0,所以必须交 =
*/
if (start >= end) {
return;
}
int mid = partition2(array, start, end);
//递归左边
quick(array, start, mid - 1);
//递归右边
quick(array, mid + 1, end);
}
//第二种快速排序,Hoare法,主要是交换的方式不同
private static int partition2(int[] array, int left, int right) {
int tmp = array[left];
int i = left;//把left开始的下标保存起来先
while (left < right) {
/*
为什么不能先走左呢?
答:因为先走左的话,最后right会停在比mid大的值的下标,left找到right停下来,交换i和left的值,导致不满足快排的结果
*/
//这两个循环,永远是right追left
while (left < right && array[right] >= tmp)
right--;
while (left < right && array[left] <= tmp)
left++;
swap(array, left, right);//自定义的交换方法
}
swap(array, i, left);
return left;
}
public static void main(String[] args) {
int[] a = {10,10,9,8,7,6,5,4,3,2,1};
Sort.quickSort2(a);
for (int x : a) {
System.out.print(x + " ");
}
}
总体思路:prev一直往后走(prev++),当array[prev]小于array[left]的时候,cur++。当array[cur] != array[prev]时。
具体看如下的过程:
//第三种排序算法:前后指针法
//写法一
private static int partition3(int[] array, int left, int right) {
int cur = left;//prev记录的是最后一位比array[left]的下标
int prev = left + 1;
while (cur <= right) {
if (array[prev] < array[left] && array[++cur] != array[prev]) {
swap(array, prev, cur);
}
prev++;
}
swap(array, left, cur);
return cur;
}
第一步:先把第一个元素定为基准值。
cur 记录第一个元素6的下标,prev记录第二个元素的1的下标。
第二步:因为第一步的1 < 6,cur++且没发生交换,prev++(注意:prev无论如何都会一直往前走) 。
第三步:因为第二步的2 < 6,cur++且没发生交换,prev++(注意:prev无论如何都会一直往前走) 。
第四步:因为第三步的7 > 6,所以 cur不往前走,prev++,来到元素9的下标。
第五步:因为第四步的9 > 6,所以 cur不往前走,prev++,来到元素3的下标。
第六步:因为第五步的3 < 6,所以cur++来到元素7的下标,接着交换array[cur]和array[prev]的元素,后prev++(代码有体现)。
第七步:因为第五步的4 < 6,所以cur++来到元素的下标,接着交换array[cur]和array[prev]的元素,后prev++(代码有体现)。
第八步:因为第七步的5 < 6,所以cur++来到元素的下标,接着交换array[cur]和array[prev]的元素,后prev++(代码有体现)。
第九步:因为第八步的10 > 6,所以 cur不往前走,prev++,来到元素8的下标。
然后排序元素6左边子序列和右边子序列,一直重复,直至排序完成!
总结:我们可以发现cur永远都是记录着小于基准值的最后一个元素的下标,不断地把比基准值大的元素往右推!
/*
下面性能部分细讲
时间复杂度:
最好情况是:O(NlogN) 均匀分割待排序列,尽量满二叉树
最坏情况是:O(N^2),有序或者逆序,单分支的树
空间复杂度:
最好情况是:O(logN) 树的高度,把持着mid变量
最坏情况是:O(N),有序或者逆序,单分支的树
稳定性:不稳定
*/
//这里为了统一其他排序算法的接口,所以只传一个参数——数组
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
//划分
private static void quick(int[] array, int start, int end) {
//这里为什么是>=,因为防止end到了start后面,导致越届
/*
s e
mid
例如:1 2
再递归进去,end == -1,start == 0,所以必须交 =
*/
if (start >= end) {
return;
}
int mid = partition3(array, start, end);
//递归左边
quick(array, start, mid - 1);
//递归右边
quick(array, mid + 1, end);
}
//第三种排序算法:前后指针法
//写法一
private static int partition3(int[] array, int left, int right) {
int cur = left;//prev记录的是最后一位比array[left]的下标
int prev = left + 1;
while (cur <= right) {
if (array[prev] < array[left] && array[++cur] != array[prev]) {
swap(array, prev, cur);
}
prev++;
}
swap(array, left, cur);
return cur;
}
public static void main(String[] args) {
int[] a = {6,1,2,7,9,3,4,5,10,8};
Sort.quickSort2(a);
for (int x : a) {
System.out.print(x + " ");
}
}
使用栈完成效果
(1)先准备一个栈,先调用一次partition1()方法,把6放到最终位置。
(2)将左边子序列的start和end下标进栈,右子序列的start和end下标进栈。
(3)然后出栈,先赋值个right,再给left,循环进行,就能完成排序。
注意:会将右子序列排序完成,再去排序左子序列,小伙伴们可以根据我们上面的部分流程,把整个算法流程画出来!
//第一种:快速排序,挖坑法,无优化版本
//每一趟的操作,交换,填坑
private static int partition1(int[] array, int left, int right) {
int tmp = array[left];//保存最左位置的值
//开始找坑
//多次左右走
while (left < right) {
//先右往左走,找一个值比tmp小的,一次循环或者说一次向右走
// 这里必须要是 >= ,不然当下标0和下标array.length的话,就会陷入死循环,这两个下标的值一直在交换
//提问:为什么不需要再次++,去进入循环呢?
while (left < right && array[right] >= tmp)
right--;
array[left] = array[right];
//从左往右走,找一个值比tmp大的,一次循环或者说一次向左走
while (left < right && array[left] <= tmp)
left++;
array[right] = array[left];
}
array[left] = tmp;
return left;
}
//非递归实现快速排序
//用栈实现
public static void quickSort2(int[] array){
Deque stack = new LinkedList<>();
int left = 0;
int right = array.length - 1;
int mid = partition1(array,left,right);
if(mid > left + 1){
stack.push(left);
stack.push(mid - 1);
}
if(mid < right - 1){
stack.push(mid + 1);
stack.push(right);
}
while(!stack.isEmpty()){
right = stack.pop();
left = stack.pop();
mid = partition1(array,left,right);
if(mid > left + 1){
stack.push(left);
stack.push(mid - 1);
}
if(mid < right - 1) {
stack.push(mid + 1);
stack.push(right);
}
}
}
(1)最好的情况:快速排序最理想的情况就是满二叉树, 此时时间复杂度是:O(nlogn);空间复杂度是:O(logn)。
(2)最坏的情况:原数据有序或者逆序,这样快速排序就成了单分支树了,此时时间复杂度是:O(n^2),空间复杂度是:O(n)。
取待排序数组中 头、中、尾三个位置的元素、取中间值作为基准元素。(此种方式最好,递归调用栈的深度最低)1.取待排序数组中 头、中、尾三个位置的元素、取中间值作为基准元素。(此种方式最好,递归调用栈的深度最低)。
//优化的方案,均匀的分割!三数取中法
private static int midThree(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{
//array[right] > array[left]
if(array[mid] < array[left]){
return left;
}else if(array[mid] > array[right]){
return right;
}else{
return mid;
}
}
}
改进后的代码是:
/*
时间复杂度:
最好情况是:O(NlogN) 均匀分割待排序列,尽量满二叉树
最坏情况是:O(N^2),有序或者逆序,单分支的树
空间复杂度:
最好情况是:O(logN) 树的高度,把持着mid变量
最坏情况是:O(N),有序或者逆序,单分支的树
稳定性:不稳定
*/
//这里为了统一其他排序算法的接口,所以只传一个参数——数组
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
//划分
private static void quick(int[] array, int start, int end) {
//这里为什么是>=,因为防止end到了start后面,导致越届
/*
s e
mid
例如:1 2
再递归进去,end == -1,start == 0,所以必须交 =
*/
if (start >= end) {
return;
}
//优化一:
//排序,返回到最终位置的元素下标
int index = midThree(array,start,end);
swap(array,start,index);
int mid = partition3(array, start, end);
//递归左边
quick(array, start, mid - 1);
//递归右边
quick(array, mid + 1, end);
}
//第一种:快速排序,挖坑法,无优化版本
//每一趟的操作,交换,填坑
private static int partition1(int[] array, int left, int right) {
int tmp = array[left];//保存最左位置的值
//开始找坑
//多次左右走
while (left < right) {
//先右往左走,找一个值比tmp小的,一次循环或者说一次向右走
// 这里必须要是 >= ,不然当下标0和下标array.length的话,就会陷入死循环,这两个下标的值一直在交换
//提问:为什么不需要再次++,去进入循环呢?
while (left < right && array[right] >= tmp)
right--;
array[left] = array[right];
//从左往右走,找一个值比tmp大的,一次循环或者说一次向左走
while (left < right && array[left] <= tmp)
left++;
array[right] = array[left];
}
array[left] = tmp;
return left;
}
//优化的方案,均匀的分割!三数取中法
private static int midThree(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{
//array[right] > array[left]
if(array[mid] < array[left]){
return left;
}else if(array[mid] > array[right]){
return right;
}else{
return mid;
}
}
}
直接插入排序:数据越有序,效率越高!
/*
时间复杂度:
最好情况是:O(NlogN) 均匀分割待排序列,尽量满二叉树
最坏情况是:O(N^2),有序或者逆序,单分支的树
空间复杂度:
最好情况是:O(logN) 树的高度,把持着mid变量
最坏情况是:O(N),有序或者逆序,单分支的树
稳定性:不稳定
*/
//这里为了统一其他排序算法的接口,所以只传一个参数——数组
public static void quickSort(int[] array) {
quick(array, 0, array.length - 1);
}
//划分
private static void quick(int[] array, int start, int end) {
//这里为什么是>=,因为防止end到了start后面,导致越届
/*
s e
mid
例如:1 2
再递归进去,end == -1,start == 0,所以必须交 =
*/
if (start >= end) {
return;
}
//优化二:
if(end - start + 1 <= 14){
insertSort2(array,start,end);
}
//优化一:
//排序,返回到最终位置的元素下标
int index = midThree(array,start,end);
swap(array,start,index);
int mid = partition3(array, start, end);
//递归左边
quick(array, start, mid - 1);
//递归右边
quick(array, mid + 1, end);
}
//第一种:快速排序,挖坑法,无优化版本
//每一趟的操作,交换,填坑
private static int partition1(int[] array, int left, int right) {
int tmp = array[left];//保存最左位置的值
//开始找坑
//多次左右走
while (left < right) {
//先右往左走,找一个值比tmp小的,一次循环或者说一次向右走
// 这里必须要是 >= ,不然当下标0和下标array.length的话,就会陷入死循环,这两个下标的值一直在交换
//提问:为什么不需要再次++,去进入循环呢?
while (left < right && array[right] >= tmp)
right--;
array[left] = array[right];
//从左往右走,找一个值比tmp大的,一次循环或者说一次向左走
while (left < right && array[left] <= tmp)
left++;
array[right] = array[left];
}
array[left] = tmp;
return left;
}
//直接插入排序
public static void insertSort2(int[] arr,int left,int right) {
//代码可以从i = 1开始算起,但是做题画图时,一定要从i = 0开始算起
for (int i = left + 1; i < arr.length; i++) {
int j = i - 1;
int tmp = arr[i];
for (; j >= left; j--) {
//如果arr[j] > tmp变成arr[j] >= tmp就变成不稳定了
if (arr[j] > tmp) {
arr[j + 1] = arr[j];
} else {
break;
}
}
arr[j + 1] = tmp;
}
}