Java 数据结构 排序 - 选择排序

Java 数据结构 排序 - 选择排序

又到排序了. 上次看算法第四版只是迅速过了一遍, 书一遍看不明白就看两遍, 有些东西必须得硬来才行. 递归的习题就没那么好写了, 有的想了一会就写出来了, 比如那个用三个栈排序的, 但是有些比如全排列和递归的动态规划就不那么明显会写了. 估计还是每一次要维持一个背包的这种对象, 在递归之间如何传递,

又到排序了. 上次看算法第四版只是迅速过了一遍, 书一遍看不明白就看两遍, 有些东西必须得硬来才行. 递归的习题就没那么好写了, 有的想了一会就写出来了, 比如那个用三个栈排序的, 但是有些比如全排列和递归的动态规划就不那么明显会写了. 估计还是每一次要维持一个背包的这种对象, 在递归之间如何传递, 比较麻烦. 看来代码还得仔细想想, 看来自学开发还算可以, 不会像要面试找工作那样压力那么大.
  1. 泛型简单回顾 - Comparable<T>接口
  2. 选择排序
  3. 重构选择排序到更灵活的版本

泛型简单回顾 - Comparable<T>接口

在排序之前, 比较元素的大小是一个重要的议题. Java提供了一个接口Comparable接口, 只要继承了该接口, 都可以用来比较大小. 该接口其实我们已经很熟悉了, 不过这里要说的是泛型. 比如我们可以利用排序接口, 实现一个简单的泛型方法:
public static <T extends Comparable<T>> T findMin(T[] array) {

    T min = array[0];
    for (int i = 1; i < array.length; i++) {
        if (array[i].compareTo(min) < 0) {
            min = array[i];
        }
    }
    return min;
}
看上去好像很简单, 但实际上有一个问题, 现在我们有一种车CarA, 还有继承CarA的CarB, 二者都有重量可以进行比较, 代码如下:
public class CarA implements Comparable<CarA> {

    private int weight;

    private String name;

    public CarA(int weight, String name) {
        this.weight = weight;
        this.name = name;
    }

    @Override
    public int compareTo(CarA carA) {
        return this.weight - carA.weight;

    }
}
public class CarB extends CarA {

    public CarB(int weight, String name) {
        super(weight, name + "B");
    }
}
CarA实现了Comparable<CarA>接口, 所以CarB也实现了Comparable<CarA>接口, 可以进行二者的比较, 不同类型的比较也可以, 因为compareTo(CarA carA)方法中的类型是CarA, CarB也是CarA, 例如:
public static void main(String[] args) {
        CarA carA1 = new CarA(10, "dfkj");
        CarA carA2 = new CarA(17, "fd");
        CarA carA3 = new CarA(3, "asd");

        System.out.println(carA1.compareTo(carA2));

        CarB carB1 = new CarB(29, "dfkbbvcj");
        CarB carB2 = new CarB(21, "bjkd");

        System.out.println(carB2.compareTo(carB1));
        System.out.println(carA2.compareTo(carB1));
        System.out.println(carB2.compareTo(carA3));


    }
但是如果我们想使用我们的泛型方法, 从CarA和CarB数组中寻找最小值, 接着上边的:
CarA[] carAS = new CarA[]{carA1, carA2, carA3};
CarB[] cars = new CarB[]{carB1, carB2};
CarA mincara = findMin(carAS);
CarB mincarb = findMin(cars);
这个时候会发现编译报错:
Error:(23, 31) java: 不兼容的类型: 推论变量 T 具有不兼容的上限
    等式约束条件:datastructure.ex03.CarA
    下限:datastructure.ex03.CarB,java.lang.Comparable<T>
