快速排序
快速排序使用了分治法策略,把一个数组分成两个数组,对数组arr[p…r]进行快速排序的三步分治过程:
分解:数组被分成两个(可能为空)子数组arr[p…q-1]跟arr[q+1…r],其中arr[p…q-1]的所有元素都小于等于arr[q],arr[q+1…r]中的所有元素都大于arr[q],并计算出下标q的数值。
解决:通过递归调用快速排序,对子数组arr[p…q-1]跟arr[q+1…r]进行排序。
合并:并不需要合并,因为都是对数组本身排序的,数组arr[p…r]已经有序。
先进行算法的分解步骤:
选择数组的其中一个元素为基准(这里选择最后一个元素),使用一个数来记录最靠左(也就是下标最小的)比基准大的数的下标,用index表示,它的初始值为数组的开始下标,然后从左到右地把数组的元素跟基准进行比较,如果比基准大的话,不做任何处理,继续比较下一个数,当出现小于基准的数时,把该数跟arr[index]进行交换,然后index加1,重复该过程,直到把数组所有元素(除了基准数)都跟基准比较完,把arr[index]跟基准互换,返回index。
举个例子:
以数组{42,1,27,33,11,33,26,1,43,28}为例:
基准数reference=28,index=0,首先数组的第一个元素跟28比较,42>28,不做处理,继续比较第二个数,1<28,此时index=0,42跟1交换,然后index=1,得到:
继续下一个数,27<28,42跟27交换,index=2,得到
继续下一个数,33>28,不做处理,继续下一个数与28比较,11<28,此时index=2,42与11交换,index=3,得到:
重复该过程,得到:
此时arr[index]是比基准大,且是最靠左的数,把arr[index]跟基准互换,得到:
就这样,把比28小的放到28前面,大的放到28后面,把原来的数组分成{1,27,11,26,1}跟{33,42 ,43,33}两个子数组。
该过程的Java代码实现:
public static int partition(int[] arr, int left, int right){
int reference = arr[right];
int index = left;
for (int i = left; i < right; i++){
if (arr[i] < reference){
int temp = arr[i];
arr[i] = arr[index];
arr[index] = temp;
index = index + 1;
}
}
arr[right] = arr[index];
arr[index] = reference;
return index;
}
对于规模为n的数组,for循环需要循环n-1次,最坏的情况下进行n-1次数组元素的互换,其他语句需要常数的时间,杂度为使用partitio解决间复程n)。
接下来是解决的过程,递归地的调用快速排序,Java代码为:
public static void quickSort(int[] arr, int start, int end)
{
if (start < end)
{
int q = partition(arr, start, end);
quickSort(arr, start, q - 1);
quickSort(arr, q + 1, end);
}
}
最终的Java代码为:
public static void quickSort(int[] arr, int left, int right)
{
if (left < right)
{
int reference = arr[right];
int index = left;
for (int i = left; i < right; i++){
if (arr[i] < reference){
int temp = arr[i];
arr[i] = arr[index];
arr[index] = temp;
index = index + 1;
}
}
arr[right] = arr[index];
arr[index] = reference;
quickSort2(arr, left, index - 1);
quickSort2(arr, index + 1, right);
}
}
- 时间复杂度
开始排序的运行时间取决于基准元素对数组划分是否平衡,如果划分是平衡的,那么快速排序的时间复杂度跟归并排序一样是O(nlog2n),而越不平衡,算法的性能就越接近插入排序,而插入排序的时间复杂度为O(n平方)。如果每次划分都是最坏的情况的话,也就是划分出来的两个子数组分别为n-1个和0个,此时快速排序的时间复杂度为O(n平方)。
如果用快速排序来排序已经有序的数组,时间复杂度也仍然为O(n平方),以上的实现对此情况的表现非常差,因为基准已经是最大的数,前面的数都是小于等于基准的数,其中小于基准的数都要与自身交换。 - 空间复杂度
快速排序具有空间原址性(任何时候都只需要常数个额外的元素空间存储临时的数据)。
测试:
接下来分别使用10000,100000,500000个随机数进行测试:
当然,还有其它的实现方式:
public static void quickSort(int[] arr, int left, int right){
if (left < right){
int i = left, j = right, x = arr[right];
while (i < j){
while (i < j && arr[i] < x)
i++;
if (i < j)
arr[j--] = arr[i];
while (i < j && arr[j] >= x)
j--;
if (i < j)
arr[i++] = arr[j];
}
arr[i] = x;
quick_sort(arr, left, i - 1);
quick_sort(arr, i + 1, right);
}
}
参考C#Array.Sort()的快速排序的实现:
public static void quickSort(int[] keys, int left, int right){
do{
int a = left;
int b = right;
int num3 = keys[right];
do{
while (keys[a] < num3){
a++;
}
while (num3 < keys[b]){
b--;
}
if (a > b){
break;
}
if (a < b){
int local2 = keys[a];
keys[a] = keys[b];
keys[b] = local2;
}
a++;
b--;
}
while (a <= b);
if ((b - left) <= (right - a)){
if (left < b){
quickSort(keys, left, b);
}
left = a;
}else{
if (a < right){
quickSort(keys, a, right);
}
right = b;
}
}
while (left < right);
}
在测试时,使用的是随机的数组,但在实际工作中,输入的数据的所有排列并不一定是等概率的, 通过在算法中引入随机性,从而使得算法对所有的输入都能获得较好的期望性能,快速排序的随机化版本比较适合大数据情况下的排序算法,Java代码实现:
public static void quickSort_random(int[] arr, int left, int right){
if (left < right){
int random = (int)((Math.random()*(right - left + 1)) + left);
int reference = arr[random];
arr[random] = arr[right];
arr[right] = reference;
int index = left;
for (int i = left; i < right; i++){
if (arr[i] < reference){
int temp = arr[i];
arr[i] = arr[index];
arr[index] = temp;
index = index + 1;
}
}
arr[right] = arr[index];
arr[index] = reference;
quickSort_random(arr, left, index - 1);
quickSort_random(arr, index + 1, right);
}
}