算法描述(上)
在数据结构(严蔚敏版)是这样描述快速排序的:通过一趟排序使得数据分成两部分,其中一部分均比另外一部分小,接着就可以对这两部分继续进行排序,以达到整个序列有序。
其排序整体规则为:任意选取一个数,不过通常选取序列中第一个数作为枢轴(又可称为key或支点),将所有比key小的数放在它的位置之前,所有比key大的数放在它之后,划分为两个子序列,这样就完成了一趟快速排序。接下来需要将两个子序列的第一个数分别作为它们新的key值,重复之前的排序操作,直至整个序列有序时算法结束。
那么如何实现所有比key小的数放在它的位置之前,所有比key大的数放在它之后呢?
需求为从小到大排列。另 i 指向第一个数并作为key,另 j 指向最后一个数。先从 j 开始 ,j 向左走直至找见一个比key小的数,若找见这样的数 j 就停下来并交换 i 和 j 指向的数字。交换完毕后,i向右走,若找到比key大的数,i 停下来,交换 i 和 j 指向的数字。
注意每次 i 和 j 移动时都要查看 i 和 j 是否碰头,如果碰头,这一轮快速排序结束,开始进行下一轮
下图是第一趟快速排序过程:
比如对于上图中,另 i 指向49且另key = 49,j 指向27。27比49小,所以 j 就不向左走了,而是交换 i 和 j 指向的值,接着 i 向右走,发现65比49大,i 停下来与 j 互换值。接下来在第三行中,j 向左走发现13比49小,交换13和49,i 向右走发现97比49大,交换97和49,j 接着向左走,最后 i 和 j 碰头了,第一轮结束。
注意上面只是进行了第一趟排序,第一趟排序结果为27 38 13 49 76 97 65
接下来需要对key即49左边的序列和49右边的序列进行同样的处理,左边的序列需要另 i 指向27并作为新的key,j 指向13;右边的序列需要 i 指向76并作为新的key,j 指向末尾65。需要递归地进行这样的操作直至有序为止。
如果使用伪代码,第一趟排序的算法可以这样描述
//条件这里也可以写成i!=j
while(i<j){
while( i < j && L[j].value > key){
j--;
}
swap(L[i].value,L[j].value);
while( i < j && L[i].value < key){
i++;
}
swap(L[i].value,L[j].value);
}
接下来只要使用递归就可以完整实现算法,不过我们先来看算法的改进
算法描述(下)算法的进一步改进
事实上,在 i 和 j与key值比较并移动的过程中,只有当 i 和 j 碰头时 (此时 i== j ) ,我们才需要将key交换到 i和 j 碰头的那个位置,而不需要反复交换key的位置。
具体实现还是让 j 先开始走,找到比key小的数,j 就停下来,接着从 i开始走,找到比key大的数,i 就停下来。接着交换 i 和 j 的值。交换完毕后,j 还是继续向左走,重复该过程直至 i 和 j 碰头。
在上图中第三行,因为 i 和 j 碰头了,所以要将key指向的值49和 i(j)指向的值13互换。
可以看到改进后的算法速度更快,而且同样实现了key左边的数比key小,key右边的数比key大的目的。
注意上图中仅仅是第一轮快速排序,如果想要整个序列有序,需要借助递归及分治的思想。用同样的方式分别处理key左边和key右边的数。
java快速排序算法实现
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
System.out.println(Arrays.toString(arr));
quickSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr,int left,int right) {
if(left>right) {
/*
*这里解释一下:例如只有一个数的排序序列,此时i==key==j==1,
*当对key左边的序列调用quickSort(arr, left, i-1);时 i>j,即表示key的左边没有元素,应当返回;
*对右边的序列调用递归quickSort(arr, i+1, right);时 i>j,即表示key右边没有元素,应当返回;
*即只要排序序列中key的左边或右边没有元素时,应当返回
*如果看不懂这里的解释可以先往下看,最后再看这里。
*/
return;
}
//key存放的就是基准数,注意这里的key并不是一成不变的,指的是每个序列中的第一个元素作为Key
int key = arr[left];
int i = left;
int j = right;
while(i!=j) {
//注意这里别忘了i<j,当j向左走时,可能会与i碰头,这时应提前结束循环
//如果想改成从大到小排,只需改动下面2处,arr[j]<=key以及arr[i]>=key
//只要j指向的值比Key小,j 就不再做--操作
while(arr[j]>=key && i<j) {
j--;
}
//只要i指向的值比Key大,i 就停下来
while(arr[i]<=key && i<j) {
i++;
}
/*
* 这里也别忘了判断条件,当i和j没有相遇即i<j时,应交换它们对应的值
* 直到交换完毕后i和j再接着走。若是已经碰头,i和j的值交换已经没有意义,
* 而是需要退出循环,与基准数进行交换
*/
if(i<j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
/*思考下面的i能换成j吗?答案是可以的。
*由于在循环内部的条件i<j以及外部条件i!=j,所以当退出while循环后,
*此时的i必定和j是碰头的,即i==j
*/
arr[left] = arr[i];
arr[i] = key;
quickSort(arr, left, i-1);//处理左边的数
quickSort(arr, i+1, right);//处理右边的数
return;
}
}
其他
- 在算法中,我们都是先让 j 开始走,那么可以先让 i 开始走吗?答案是可以的,只需将key定为每个序列中的最后的数字即可。
- 快速排序时间复杂度O(nlogn)
- 快速排序稳定性:不稳定
什么叫稳定性?例如对于用户录入数据39 38 38,后面两个的关键字都是38,为了区分它们,人为给最后一个数上面加上一横(这也是为什么有些快速排序书上的数字要加一横的原因)
若经过排序,这些关键字相同的数字的相对顺序不变,就称这个算法是稳定的。例如只要排序后38还是相对 38’ 在前(不一定紧挨),算法就是稳定的。
排序过程为:因为 j 指向的 38’ 比key小,j停下来, i 开始向右走,直至i 和 j 碰头,交换key和 i 的值 39 和 38’。再对39左递归,由于两个数关键字相同,无需改变顺序,最终结果如下:
显然 38’ 和 38 的相对位置发生了改变