这个问题在于泛型方法的要求是<T extends Comparable<T>>, 当泛型方法接收的类型是CarA的时候, CarA是实现了Comparable<CarA>的, 所以不会有问题. 但泛型接收的类型是CarB的时候, CarB并没有实现Comparable<CarB>类型, CarB依然实现的是Comparable<CarA>类型; 而且Comparable<CarB>并不是一个Comparable<CarA>类型. 想要避免这种问题, 很显然, 如果一个类是父类并且实现了Comparable接口, 这个父类的任意层级的子类, 一定实现了一个Comparable<某个父类>接口. 因此泛型方法需要修改成:
public static <T extends Comparable<? super T>> T findMin(T[] array)
这样就可以正常工作了. 如果以后碰到泛型方面的问题, 就可以还原一下来思考结果. 此外通配符表示任意类型, 而不能使用某个具体类型. 因为一个SomeClass<A>类型, 并不是一个SomeClass<B>类型, 即使A和B有继承关系, 泛型类也不是同一个类型. 在后边实现排序算法的时候, 就会经常使用public static <T extends Comparable<? super T>> void sort(T[] array)这样的泛型方法了.

选择排序

一开始都先排数组. 选择排序的原理是, 先找到整个序列里的最小的元素, 然后将其与第一个元素进行交换, 然后对数组中剩下的部分重复这个操作. 这个操作可以非常方便的用递归或者迭代的方式写出来. 先看迭代方法, 很容易理解:
//对外暴露的公有方法
public static <T extends Comparable<? super T>> void sort(T[] array) {
    int length = array.length;
    if (!(length == 0 || length == 1)) {
        //对数组中每一个元素, 先找到这个元素到结尾的最小值, 然后和自己替换
        for (int i = 0; i < length; i++) {
            //利用辅助方法找到当前元素到数组结尾的最小值对应的索引
            int minindex = findMin(array, i, length - 1);

            //交换当前元素与最小值元素
            T temp = array[i];
            array[i] = array[minindex];
            array[minindex] = temp;
        }
    }
}

//辅助排序方法的私有方法, 根据给定的索引在一个数组中寻找索引范围内的最小值对应的索引
private static <T extends Comparable<? super T>> int findMin(T[] array, int startIndex, int lastIndex) {
    int min = startIndex;

    for (int i = startIndex; i <= lastIndex; i++) {
        if (array[min].compareTo(array[i]) > 0) {
            min = i;
        }
    }
    return min;
}
递归的思路是, 递归方法的功能是可以找到数组中的最小值, 然后交换最小值与当前元素, 之后对剩下的部分继续应用该方法进行处理, 要额外传递一个当前变量的数值, 以表示从哪里继续处理:
//对外暴露的公有方法
public static <T extends Comparable<? super T>> void resursionSort(T[] array) {
    recursionFindMinAndSwap(array, 0);
}

//实际的递归方法, 懒得使用每次复制数组的方式, 本来就是O(n^2)级别的算法, 就少占用点空间吧.
private static <T extends Comparable<? super T>> void recursionFindMinAndSwap(T[] array, int index) {
    //停机条件是当前索引等于数组的最大索引
    if (index != array.length - 1) {
        //找到当前索引到数组末尾的最小元素对应的索引
        int minIndex = findMin(array, index, array.length - 1);

        //交换第一个元素和最小元素的值
        T temp = array[minIndex];
        array[minIndex] = array[index];
        array[index] = temp;
        //然后继续对剩下的数组进行操作
        recursionFindMinAndSwap(array, index + 1);
    }

}
这里还可以加入一些变化, 比如只想对一个数组的前n个元素进行排序, 则只需要额外传入一个n, 然后将索引设置一下即可:
public static <T extends Comparable<? super T>> void sort(T[] array, int n) {
    if (n > array.length) {
        throw new RuntimeException("数组的项比参数n要少!");
    }
    if (!(n == 0 || n == 1)) {
        //对数组中每一个元素, 先找到这个元素到结尾的最小值, 然后和自己替换
        for (int i = 0; i < n; i++) {
            int minindex = findMin(array, i, n - 1);

            //交换两个元素的位置
            T temp = array[i];
            array[i] = array[minindex];
            array[minindex] = temp;
        }
    }
}
有了这个方法之后, 原来的sort只需要改成调用这个方法, n=数组长度即可:
public static <T extends Comparable<? super T>> void sort(T[] array) {
    sort(array, array.length);
}
递归版本也可以再加入一个参数n, 来控制排序到第几项, 就不再一一写了,原理是一样的, 还可以将交换数组的两个索引也抽出到一个函数中. 选择排序由于是最直观的, 现在无需看教材, 自己都能够实现了.

