什么是快速排序?
快速排序是一种常用的排序算法,也是基于比较的排序算法。它通过分治的思想将一个大问题转化为多个小问题来解决。
实现快速排序的核心思想是选取一个基准元素(通常选取第一个元素),将数组分成两部分,一部分是小于基准元素的,另一部分是大于基准元素的。然后对这两部分分别进行递归快速排序,最终得到有序的结果。
具体实现步骤如下:
快速排序适合解决需要排序的问题。它的时间复杂度为平均情况下的O(nlogn),最坏情况下的时间复杂度为O(n^2)。由于快速排序使用了递归,所以在处理大规模的数据时可能会出现栈溢出的问题,可以通过优化算法或者使用尾递归来解决。
简单的例子讲解
假设有一个数组[5, 9, 3, 7, 2],使用快速排序对其进行排序。
这个例子展示了快速排序在每一轮选择基准元素、划分子数组和递归排序的过程。通过多次划分和排序,最终可以得到整个数组的有序结果。
补充:
常用的排序算法有以下几种:
- 冒泡排序(Bubble Sort):通过不断交换相邻元素使得最大(或最小)元素逐渐“冒泡”到数组的末尾(或开头)。时间复杂度为O(n^2),适用于小规模数据。
- 选择排序(Selection Sort):每次从待排序的数据中选择最小(或最大)的元素,放到已排序部分的末尾。时间复杂度为O(n^2),适用于小规模数据。
- 插入排序(Insertion Sort):将一个待排序的元素插入到已排好序的部分中的合适位置,使得已排序部分始终保持有序。时间复杂度为O(n^2),适用于基本有序的数据。
- 快速排序(Quick Sort):采用分治的思想,选择一个基准元素,将小于它的元素放在左边,大于它的元素放在右边,然后对左右两个子序列递归地进行快速排序。时间复杂度为O(nlogn),适用于大规模数据。
- 归并排序(Merge Sort):采用分治的思想,将待排序的序列分成两个子序列,然后分别对两个子序列进行排序,最后将两个有序的子序列合并成一个有序序列。时间复杂度为O(nlogn),适用于大规模数据。
- 堆排序(Heap Sort):将待排序的序列构建成一个堆,然后将堆顶元素与堆的最后一个元素交换,并调整堆结构,重复此过程直到整个序列有序。时间复杂度为O(nlogn),适用于大规模数据。
快速排序的核心框架是**“二叉树的前序遍历+对撞型双指针”**。我们在《一维数组》一章提到过”双指针思路“:在处理奇偶等情况时会使用两个游标,一个从前向后,一个是从后向前来比较,根据结果来决定继续移动还是停止等待。快速排序的每一轮进行的时候都是类似的双指针策略,而递归的过程本质上就是二叉树的前序递归调用。
快速排序的核心框架有很多种方式实现,下面这个就是采用对撞型双指针的方式来进行元素的交换和移动。
void quickSort(int[] array, int start, int end) {
// 如果start >= end,表示当前子序列只有一个或没有元素,不需要排序,直接返回
if (start >= end) {
return;
}
// 这里就是一个对撞的双指针操作
int left = start, right = end;
// 选择基准元素为中间位置的元素
int pivot = array[(start + end) / 2];
while (left <= right) {
// 左指针向右移动,找到第一个大于等于基准元素的值
while (left <= right && array[left] < pivot) {
left++;
}
// 右指针向左移动,找到第一个小于等于基准元素的值
while (left <= right && array[right] > pivot) {
right--;
}
// 如果左指针小于等于右指针,则交换两个指针所指的元素,并更新指针位置
if (left <= right) {
int temp = array[left];
array[left] = array[right];
array[right] = temp;
left++;
right--;
}
}
// 先处理元素再分别递归处理两侧分支,与二叉树的前序遍历非常像
quickSort(array, start, right);
quickSort(array, left, end);
}
下一步通过一个leetcode题来感受一下:
leetcode 912. 排序数组
class Solution {
public int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}
private void quickSort(int[] nums, int start, int end) {
if (start >= end) {
return;
}
int left = start;
int right = end;
int pivot = nums[(start + end) / 2];
while (left <= right) {
while (left <= right && nums[left] < pivot) {
left++;
}
while (left <= right && nums[right] > pivot) {
right--;
}
if (left <= right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
quickSort(nums, start, right);
quickSort(nums, left, end);
}
}
这道题非常经典,还可以使用各自算法来实现,也是一道练习八大算法的题目,不过使用leetcode很多算法可能会超时,例如:冒泡
所以这道题还可以用,堆和归并来实现。
复杂度分析
快速排序的时间复杂度计算比较麻烦一些。从原理来看,如果我们选择的pivot每次都正好在中间,效率是最高的,但是这是无法保证的,因此我们需要从最好、最坏和中间情况来分析
最坏情况就是如果每次选择的恰好都是low结点作为pivot,如果元素恰好都是逆序的,此时时间复杂度为O(n^2)
如果元素恰好都是有序的,则时间复杂度为O(n^2)
折中的情况是每次选择的都是中间结点,此时序列每次都是长度相等的序列,此时的时间复杂度为(O(nlogn))
最大堆是一种特殊的二叉树数据结构,满足以下性质:
最大堆常用数组来表示,其中根节点存储在索引位置1上,而对于任意节点i,其左子节点存储在索引位置2i上,右子节点存储在索引位置2i+1上。
最大堆主要用于实现优先队列、堆排序等算法。通过保持最大堆的性质,可以快速找到堆中的最大元素,并能够高效地插入和删除元素。
当数组表示的最大堆为 [25, 17, 12, 10, 6, 9, 5],对应的二叉树如下所示:
25
/ \
17 12
/ \ /
10 6 9
/
5
注意:该图是一种常见的表示方式,但实际上最大堆在内存中并不以这种形式存在,而是通过数组来表示。
所以构建最大堆的算法如下:
private void buildMaxHeap(int[] nums) {
// 从最后一个非叶子节点开始,依次向上调整堆
for (int i = (nums.length - 1) / 2; i >= 0; i--) {
adjustHeap(nums, i, nums.length);
}
}
private void adjustHeap(int[] nums, int parent, int len) {
int temp = nums[parent];
int child = 2 * parent + 1; // 左孩子节点
while (child < len) {
// 如果存在右孩子节点并且右孩子比左孩子大,则选择右孩子作为交换对象
if (child + 1 < len && nums[child] < nums[child + 1]) {
child++;
}
// 如果父节点比子节点大,则无需调整,直接退出循环
if (temp >= nums[child]) {
break;
}
// 将子节点的值赋给父节点
nums[parent] = nums[child];
// 进入下一层调整
parent = child;
child = 2 * parent + 1;
}
// 将原父节点的值放入最终位置
nums[parent] = temp;
}
如果使用堆排序实现对整数数组进行排序,并且按照升序排列,也就是上面那一题,代码如下:
class Solution {
public int[] sortArray(int[] nums) {
// 构建最大堆
buildMaxHeap(nums);
// 循环将堆顶元素与末尾元素交换,并重新调整堆
for (int i = nums.length - 1; i > 0; i--) {
swap(nums, 0, i);
adjustHeap(nums, 0, i);
}
return nums;
}
private void buildMaxHeap(int[] nums) {
// 从最后一个非叶子节点开始,依次向上调整堆
for (int i = (nums.length - 1) / 2; i >= 0; i--) {
adjustHeap(nums, i, nums.length);
}
}
private void adjustHeap(int[] nums, int parent, int len) {
int temp = nums[parent];
int child = 2 * parent + 1; // 左孩子节点
while (child < len) {
// 如果存在右孩子节点并且右孩子比左孩子大,则选择右孩子作为交换对象
if (child + 1 < len && nums[child] < nums[child + 1]) {
child++;
}
// 如果父节点比子节点大,则无需调整,直接退出循环
if (temp >= nums[child]) {
break;
}
// 将子节点的值赋给父节点
nums[parent] = nums[child];
// 进入下一层调整
parent = child;
child = 2 * parent + 1;
}
// 将原父节点的值放入最终位置
nums[parent] = temp;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
over~~