九大排序算法及耗时测试、适用场合

九大排序算法简单集合

本人刚学习完排序算法,看的是《数据结构与算法分析(Java描述版)(第四版)》,与C描述版还是有些不一样的地方,写下此博客与各位初学者分享,代码中已写好详细的注释,也分析了各种算法的适用场合,如有不懂可以留言。最后给出了Github上源代码获取网址,建议在IDE中边看边测试。第一次写,写得不好的地方请谅解。

  • 插入排序
    • 直接插入排序
    • 希尔排序
  • 选择排序
    • 直接选择排序
    • 堆排序
  • 交换排序
    • 冒泡排序
    • 快速排序
  • 归并排序
  • 基数排序
  • 外部排序

/*
 *         ┏┓      ┏┓+ +
 *           ┏━┛┻━━━━━━┛┻━┓ + +
 *           ┃       ┃ + + +
 *           ┃     ━      ┃ + + + +
 *           ┃ ████━████  ┃
 *           ┃            ┃ + +
 *           ┃     ┻      ┃
 *           ┃            ┃ +
 *           ┗━━┓      ┏━━┛
 *              ┃      ┃
 *              ┃      ┃ + + + +
 *              ┃      ┃     Code is far away from bug with the animal protecting
 *              ┃      ┃ +      神兽保佑,代码无bug
 *              ┃      ┃
 *              ┃      ┃  +
 *              ┃      ┗━━━━━━━┓ + +
 *              ┃              ┣━┓
 *              ┃              ┏━┛
 *              ┗━┓ ┓ ┏━━┳━┓ ┏━┛ + + + +
 *                ┃ ┫ ┫  ┃ ┫ ┫
 *                ┗━┻━┛  ┗━┻━┛+ + + +
 */

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;

/**
 * 多种排序算法集合
 * 为什么快排在实际使用中通常优于堆排序?
 * 局部性原理,越靠近的数据比较和交换用时越少,而且CPU的Cache中缓存了最近频繁读写的数据。
 * 在堆排中,每一个操作都是不利于程序的局部性原理的,每次元素间的比较、数的调整等,
 * 最大堆调整每次都从堆底拿元素换到堆顶然后又下滤下去,在堆底一般都比较小,很大可能再次下滤到底层,
 * 这个过程做了很多无用功,需要不断地跨度大地读写数据。
 * 反观快速排序和归并排序,利用分而治之的方法,元素间的比较都在某个段内,操作比较集中,局部性相当好。
 *
 * 快排<归并<堆排<希尔<<<插入<选择<冒泡
 *
 * @author Macer_YGG
 * @version V1.1
 * @date 2018/04/30 23:08
 */
public class SortDemo {
    /**
     * timer 计时用
     */
    private static long timer;

    /**
     * Simple insertion sort 直接插入排序
     * 每步将一个待排序的元素,按大小插入前面已经排序的序列中适当位置上,直到全部插入完为止
     * 时间复杂度O(n^2),空间复杂度O(1),稳定
     * 适用场合:数据量不大,对算法的稳定性有要求,且数据局部或者整体有序的情况,小规模如10个数平均时间最快,可以在快排中调用
     *
     * @param a   an array of Comparable items.
     * @param  实现Comparable接口的类
     */
    public static super T>> void insertionSort(T[] a) {
        long start = System.currentTimeMillis();
        int j;
        T tmp;
//        从第二个元素开始,向前试探性插入
        for (int i = 1; i < a.length; i++) {
            if (a[i].compareTo(a[i - 1]) < 0) {
//                将当前元素暂存
                tmp = a[i];
//                向前比较,若符合要求则将比较项往右移存
                for (j = i; j > 0 && tmp.compareTo(a[j - 1]) < 0; j--) {
                    a[j] = a[j - 1];
                }
//                插入
                a[j] = tmp;
            }
        }
        timer = System.currentTimeMillis() - start;
    }