重构选择排序和到更灵活的版本

现在的排序方法还不够通用, 必须编写一个可以指定要排序的数组, 以及开始和结束索引的方法, 以对这个索引范围(包含两端索引)的内容进行排序. 而且这个方法可以作为类中其他方法的基础方法, 比如实现了索引范围排序, 那么一个对数组进行全排序的方法就可以在内部引用这个范围排序,只需要把范围设置为0-数组最大索引即可. 先来重构选择排序的类, 由于选择排序内部需要执行两大功能, 即寻找一个范围内的最小索引, 和交换数组中两个索引的值, 所以编写一个新的方法, 同时将寻找最小索引和交换数组索引的功能抽取出来: 此外, 还有一个问题, 默认是升序排列, 是不是可以选择降序排列, 其实也很简单, 只需要将寻找最小索引改成寻找最大索引即可, 可以通过给核心方法添加一个reverse参数来控制寻找最大还是最小的索引. 于是最后写成的类中包含一个核心私有迭代排序方法, 一个核心私有递归排序方法, 三个私有静态辅助方法, 然后就暴露给外界使用的排序整个数组, 排序指定索引间的数组, 排序前n个和后n个元素的公有方法, 这些公有方法全部都调用两个核心私有方法之一. 完整的类如下:
public class Selection {

    /**
     * 给数组开头的count个元素进行升序排列
     *
     * @param array 数组
     * @param count 从开始要排序的元素的数量
     * @param <T>   泛型参数,必须实现Comparable接口
     */
    public static <T extends Comparable<? super T>> void sortFromStart(T[] array, int count) {

        //判断参数是否合理, 由于核心函数会判断参数是否有问题, 所以这里只需要判断count即可, 等于0或者1直接返回
        //如果count等于其他值, 会由核心函数进行判断
        if (count == 0 || count == 1) {
            return;
        }

        //调用核心函数, 开始索引为0, 结束索引为count-1即可
        sort(array, 0, count - 1, false);
    }

    /**
     * 给数组开头的count个元素进行降序排列
     *
     * @param array 数组
     * @param count 从开始要排序的元素的数量
     * @param <T>   泛型参数,必须实现Comparable接口
     */
    public static <T extends Comparable<? super T>> void sortFromStartDesc(T[] array, int count) {

        //判断参数是否合理, 由于核心函数会判断参数是否有问题, 所以这里只需要判断count即可, 等于0或者1直接返回
        //如果count等于其他值, 会由核心函数进行判断
        if (count == 0 || count == 1) {
            return;
        }

        //调用核心函数, 开始索引为0, 结束索引为count-1即可
        sort(array, 0, count - 1, true);
    }


    /**
     * 给从数组末尾向前数起的count个元素进行升序排序
     *
     * @param array 要排序的数组
     * @param count 末尾的count个元素
     * @param <T>   泛型参数,必须实现Comparable接口
     */
    public static <T extends Comparable<? super T>> void sortFromEnd(T[] array, int count) {

        //判断参数是否合理, 由于核心函数会判断参数是否有问题, 所以这里只需要判断count即可, 等于0或者1直接返回
        //如果count等于其他值, 会由核心函数进行判断
        if (count == 0 || count == 1) {
            return;
        }

        //调用核心函数, 开始索引为array.length-1-count, 结束索引为 array.length-1
        recursionSort(array, array.length - count, array.length - 1, false);
    }

    /**
     * 给从数组末尾向前数起的count个元素进行降序排序
     *
     * @param array 要排序的数组
     * @param count 末尾的count个元素
     * @param <T>   泛型参数,必须实现Comparable接口
     */
    public static <T extends Comparable<? super T>> void sortFromEndDesc(T[] array, int count) {

        //判断参数是否合理, 由于核心函数会判断参数是否有问题, 所以这里只需要判断count即可, 等于0或者1直接返回
        //如果count等于其他值, 会由核心函数进行判断
        if (count == 0 || count == 1) {
            return;
        }

        //调用核心函数, 开始索引为array.length-1-count, 结束索引为 array.length-1
        recursionSort(array, array.length - count, array.length - 1, true);
    }

