Java 数据结构和算法 - 排序

本文讨论数组元素的排序问题,数据量不大,可以在内存中完成。
实现了Comparable接口的对象才可以排序。

插入排序和其他简单排序

Insertion sort是一个简单的排序算法,适用于数据量少的情况。

    /**
     * Simple insertion sort
     */
    public static <AnyType extends Comparable<? super AnyType>> void insertionSort(AnyType[] a) {
        for (int p = 1; p < a.length; p++) {
            AnyType tmp = a[p];
            int j = p;
            for (; j > 0 && tmp.compareTo(a[j - 1]) < 0; j--)
                a[j] = a[j - 1];
            a[j] = tmp;
        }
    }

插入排序的过程如下图
insertion sort

可以看到,算法的复杂度是O(N2)。

希尔排序

第一个提升插入排序性能的是Shellsort。虽然它不是已知的最快算法,但它是次二次(subquadratic)算法,代码比插入排序稍长,是最简单的快速算法。
Shell的想法是避免大量数据移动,首先比较离的远的元素,然后比较稍微近一些的元素,逐渐退回到基本的插入排序。Shellsort使用一个序列h1、h2、…、ht,叫做增量序列。只要h1 = 1,任何增量序列都会起作用,但有些选择比其他选择更好。一个阶段完成以后,使用一些增量hk,当i+hk是有效的索引的时候,对每个i,我们有a[i] ≤ a[i+hk];间隔hk的所有元素都被排序。数组就是hk-sorted。

Shellsort after

比如,上图就是使用Shellsort几个阶段之后的数组。在5-sort之后,间隔为5的元素已经排好序了。
类似地,在3-sort之后,间隔为3的元素已经排好序了。Shellsort的一个重要特性(还没有被证明)是一个hk-sorted的数组经过hk-1-sorted排序之后,仍然是hk-sorted。
一般来说,一个hk-sort需要,对hk、hk+1、…、N-1的每个位置i,把元素放到i、i-hk、i-2hk等等正确的位置。虽然这个顺序不影响实现,还是要仔细检查hk-sort在hk个独立的子序列上执行插入排序(见上图)。
看下面的代码,内循环代表一个gap insertion sort。在间隙插入排序内,在循环被执行后,数组中由间隙分隔的元素被排好序。如果gap是1,就和插入排序一样了。这样,Shellsort也叫缩小差距排序。

    /**
     * Shellsort, using a sequence suggested by Gonnet.
     *
     * @param a an array of Comparable items.
     */
    public static <AnyType extends Comparable<? super AnyType>> void shellsort(AnyType[] a) {
        for (int gap = a.length / 2; gap > 0;
             gap = gap == 2 ? 1 : (int) (gap / 2.2))
            for (int i = gap; i < a.length; i++) {
                AnyType tmp = a[i];
                int j = i;

                for (; j >= gap && tmp.compareTo(a[j - gap]) < 0; j -= gap)
                    a[j] = a[j - gap];
                a[j] = tmp;
            }
    }

我们已经看到,当gap是1的时候,内循环就把数组排好序了。如果gap不是1,总有一些元素还没排好。
Shell建议gap从N/2开始,每次减半,直到1,问题就解决了。从下图可以看到,不同的增量,排序性能是不一样的。
Running time of the insertion sort and Shellsort

归并排序

Mergesort采用分治算法,递归地排序。
基本的归并算法接受两个输入数组A和B,输出一个数组C。有三个计数器Actr、Bctr和Cctr,初始设置为各自数组的开头。A[Actr]和B[Bctr]之中比较小的那个被拷贝到C的下一个元素,相应的计数器加1。当一个数组空了,另一个数组的剩余元素被拷贝到C。
我们看一个例子:
在这里插入图片描述

如果数组A包含1、13、24、26,B包含2、15、27、38,算法专业执行。首先,比较1和2,1被插入C,然后比较13和2:
在这里插入图片描述

2被加到C,比较13和15
在这里插入图片描述

