排序算法介绍

在介绍排序之前,首先来介绍几个在每个排序算法中都需要使用的方法,我将它们单独写在了一个类中,它们分别是 less(), exchange(), isSort() 方法,less() 方法用于判断两个元素的大小, exchange() 用于交换两个元素的位置,isSort() 方法用于判断当前数组是否有序,它们的实现如下所示

package com.sort;

@SuppressWarnings("rawtypes")
public class Sort {

    @SuppressWarnings("unchecked")
    public static boolean less(Comparable a, Comparable b) {
        return a.compareTo(b) < 0;
    }

    public static void exchange(Comparable[] a, int i, int j) {
        if (i == j)
            return;
        Comparable t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

    public static boolean isSorted(Comparable[] a) {
        for (int i = 1; i < a.length; i++) {
            if (less(a[i], a[i - 1])) {
                return false;
            }
        }
        return true;
    }
}

这里之所以将数据类型设置为 Comparable 是为了兼容更多种类型的数据,比如这样写,我们可以对任何实现了 Comparable 接口的数据进行操作,比如系统系统的 Integer,Double,String 等。如果我们想要对自定义的数据进行操作,那么就实现 Comparable 接口并且重写 compareTo() 方法,定义自己的判断规则即可。

选择排序

这种排序是最简单的排序算法之一,另一个是下面将要介绍的插入排序。选择排序的基本思想是:首先找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置。再次,在剩下的元素中找到最小的元素,将它和第二个元素交换位置。如此往复,直到整个数组排序。

对于一个长度为 N 的数组,选择排序需要大约 N^2/2 次比较 和 N 次交换,所以说它的执行时间总是和 N^2 成正比的。

这种算法有两个特点
1. 运行时间和输入无关。也就是说我们为了找出最小的那个元素而遍历一遍数组并没有为下次扫描提供任何有用的信息。这也就意味着,一个已经有序的数组或者主键全部相等的数组和一个元素随机排列的数组所用的排序时间竟然一样长。
2. 数据的移动是最少的。因为我们每次交换都会改变两个元素的值,所以选择排序只会进行 N 次交换,交换次数和数组的大小呈现线性关系,这个特征是选择排序独有的,其他排序算法都不具备此特性。

下面来看看选择排序的实现

public final class SelectionSort {
    @SuppressWarnings("rawtypes")
    public static void sort(Comparable[] a) {
        if (Sort.isSorted(a)) {
            return;
        }
        int length = a.length;
        for (int i = 0; i < length; i++) {
            int min = i;
            for (int j = i + 1; j < length; j++) {
                if (Sort.less(a[j], a[min])) {
                    min = j;
                }
            }
            if (min != i) {
                Sort.exchange(a, i, min);
            }

        }
    }
}

插入排序

插入排序和选择排序的共同点是,当前索引的左边的所有元素都是有序的,但是不同的是插入排序只对当前索引左边的值进行操作,而选择排序是对索引右边的数进行操作。

在性能上,插入排序所需要的时间取决于输入元素的初始顺序,如果一个很大而且其中的元素已经基本有序的数组,插入排序所需的时间会非常短。在以下几种情况下,插入排序是一个非常高效的算法

  1. 数组中每个元素距离它的最终位置都不远
  2. 一个有序的大数组接一个小数组
  3. 数组中只有那么几个元素位置不正确

关于选择排序和插入排序,如果是针对一个随机的数组,那么这两种排序算法所需要的时间之比是一个很小的常数,如果是针对部分有序的数组,差距将会更加明显。

插入排序的实现

package com.sort;

public class InsertSort {

    @SuppressWarnings("rawtypes")
    public static void sort(Comparable[] a) {

        int length = a.length;

        for (int i = 1; i < length; i++) {
            for (int j = i; j > 0 && Sort.less(a[j], a[j - 1]); j--) {
                Sort.exchange(a, j, j - 1);
            }
        }
    }

}

希尔排序

希尔排序是基于插入排序的,它是一种快速的插入排序。想象一下,对于一个很大的数组,最小的元素在数组的末尾,如果使用插入排序,它将一步步的移动到数组的开头,这是十分消耗时间的。所以希尔排序主张将数组中任意间隔为 h 的元素排成有序,称之为 h 有序数组,换言之就是将所有间隔为 h 的元素组成一个小的数组,如果 h 很大,那么我们一次就可以把较小的元素移动到很远的地方,而不是一步步的移动。

希尔排序高效的原因是它权衡了子数组的规模和有序性。当然希尔排序的性能还与 h 有关,对于不同的 h 表现出不同的速度,但是无法确定哪一种是最好的,所以这里选取 h 从 N/3 递减至 1.

希尔排序的运行时间是达不到 平方级别的,它在最坏的情况下 和 N^3/2 成正比。

希尔排序的实现

package com.sort;

public class ShellSort {