    /**
     * 对数组全部元素进行升序排列, 内部直接调用核心私有方法或者另外一个排序前N个的都可以
     *
     * @param array 要排序的数组
     * @param <T>   泛型参数,必须实现Comparable接口
     */
    public static <T extends Comparable<? super T>> void sort(T[] array) {
        sort(array, 0, array.length - 1, false);
    }

    /**
     * 对数组全部元素进行降序排列, 内部直接调用核心私有方法或者另外一个排序前N个的都可以
     *
     * @param array 要排序的数组
     * @param <T>   泛型参数,必须实现Comparable接口
     */
    public static <T extends Comparable<? super T>> void sortDesc(T[] array) {
        sort(array, 0, array.length - 1, true);
    }


    /**
     * 对指定索引范围内的数组元素进行升序排序, 包含起始索引与结束索引
     *
     * @param array      要排序的数组
     * @param startIndex 要排序的部分的起始索引
     * @param endIndex   要排序的部分的结束索引
     * @param <T>        泛型参数,必须实现Comparable接口
     */
    public static <T extends Comparable<? super T>> void sortBetweenIndex(T[] array, int startIndex, int endIndex) {
        //内部直接调用私有核心方法即可
        sort(array, startIndex, endIndex, false);
    }

    /**
     * 对指定索引范围内的数组元素进行降序排序, 包含起始索引与结束索引
     *
     * @param array      要排序的数组
     * @param startIndex 要排序的部分的起始索引
     * @param endIndex   要排序的部分的结束索引
     * @param <T>        泛型参数,必须实现Comparable接口
     */
    public static <T extends Comparable<? super T>> void sortBetweenIndexDesc(T[] array, int startIndex, int endIndex) {
        //内部直接调用私有核心方法即可
        sort(array, startIndex, endIndex, true);
    }


    /**
     * 对起始索引索引和结束索引之间(包含起始索引和结束索引)的部分进行排序, 就地排序, 不返回新数组.
     * 本方法是数组排序的核心私有方法, 对外暴露的其他方法内部调用该方法.
     * 数组元素需要实现Comparable接口
     *
     * @param array      要排序的数组,
     * @param startIndex 要进行排序部分的起始索引
     * @param endIndex   要进行排序部分的结束索引
     * @param reverse    布尔类型, 是否降序排序, 为true表示降序排序, 为false表示升序排序
     * @param <T>        泛型参数,必须实现Comparable接口
     */
    private static <T extends Comparable<? super T>> void sort(T[] array, int startIndex, int endIndex, boolean reverse) {
        //判断数组长度, 如果为0或者1, 直接返回
        if (array.length == 0 || array.length == 1) {
            return;
        }

        //判断两个索引参数是否符合要求
        if (startIndex < 0 || startIndex > array.length - 1 || endIndex < 0 || endIndex > array.length - 1 || startIndex > endIndex) {
            throw new IllegalArgumentException("索引超出范围");
        }

        //主排序算法, 对于数组中每一个元素, 获取其到endIndex之间的最小值, 然后交换二者的位置, 注意, 其实只需要遍历到倒数第二个元素即可, 因为不管交换不交换, 结果都正确.
        for (int i = startIndex; i < endIndex; i++) {
            //根据是否升序或者降序, 查找当前索引到结束索引之间的最小/大值对应的索引
            int swapIndex;
            if (reverse) {
                swapIndex = findMaxIndex(array, i, endIndex);
            } else {
                swapIndex = findMinIndex(array, i, endIndex);
            }

            //交换当前索引和最大/最小值索引的内容
            swap(array, i, swapIndex);
        }
    }

