快速排序是目前使用最广的排序算法,Java的默认排序方法就是快速排序,其特点为原地排序(不需要辅助数组,节省了空间);而且具有较为优秀的排序时间复杂度 N l o g N NlogN NlogN。
快排的思路
快速排序是一种分治思想的递归排序算法,它每次排序都把整个数组划分为两个子数组,然后递归的对数组进行排序,最终使递归下面的全部子数组排序,从而让整个数组有序。
常规思路:
-
以数组第一个元素为切分元素。
-
采用双指针,从数组的左边开始寻找大于等于切分元素的值,从数组的右边开始寻找小于等于切分元素的值,并且保证左指针小于右指针,然后交换两个值。
-
循环进行步骤2,直到两个指针碰撞,这样就保证了指针碰撞处左边所有元素小于切分元素,所有右边元素大于切分元素,然后将指针碰撞处的元素和第一个元素交换。
-
重复递归1,2,3过程,最终保证整个数组有序。
快排实现
public void quickSort(int[] a,int s,int e){
// 首先设立终止条件,判断起始点是否小于终止点,如果起始点大于等于终止点则直接返回
if (s >= e) return;
//取切分元素
int index = a[s];
//定义左右指针
int low = s;
int high = e;
// 循环判断左指针和右指针的大小关系
while (low < high){
//循环寻找右边小于切分元素的值
while (low < high && a[high] >= index){
high--;
}
//最开始的左指针指向第一个元素,因此可以将之前寻找到的右边小于切分元素的值与左指针元素互换,此时有两个重复的元素,去掉了切分元素,后面会加进数组。
if (low < high && a[high] < index){
a[low++] = a[high];
}
//循环寻找左边大于切分元素的值
while (low < high && a[low] <= index){
low++;
}
// 将原本重复的数替换为找到的左边大于切分元素的值,这样就又多了一个重复元素。
if (low < high && a[low] > index){
a[high--] = a[low];
}
}
// 当执行完上面循环后,一次递归就接近结束了,最后一次递归时数组中左右指针中的一个指向重复的值,另一个指针向这个指针靠近,但是由于两个指针碰撞跳出循环,所以此时两个指针都指向重复值。
a[low] = index;
//将重复值替换为切分元素后,进行递归
quickSort(a,s,low-1);
quickSort(a,low+1,e);
}
找到一个动图可以很形象的形容快排过程
算法改进
切分元素的随机选择
快排最糟糕的情况是需要进行递归 N − 1 N-1 N−1次时间复杂度为 O l o g ( N 2 ) Olog(N^2) Olog(N2),为了避免这种情况,我们需要优化切分元素的选择,加入随机性。
int index = a[new Random().nextInt(e-s) + s];
经过测试
public static void main(String[] args){
test t = new test();
int[] a = new int[10000];
for (int i = 0;i < 10000;i++){
a[i] = 10000 - i;
}
long st = System.currentTimeMillis();
t.quickSort(a,0,a.length-1);
long et = System.currentTimeMillis();
System.out.println(et - st);
}
在如上糟糕的初始数组中进行排序,如果不适用随机选取切分元素的方法,得到需要用时平均为39ms,而采用随机选择切分元素时,只需要平均2ms。
三向切分
当我们需要排序的数组中有大量的重复元素时,我们实现在快速排序在递归时会遇到大量的重复子数组,因此为了优化这种情况,我们对算法进行切分。
先随机选取一个切分元素,然后把数组切成大于,等于,小于这个元素的三个部分,一次递归可以排序好所有等于切分元素的值,然后单独排序小于切分元素的和大于切分元素的值。
修改为如下代码:
public void quickSort(int[] a,int s,int e){
if (s >= e) return;
// int index = a[s];
int index = a[new Random().nextInt(e-s) + s];
int i = s+1;
//此处左右指针用来指示小于和大于切分元素的值的区间
int low = s;
int high = e;
//循环判断条件为遍历指针不能超过右指针,因为要保证右边全是大于切分元素的值
while (i <= high){
// 当遍历到的元素大于切分元素就与右指针指向的值换位置,反之则与左指针指向的值换位置,
//但是由于是从左开始遍历,
//因此如果和左指针互换的话必须左指针和遍历指针同时增加,要保证左指针和遍历指针不会重叠。
//如果是和切分元素相等,则遍历指针增加,因为后面这个相同的值会被左指针指到,然后被换到遍历指针的地方。
//而被换到左指针的值必定是小于切分元素的值,这样就将与切分元素相等的值都集中到了中间部分。
if (i <= high && a[i] > index){
int tmp = a[high];
a[high--] = a[i];
a[i] = tmp;
}else if (i <= high && a[i] < index){
int tmp = a[low];
a[low++] = a[i];
a[i++] = tmp;
}else if (i <= high && a[i] == index){
i++;
}
}
//递归左边小于切分元素的数组和右边大于切分元素的数组。
quickSort(a,s,low-1);
quickSort(a,low+1,e);
}
使用这样的代码进行测试:
public static void main(String[] args){
test t = new test();
int[] a = new int[10000];
for (int i = 0;i < 2000;i++){
a[i] = 5444;
}
for (int i = 2000;i < 5000;i++){
a[i] = 333;
}
for (int i = 5000;i<10000;i++){
a[i] = 556;
}
long st = System.currentTimeMillis();
t.quickSort(a,0,a.length-1);
long et = System.currentTimeMillis();
System.out.println(et - st);
}
发现对数组进行三向切分后的算法,运行时间平均为1ms,而不加入三向切分则需要用时44ms。