快速选择算法 (Quick Select) 详解
这段代码实现的是 快速选择(Quick Select)算法,它是一种在 无序数组 中寻找 第 k 小的元素 的算法。与快速排序类似,快速选择也基于 分治法(Divide and Conquer),但它并不需要对整个数组进行排序,只需进行部分排序以找到目标元素。
该实现的目标是找到数组中的 第 k 小的元素,而不需要排序整个数组。通过修改标准的快速排序,使其只关心数组的分区而忽略其他部分,从而优化了性能。
1. quickFind
方法
public static int quickFind(int[] array, int aim) {
return quickFindCore(array, aim, 0, array.length - 1);
}
- 功能:这是快速选择的入口方法。它接收两个参数:
array
是待查找的数组,aim
是目标位置,即我们希望找到数组中第aim
小的元素。 quickFindCore
是递归调用的核心方法,它会在指定的左右区间[left, right]
内进行查找,直到找到目标元素。
2. quickFindCore
方法(核心递归)
public static int quickFindCore(int[] array, int aim, int left, int right) {
if (left >= right) {
return array[left];
}
int pivotIndex = partition(array, left, right);
if (pivotIndex > aim) {
return quickFindCore(array, aim, left, pivotIndex - 1);
} else if (pivotIndex < aim) {
return quickFindCore(array, aim, pivotIndex + 1, right);
} else {
return array[pivotIndex];
}
}
- 功能:这个方法是快速选择的核心,采用了递归方式来定位目标元素。
- 递归基准条件:如果
left >= right
,表示数组已经缩小到只有一个元素,返回该元素。 - 分区操作:通过调用
partition
方法,将数组分成两部分并返回分区的枢轴位置pivotIndex
。 - 递归选择方向:根据
pivotIndex
的位置来决定递归的方向:- 如果
pivotIndex > aim
,说明目标元素在左侧子数组中,递归处理左侧子数组。 - 如果
pivotIndex < aim
,说明目标元素在右侧子数组中,递归处理右侧子数组。 - 如果
pivotIndex == aim
,说明找到了目标元素,直接返回。
- 如果
3. partition
方法(分区操作)
public static int partition(int[] array, int left, int right) {
int pivot = array[right]; // 枢轴是当前子数组的最后一个元素
int leftIndex = left;
int rightIndex = right - 1;
while (true) {
// 左指针移动
while (array[leftIndex] <= pivot && leftIndex < right) {
leftIndex++;
}
// 右指针移动
while (array[rightIndex] > pivot && rightIndex > 0) {
rightIndex--;
}
if (leftIndex >= rightIndex) {
break;
} else {
swap(array, leftIndex, rightIndex);
}
}
swap(array, leftIndex, right);
return leftIndex;
}
- 功能:分区操作用于将数组根据枢轴元素进行划分,使得左侧部分小于枢轴,右侧部分大于枢轴,最终返回枢轴元素的索引。
- 枢轴选择:该实现选择数组的最后一个元素作为枢轴。
- 双指针法:
leftIndex
从数组的左侧开始,寻找比枢轴大的元素。rightIndex
从右侧开始,寻找比枢轴小的元素。- 当
leftIndex
小于rightIndex
时,交换这两个元素,直到指针交叉。
- 交换枢轴:在分区完成后,将枢轴与
leftIndex
位置的元素交换,确保枢轴元素在其最终位置。
4. swap
方法(交换数组元素)
public static void swap(int[] array, int index1, int index2) {
int temp = array[index1];
array[index1] = array[index2];
array[index2] = temp;
}
- 功能:交换数组中两个指定位置的元素。通过一个临时变量
temp
保存一个元素的值,然后交换这两个元素。
5. main
方法(测试)
public static void main(String[] args) {
int[] array = {72, 77, 48, 17, 71, 2, 25, 97, 82, 5, 2, 18, 15, 57, 7, 48, 93, 47, 38, 74, 18, 93, 98, 41, 54, 4, 47, 4, 63, 76};
System.out.println("raw: " + Arrays.toString(array));
// 目标是倒数第 6 个元素
int result = quickFind(array, array.length - 6);
System.out.println("result: " + result);
}
- 功能:在
main
方法中,我们初始化一个数组array
,并使用quickFind
方法查找数组中倒数第 6 个元素(即array.length - 6
)。 quickFind
返回数组中第aim
小的元素,并打印出结果。
快速选择的工作原理:
- 分区:首先将数组分区,选择一个枢轴元素,分成两部分,左侧部分小于枢轴,右侧部分大于枢轴。
- 递归:根据目标位置
aim
与枢轴位置的比较决定递归的方向:- 如果目标位置在枢轴左边,则只对左侧子数组递归。
- 如果目标位置在枢轴右边,则只对右侧子数组递归。
- 如果目标位置等于枢轴位置,则返回枢轴元素,即目标元素。
- 停止条件:当递归的子数组只剩一个元素时,返回该元素。
快速选择的时间复杂度:
- 平均时间复杂度:
O(n)
,由于每次分区大约能将数组减半,且递归深度为O(log n)
,但是不需要对整个数组进行排序,因此可以在O(n)
时间内找到第k
小的元素。 - 最坏时间复杂度:
O(n^2)
,在最坏的情况下(例如每次选择的枢轴都是最小或最大元素),递归将遍历整个数组。类似于快速排序的最坏情况。
新手需要了解的关键点:
-
快速选择与快速排序的区别:
- 快速排序:对整个数组进行排序,时间复杂度
O(n log n)
。 - 快速选择:只关心找到第
k
小的元素,时间复杂度O(n)
,无需对数组完全排序。
- 快速排序:对整个数组进行排序,时间复杂度
-
分区操作(Partition):这是快速排序和快速选择算法的核心。通过分区操作,我们能够确定某个元素的最终位置,而无需对数组进行完全排序。
-
递归:快速选择使用递归来逐步缩小查找范围,直到找到目标元素的位置。
-
双指针法:用于分区的双指针方法通过从两端扫描数组来寻找需要交换的元素。
-
枢轴元素:枢轴元素是快速排序和快速选择中的核心,通过它来分区数组。选取合适的枢轴元素可以提高算法性能。
示例输出:
假设我们希望找到倒数第 6 个元素。
raw: [72, 77, 48, 17, 71, 2, 25, 97, 82, 5, 2, 18, 15, 57, 7, 48, 93, 47, 38, 74, 18, 93, 98, 41, 54, 4, 47, 4, 63, 76]
result: 57
总结:
快速选择算法通过改进快速排序的思想,只关注数组的一部分,能够在 O(n) 的时间内找到数组中的第 k
小元素,性能上比排序整个数组更优。它通过递归和分区来缩小查找范围,是一种高效的元素查找算法。