快速排序算法:原理与最优实现
快速排序(Quick Sort)是一种高效的排序算法,它的时间复杂度为O(nlogn),在实际应用中表现优异。快速排序的思路是通过分治的方式,将待排序的序列分成两个子序列,其中一个子序列的所有元素都比另一个子序列的所有元素小(或大),然后再对这两个子序列分别进行排序,最后将排好序的子序列合并成最终的排序序列。
快速排序算法的原理
快速排序算法的基本步骤如下:
- 选择一个基准值(pivot),通常选择第一个元素或者最后一个元素。
- 将序列中的所有元素与基准值进行比较,小于基准值的元素放到基准值的左边,大于基准值的元素放到基准值的右边。这个过程称为分区(partition)。
- 对基准值左边的子序列和右边的子序列分别进行递归排序。
快速排序的关键在于分区操作,它决定了算法的效率。理想情况下,每次分区都能将序列分成两个相等长度的子序列,这样递归深度为logn,时间复杂度为O(nlogn)。然而,在最坏情况下,每次分区只能将序列分成一个元素和n-1个元素的两个子序列,此时递归深度为n,时间复杂度退化为O(n^2)。
为了避免最坏情况的发生,我们可以采用随机化策略,即随机选择基准值。这样,快速排序的平均时间复杂度仍为O(nlogn)。
快速排序算法的实现
以下是使用JavaScript实现的快速排序算法示例,采用了随机化策略和双指针法进行分区:
版本一:
// 快速排序主函数
function quickSort(arr, left = 0, right = arr.length - 1) {
// 如果左边界大于等于右边界,说明数组已经排序完成
if (left >= right) {
return;
}
// 对数组进行分区操作,返回分区后基准值的索引
const pivotIndex = partition(arr, left, right);
// 对基准值左侧的子数组进行递归排序
quickSort(arr, left, pivotIndex - 1);
// 对基准值右侧的子数组进行递归排序
quickSort(arr, pivotIndex + 1, right);
}
// 分区函数
function partition(arr, left, right) {
// 随机选择一个基准值索引,这里使用随机化策略
const pivotIndex = Math.floor(Math.random() * (right - left + 1)) + left;
// 获取基准值
const pivot = arr[pivotIndex];
// 初始化左右指针
let i = left,
j = right;
// 当左指针小于等于右指针时,继续循环
while (i <= j) {
// 如果左指针指向的元素小于基准值,左指针向右移动
while (arr[i]< pivot) {
i++;
}
// 如果右指针指向的元素大于基准值,右指针向左移动
while (arr[j] > pivot) {
j--;
}
// 如果左指针小于等于右指针,说明找到了需要交换的元素
if (i <= j) {
// 交换左右指针指向的元素
[arr[i], arr[j]] = [arr[j], arr[i]];
// 交换后,左指针向右移动,右指针向左移动
i++;
j--;
}
}
// 返回分区后基准值的索引
return i;
}
// 示例用法
const arr = [64, 34, 25, 12, 22, 11, 90];
quickSort(arr);
console.log(arr); // 输出:[11, 12, 22, 25, 34, 64, 90]
这段代码实现了一个基本的快速排序算法,使用了随机化策略来选择基准值,以提高算法的平均时间复杂度。通过递归调用quickSort函数,对基准值左右两侧的子数组进行排序。partition函数负责将数组分为两个子数组,其中一个子数组的所有元素都小于基准值,另一个子数组的所有元素都大于基准值。这样,通过不断递归调用,最终可以得到一个完全有序的数组。
当然还有其他很多实现方式,例如以下版本二:
版本二:
function quickSort(arr) {
// 如果数组长度小于等于1,说明数组已经排序完成,直接返回
if (arr.length <= 1) {
return arr;
}
// 选择第一个元素作为基准值
const pivot = arr[0];
// 初始化左侧子数组和右侧子数组
const left = [];
const right = [];
// 遍历数组中的元素(从第二个元素开始,因为第一个元素是基准值)
for (let i = 1; i < arr.length; i++) {
// 如果当前元素小于基准值,将其放入左侧子数组
if (arr[i]< pivot) {
left.push(arr[i]);
} else {
// 如果当前元素大于等于基准值,将其放入右侧子数组
right.push(arr[i]);
}
}
// 递归地对左侧子数组和右侧子数组进行排序,然后将它们和基准值合并
return [...quickSort(left), pivot, ...quickSort(right)];
}
// 示例用法
const arr = [64, 34, 25, 12, 22, 11, 90];
const sortedArr = quickSort(arr);
console.log(sortedArr); // 输出:[11, 12, 22, 25, 34, 64, 90]
方法二这种实现是递归排序,它通过递归地将数组分为左右两部分,然后将左右两部分的结果合并。这种方法的优点是代码简洁易懂,但缺点是空间复杂度较高,为O(n),因为每次递归都会创建新的数组。
结论
从性能角度来看,第一种实现通常更优,因为它的空间复杂度较低。然而,第二种实现(递归排序)的代码更简洁,易于理解。在实际应用中,可以根据项目需求和团队偏好来选择合适的实现方式。如果内存资源充足,第二种实现也是一个不错的选择。如果对性能有较高要求,建议使用第一种实现。