13被加到C,比较24和15
在这里插入图片描述
在这里插入图片描述

当26被加到C,A数组空了
在这里插入图片描述

最后,B被拷贝到C
在这里插入图片描述

实现代码如下

    /**
     * Internal method that makes recursive calls.
     *
     * @param a        an array of Comparable items.
     * @param tmpArray an array to place the merged result.
     * @param left     the left-most index of the subarray.
     * @param right    the right-most index of the subarray.
     */
    private static <AnyType extends Comparable<? super AnyType>> void mergeSort(AnyType[] a, AnyType[] tmpArray, int left, int right) {
        if (left < right) {
            int center = (left + right) / 2;
            mergeSort(a, tmpArray, left, center);
            mergeSort(a, tmpArray, center + 1, right);
            merge(a, tmpArray, left, center + 1, right);
        }
    }

    /**
     * Internal method that merges two sorted halves of a subarray.
     *
     * @param a        an array of Comparable items.
     * @param tmpArray an array to place the merged result.
     * @param leftPos  the left-most index of the subarray.
     * @param rightPos the index of the start of the second half.
     * @param rightEnd the right-most index of the subarray.
     */
    private static <AnyType extends Comparable<? super AnyType>> void merge(AnyType[] a, AnyType[] tmpArray, int leftPos, int rightPos, int rightEnd) {
        int leftEnd = rightPos - 1;
        int tmpPos = leftPos;
        int numElements = rightEnd - leftPos + 1;

        // Main loop
        while (leftPos <= leftEnd && rightPos <= rightEnd)
            if (a[leftPos].compareTo(a[rightPos]) <= 0)
                tmpArray[tmpPos++] = a[leftPos++];
            else
                tmpArray[tmpPos++] = a[rightPos++];

        while (leftPos <= leftEnd)    // Copy rest of first half
            tmpArray[tmpPos++] = a[leftPos++];

        while (rightPos <= rightEnd)  // Copy rest of right half
            tmpArray[tmpPos++] = a[rightPos++];

        // Copy tmpArray back
        for (int i = 0; i < numElements; i++, rightEnd--)
            a[rightEnd] = tmpArray[rightEnd];
    }

    /**
     * Mergesort algorithm.
     *
     * @param a an array of Comparable items.
     */
    public static <AnyType extends Comparable<? super AnyType>> void mergeSort(AnyType[] a) {
        AnyType[] tmpArray = (AnyType[]) new Comparable[a.length];
        mergeSort(a, tmpArray, 0, a.length - 1);
    }

归并排序在运行时严重依赖比较元素和移动元素的代价。Java的元素比较是昂贵的,因为比较操作由函数对象实现。而移动对象是廉价的,因为元素不用拷贝,而是简单地修改引用。在所有的通用排序算法中,归并排序的比较操作比较少,所以是Java里通用排序的好的候选。事实上,java.util.Arrays.sort对对象数组排序时就使用了该算法。而对于原始类型,java.util.Arrays.sort使用的是quicksort。

快速排序

quicksort是快速的分治算法。它的平均执行时间是O(N log N)。它的速度主要归功于紧凑而高度优化的内循环。一方面,快排算法相对简单容易懂,也已经被证明了,因为它依赖递归。另一方面,它是个棘手的算法,代码的小改动,都可能造成运行时间的大波动。
考虑使用下面的简单的排序算法给列表排序:任意选择一个元素,分成三组,小于它的、等于它的和大于它的。递归地个第一组和第三组排序,最后把三个组连接起来。代码见下,它的性能一般都不错。事实上,如果列表含有大量的重复数据,只有几个不同元素,性能会非常好。

    public static void sort(List<Integer> items) {
        if (items.size() > 1) {
            List<Integer> smaller = new ArrayList<Integer>();
            List<Integer> same = new ArrayList<Integer>();
            List<Integer> larger = new ArrayList<Integer>();

            Integer chosenItem = items.get(items.size() / 2);
            for (Integer i : items) {
                if (i < chosenItem)
                    smaller.add(i);
                else if (i > chosenItem)
                    larger.add(i);
                else
                    same.add(i);
            }

            sort(smaller);   // Recursive call!
            sort(larger);    // Recursive call!

            items.clear();

            items.addAll(smaller);
            items.addAll(same);
            items.addAll(larger);
        }
    }

