最基本的快速排序
这里介绍最经典的快速排序思想,也是最基础的一种
快速排序(Quick Sort)思想:
通过一趟排序将无序序列分割成独立的左右两部分,左边部分均为右边部分小,然后再分别对这两部分序列进行排序,可以通过递归的调用完成。
在快速排序中,通过定义一个基准值,左右指针,一般取左指针指向的数为基准值,然后从右指针开始遍历找比基准值小的数,找到了就和左指针的数交换;然后通过左指针遍历找比基准大的数,找到了就和右指针的数交换,循环往复,基准值将会归位,左右指针也会指向基准值,这时候就退出循环,开始递归左右序列。
左边取基准
右左交替遍历(右边找到比基准小的,交换到左边去;左边找到比基准大的,交换到右边去):
之所以先取右边,因为如果先取左边,就是一个向右的过程,所有的元素已经在基准的右边了
本身左边就是找比基准大的数,然后交换到右边去,这就完全不合理了,没意义。
如果从右边开始遍历找比基准小的数,通过交换,小的数就在基准的左边了
然后再遍历左指针,遇到比基准大的数,和基准值进行交换
直到左右指针相遇,那么此时基准的左边就是刚刚已经排序的小数了,而右边是大数,指针指向的也是基准值了
例如:[ 5 ,1,9,3,7,4,8,6, 2 ] pivot:5 left:arr[0]:5 right:arr[8]:2
第一轮比较:2<5 交换 [2, 1 ,9,3,7,4,8,6, 5 ] 移动左指针 left:arr[1]:1 right:arr[8]:5
第二轮比较:1<5 不交换 继续移动左指针 left:arr[2]:9 right:arr[8]:5
第三轮比较:9>5 交换 [2,1, 5 ,3,7,4,8, 6 ,9] 移动右指针 left:arr[2]:5 right:arr[7]:6
第四轮比较:5<6 不交换 继续移动右指针 left:arr[2]:5 right:arr[6]:8
第五轮比较:5<8 不交换 继续移动右指针 left:arr[2]:5 right:arr[5]:4
第六轮比较:5>4 交换 [2,1,4, 3 ,7, 5 ,8,6,9] 移动左指针 left:arr[3]:3 right:arr[5]:5
第七轮比较:3<5 不交换 继续移动左指针 left:arr[4]:7 right:arr[5]:5
第八轮比较:7>5 交换 [2,1,4,3, 5 , 7 ,8,6,9] 移动右指针 left:arr[4]:5 right:arr[4]:5
左右指针相遇 该轮排序完成,返回基准索引4
两指针相遇即完成一轮排序
package com.sort;
import java.util.Arrays;
public class QuickSortOfPivot {
public static void main(String[] args) {
int[] arr = {2, 4, 6, 7, 9, 1, 3, 8};
// int[] arr = {5, 1, 9, 3, 7, 4, 8, 6, 2};
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr, int left, int right) {
if (left < right) {
int pivot = partition(arr, left, right);
// 对小的序列继续递归 传递基准左边第一个作为下一个序列的最右边
quickSort(arr, left, pivot - 1);
// 对大的序列继续递归 传递基准右边第一个作为下一个序列的最左边
quickSort(arr, pivot + 1, right);
}
}
public static int partition(int[] arr, int left, int right) {
if (left < right) {
int pivotKey = arr[left];
while (left < right) {
// 先从右边开始找比基准小的值 一定要记得是 >= 下面是<=
// 因为如果是这样的序列{2, 4, 6, 7, 9, 1, 8, 2},就会导致死循环
// 因为第一次比较的时候,如果不是 >= 或者 <= 那么while就会因为条件被破坏导致无法递减right和递增left,因为会认为需要交换
// 但是实际上2和2交换过后,之后还会继续交换,就导致了死循环
while (left < right && arr[right] >= pivotKey) {
right--;
}
swap(arr, left, right);
// 开始从左边找一个比基准大的值
while (left < right && arr[left] <= pivotKey) {
left++;
}
swap(arr, left, right);
}
}
return left;
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
快速排序优化
优化1:优化选取枢轴
当我们选择基准值的时候,每次都是选择最左边的值,这显然不是很合理,如果最左边是最大值,那么就会导致,当这个值交换到最右边的时候,整个最左边都是小于基准值的元素,递归树就不平衡。(下图取自大话数据结构)
优化方案:
/**
* 优化1:
* 取左中右三个数,将三个数的中间数放到最左边。这又叫三数取中
* 还有9数取中,也就是取9个数,分为3个数组,每个数组取出中间的数后,再取中间的数
*
* @param arr 待排序数组
* @param left 左边界
* @param right 右边界
*/
public static void randomLow(int[] arr, int left, int right) {
// 不要使用arr.length,因为这个时候,可能是递归在调用它,数组大小虽然没变,但是子数组的边界已经修改
int mid = left + (right - left) / 2;
// 保证左边最小
if (arr[left] > arr[right]) {
swap(arr, left, right);
}
// 保证中间最小
if (arr[mid] > arr[right]) {
swap(arr, mid, right);
}
// 此时右边是最大的,比较左边和中间的数,使较大的放在左边
if (arr[left] < arr[mid]) {
swap(arr, left, mid);
}
}
public static int partition(int[] arr, int left, int right) {
int pivotKey;
// 优化一:对基准数随机化
randomLow(arr, left, right);
pivotKey = arr[left];
while (left < right) {
...
}
return left;
}
优化二:优化不必要的交换
在这个优化中,可以发现我们将交换修改成了覆盖:
arr[left] = arr[right];
arr[right] = arr[left];
// 将基准值归位
arr[left] = pivotKey;
public static int partition(int[] arr, int left, int right) {
int pivotKey;
// 对基准数随机化
randomLow(arr, left, right);
pivotKey = arr[left];
while (left < right) {
while (left < right && arr[right] >= pivotKey) {
right--;
}
// 将在右边找到的比基准值大的数放到左边去
// 这是交换的方式,会有性能开销
// swap(arr, left, right);
// 优化2:采用直接替换的方式减少性能开销
// 不必担心left的数被覆盖找不到,因为第一次覆盖的是基准值,基准值被保存在pivotKey中,相当于最开始基准值的位置被置空了
// 也不必担心right的值没有被修改而重复,因为接下来会移动left,找到一个比基准大的值,放到right的位置上,如果没有找到,
// 那么就会left和right就会重叠,最后基准值将会覆盖上去
arr[left] = arr[right];
// 开始从左边找一个比基准大的值
while (left < right && arr[left] <= pivotKey) {
left++;
}
// swap(arr, left, right);
arr[right] = arr[left];
}
// 将基准值归位
arr[left] = pivotKey;
return left;
}
优化三:优化小数组时的排序方案
因为快速排序可以用来处理数据量非常大的情况,但是对于数组比较小的时候,反而不如使用直接插入排序(直接插入排序是简单排序中性能最好的—),因为数组较小的时候,也会使用递归,对性能上有一定的影响。(有资料认为7比较合适,也有认为50比较合理的,实际应用可适当调整)
public static final int MAX_LENGTH_INSERT_SORT = 7;
// 优化三:使用直接插入排序来优化小数组时的排序
public static void quickSort3(int[] arr, int left, int right) {
int pivot;
if ((right - left) > MAX_LENGTH_INSERT_SORT) {
pivot = partition(arr, left, right);
// 对小的序列继续递归 传递基准左边第一个作为下一个序列的最右边
quickSort3(arr, left, pivot - 1);
// 对大的序列继续递归 传递基准右边第一个作为下一个序列的最左边
quickSort3(arr, pivot + 1, right);
} else {
InsertSortKt.insertSort(arr, left, right);
}
}
直接插入排序:使用Kotlin拓展方法完成
// 插入排序
fun IntArray.insertSort(left:Int, right:Int) {
for (i in left .. right) {
val insertVal = this[i]
// 当前元素的前一个位置
var insertIndex = i - 1
// 然后依次将该元素和之前的元素进行比较
// insertIndex表示检查插入是否合法的位置
// 1.insertIndex >= 0 表示检查的位置不能小于0
// 2.insertVal < array[insertIndex] 表示待插入的元素小于检查的位置
// 如果小于就不插入,因为说不定前面还有更小的数,只有比检查的位置的数大的时候才进行插入
// 这样就说明比当前检查的数大,比前一个检查的数小,是合适的位置
while (insertIndex >= 0 && insertVal < this[insertIndex]) {
// 将检查的数后移
// 本身insertIndex就是i-1,所以再次减1表示是当前拿到的需要排序的数的前前个位置
this[insertIndex + 1] = this[insertIndex]
insertIndex--
}
// 说明找到了位置,之所以+1是因为当前insertIndex是检查的数的索引,也就是说这个检查的数是比待插入的数大。
// 或者因为找到了-1的索引,不符合条件,到达了最前面
// 优化:如果当前找到的位置的后一位就是当前这个数,那么不需要插入,因为就是它本身
if (insertIndex + 1 != i) {
this[insertIndex + 1] = insertVal
}
}
}
优化四:优化递归操作
由于递归对性能有一定的影响,并且可能导致堆栈溢出。如果待排序的序列划分极端不平衡,递归深度将会趋近于n,这不仅仅是速度慢的问题了,还有可能导致堆栈溢出。
因此可以进行尾递归优化(对于剩下的右半部分的数组,可以对其使用迭代,也就是一直使用第一次递归,也就是左递归,直到左右指针在同一元素上,就完成了递归):
// 优化四:减少不必要的递归
public static void quickSort4(int[] arr, int left, int right) {
int pivot;
if ((right - left) > MAX_LENGTH_INSERT_SORT) {
while (left < right) {
pivot = partition(arr, left, right);
// 对小的序列继续递归 传递基准左边第一个作为下一个序列的最右边
quickSort4(arr, left, pivot - 1);
// 尾递归优化,将之前的递归修改为这种方式,比如在之前执行到这个位置的时候右序列(比基准值大的序列)是[9,8,6,7]
// 这个时候,left是4(9的索引是5),因此+1,将left指向到9的位置,然后开始循环
// 也就是说原先是对左边部分进行了递归后再执行右边部分的递归,现在使用迭代的方式,使得每次循环都是只执行第一次的递归即可
// 相当于一直截断,然后一直对左边进行排序,直到left和right相遇(当分割的数组只有一个数的时候,那么就完成了排序)
left = pivot + 1;
}
} else {
InsertSortKt.insertSort(arr, left, right);
}
}
完整代码
package com.sort;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
public class QuickSortOfPivot {
public static final int MAX_LENGTH_INSERT_SORT = 7;
public static void main(String[] args) {
int[] arr = {2, 4, 6, 7, 9, 1, 8, 2};
// int[] arr = {5, 1, 9, 3, 7, 4, 8, 6, 2};
// quickSort4(arr, 0, arr.length - 1);
// System.out.println(Arrays.toString(arr));
int[] randomArray = new int[80000000];
for (int i = 0; i < 80000000; i++) {
randomArray[i] = (int) (Math.random() * 800000000);
}
Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formatted = simpleDateFormat.format(date);
System.out.println("排序前的时间:" + formatted);
quickSort(randomArray, 0, 79999999);
Date dateAfter = new Date();
String dateAfterFormat = simpleDateFormat.format(dateAfter);
System.out.println("排序后的时间:" + dateAfterFormat);
// System.out.println(Arrays.toString(randomArray));
}
/**
* 左边取基准
* 右左交替遍历(右边找到比基准小的,交换到左边去;左边找到比基准大的,交换到右边去):
* 之所以先取右边,因为如果先取左边,就是一个向右的过程,所有的元素已经在基准的右边了,
* 本身左边就是找比基准大的数,然后交换到右边去,这就完全不合理了,没意义。
* 如果从右边开始遍历找比基准小的数,通过交换,小的数就在基准的左边了,
* 然后再遍历左指针,遇到比基准大的数,和基准值进行交换,
* 直到左右指针相遇,那么此时基准的左边就是刚刚已经排序的小数了,而右边是大数,指针指向的也是基准值了
* 例如:[ 5 ,1,9,3,7,4,8,6, 2 ] pivot:5 left:arr[0]:5 right:arr[8]:2
* 第一轮比较:2<5 交换 [2, 1 ,9,3,7,4,8,6, 5 ] 移动左指针 left:arr[1]:1 right:arr[8]:5
* 第二轮比较:1<5 不交换 继续移动左指针 left:arr[2]:9 right:arr[8]:5
* 第三轮比较:9>5 交换 [2,1, 5 ,3,7,4,8, 6 ,9] 移动右指针 left:arr[2]:5 right:arr[7]:6
* 第四轮比较:5<6 不交换 继续移动右指针 left:arr[2]:5 right:arr[6]:8
* 第五轮比较:5<8 不交换 继续移动右指针 left:arr[2]:5 right:arr[5]:4
* 第六轮比较:5>4 交换 [2,1,4, 3 ,7, 5 ,8,6,9] 移动左指针 left:arr[3]:3 right:arr[5]:5
* 第七轮比较:3<5 不交换 继续移动左指针 left:arr[4]:7 right:arr[5]:5
* 第八轮比较:7>5 交换 [2,1,4,3, 5 , 7 ,8,6,9] 移动右指针 left:arr[4]:5 right:arr[4]:5
* 左右指针相遇 该轮排序完成,返回基准索引4
* 两指针相遇即完成一轮排序
*/
public static void quickSort(int[] arr, int left, int right) {
int pivot;
if (left < right) {
pivot = partition(arr, left, right);
// 对小的序列继续递归 传递基准左边第一个作为下一个序列的最右边
quickSort(arr, left, pivot - 1);
// 对大的序列继续递归 传递基准右边第一个作为下一个序列的最左边
quickSort(arr, pivot + 1, right);
}
}
// 优化三:使用直接插入排序来优化小数组时的排序
public static void quickSort3(int[] arr, int left, int right) {
int pivot;
if ((right - left) > MAX_LENGTH_INSERT_SORT) {
pivot = partition(arr, left, right);
// 对小的序列继续递归 传递基准左边第一个作为下一个序列的最右边
quickSort3(arr, left, pivot - 1);
// 对大的序列继续递归 传递基准右边第一个作为下一个序列的最左边
quickSort3(arr, pivot + 1, right);
} else {
InsertSortKt.insertSort(arr, left, right);
}
}
// 优化四:减少不必要的递归
public static void quickSort4(int[] arr, int left, int right) {
int pivot;
if ((right - left) > MAX_LENGTH_INSERT_SORT) {
while (left < right) {
pivot = partition(arr, left, right);
// 对小的序列继续递归 传递基准左边第一个作为下一个序列的最右边
quickSort4(arr, left, pivot - 1);
// 尾递归优化,将之前的递归修改为这种方式,比如在之前执行到这个位置的时候右序列(比基准值大的序列)是[9,8,6,7]
// 这个时候,left是4(9的索引是5),因此+1,将left指向到9的位置,然后开始循环
// 也就是说原先是对左边部分进行了递归后再执行右边部分的递归,现在使用迭代的方式,使得每次循环都是只执行第一次的递归即可
// 相当于一直截断,然后一直对左边进行排序,直到left和right相遇(当分割的数组只有一个数的时候,那么就完成了排序)
left = pivot + 1;
}
} else {
InsertSortKt.insertSort(arr, left, right);
}
}
public static int partition(int[] arr, int left, int right) {
int pivotKey;
// 对基准数随机化
randomLow(arr, left, right);
pivotKey = arr[left];
while (left < right) {
// 先从右边开始找比基准小的值 一定要记得是 >= 下面是<=
// 因为如果是这样的序列{2, 4, 6, 7, 9, 1, 8, 2},就会导致死循环
// 因为第一次比较的时候,如果不是 >= 或者 <= 那么while就会因为条件被破坏导致无法递减right和递增left,因为会认为需要交换
// 但是实际上2和2交换过后,之后还会继续交换,就导致了死循环
while (left < right && arr[right] >= pivotKey) {
right--;
}
// 将在右边找到的比基准值大的数放到左边去
// 这是交换的方式,会有性能开销
// swap(arr, left, right);
// 优化2:采用直接替换的方式减少性能开销
// 不必担心left的数被覆盖找不到,因为第一次覆盖的是基准值,基准值被保存在pivotKey中,相当于最开始基准值的位置被置空了
// 也不必担心right的值没有被修改而重复,因为接下来会移动left,找到一个比基准大的值,放到right的位置上,如果没有找到,
// 那么就会left和right就会重叠,最后基准值将会覆盖上去
arr[left] = arr[right];
// 开始从左边找一个比基准大的值
while (left < right && arr[left] <= pivotKey) {
left++;
}
// swap(arr, left, right);
arr[right] = arr[left];
}
// 将基准值归位
arr[left] = pivotKey;
return left;
}
/**
* 优化1:
* 取左中右三个数,将三个数的中间数放到最左边。这又叫三数取中
* 还有9数取中,也就是取9个数,分为3个数组,每个数组取出中间的数后,再取中间的数
*
* @param arr 待排序数组
* @param left 左边界
* @param right 右边界
*/
public static void randomLow(int[] arr, int left, int right) {
// 不要使用arr.length,因为这个时候,可能是递归在调用它,数组大小虽然没变,但是子数组的边界已经修改
int mid = left + (right - left) / 2;
// 保证左边最小
if (arr[left] > arr[right]) {
swap(arr, left, right);
}
// 保证中间最小
if (arr[mid] > arr[right]) {
swap(arr, mid, right);
}
// 此时右边是最大的,比较左边和中间的数,使较大的放在左边
if (arr[left] < arr[mid]) {
swap(arr, left, mid);
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}