在之前分享过关于排序算法的学习笔记,里面有提及快速排序。但是在之后发现之前提供的不过是普通快排算法,而在面对一些特殊情况时(例如序列原本就有序、有大量重复元素等等)会进行很多完全不必要的操作,耗费大量时间。为此,学习了一下基于普通快速排序算法一步步进行优化。为了方便阅读,我还是把普通快排的代码放出来吧。
public class qSort {
public static void qSort(int[] arr,int low,int high){
int i,j,temp,t;
if(low>=high){//当它俩指向同一个元素时表示探测停止
return;
}
i=low;
j=high;
//temp就是基准位
temp = arr[low];
while (i<j) {
//先看右边,依次往左递减
while (temp<=arr[j]&&i<j) {
j--;
}
//再看左边,依次往右递增
while (temp>=arr[i]&&i<j) {
i++;
}
//如果满足条件则交换
if (i<j) {
t = arr[j];
arr[j] = arr[i];
arr[i] = t;
}
}
//最后将基准为与i和j相等位置的数字交换
arr[low] = arr[i];
arr[i] = temp;
//递归调用左半数组
quickSort(arr, low, j-1);
//递归调用右半数组
quickSort(arr, j+1, high);
}
public static void main(String[] args){
int[] arr = new int [1000];
int n;
Scanner in=new Scanner(System.in);
n=in.nextInt();
for(int i=0;i<n;i++)
{
arr[i]=in.nextInt();
}
quickSort(arr, 0, n-1);
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}
随机化
如果永远取第一个元素作为基准数的话,在数组已经有序的情况下每次划分都将得到最坏的结果。
然而,我们可以通过随机选取基准数元素来打破这种固定模式,这样每次都是最坏划分的概率就非常小了。实现起来只需要先将随机选中的元素和第一个元素交换一下位置作为枢轴元素,然后就可以接着用原来的方法进行排序了。
public static void qSort(int a[], int low, int high)
{
if (low >= high)
return;
int i = low, j = high,t;
Random rand=new Random();
int temp = rand.nextInt(high-low+1)+low;//赋值一个low和high范围内的随机数下标
//将随机数和最左边的数交换得到一个随机的基准数
t=a[temp];
a[temp]=a[low];
a[low]=t;
while (i < j)
{
while (i < j && a[j] >= a[low])
j--;
while (i < j && a[i] <= a[low])
i++;
if (i<j) {
int k=a[i];
a[j]=a[i];
a[i]=k;
}
}
a[low]=a[i];
a[i]=t;
qSort(a, low, j-1);
qSort(a, j+1, high);
}
小区间插入排序
快速排序适用于非常大的数组的解决办法, 那么相反的情况,如果数组非常小,其实快速排序反而不如直接插入排序来得更好(直接插入是简单排序中性能最好的)。其原因在于快速排序用到了递归操作,在大量数据排序时,这点性能影响相对于它的整体算法优势是可以忽略的,但如果数组只有几个记录需要排序时,这就成了大材小用。所以当序列长度分割到足够小后,继续使用快速排序递归分割的效率反而没有直接插入排序高。因此我们可以增加一个判断,当区间长度小于10以后改为使用插入排序。
public static void qSort(int a[], int low, int high)
{
if (low >= high)
return;
if (high-low+1<10) {
for (int i = low+1; i <=high; i++) {
for (int j = i; j >0&&a[j]<a[j-1]; j--) {
int c=a[j];
a[j]=a[j-1];
a[j-1]=c;
}
}
return;
}//insertSort
int i = low, j = high,t;
Random rand=new Random();
int temp = rand.nextInt(high-low+1)+low;//赋值一个low和high范围内的随机数下标
//将随机数和最左边的数交换得到一个随机的基准数
t=a[temp];
a[temp]=a[low];
a[low]=t;
while (i < j)
{
while (i < j && a[j] >= a[low])
j--;
while (i < j && a[i] <= a[low])
i++;
if (i<j) {
int k=a[i];
a[j]=a[i];
a[i]=k;
}
}
a[low]=a[i];
a[i]=t;
qSort(a, low, j-1);
qSort(a, j+1, high);
}
聚拢重复元素
这种方法的主要思想是,在j向前扫描的过程中,每次遇到和基准数相同的元素,就将其与前方第一个异于基准数的元素交换位置,然后继续原本的工作。把重复的元素都聚拢到了中间。这时我们再想办法把基准数也加入到这个重复序列中,然后就不必继续向中间扫描了,直接以这个重复序列的两端作为分割线即可。同理,i向后扫描的过程中也可以运用这种思想。思路应该还是比较好理解的,但是具体实现起来有些麻烦。
public static void qSort(int a[], int low, int high)
{
if (low >= high)
return;
if (high-low+1<10) {
for (int i = low+1; i <=high; i++) {
for (int j = i; j >0&&a[j]<a[j-1]; j--) {
int c=a[j];
a[j]=a[j-1];
a[j-1]=c;
}
}
return;
}
int i = low, j = high,t,k,flag=0;
Random rand=new Random();
int temp = rand.nextInt(high-low+1)+low;//赋值一个low和high范围内的随机数下标
//将随机数和最左边的数交换得到一个随机的基准数
t=a[temp];
a[temp]=a[low];
a[low]=t;
while (i < j)
{
while (i < j && a[j] >= a[low]) {
if(a[j]==a[low]) {//如果当前扫描到的元素等于基准数
for (k= j-1; k >i; k--) {//继续向前找第一个和基准数不同的元素
if (a[k]!=a[j]) {
int swap=a[k];
a[k]=a[j];
a[j]=swap;
break;
}
}
if (k==i) {//没找到,i和j之间此时都是重复元素
//把放在最左边的基准数加进来
if (a[low]>=a[i]) {
int swap=a[low];
a[low]=a[i];
a[i]=swap;
}else {
int swap=a[i];//较大的a[i]先和a[j]换到重复序列右端
a[j]=a[i];
a[i]=swap;
swap=a[low];//此时a[i-1]一定比基准数小
a[low]=a[i-1];
a[i-1]=swap;
//调整i,j的位置
i--;
j--;
}
flag=1;//标记一下,表明聚拢操作完成
break;
}
else continue;
}
j--;
}
if (flag==1) {//如果聚拢已完成,则直接跳出大循环进行分割,i无需再向后扫描
break;
}
while (i < j && a[i] <= a[low]) {
if (a[i] == a[low] && i != low) //增加i!=low条件以跳过枢轴元素本身
{
for (k = i+1; k < j; k++)
{
if (a[k] != a[i])
{
int swap=a[k];
a[k]=a[i];
a[i]=swap;
break;
}
}
if (k == j)
{
//这里比j向前扫描对应的地方简单一些,因为a[j]一定小于枢轴元素,无需分情况讨论
int swap=a[low];
a[low]=a[j];
a[j]=swap;
flag = 1;
break;
}
else continue;
}
i++;
}
if (flag==1) break;
if (i<j) {
int c=a[i];
a[j]=a[i];
a[i]=c;
}
}
a[low]=a[i];
a[i]=t;
qSort(a, low, j-1);
qSort(a, j+1, high);
}