    @SuppressWarnings("rawtypes")
    public static void sort(Comparable[] a) {

        int length = a.length;
        int h = 1;

        while (h < length / 3)
            h = 3 * h + 1;

        while (h >= 1) {
            for (int i = h; i < length; i++) {
                for (int j = i; j >= h && Sort.less(a[j], a[j - h]); j -= h) {

                    Sort.exchange(a, j, j - h);
                }
            }
            h = h / 3;
        }
    }
}

归并排序

实际上,当问题的规模到达一定的程度,上面的算法就已经无能为力了,尤其是 插入排序和选择排序,所以下面来介绍归并排序。

归并排序的主要思想是将一个数组分割成两部分,分别进行排序,然后将结果归并起来.

归并排序的优点是能够保证将任意长度为 N 的数组排序时间和 NlogN 成正比,缺点是他所需的 额外空间和 N 成正比,也就是说排序需要借助于辅助空间。

下面来看看代码

package com.sort;

public class MergeSort {
    private static Comparable[] aux;

    public static void sort(Comparable[] a) {
        aux = new Comparable[a.length];
        sort(a, 0, a.length - 1);
    }

    private static void sort(Comparable[] a, int low, int high) {
        if (high <= low)
            return;
        int mid = low + (high - low) / 2;

        sort(a, low, mid);
        sort(a, mid + 1, high);

        merge(a, low, mid, high);
    }

    private static void merge(Comparable[] a, int low, int mid, int high) {
        int i = low;
        int j = mid + 1;

        for (int k = low; k <= high; k++) {
            aux[k] = a[k];
        }

        for (int k = low; k <= high; k++) {
            if (i > mid)
                a[k] = aux[j++];
            else if (j > high)
                a[k] = aux[i++];
            else if (Sort.less(aux[i], aux[j]))
                a[k] = aux[i++];
            else
                a[k] = aux[j++];
        }
    }

}

分析一下以上的代码,首先申请一个和原数组 a 相同大小的辅助空间,调用 sort() 方法,递归分割数组,直到 high <= low,这说明数组中只存在一个元素了,递归分割的意义在于不断缩小数组的规模,然后对数组进行排序合并,用二叉树来描述这一过程最为合适,左右孩子排序后合成根节点。

快速排序

快速排序是当前最流行的排序算法,是使用最广泛的排序算法了。之所以它这么受欢迎是因为它只需要一个很小的辅助栈,而且将一个长度为N 的数组排序所需时间和 NlgN 成正比。之前学过的所有算法都无法将这两个优点结合起来,但是快速排序做到了。

快速排序实现的思想是将一个数组分成两个数组,将两部分进行独立排序。但是这个分割不和归并排序一样,这里的切分位置是取决于数组的内容的。切分的过程需要满足以下三个条件

  1. 对于某个 j, a[j] 已经排定;
  2. a[low] 到 a[j-1] 的所有元素都不大于 a[j];
  3. a[j+1] 到 a[high] 的所有元素都不小于 [j];

然后递归的调用切分就可以实现排序,因为每次切分都能排定一个元素。要完成这个实现,需要实现切分方法,一般策略是先随意的取 a[low] 作为切分元素。

算法 如下

public class QuickSort {

    public static void sort(Comparable[] a) {
        if (Sort.isSorted(a)) {
            return;
        }

        sort(a, 0, a.length - 1);
    }

    private static void sort(Comparable[] a, int low, int high) {

        if (high <= low){
            return;
        }
        int j = partition(a, low, high);
        sort(a, low, j - 1);
        sort(a, j + 1, high);
    }