    /**
     * ShellSort 希尔排序
     * 是插入排序的一种又称"缩小增量排序"(Diminishing Increment Sort)
     * 使用简单的希尔增量序列,复杂的增量序列中Sedgewick 增量序列最坏时间复杂度达到O(n^1.3)
     * 时间复杂度取决于增量序列,空间复杂度O(1),不稳定!!!
     * 适用场合:数据量较大,不要求稳定性
     *
     * @param a   an array of Comparable items.
     * @param  实现Comparable接口的类
     */
    public static super T>> void shellSort(T[] a) {
        long start = System.currentTimeMillis();
        T tmp;
        int j;
//        使用希尔增量序列
        for (int gap = a.length / 2; gap > 0; gap /= 2) {
//            里层用的是直接插入排序算法
            for (int i = gap; i < a.length; i++) {
                if (a[i].compareTo(a[i - gap]) < 0) {
                    tmp = a[i];
                    for (j = i; j >= gap && tmp.compareTo(a[j - gap]) < 0; j -= gap) {
                        a[j] = a[j - gap];
                    }
                    a[j] = tmp;
                }
            }
        }
        timer = System.currentTimeMillis() - start;
    }

    /**
     * SelectionSort 直接选择排序
     * 每一次从待排序的数据元素中选出最小的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完
     * 时间复杂度O(n^2),空间复杂度O(1),不稳定
     * 适用场合:当数据量不大,且对稳定性没有要求的时候
     *
     * @param a   an array of Comparable items.
     * @param  实现Comparable接口的类
     */
    public static super T>> void selectionSort(T[] a) {
        long start = System.currentTimeMillis();
//        第 i+1 个最小元素位置标记
        int minIndex;
        T tmp;
        for (int i = 0; i < a.length; i++) {
            minIndex = i;
            for (int j = i + 1; j < a.length; j++) {
//                比标记的最小元素还小,则将标记让出
                if (a[j].compareTo(a[minIndex]) < 0) {
                    minIndex = j;
                }
            }
//            如果标记位置改变,则将第 i+1 个最小元素交换到标记位置
            if (i != minIndex) {
                tmp = a[i];
                a[i] = a[minIndex];
                a[minIndex] = tmp;
            }
        }
        timer = System.currentTimeMillis() - start;
    }

    /**
     * HeapSort 堆排序 升序 用最大堆
     * 它是选择排序的一种。可以利用数组的特点快速定位指定索引的元素。
     * 时间复杂度O(n log n),空间复杂度O(1),不稳定
     * 适用场合:数据量较大,且对稳定性无要求,不如用快排或归并,但取前几个最值时可以不用排序完,奇效
     *
     * @param a   an array of Comparable items.
     * @param  实现Comparable接口的类
     */
    public static super T>> void heapSort(T[] a) {
        long start = System.currentTimeMillis();
        T tmp;
//        从第一个非叶子节点开始下滤,顺序是从下至上,从右至左
        for (int i = a.length / 2 - 1; i >= 0; i--) {
            percDown(a, i, a.length);
        }
//        将堆顶(第k个最大元素)交换至 a.length - k 处
        for (int i = a.length - 1; i > 0; i--) {
            tmp = a[i];
            a[i] = a[0];
            a[0] = tmp;
//            换上来的元素再下滤下去,第k+1个最大值在堆顶的左或右子树,或堆顶本身
//            长度为i,不再包括已排好的元素
            percDown(a, 0, i);
        }
        timer = System.currentTimeMillis() - start;
    }

    /**
     * percolate down 最大堆的下滤操作
     *
     * @param a      an array of Comparable items.
     * @param i      传入的元素索引
     * @param length 堆的大小
     * @param     实现Comparable接口的类
     */
    private static super T>> void percDown(T[] a, int i, int length) {
//        暂存当前元素
        T tmp = a[i];
//        左子树为2 * i + 1,右子树为2 * i + 2
//        找到存放有更大元素的子树作为下滤口
        for (int child = 2 * i + 1; child < length; child = 2 * child + 1) {
//            如果存在右子树(即不是堆的最后一个元素)并且右子树的元素比左子树更大,则选择右子树作为下滤口
            if (child != length - 1 && a[child].compareTo(a[child + 1]) < 0) {
                child++;
            }
//            如果下滤口的元素比父节点的元素更大,则往父节点插入,否则下滤到底,整个循环结束
            if (tmp.compareTo(a[child]) < 0) {
                a[i] = a[child];
                i = child;
            } else {
                break;
            }
        }
//        将暂存元素取出放在最后找到的底,可能没进行下滤,底就是它自身的位置
        a[i] = tmp;
    }