该算法描述了快排的基本形式。对于其他列表,这样递归,很难比归并排序更快。为了做地更好,我们必须避免使用大量的额外内存和保持内循环的干净。这样,快排一般避免增加第二个组(等于组),算法有很多影响性能的微小细节。下面描述最常用的快排实现,输入是一个数组,算法不增加额外的数组。
快排算法Quicksort(S)可分为四步:

  • 如果S的元素数量是0或者1,就返回
  • 选择S中的任一元素v,叫pivot
  • 把S–{v}(S的剩余元素)分成两个不相交的组:L = {x∈S – {v} | x≤v}和R = {x∈S – {v} | x≥v}
  • 返回Quicksort(L)的结果,然后是v,接着是Quicksort®

有几点值得注意。首先,递归的S可能是空的。其次,任何元素都可以当pivot,但是不同的选择,会影响性能。三,算法允许等于pivot的其他元素放到L或者R。
The steps of quicksort

上面的图,pivot是65。剩余元素分成两个子集。然后,每个组都递归地排序。Java的实现,元素被保存在由小于的和大于的分隔的数组的一部分中。分区之后,pivot在数组的索引p处,后来的递归调用,会在小于p-1的部分和大于p+1的两部分分别进行。
快排比归并算法快,是因为分区比归并快。

选择pivot

一般可以选择中间元素,(low+high)/2。
median-of-three partitioning是个更好的办法-选择第一个、中间一个和最后一个的中位数。

分区策略

有几种选择策略,先讨论最简单的一种。可分为三步

  • 交换pivot和最后一个元素
  • 把小于pivot的放到左边,大于的放到右边-使用计数器i,从左往右搜比pivot大的元素,找到就停止;使用计数器j,从右往左搜比pivot小元素,找到就停止;如果i<j,就交换并继续;否则停止
  • 交换位置i的元素和pivot元素
    Partitioning algorithm

代码是这样的

    /**
     * Quicksort algorithm.
     *
     * @param a an array of Comparable items.
     */
    public static <AnyType extends Comparable<? super AnyType>> void quicksort(AnyType[] a) {
        quicksort(a, 0, a.length - 1);
    }

    private static final int CUTOFF = 10;

    /**
     * Method to swap to elements in an array.
     *
     * @param a      an array of objects.
     * @param index1 the index of the first object.
     * @param index2 the index of the second object.
     */
    public static final <AnyType> void swapReferences(AnyType[] a, int index1, int index2) {
        AnyType tmp = a[index1];
        a[index1] = a[index2];
        a[index2] = tmp;
    }

    /**
     * Internal quicksort method that makes recursive calls.
     * Uses median-of-three partitioning and a cutoff of 10.
     *
     * @param a    an array of Comparable items.
     * @param low  the left-most index of the subarray.
     * @param high the right-most index of the subarray.
     */
    private static <AnyType extends Comparable<? super AnyType>> void quicksort(AnyType[] a, int low, int high) {
        if (low + CUTOFF > high)
            insertionSort(a, low, high);
        else {
            // Sort low, middle, high
            int middle = (low + high) / 2;
            if (a[middle].compareTo(a[low]) < 0)
                swapReferences(a, low, middle);
            if (a[high].compareTo(a[low]) < 0)
                swapReferences(a, low, high);
            if (a[high].compareTo(a[middle]) < 0)
                swapReferences(a, middle, high);

            // Place pivot at position high - 1
            swapReferences(a, middle, high - 1);
            AnyType pivot = a[high - 1];

            // Begin partitioning
            int i, j;
            for (i = low, j = high - 1; ; ) {
                while (a[++i].compareTo(pivot) < 0)
                    ;
                while (pivot.compareTo(a[--j]) < 0)
                    ;
                if (i >= j)
                    break;
                swapReferences(a, i, j);
            }

            // Restore pivot
            swapReferences(a, i, high - 1);

            quicksort(a, low, i - 1);    // Sort small elements
            quicksort(a, i + 1, high);   // Sort large elements
        }
    }

    /**
     * Internal insertion sort routine for subarrays
     * that is used by quicksort.
     *
     * @param a   an array of Comparable items.
     * @param low the left-most index of the subarray.
     */
    private static <AnyType extends Comparable<? super AnyType>> void insertionSort(AnyType[] a, int low, int high) {
        for (int p = low + 1; p <= high; p++) {
            AnyType tmp = a[p];
            int j;

            for (j = p; j > low && tmp.compareTo(a[j - 1]) < 0; j--)
                a[j] = a[j - 1];
            a[j] = tmp;
        }
    }

