一、基本思路
给定一个长度为 N 的数组,使用快速排序由小到大进行排序:选择数组中的某个元素作为“标尺”,把这个数组分成三部分。把小于该元素的数全部放到它的左边,大于它的数则放到他的右边,等于它的数放在中间,然后再对左右两边的元素进行同样的操作。
下面用一组图片来解释一下:
初始状态下,小于区域和大于区域都为空。假设我们选取数组的
最后一个元素作为“标尺”。
我们把数组的第一个元素(4)和标尺(3)进行比较
,4 > 3,把 4 和大于区域的前一个元素交换,大于区域长度加一,下标不动。完成这个步骤后的情况如下:
然后,再把当前元素(3)和标尺(3)进行比较,
相等,把下标右移一位,完成后情况如下:
再比较下标对应的元素(1)和标尺(3),
1 < 3,把小于区域的下一个元素和当前元素互换,小于区域的长度加一,下标右移一位,完成后情况如下:
同样,再把(6)和(3)进行比较,
6 > 3,把大于区域的前一个元素和当前元素(6)交换,大于区域长度加一,下标不动,完成后情况如下:
再把(2)和(3)比较,
2 < 3,把当前元素和小于区域下一个元素交换,下标右移一位,完成后情况如下:
最后,把(5)和(3)比较,
5 > 3,把大于区域的前一个元素和当前元素(5)交换,大于区域长度加一,下标不动。完成后情况如下:
此时,
当前元素的下标和大于区域的边界已经重合了,因此本轮排序结束。
最后,我们发现整个数组被标尺(3)分成了两个部分,我们只要再把大于区域和小于区域分别进行这样的操作,最后就能得到一个有序的数组了。
二、代码实现
实现代码做了一些小小的优化,建议看的时候可以自己写一个数组,按着代码的步骤一步步来,这样就可以明白有的地方为什么是那样写的了。
public static void quickSort(int[] arr, int l, int r) {
if(arr == null || arr.length < 2) return;
if(l < r) {
swap(arr, l + (int) (Math.random() * (r - l + 1)), r); // 随机快排
int[] p = partition(arr, l, r);
quickSort(arr, l, p[0]);
quickSort(arr, p[1], r);
}
}
public static int[] partition(int[] arr, int l, int r) {
int less = l - 1;
int more = r;
while(l < more) {
if(arr[l] < arr[r]) {
swap(arr, ++less, l++);
} else if(arr[l] > arr[r]) {
swap(arr, --more, l);
} else l++;
}
swap(arr, more, r);
return new int[] { less, more + 1 };
}
三、时间复杂度分析及经典快排存在的问题。
1.时间复杂度分析
上述排序方法,我们称之为经典快排。假如我们每次通过 partition 这一过程都能把原数组分成长度基本相等的大于区域和小于区域,那么算法的时间复杂度应为:O(N long2N)。
2.可能存在的问题
假设要排序的数组是一个已经按照升序排列的数组(例如【1, 2, 3, 4, 5】),我们选取数组的最后一个元素(5)作为标尺,那么进行 partition 后,我们把数组分成大于区域和小于区域,大于区域的长度为0,小于区域的长度为 N-1,然后又要对长度为 N-1 的数组进行 partition,此时,算法的时间复杂度就变成了O(N2)。显然这是一个特别差的事件复杂度。
3.如何优化?---- 随机快排
分析问题出现的原因,为什么会导致时间复杂度变高呢?因为数组不再是均等分了。那为什么会这样呢?因为标尺的选择不够好,我们选择了一个最大的数作为标尺。因此,我们在选择标尺时,可以在要进行 partition 的数组中,随机选择一个数作为标尺。
尽管这样,我们还是有可能选到最大的数作为标尺,因此,我们不能保证快速排序的时间复杂的一定是O(N lon2N),只能说在一般情况下是这样的。