    /**
     * BubbleSort 冒泡排序
     * 重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来
     * 时间复杂度O(n^2),空间复杂度O(1),稳定
     * 适用场合:数据量量不大,对稳定性有要求,且数据基本有序的情况下
     *
     * @param a   an array of Comparable items.
     * @param  实现Comparable接口的类
     */
    public static super T>> void bubbleSort(T[] a) {
        long start = System.currentTimeMillis();
//        外层循环,i来控制内层循环的深度,每轮将第k个最大元素冒泡至a.length - k的位置后,内循环不再触碰这些位置
        for (int i = 0; i < a.length - 1; i++) {
            for (int j = 0; j < a.length - 1 - i; j++) {
                if (a[j].compareTo(a[j + 1]) > 0) {
                    T tmp = a[j];
                    a[j] = a[j + 1];
                    a[j + 1] = tmp;
                }
            }
        }
        timer = System.currentTimeMillis() - start;
    }

    /**
     * 将T tmp声明在循环外的冒泡排序,证明多次循环中局部变量的分配内存和释放内存是很大的开销
     *
     * @param a   an array of Comparable items.
     * @param  实现Comparable接口的类
     */
    public static super T>> void bubbleSort2(T[] a) {
        long start = System.currentTimeMillis();
//        将tmp的声明放在循环外,只需分配一次内存,释放一次内存
        T tmp;
        for (int i = 0; i < a.length - 1; i++) {
            for (int j = 0; j < a.length - 1 - i; j++) {
                if (a[j].compareTo(a[j + 1]) > 0) {
                    tmp = a[j];
                    a[j] = a[j + 1];
                    a[j + 1] = tmp;
                }
            }
        }
        timer = System.currentTimeMillis() - start;
    }

    /**
     * QuickSort 快速排序的驱动程序
     *
     * @param a   an array of Comparable items.
     * @param  实现Comparable接口的类
     */
    public static super T>> void quickSort(T[] a) {
        long start = System.currentTimeMillis();
        quickSort(a, 0, a.length - 1);
        timer = System.currentTimeMillis() - start;
    }

    /**
     * QuickSort 快速排序
     * 是对冒泡排序的一种改进。通过一趟排序将要排序的数据分割成独立的两部分,
     * 其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对
     * 这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
     * 时间复杂度O(n log n),空间复杂度O(n log n),不稳定,选好枢纽元是使坏情况发生的关键
     * 截断范围选10比较合适,进入截断范围改用小规模高效率算法对小片段排序
     * 适用场合:数值范围较大,相同值的概率较小,数据量大且不考虑稳定性的情况,数值远大于数据量时威力更大
     *
     * @param a     an array of Comparable items.
     * @param    实现Comparable接口的类
     * @param left  左起始位置
     * @param right 右起始位置
     */
    private static super T>> void quickSort(T[] a, int left, int right) {
//        进入截断值范围内,该用插入排序算法处理
        final int cutoff = 10;
        if (left + cutoff <= right) {
            T tmp;
//            pivot枢纽元,调用三数中值分割法产生,放在left - 1位置
            T pivot = median3(a, left, right);
            int i = left;
            int j = right - 1;
            for (; ; ) {
//                左标i跃过小于枢纽元的元素到达大于枢纽元的元素位置,右标j反之
                while (a[++i].compareTo(pivot) <= 0 && i < right) ;
                while (a[--j].compareTo(pivot) >= 0 && j > left) ;
//                如果游标碰撞,说明分组完成,结束循环,否则交换两个错位元素
                if (i >= j) break;
                tmp = a[i];
                a[i] = a[j];
                a[j] = tmp;
            }
//            将枢纽元放回中间位置
            tmp = a[i];
            a[i] = a[right - 1];
            a[right - 1] = tmp;
//            左右分区进行递归
            quickSort(a, left, i - 1);
            quickSort(a, i + 1, right);
        } else {
            int j;
            T tmp;
//            直接插入排序,操作数组的left到right段
            for (int i = left + 1; i <= right; i++) {
                if (a[i].compareTo(a[i - 1]) < 0) {
                    tmp = a[i];
//                    j要大于left而不是0
                    for (j = i; j > left && tmp.compareTo(a[j - 1]) < 0; j--) {
                        a[j] = a[j - 1];
                    }
                    a[j] = tmp;
                }
            }
        }
    }

