警示:老弟我对此算法还有待加强,如有问题欢迎各位大佬评论区交流指正,留下你对快排的优质解法。
本算法所用知识点:
Java二分查找算法https://blog.csdn.net/ouhexie/article/details/140981942?spm=1001.2014.3001.5501Java递归算法https://blog.csdn.net/ouhexie/article/details/140984060?spm=1001.2014.3001.5501
什么是快速排序?
快速排序(Quick Sort)是一种高效的排序算法,采用分治法(Divide and Conquer)策略来对数据进行排序。
快速排序的基本步骤:
-
选择支点(Pivot): 从待排序的数组中选择一个元素作为“支点”。支点的选择可以是数组的第一个元素、最后一个元素或随机选择。
-
分区(Partition): 将数组重新排列,使得所有小于支点的元素都位于支点的左侧,所有大于支点的元素都位于支点的右侧。这一步骤完成后,支点就处于其最终位置上。
-
递归排序: 对支点左侧和右侧的两个子数组分别应用步骤1和步骤2,直到整个数组有序。
源码案例:
import java.util.Arrays;
/**
* @version 1.0
* @ClassName QuickSort
* @Description 快速排序
* @Author NanJi
* @Date 2024/8/19 - 15:29
*/
public class QuickSort {
public static void main(String[] args) {
Integer[] nums = {6, 1, 2, 7, 9, 3, 4, 5, 8};
QuickSort.sort(nums);
System.out.println(Arrays.toString(nums));
}
/**
* 比较v元素是否小于w元素
*
* @param v 元素
* @param w 元素
* @return true: v元素小于w元素
*/
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
/**
* 数组元素 i 和 j 交换位置
*
* @param a 数组
* @param i 索引
* @param j 索引
*/
private static void exch(Comparable[] a, int i, int j) {
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
/**
* 对数组内的元素进行排序
*
* @param a 数组
*/
public static void sort(Comparable[] a) {
int lo = 0;
int hi = a.length - 1;
sort(a, lo, hi);
}
/**
* 对数组 a 中从索引 lo 到索引 hi 的元素进行排序
*
* @param a 数组
* @param lo 索引
* @param hi 索引
*/
private static void sort(Comparable[] a, int lo, int hi) {
// 安全性校验,避免越界
if (hi <= lo) {
return;
}
//需要对数组中 lo 索引到 hi 索引处的元素进行分组(左子组和右子组)
int partition = partition(a, lo, hi); //返回的是分组的分界值所在的索引,分界值位置变换后的索引
//让左子组有序
sort(a, lo, partition - 1);
//让右子组有序
sort(a, partition + 1, hi);
}
/**
* 对数组 a 中从索引 lo 到索引 hi 之间的元素进行分组,并返回分组界限对应的索引
*
* @param a 数组
* @param lo 索引
* @param hi 索引
* @return 分组界限对应的索引
*/
public static int partition(Comparable[] a, int lo, int hi) {
//确定分界值
Comparable key = a[lo];
//定义两个指针,分别指向待切分元素的最小索引处和最大索引处的下一个位置
int left = lo;
int right = hi + 1;
//切分
while (true) {
//先从右往左扫描,移动 right 指针,找到一个比分界值小的元素,停止
while (less(key, a[--right])) {
//如果 right 指针移动到 lo 处,证明元素扫描完毕,结束循环
if (right == lo) {
break;
}
}
//再从左往右扫描,移动 left 指针,找到一个比分界值大的元素,停止
while (less(a[++left], key)) {
//如果 left 指针移动到 hi 处,证明元素扫描完毕,结束循环
if (left == hi) {
break;
}
}
//判断 left >= right,如果是,则证明元素扫描完毕,结束循环
if (left >= right) {
break;
} else {
exch(a, left, right);
}
}
//交换分界值
exch(a, lo, right);
//返回right索引
return right;
}
}
代码详细解释:
1. less (Comparable v, Comparable w)
/**
* 比较v元素是否小于w元素
*
* @param v 元素
* @param w 元素
* @return true: v元素小于w元素
*/
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
功能: 比较两个可比较对象 v
和 w
的大小关系。
实现: 使用 compareTo
方法比较两个对象。如果 v
小于 w
,则返回 true
,否则返回 false
。
2. exch (Comparable [] a, int i, int j)
/**
* 数组元素 i 和 j 交换位置
*
* @param a 数组
* @param i 索引
* @param j 索引
*/
private static void exch(Comparable[] a, int i, int j) {
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
功能: 交换数组 a
中索引 i
和 j
的两个元素。
实现: 使用一个临时变量 temp
存储 a[i]
,然后将 a[j]
赋值给 a[i]
,最后将 temp
赋值给 a[j]
。
3. sort (Comparable [] a)
/**
* 对数组内的元素进行排序
*
* @param a 数组
*/
public static void sort(Comparable[] a) {
int lo = 0;
int hi = a.length - 1;
sort(a, lo, hi);
}
功能: 公开的排序方法,接受一个数组 a
。
实现: 调用私有的 sort
方法,传递整个数组的范围 0
到 a.length - 1
。
4. sort (Comparable [] a, int lo, int hi)
/**
* 对数组 a 中从索引 lo 到索引 hi 的元素进行排序
*
* @param a 数组
* @param lo 索引
* @param hi 索引
*/
private static void sort(Comparable[] a, int lo, int hi) {
// 安全性校验,避免越界
if (hi <= lo) {
return;
}
//需要对数组中 lo 索引到 hi 索引处的元素进行分组(左子组和右子组)
int partition = partition(a, lo, hi); //返回的是分组的分界值所在的索引,分界值位置变换后的索引
//让左子组有序
sort(a, lo, partition - 1);
//让右子组有序
sort(a, partition + 1, hi);
}
功能: 对数组 a
从索引 lo
到 hi
的部分进行排序。
实现:
-
基线条件: 如果
hi
小于或等于lo
,则返回(即子数组的长度为零或负数时无需排序)。 -
分区操作: 调用
partition
方法获取分界点j
。 -
递归排序: 对分界点左侧和右侧的子数组进行递归排序。
5. partition (Comparable [] a, int lo, int hi)
/**
* 对数组 a 中从索引 lo 到索引 hi 之间的元素进行分组,并返回分组界限对应的索引
*
* @param a 数组
* @param lo 索引
* @param hi 索引
* @return 分组界限对应的索引
*/
public static int partition(Comparable[] a, int lo, int hi) {
//确定分界值
Comparable key = a[lo];
//定义两个指针,分别指向待切分元素的最小索引处和最大索引处的下一个位置
int left = lo;
int right = hi + 1;
//切分
while (true) {
//先从右往左扫描,移动 right 指针,找到一个比分界值小的元素,停止
while (less(key, a[--right])) {
//如果 right 指针移动到 lo 处,证明元素扫描完毕,结束循环
if (right == lo) {
break;
}
}
//再从左往右扫描,移动 left 指针,找到一个比分界值大的元素,停止
while (less(a[++left], key)) {
//如果 left 指针移动到 hi 处,证明元素扫描完毕,结束循环
if (left == hi) {
break;
}
}
//判断 left >= right,如果是,则证明元素扫描完毕,结束循环
if (left >= right) {
break;
} else {
//交换 left 和 right 元素
exch(a, left, right);
}
}
//交换分界值
exch(a, lo, right);
//返回right索引
return right;
}
功能: 将数组 a
从索引 lo
到 hi
进行分区,并返回分界点的索引。
实现:
-
选择支点: 选取当前部分的第一个元素
a[lo]
作为支点。 -
初始化指针:
left
指向lo + 1
,right
指向hi
。 -
扫描与交换:
-
向右移动
left
指针,直到找到第一个大于支点的元素。 -
向左移动
right
指针,直到找到第一个小于支点的元素。 -
如果
left
指针小于或等于right
指针,交换a[left]
和a[right]
,并移动指针。
-
-
结束条件: 当
left
指针大于right
指针时,停止扫描。 -
交换支点: 将支点
a[lo]
交换到正确的位置,即right
索引。 -
返回分界点: 返回分界点的索引
right
。
运行过程举例
假设我们有一个数组 [6, 1, 2, 7, 9, 3, 4, 5, 8]
进行排序:
初次调用 sort
:
-
调用
sort(a, 0, 8)
,即处理整个数组。 -
partition
选择支点6
,并将数组分为[1, 2, 3, 4, 5]
和[9, 7, 8]
,支点6
移到位置4
。
递归处理左子数组 [1, 2, 3, 4, 5]
:
-
调用
sort(a, 0, 4)
。 -
partition
选择支点1
,支点1
移到位置0
,分为空数组和[2, 3, 4, 5]
。 -
递归处理
[2, 3, 4, 5]
,重复以上步骤,最终排序为[1, 2, 3, 4, 5]
。
递归处理右子数组 [9, 7, 8]
:
-
调用
sort(a, 5, 8)
。 -
partition
选择支点9
,支点9
移到位置7
,分为[7, 8]
和 空数组。 -
递归处理
[7, 8]
,排序完成后得到[7, 8]
。
最终结果:
-
合并所有部分,得到排序后的数组
[1, 2, 3, 4, 5, 6, 7, 8, 9]
。
打印排序后的数组,结果为:
[1, 2, 3, 4, 5, 6, 7, 8, 9]
时间复杂度
-
平均时间复杂度: O(n log n)
-
最坏时间复杂度: O(n²)(当数组已经是有序的或逆序的,且每次选择的支点都是最小或最大元素时)
-
最好时间复杂度: O(n log n)
空间复杂度
快速排序的空间复杂度为 O(log n),这主要是由于递归调用栈的空间消耗。
优缺点
优点:
-
平均情况下性能优秀。
-
不需要额外的存储空间(原地排序)。
缺点:
-
在最坏情况下效率低下。
-
不稳定排序(相同元素的相对位置可能会改变)。
欧了,到这里我应该解释的差不多啦,我是南极,大胆做自己,活出精彩的人生👊👊👊