冒泡排序、希尔排序和排序算法大总结02:希尔排序法

基本思想:通过循环,让数组越来越有序

冒泡排序法在每次循环时只处理相邻的逆序对,当一个较小的元素在后面的时候,每次也只能往前挪一位,效率很低,但会让数组变得更加有序

而插入排序法在数组有序的时候,只需要和前面的一个元素比较大小,就可以终止循环

因此,希尔排序法在冒泡排序法和插入排序法的基础上,不仅仅只是处理相邻的逆序对,而是依次选择间隔为n / 2、n / 4...2、1(间隔等于子数组的数量)的元素组成子数组,分别对这些子数组对进行插入排序,可以让靠前的大元素很快移动到后面,靠后的小元素很快的移动到前面,使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小间隔,到间隔为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动

实现希尔排序法

import java.util.Arrays;

public class Algorithm {

    public static void main(String[] args) {

        Integer[] arr = {4,7,7,5,8,4,2,1};

        ShellSort.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

class ShellSort {

    private ShellSort(){}

    public static<E extends Comparable<E>> void sort(E[] arr){

        /**
         * 初始间隔为n / 2,每次循环减半,最小为1
         */
        int h = arr.length / 2;

        while (h >= 1) {

            /**
             * 间隔等于子数组的数量,因此前h个元素分别是这h个子数组的第一个元素
             * 根据每个子数组的第一个元素找到整个子数组的元素, 每个元素间隔为h
             */
            for (int start = 0; start < h; start++) {

                /**
                 * 对arr[start, start + h, start + 2h...]进行插入排序
                 * 使用了插入排序,时间复杂度为O((n / h)^2) * h = O(n^2 / h)
                 */
                for (int i = start + h; i < arr.length; i += h) {

                    E temp = arr[i];
                    int j;

                    for (j = i; j - h >= 0 && temp.compareTo(arr[j - h]) < 0; j -= h) {
                        arr[j] = arr[j - h];
                    }

                    arr[j] = temp;
                }
            }

            h = h / 2;
        }
    }
}

希尔排序法的小优化——减少一轮循环

import java.util.Arrays;

public class Algorithm {

    public static void main(String[] args) {

        Integer[] arr = {4,7,7,5,8,4,2,1};

        ShellSort.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

class ShellSort {

    private ShellSort(){}

    public static<E extends Comparable<E>> void sort(E[] arr){

        int h = arr.length / 2;

        while (h >= 1) {

            /**
             * 每个元素都可以根据加h来找到子数组的下一个元素,因此不需要预先找到每个子数组的第一个元素
             * 直接从第一个子数组的第二个元素也就是h开始,进行插入排序
             * 也就是每次进行插入排序的元素都不是同一个子数组的,而之前是先对第一个子数组排序,然后再对第二个排序...
             */
            for (int i = h; i < arr.length; i++) {

                E temp = arr[i];
                int j;

                for (j = i; j - h >= 0 && temp.compareTo(arr[j - h]) < 0; j -= h) {
                    arr[j] = arr[j - h];
                }

                arr[j] = temp;
            }

            h = h / 2;
        }
    }
}

步长序列(提升性能)

希尔排序默认的间隔序列是持续减半,但是使用一些其他的步长序列,可能会对性能产生很大的提升

使用不同的步长序列,时间复杂度不同

import java.util.Arrays;

public class Algorithm {

    public static void main(String[] args) {

        Integer[] arr = {4,7,7,5,8,4,2,1};

        ShellSort.sortOptimized((arr);
        System.out.println(Arrays.toString(arr));
    }
}

class ShellSort {

    private ShellSort() {}

    public static <E extends Comparable<E>> void sortOptimized((E[] arr) {

         /**
         * 自定义步长序列,让h每次缩小3倍
         */
        int h = 1;
        while (h < arr.length){
            h = 3 * h + 1;
        }

        while (h >= 1){

            for (int i = h; i < arr.length; i++) {

                E temp = arr[i];
                int j;

                for (j = i; j - h >= 0 && temp.compareTo(arr[j - h]) < 0; j -= h) {
                    arr[j] = arr[j - h];
                }

                arr[j] = temp;
            }

            h /= 3;
        }
    }
}

希尔排序法和快速排序法性能比较

import java.util.Arrays;
import java.util.Random;

public class Algorithm {

    public static void main(String[] args) {

        Integer[] testScale = {10000, 100000};

        for (Integer n : testScale){

            Integer[] randomArr = ArrayGenerator.generatorRandomArray(n, n);
            Integer[] sortedArr = ArrayGenerator.generatorSortedArray(n);

            Integer[] arr1 = Arrays.copyOf(randomArr, randomArr.length);
            Integer[] arr3 = Arrays.copyOf(randomArr, randomArr.length);
            Integer[] arr5 = Arrays.copyOf(randomArr, randomArr.length);

            Integer[] arr2 = Arrays.copyOf(sortedArr, sortedArr.length);
            Integer[] arr4 = Arrays.copyOf(sortedArr, sortedArr.length);
            Integer[] arr6 = Arrays.copyOf(sortedArr, sortedArr.length);

            System.out.println("测试随机数组排序性能");
            System.out.println();

            Verify.testTime("ShellSort", arr1);
            Verify.testTime("ShellSortOptimized", arr3);
            Verify.testTime("QuickSort3Ways", arr5);

            System.out.println();

            System.out.println("测试有序数组排序性能");
            System.out.println();

            Verify.testTime("ShellSort", arr2);
            Verify.testTime("ShellSortOptimized", arr4);
            Verify.testTime("QuickSort3Ways", arr6);

            System.out.println();
        }
    }
}

class ShellSort {

    private ShellSort(){}

    public static<E extends Comparable<E>> void sort(E[] arr){

        int h = arr.length / 2;

        while (h >= 1) {

            for (int i = h; i < arr.length; i++) {

                E temp = arr[i];
                int j;

                for (j = i; j - h >= 0 && temp.compareTo(arr[j - h]) < 0; j -= h) {
                    arr[j] = arr[j - h];
                }

                arr[j] = temp;
            }

            h = h / 2;
        }
    }

    public static <E extends Comparable<E>> void sortOptimized(E[] arr) {

        int h = 1;
        while (h < arr.length){
            h = 3 * h + 1;
        }

        while (h >= 1){

            for (int i = h; i < arr.length; i++) {

                E temp = arr[i];
                int j;

                for (j = i; j - h >= 0 && temp.compareTo(arr[j - h]) < 0; j -= h) {
                    arr[j] = arr[j - h];
                }

                arr[j] = temp;
            }

            h /= 3;
        }
    }
}

class QuickSort {

    private QuickSort() {}

    public static<E extends Comparable<E>> void sort3ways(E[] arr){

        Random random = new Random();
        E temp = null;
        sort3ways(arr, 0, arr.length - 1, temp, random);
    }

    public static<E extends Comparable<E>> void sort3ways(E[] arr, int left, int right, E temp, Random random){

        if (left >= right){

            return;
        }

        int[] res = partition3ways(arr, left, right, temp, random);

        sort3ways(arr, left, res[0], temp, random);
        sort3ways(arr, res[1], right, temp, random);
    }

    public static<E extends Comparable<E>> int[] partition3ways(E[] arr, int left, int right, E temp, Random random){

        int p = random.nextInt(right - left + 1) + left;

        swap(arr, p, left, temp);

        int i = left + 1;
        int lt = left;
        int gt = right + 1;

        while (i < gt){

            if (arr[i].compareTo(arr[left]) < 0){

                lt++;
                swap(arr, lt, i, temp);
                i++;
            }

            else if (arr[i].compareTo(arr[left]) == 0){
                i++;
            }

            else if (arr[i].compareTo(arr[left]) > 0){

                gt--;
                swap(arr, gt, i, temp);
            }
        }

        swap(arr, lt, left, temp);
        int[] res = {lt - 1, gt};

        return res;
    }

    public static<E extends Comparable<E>> void swap(E[] arr, int i, int j, E temp){

        temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

class ArrayGenerator {

    private ArrayGenerator (){}

    public static Integer[] generatorRandomArray (Integer n, Integer maxBound){

        Integer[] arr = new Integer[n];
        Random random = new Random();

        for (int i = 0; i < n; i++) {
            arr[i] = random.nextInt(maxBound);
        }

        return arr;
    }

    public static Integer[] generatorSortedArray (Integer n){

        Integer[] arr = new Integer[n];

        for (int i = 0; i < n; i++) {
            arr[i] = i;
        }

        return arr;
    }
}

class Verify {

    private Verify (){}

    public static<E extends Comparable<E>> boolean isSorted(E[] arr){

        for (int i = 0; i < arr.length - 1; i++) {

            if (arr[i].compareTo(arr[i + 1]) > 0) {
                return false;
            }
        }

        return true;
    }

    public static<E extends Comparable<E>> void testTime(String AlgorithmName, E[] arr) {

        long startTime = System.nanoTime();

        if (AlgorithmName.equals("ShellSort")) {
            ShellSort.sort(arr);
        }

        if (AlgorithmName.equals("ShellSortOptimized")) {
            ShellSort.sortOptimized(arr);
        }

        if (AlgorithmName.equals("QuickSort3Ways")) {
            QuickSort.sort3ways(arr);
        }

        long endTime = System.nanoTime();

        if (!Verify.isSorted(arr)){
            throw new RuntimeException(AlgorithmName + "算法排序失败!");
        }

        System.out.println(String.format("%s算法,测试用例为%d,执行时间:%f秒", AlgorithmName, arr.length, (endTime - startTime) / 1000000000.0));
    }
}

复杂度分析

通过不严谨的数学推导,希尔排序法的时间复杂度为O(n^2 * (1 - 1 / 2^logn))

看起来好像和O(n^2)差不多,但因为每次循环使得数组变得更有序,实际上的性能很接近O(nlogn)

posted @ 2021-11-04 17:37  振袖秋枫问红叶  阅读(160)  评论(0)    收藏  举报