    /**
     * Median3 三数中值分割法
     * 选出左端、右端和中心位置上的三个元素的中值作为枢纽元
     *
     * @param a     an array of Comparable items.
     * @param left  左起始位置
     * @param right 右起始位置
     * @param    实现Comparable接口的类
     * @return 返回pivot枢纽元
     */
    private static super T>> T median3(T[] a, int left, int right) {
        int center = (left + right) / 2;
        T tmp;
        if (a[center].compareTo(a[left]) < 0) {
            tmp = a[center];
            a[center] = a[left];
            a[left] = tmp;
        }
        if (a[left].compareTo(a[right]) > 0) {
            tmp = a[right];
            a[right] = a[left];
            a[left] = tmp;
        }
        if (a[right].compareTo(a[center]) < 0) {
            tmp = a[right];
            a[right] = a[center];
            a[center] = tmp;
        }
//        a[left]已经确认过比a[center]大,所以直接排在a[center]后面
        tmp = a[center];
        a[center] = a[right - 1];
        a[right - 1] = tmp;
        return a[right - 1];
    }

    /**
     * MergeSort 归并排序的驱动程序
     *
     * @param a   an array of Comparable items.
     * @param  实现Comparable接口的类
     */
    public static super T>> void mergeSort(T[] a) {
        long start = System.currentTimeMillis();
//        泛型不能直接实例化,可以new一个实现了接口的数组,再强制转型
        T[] tmpA = (T[]) new Comparable[a.length];
        mergeSort(a, tmpA, 0, a.length - 1);
        timer = System.currentTimeMillis() - start;
    }

    /**
     * MergeSort 归并排序 比较次数最少,Arrays.sort()调用该算法
     * 时间复杂度O(n log n),空间复杂度O(n),稳定!!!
     * 适用场合:数据规模较大,对稳定性有要求,一般用于总体无序,但是各子项相对有序的数列
     *
     * @param a     an array of Comparable items.
     * @param tmpA  存放归并结果的临时数组
     * @param left  the left-most index of the subarray
     * @param right the right-most index of the subarray
     * @param    实现Comparable接口的类
     */
    private static super T>> void mergeSort(T[] a, T[] tmpA, int left, int right) {
        if (left < right) {
//            分
            int center = (right + left) / 2;
            mergeSort(a, tmpA, left, center);
            mergeSort(a, tmpA, center + 1, right);
//            治
            merge(a, tmpA, left, center + 1, right);
        }
    }

    /**
     * merge方法,归并操作
     *
     * @param a        an array of Comparable items.
     * @param tmpA     存放归并结果的临时数组
     * @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
     * @param       实现Comparable接口的类
     */
    private static super T>> void merge(T[] a, T[] tmpA, int leftPos, int rightPos, int rightEnd) {
        int leftEnd = rightPos - 1;
        int tmpPos = leftPos;
        int numElements = rightEnd - leftPos + 1;
//        两组升序序列比较把小的先放进临时数组里,直到有一组取完
        while (leftPos <= leftEnd && rightPos <= rightEnd) {
//            想要保持排序稳定性,这里要大于等于
            if (a[leftPos].compareTo(a[rightPos]) <= 0) {
                tmpA[tmpPos++] = a[leftPos++];
            } else {
                tmpA[tmpPos++] = a[rightPos++];
            }
        }
//        左数组剩余的元素复制到临时数组后面
        while (leftPos <= leftEnd) {
            tmpA[tmpPos++] = a[leftPos++];
        }
//        右数组剩余的元素复制到临时数组后面
        while (rightPos <= rightEnd) {
            tmpA[tmpPos++] = a[rightPos++];
        }
//        起始位置注意不能用leftPos,因为leftPos已经改变了
        System.arraycopy(tmpA, rightEnd - numElements + 1, a, rightEnd - numElements + 1, numElements);
    }

    /**
     * countingRadixSort 计数基数排序 线性时间复杂度
     * 基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)
     * 最高位优先法 MSD 和最低位优先 LSD,d个关键码,n个数据,r是基数
     * LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。MSD的方式与LSD相反,
     * 是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,
     * 将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。
     * 时间复杂度O(d(r+n)),空间复杂度O(rd+n),稳定
     * 适用场合:数值都是非负整数,且范围较小的情况,尤其适用于256位的ASCII码,用作字符串的字典顺序排序
     *
     * @param a 数组
     * @param d 关键码个数
     */
    public static void radixSort(Integer[] a, int d) {
        long start = System.currentTimeMillis();
//        10进制
        final int bucketNum = 10;
//        计数标记
        int counter = 0;
//        10^(位数-1)
        int num = 1;
        int[][] bucket = new int[bucketNum][a.length];
//        存放顺序的数组,数组order[i]用来表示该位是i的数的个数
        int[] order = new int[bucketNum];
        for (int n = 1; n <= d; num *= bucketNum, n++) {
            for (int i = 0; i < a.length; i++) {
//                最小优先法LSD,remainder余数
                int rem = ((a[i] / num) % bucketNum);
                bucket[rem][order[rem]] = a[i];
                order[rem]++;
            }
            for (int i = 0; i < bucketNum; i++) {
                if (order[i] != 0)
                    for (int j = 0; j < order[i]; j++) {
                        a[counter] = bucket[i][j];
                        counter++;
                    }
                order[i] = 0;
            }
            counter = 0;
        }
        timer = System.currentTimeMillis() - start;
    }