快速选择

和排序密切相关的问题是选择,比如在含有N个元素的数组中找到kth个最小元素。还有变种是找到中位数、或者N/2th个最小元素。修改一下快排算法,可以解决选择问题。
Quickselect(S, k)的步骤如下

  • 如果S只有一个元素,想必k也是1,可以返回元素
  • 在S中任意选择元素v,它是pivot
  • 把S–{v}分成L和R,就像快排中做过的
  • 如果k小于或者等于L中的元素数量,搜索的元素肯定在L中。递归调用Quickselect(L, k)。否则,如果k恰好比L中元素数量大1,则pivot就是最小kth元素,可以返回。否则,最小kth元素位于R,并且是R中的最小(k–|L|–1)th元素。然后,我们递归调用返回结果

Quickselect只有一个递归,而快排是两个。quickselect的最坏情况与快排的情况相同,也是二次的(如果递归发生在空集上)。

/**
     * Quick selection algorithm.
     * Places the kth smallest item in a[k-1].
     *
     * @param a an array of Comparable items.
     * @param k the desired rank (1 is minimum) in the entire array.
     */
    public static <AnyType extends Comparable<? super AnyType>> void quickSelect(AnyType[] a, int k) {
        quickSelect(a, 0, a.length - 1, k);
    }

    /**
     * Internal selection method that makes recursive calls.
     * Uses median-of-three partitioning and a cutoff of 10.
     * Places the kth smallest item in a[k-1].
     *
     * @param a    an array of Comparable items.
     * @param low  the left-most index of the subarray.
     * @param high the right-most index of the subarray.
     * @param k    the desired rank (1 is minimum) in the entire array.
     */
    private static <AnyType extends Comparable<? super AnyType>> void quickSelect(AnyType[] a, int low, int high, int k) {
        if (low + CUTOFF > high)
            insertionSort(a, low, high);
        else {
            // Sort low, middle, high
            int middle = (low + high) / 2;
            if (a[middle].compareTo(a[low]) < 0)
                swapReferences(a, low, middle);
            if (a[high].compareTo(a[low]) < 0)
                swapReferences(a, low, high);
            if (a[high].compareTo(a[middle]) < 0)
                swapReferences(a, middle, high);

            // Place pivot at position high - 1
            swapReferences(a, middle, high - 1);
            AnyType pivot = a[high - 1];

            // Begin partitioning
            int i, j;
            for (i = low, j = high - 1; ; ) {
                while (a[++i].compareTo(pivot) < 0)
                    ;
                while (pivot.compareTo(a[--j]) < 0)
                    ;
                if (i >= j)
                    break;
                swapReferences(a, i, j);
            }

            // Restore pivot
            swapReferences(a, i, high - 1);

            // Recurse; only this part changes
            if (k <= i)
                quickSelect(a, low, i - 1, k);
            else if (k > i + 1)
                quickSelect(a, i + 1, high, k);
        }
    }

猜你喜欢

转载自blog.csdn.net/weixin_43364172/article/details/84838409
今日推荐