    /**
     * 递归版本的核心排序私有方法, 方法原理是, 寻找当前数组的最小/大值, 与第一个进行交换. 然后对数组的第二个元素开始到结束的部分进行同样的空操作, 直到剩余部分长度为1结束.
     * 这个递归方法的名称应该叫交换第一个元素与最大/最小值更加合适
     * @param array      要排序的数组
     * @param startIndex 开始的索引
     * @param endIndex   结束的索引
     * @param reverse    是否反向(降序)排列, true为降序, false为升序
     * @param <T>        泛型参数,必须实现Comparable接口
     */
    private static <T extends Comparable<? super T>> void recursionSort(T[] array, int startIndex, int endIndex, boolean reverse) {
        //基础的参数判断与非递归版本一致
        if (array.length == 0 || array.length == 1) {
            return;
        }

        //判断两个索引参数是否符合要求
        if (startIndex < 0 || startIndex > array.length - 1 || endIndex < 0 || endIndex > array.length - 1 || startIndex > endIndex) {
            throw new IllegalArgumentException("索引超出范围");
        }

        //停机条件是startIndex=endIndex
        if (startIndex != endIndex) {
            //根据是否降序取最小/最大值对应的索引
            int swapIndex;
            if (reverse) {
                swapIndex = findMaxIndex(array, startIndex, endIndex);
            } else {
                swapIndex = findMinIndex(array, startIndex, endIndex);
            }
            //交换第一个索引与最大/最小值对应的索引的内容
            swap(array, startIndex, swapIndex);
            //对从start+1开始到endIndex的部分继续排序
            recursionSort(array, startIndex + 1, endIndex, reverse);
        }

    }
    
    
    /**
     * 寻找一个数组在起始索引和结束索引之间的最小值对应的索引, 数组元素需要实现Comparable接口. 私有方法, 为支持排序方法.
     *
     * @param array      数组对象
     * @param startIndex 起始索引
     * @param lastIndex  结束索引
     * @param <T>        泛型参数,必须实现Comparable接口
     * @return 在起始索引和结束索引之间的最小值对应的索引
     */
    private static <T extends Comparable<? super T>> int findMinIndex(T[] array, int startIndex, int lastIndex) {
        int min = startIndex;

        for (int i = startIndex; i <= lastIndex; i++) {
            if (array[min].compareTo(array[i]) > 0) {
                min = i;
            }
        }
        return min;
    }


    /**
     * 寻找一个数组在起始索引和结束索引之间的最大值对应的索引, 数组元素需要实现Comparable接口. 私有方法, 为支持排序方法.
     *
     * @param array      数组对象
     * @param startIndex 起始索引
     * @param lastIndex  结束索引
     * @param <T>        泛型参数,必须实现Comparable接口
     * @return 在起始索引和结束索引之间的最大值对应的索引
     */
    private static <T extends Comparable<? super T>> int findMaxIndex(T[] array, int startIndex, int lastIndex) {
        int max = startIndex;

        for (int i = startIndex; i <= lastIndex; i++) {
            if (array[max].compareTo(array[i]) < 0) {
                max = i;
            }
        }
        return max;
    }

    /**
     * 交换数组中两个元素的方法, 不返回任何值, 就地交换. 私有方法, 为支持排序方法
     *
     * @param array  数组
     * @param index1 要交换的两个元素的其中一个索引
     * @param index2 要交换的两个元素的另外一个索引
     * @param <T>    泛型参数,必须实现Comparable接口
     */
    private static <T> void swap(T[] array, int index1, int index2) {
        T temp = array[index1];
        array[index1] = array[index2];
        array[index2] = temp;
    }
}
算法真的不能着急, 必须一点一点慢慢吃透, 不仅练会算法本身, 还得一并提高工程实践能力才行. 这样写出来的类灵活性高多了. 选择排序的复杂度非常明显, 最差的情况下, 第一个元素要遍历n次才能找到最小值/最大值, 然后第二个元素是n-1次, 一直到第n个元素, 很显然是平方级别的复杂度, 这个已经很熟悉了.
LICENSED UNDER CC BY-NC-SA 4.0
Comment