    /**
     * 外部排序 用了归并算法的思路
     * 在主存中最大限度地创建三块等长的空间,将辅存中的数据分割成该长度的小片段,第一次导入主存时,将两个小片段各自排序
     * 然后两个小片段就可以归并了,第三块空间存放结果,分两次向辅存输回结果。之后数据一边进来,一边归并,一边传输结果回去
     */

    /**
     * print 输出结果
     *
     * @param sortResult 文件对象
     * @param aa         二维数组
     */
    private static void print(PrintWriter sortResult, Integer[][] aa) {
//去掉注释可用,建议数据规模很大时,不要序列输出,把拖时间的三大低效率去掉
//        sortResult.println("原始序列:");
//        for (Integer i : aa[0]) {
//            sortResult.write(i + " ");
//        }
//        insertionSort(aa[0]);
//        sortResult.println("\n\n直接插入排序");
//        printResult(sortResult, aa[0]);

        shellSort(aa[1]);
        sortResult.println("\n\n希尔排序");
        printResult(sortResult, aa[1]);

//        selectionSort(aa[2]);
//        sortResult.println("\n\n直接选择排序");
//        printResult(sortResult, aa[2]);

        heapSort(aa[3]);
        sortResult.println("\n\n堆排序");
        printResult(sortResult, aa[3]);

//        bubbleSort(aa[4]);
//        sortResult.println("\n\n冒泡排序");
//        printResult(sortResult, aa[4]);
//
//        bubbleSort2(aa[5]);
//        sortResult.println("\n\n优化的冒泡排序");
//        printResult(sortResult, aa[5]);

        quickSort(aa[6]);
        sortResult.println("\n\n快速排序");
        printResult(sortResult, aa[6]);

        mergeSort(aa[7]);
        sortResult.println("\n\n归并排序");
        printResult(sortResult, aa[7]);

        radixSort(aa[8], 6);
        sortResult.println("\n\n基数排序");
        printResult(sortResult, aa[8]);

//        Arrays.sort()方法
        long start = System.currentTimeMillis();
        Arrays.sort(aa[9]);
        timer = System.currentTimeMillis() - start;
        System.out.println(timer);
    }

    /**
     * printResult print的子函数
     *
     * @param sortResult 文件对象
     * @param a          数组
     */
    private static void printResult(PrintWriter sortResult, Integer[] a) {
//        sortResult.write("升序序列:");
//        for (Integer i : a) {
//            sortResult.write(i + " ");
//        }
        sortResult.println("\n耗时:" + timer);
    }

    /**
     * main 主函数
     *
     * @param args 外部字符串参数
     */
    public static void main(String[] args) {
        Integer[] a = new Integer[100000];
        Random rand = new Random(System.currentTimeMillis());
//        伪随机数填充数组
        for (int i = 0; i < a.length; i++) {
            a[i] = rand.nextInt(999999);
        }
//        复制多组相同的原始伪随机序列
        Integer[][] aa = new Integer[10][a.length];
        for (int i = 0; i < aa.length; i++) {
//            System.arraycopy方法是native方法,比clone()更高效,Arrays.copyOf调用该方法
//            传入参数(源数组,源数组起始位置,目的数组,目的数组起始位置,复制长度)都是浅拷贝
            System.arraycopy(a, 0, aa[i], 0, a.length);
        }

        try (
                PrintWriter sortResult = new PrintWriter(new FileOutputStream("SortResult.txt"));
        ) {
            print(sortResult, aa);
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }
}

本人测试了各种情况下的耗时
这里只是举个例子,感兴趣的同学可以自行测试
一次不全面测试(规模一亿个数时,可能堆内存不足,需要设置JVM)
数值范围一亿,非负整数

算法 规模 耗时/ms
希尔排序 1千万 32402
堆排序 1千万 43526
快速排序 1千万 7183
归并排序 1千万 9753
基数排序 1千万 7067

这里附上代码.java文件,方便读者自行测试,加深了解

Github源代码下载

你可能感兴趣的:(算法)