数据结构与算法
- 快速排序为应用最多的排序算法,因为快速二字而闻名。快速排序和归并排序一样,采用的都是分治思想。
- 分治法的基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。
- 我们只需关注最小问题该如何求解,和如何去递归既可以得到正确的算法实现。
- 快速排序可以分为:单路快速排序,双路快速排序,三路快速排序,他们区别在于选取几个指针来对数组进行遍历下面我们依次来讲解。
1. 单路快速算法
1.1 单路快速算法的思想:
首先我们选取数组中的一个数,将其放在合适的位置,这个位置左边的数全部小于该数值,这个位置右边的数全部大于该数值 。
- 假设数组为 arr[l...r] 假设指定数值为数组第一个元素 int v = arr[l],假设 j 标记为比 v 小的最后一个元素, 即 arr[j+1] > v。当前考察的元素为 i 则有arr[l + 1 ... j] < v , arr[j+1,i) >= v 如上图所示。
- 假设正在考察的元素值为 e ,e >= v 的时候我们只需交将不动,直接 i++ 去考察下一个元素,
- 当e < v 由上述假设我们需要将 e 放在
- 最后一个元素考察完成以后,我们再讲 arr[l]和 arr[j]调换一下位置就可以了。
- 上述遍历完成以后 arr[l + 1 ... j] < v , arr[j+1,i) >= v 就满足了,接下来我们只需要递归的去考察 arr[l + 1 ... j] 和 arr[j+1,r] 即可。
1.2 单路快速排序的 Java 实现:
public static void quickSortOneWay(int[] arr, int l, int r) {
if (l >= r) {
return;
}
int p = partition(arr, l, r);
quickSortOneWay(arr, l, p - 1);
quickSortOneWay(arr, p + 1, r);
}
private static int partition(int[] arr, int l, int r) {
// 为了提高效率,减少造成快速排序的递归树不均匀的概率,
// 对于一个数组,每次随机选择的数为当前 partition 操作中最小最大元素的可能性为 1/n
int randomNum = (int) (Math.random() * (r - l + 1) + l);
swap(arr, l, randomNum);
//将小于v的数据放在索引为j的位置
int v = arr[l];
int j = l;
for (int i = l + 1; i <= r; i++) {
if (arr[i] < v) {
swap(arr, j + 1, i);
j++;
}
}
swap(arr, l, j);
return j;
}
//交换位置
private static void swap(int[] arr, int l, int r) {
int temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
}
对于上述算法中为什么选取了当前排序数组中随机一个元素进行比较,假设我们在考察的数组已经为已经排序好的数组,那么我们递归树就会向右侧延伸 N 的深度,这种情况使我们不想要看到的,如果我们每次 partition 都随机从数组中取一个数,那么这个数是当前排序数组中最小元素可能性为 1/n 那么每次都取到最小的数的可能性就很低了。
2双路快速排序
2.1 双路快速排序算法思想:
- 跟单路一样,双路快速排序,同样选择数组的第一个元素当做标志位(经过随机选择后的)
- 双路快速排序要求有两个指针,指针 i j 分别指向 l+1 和 r 的位置然后两者同时向数组中间遍历 在遍历过程中要保证arr[l+1 ... i) <= v, arr(j....r] >= v 因此我们可以初始化 i = l+1 以保证左侧区间初始为空,j = r 保证右侧空间为空
- 遍历过程中要 i <= r 且 arr[i] <= v 的时候 i ++ 就可以了
当 arr[i] > v 时表示遇到了 i 的值大于 v 数值 此刻能等待 j 角标的值,从右向左遍历数组 当 arr[i] < v 表示遇到了 j 的值小于 v 的元素,它不该在这个位置呆着, - 得到了 i j 的角标后 先要判断是否到了循环结束的时候了,即 i 是否已经 大于 j 了。
- 否则 应该讲 i 位置的元素和 j 位置的元素交换位置,然后 i++ j-- 继续循环
-
遍历结束的条件是 i>j 此时 arr[j]为最后一个小于 v 的元素 arr[i] 为第一个大于 v 的元素 因此 j 这个位置 就应该是 v 所应该在数组中的位置 因此遍历结束后需要交换 arr[l] 与 arr[j]
2.2 双路快速排序的 Java 实现:
public static void quickSortOneWay(int[] arr, int l, int r) {
if (l >= r) {
return;
}
int p = partition(arr, l, r);
quickSortOneWay(arr, l, p - 1);
quickSortOneWay(arr, p + 1, r);
}
private static int partition(int[] arr, int l, int r) {
// 为了提高效率,减少造成快速排序的递归树不均匀的概率,
// 对于一个数组,每次随机选择的数为当前 partition 操作中最小最大元素的可能性为 1/n
int randomNum = (int) (Math.random() * (r - l + 1) + l);
swap(arr, l, randomNum);
//将小于v的数据放在索引为j的位置
int v = arr[l];
int i = l;
int j = r;
while (true) {
while (i < r && arr[i] <= v) i++;
while (j > l + 1 && arr[j] >= v) j--;
if (i > j) {
break;
}
swap(arr, i, j);
i++;
j--;
}
swap(arr, l, j);
return j;
}
//交换位置
private static void swap(int[] arr, int l, int r) {
int temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
}
3 三路快速排序
3.1 三路快速排序算法思想
上述两种算法我们发现对于与标志位相同的值得处理总是,做了多余的交换处理,如果我们能够将数组分为> = <三部分的话效率可能会有所提高。
- 我们将数组划分为 arr[l+1...lt]
v三部分 其中 lt 指向 < v 的最后一个元素前一个元素,gt 指向>v的第一个元素的前一个元素,i 为当前考察元素 - 定义初始值得时候依旧可以保证这初始的时候这三部分都为空 int lt = l; int gt = r; int i = l + 1;
- 当 e > v 的时候我们需要将 arr[i] 与 arr[gt-1] 交换位置,并将 > v 的部分扩大一个元素 即 gt-- 但是此时 i 指针并不需要操作,因为换过过来的数还没有被考察。
- 当 e = v 的时候 i ++ 继续考察下一个
- 当 e < v 的时候我们需要将 arr[i] 与 arr[lt+1] 交换位置
-
当循环结束的时候 lt 位于小于 v 的最后一个元素位置所以最后我们需要将arr[l] 与 arr[lt] 交换一下位置。
3.2 三路快速排序 Java 代码实现:
@Override
public void quickSort(int[] arr, int left, int right) {
if (left >= right) {
return;
}
// 为了提高效率,减少造成快速排序的递归树不均匀的概率,
// 对于一个数组,每次随机选择的数为当前 partition 操作中最小最大元素的可能性 降低 1/n!
int randomNum = (int) (Math.random() * (right - left + 1) + left);
swap(arr, left, randomNum);
int v = arr[left];
// 三路快速排序即把数组划分为大于 小于 等于 三部分
//arr[l+1...lt] v 三部分
// 定义初始值得时候依旧可以保证这初始的时候这三部分都为空
int leftEnd = left;
int rightStart = right;
int i = left + 1;
while (i <= rightStart) {
if (arr[i] < v) {
swap(arr, i, leftEnd + 1);
i++;
leftEnd++;
} else if (arr[i] == v) {
i++;
} else {
swap(arr, i, rightStart);
rightStart--;
//i++ 注意这里 i 不需要加1 因为这次交换后 i 的值仍不等于 v 可能小于 v 也可能等于 v 所以交换完成后 i 的角标不变
}
}
swap(arr, left, leftEnd);
leftEnd--;
quickSort(arr, left, leftEnd);
quickSort(arr, rightStart, right);
}
4 快速排序时间复杂度空间复杂度
4.1 时间复杂度
- 在最优的情况下,快速排序算法的时间复杂度为O(nlogn)。
- 在最坏的情况下,待排序的序列为正序或者逆序最终其时间复杂度为O(n2)。
- 平均的情况,设枢轴的关键字应该在第k的位置(1≤k≤n),那么:由数学归纳法可证明,其数量级为O(nlogn)。
4.2 空间复杂度
就空间复杂度来说,主要是递归造成的栈空间的使用
- 最好情况,递归树的深度为log2n,其空间复杂度也就为O(logn)
- 最坏情况,需要进行n‐1递归调用,其空间复杂度为O(n)
- 平均情况,空间复杂度也为O(logn)。
可惜的是,由于关键字的比较和交换是跳跃进行的,因此,快速排序是一种不稳定的排序方法。
参考
搞懂基本排序算法
快速排序最好,最坏,平均复杂度分析