快速排序算法解析
快速排序可以说是最频繁的算法之一了,但是它又颇有难度,今天就让我们一起来掌握它吧!
快速排序的核心框架是——二叉树的前序遍历(按中左右顺序排序) + 对撞型双指针。
快速排序的基本思想:
通过一个标记 pivot 元素将n个元素划分为左右两个子序列 left 和 right,其中 left 中的元素都比 pivot 小,right 的都比 pivot 的大。然后再通过递归再次对left 和 right 各自执行快速排序,将左右子序列排序好后,整个序列九有序了。
注意:这里排序进行左右划分的时候是一直划分到子序列只包含一个元素,然后再递归返回。
和二叉树的前序遍历是不是非常想呢,思路是一样的,只不过还运用了对撞双指针来实现。
在这里我们我们以序列 {26,53,48,15,13,48,32,15} 观察划分一次的过程:
上面红框位置表示当前已经被赋值给了元素pivot 或者其他位置,这些位置可以空出来放置需要交换位置的元素。通过一次排序后,可以看到26最终放到属于自己的位置上,不会再变化。左侧的数比26都小,右侧的都比26大,因此26的左右两侧可以分别再进行排序。后面就是递归实现啦!
原理不是很简单,接下来就让我们一起通过代码来解析吧!
public static void quickSort(int[] arr, int left, int right) {
// 若left >= right 说明此时该(子)序列已经排序完毕
if (left < right) {
// 这里以数组最后一个元素作为 pivot
int pivot = arr[right];
// i是可以用来存放比pivot小的、交换位置过来的数
int i = left - 1;
// j是遍历数组的指针,用来比较数组每个数与pivot的大小关系
for (int j = left; j < right; j++) {
// 若arr[j]比pivot大,则此时不需要交换位置
// 若arr[j]比pivot小,则此时需要把若arr[j]与arr[i+1]交换位置
// 这是因为保证比pivot小的数一定在左边,把比其大的逐渐换到右边去
if (arr[j] < pivot) {
i++; // i往右移一格,该处为可存放比pivot小的数的最新位置
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 在将i个比pivot小的数移到其左边后,此时哨兵应该位置i+1上
// 故将最右边的pivot换到其固定的位置上
int pivotIndex = i + 1;
int temp = arr[pivotIndex];
arr[pivotIndex] = arr[right];
arr[right] = temp;
// 通过递归对其左右子序列分别执行快速排序
quickSort(arr, left, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, right);
}
}
通过上面非常非常详细的解析,是不是已经掌握了呢,下面让我们再来学习另外一种实现方式,这里采用的就是一个对撞的双指针操作,思路其实差不多,也同样是选出一个pivot元素,然后将比它小的移到其左边,比他大的移到其右边。然后再分别递归处理其两侧分支,与二叉树的前序遍历本质上也是一样的,所以下面我们直接通过代码来详细解析下!
private static void quickSort(int[] array, int start, int end) {
// 当start索引大于等于end索引,说明此时该(子)序列已经排序完毕,直接返回
if (start >= end) {
return;
}
// left指针:可存放比pivot小的数的最新位置
// right指针:可存放比pivot大的数的最新位置
int left = start, right = end;
// pivot:这里选取了中间元素作为比较元素,以减小判断移动次数,提高效率
int pivot = array[(start + end) / 2];
while (left <= right) {
// 当left指针的数比pivot小,left指针右移,更新可存放比其小的数的位置
while (left <= right && array[left] < pivot) {
left++;
}
// 当right指针的数比pivot大,left指针左移,更新可存放比其大的数的位置
while (left <= right && array[right] > pivot) {
right--;
}
// 当left上的数比pivot大,right上的数比pivot小,则交换两者位置,然后更新两指针位置
if (left <= right) {
int temp = array[left];
array[left] = array[right];
array[right] = temp;
left++;
right--;
}
}
// 通过递归对其两侧子序列执行快速排序
quickSort(array, start, right);
quickSort(array, left, end);
}
OK~ 到这里快速排序原理以及代码的解析就全部结束啦~
最后让我们分析一下其复杂度:
快速排序的时间复杂度计算比较麻烦一些。从原理来看,如果我们选择的pivot每次都正好在中间,则效率达到最高,但这是无法保证的,所以我们由以下三个情况来分析:
⚪ 最坏的情况就是如果每次选择的恰好都是low结点作为pivot,而元素恰好都是逆序的,此时时间复杂度为O(n^2)
⚪ 最好的情况就是如果元素恰好都是有序的,则时间复杂度为O(n)
⚪ 折中的情况是每次选择的都是中间结点,此时两侧序列每次都是长度相等的序列,类似于折半,则此时的时间复杂度为O(nlogn))