    private static int partition(Comparable[] a, int low, int high) {
        int i = low, j = high + 1;
        Comparable v = a[low];
        while (true) {
            while (Sort.less(a[++i], v))
                if (i == high)
                    break;
            while (Sort.less(v, a[--j]))
                if (j == low)
                    break;
            if (i >= j)
                break;

            Sort.exchange(a, i, j);
        }

        Sort.exchange(a, low, j);
        return j;
    }

}

算法中的 partition() 函数主要实现的就是将 a[low] 这个元素排定到指定的位置并且将该位置作为返回值,返回给 sort() 函数,用于分割数组,随着 sort() 函数的递归调用,会将每个元素排定到指定位置,从而实现排序。

快速排序虽然有很多优点,但是有一个潜在的缺点,在切份不平衡时这个程序可能会极为低效。比如说第一次从最小的那个元素切分,第二次从第二小的元素进行切分,这样的话每次调用只会移除一个元素,这个时候性能就非常低了。所以在使用快速排序的时候最好首相将数组随机打乱,这样的话会将上述概率降到最低。

实际上上述快速排序的实现还是有待改进的,比如说我们可以在数组特别小的时候,使用插入排序,只需要在 sort() 方法中将递归的判定条件改成如下所示

if (high <= low + X){
    InsertSort.sort(a,low,high);
    return;
}   

X 可以是 5 - 15 内的数,这可以在一定程度上改善,尤其是在小数组基本有序的情况下。

当然,现在比较流行的还是三向切分排序,这种算法主要应对在数组中存在大量重复的情况,算法的基本思想就是首先选取到一个切分元素,一般是 a[low],然后取出该元素并命名为v。设置 lt 指向 low , gt 指向 high,i 指向 low+1。 在 i <= gt 的时候执行以下操作:

1.如果a[i] < v,那么将 a[i] 和 a[lt] 互换,并且lt 和 i 加 1 。
2.如果a[i] > v,那么将a[i] 与 a[gt] 互换,并且 gt -1.
3.如果 a[i] = v ,那么直接 i +1,不执行交换操作。

待程序跳出以上循环的时候,调用 sort() 方法,并且左边的数组以 lt-1 位上界,右边的数组以 gt+1 为下界。这个时候就可以很明显的看出来,每次切分就不会再将以前已经排过序的元素纳入数组,这样在元素有大量重复的情况下可以显著提高排序效率,当然在随机情况下,它的效率也是非常好的。下面来看看具体实现。

public class Quick3Way {

    public static void sort(Comparable[] a) {
        sort(a, 0, a.length - 1);
    }

    private static void sort(Comparable[] a, int low, int high) {
        if (high <= low)
            return;
        int lt = low, i = low + 1, gt = high;
        Comparable v = a[low];
        while (i <= gt) {
            int cmp = a[i].compareTo(v);
            if (cmp < 0)
                Sort.exchange(a, lt++, i++);
            else if (cmp > 0)
                Sort.exchange(a, i, gt--);
            else
                i++;
        }

        sort(a, low, lt - 1);
        sort(a, gt + 1, high);
    }

}

各个排序算法的比较

为了验证算法的优劣,我对以上算法分别进行了 1 0000数量级,10 0000数量级,100 0000数量级的数组进行了测试,测试结果如下:

1.1 0000 数量级

选择排序 : 0.112s
插入排序 : 0.16s
希尔排序 : 0.016s
归并排序 : 0.011s
快速排序 : 0.008s

2.10 0000 数量级

选择排序 : 11.19s
插入排序 : 16.521s
希尔排序 : 0.084 s
归并排序 : 0.114s
快速排序 : 0.066s

3.100 0000 数量级
选择排序 : 等了几分钟未果
插入排序 : 等了几分钟未果
希尔排序 : 2.022s
归并排序 : 0.742s
快速排序 : 0.521s

上述用于测试的数据由 Random 函数生成的,并且与数量级相乘,比如说 10000 数量级就是用 (int) (Math.random() * 10000) 生成,由于数是随机的,所以上述时间也不是一个固定的值,但相差不会太大。

有上述实验结果反映出来一些问题

  1. 选择排序和插入排序对规模大的数组是无能为力的
  2. 规模越大,快速排序和归并排序的优势就越大,尤其是快速排序
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值