十大内部排序算法综述

图片说明

  • 关于时间复杂度: 1)平方阶 (O(n2)) 排序 各类简单排序:直接插入、直接选择和冒泡排序。 2)线性对数阶 (O(nlog2n)) 排序 快速排序、堆排序和归并排序。 3)O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数。希尔排序。 4)线性阶 (O(n)) 排序 基数排序,此外还有桶、箱排序。
  • 关于稳定性: 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。 不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
  • 名词解释: n:数据规模 k:“桶”的个数 In-place:占用常数内存,不占用额外内存 Out-place:占用额外内存
  • 稳定性:排序后 2 个相等键值的顺序和排序之前它们的顺序相同

排序使用场景:

(1)若n较小(如n≤50),可采用直接插入或直接选择排序; 当记录规模较小时,直接插入排序较好; 否则因为直接选择移动的记录数少于直接插人,应选直接选择排序为宜。 (2)若文件初始状态基本有序(指正序,例如大部分人已按顺序排好队,此时一位迟到的同学来了,怎么快速找到他的位置),则应选用直接插人或冒泡排序为宜; (3)若文件初始状态随机分布,则应选用快速排序为宜; (4)若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序; 快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短; 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。 若要求排序稳定,则可选用归并排序。但本章介绍的从单个记录起进行两两归并的 排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定 的,所以改进后的归并排序仍是稳定的。

一、冒泡排序

冒泡排序是最简单的排序算法之一,它还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。

  • 思路原理:(以n个数进行排序为例) 每一轮排序都从第一数开始与后面的数进行比较,大了则交换,每一轮找出本轮的最大数,最多一共进行(n - 1)轮比较。
  • 复杂度与稳定性 1)平均时间复杂度:O(n^2) 2)空间复杂度:O(1) 3)稳定性:稳定
  • 过程图示 以数组数据{ 70,50,30,20,10,70,40,60}为例: 图片说明
  • 核心代码 public static void bubbleSort(int a[]) { for (int i = 1; i < a.length; i++) {//(n - 1)轮比较 for (int j = 0; j < a.length - i; j++) { if (a[j] > a[j + 1]) { swap(a[j], a[j + 1]); //交换数据 } } } } public static void swap(int a, int b) { //交换 int temp = a; a = b; b = temp; }
  • 应用场景 优化后的冒泡排序可用于当数据已经基本有序、数据量较小(且对稳定性有要求时)。

二、选择排序

选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。

  • 思路原理 1)首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置 2)再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。 3)重复第二步,直到所有元素均排序完毕。
  • 复杂度与稳定性 1)平均时间复杂度:O(n^2) 2)空间复杂度:O(1) 3)稳定性:不稳定(例如对于序列5 8 5 2 9,第一轮排序会将5与2交换)
  • 过程图示 图片说明
  • 核心代码 public static void selectSort(int[] arr) { //每一轮都找到本轮的最小值并放到最前面 for (int i = 0; i < arr.length - 1; i++) { //假定每一轮的最小值就是第一个数arr[i] int min = arr[i];//假定每一轮的最小值就是第一个数arr[i] int minIndex = i;//minIndex:最小值对应的索引 for (int j = i + 1; j < arr.length; j++) { if (arr[j] < min) { min = arr[j]; minIndex = j; } } //将每一轮的最小值与第一个值arr[i]进行交换 if(minIndex != i){ arr[minIndex] = arr[i]; arr[i] = min; } } }
  • 应用场景 当数据规模较小时,选择排序性能较好

三、插入排序

插入排序是一种最简单的排序方法,对于少量元素的排序,它是一个有效的算法。 插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。 插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。

  • 思路原理 1)将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。 2)从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
  • 复杂度与稳定性 1)平均时间复杂度:O(n^2) 2)空间复杂度:O(1) 3)稳定性:稳定
  • 过程图示 插入排序 动图演示可参考链接:https://segmentfault.com/a/1190000010413296
  • 核心代码 public static void insertSort(int[] arr) { for (int i = 1; i < arr.length; i++) {//从第二个数(i=1)开始插 int a = arr[i];//取未分区数据 int j = i - 1; for ( ; j >=0 ; j--) { if (a < arr[j]) { arr[j + 1] = arr[j];//挪位置 } else { break; } } arr[j + 1] = a;//插入 } }
  • 应用场景 若数组基本有序且数据规模较小时,选用插入排序较好。

四、希尔排序

希尔排序,也称缩小增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。 希尔排序是基于插入排序的以下两点性质而提出改进方法的: 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率; 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位;

  • 思路原理: 希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
  • 复杂度与稳定性 1)平均时间复杂度:O(nlog n) 2)空间复杂度:O(1) 3)稳定性:不稳定
  • 过程图示 图片说明
  • 核心代码
public static void shellSort(int[] arr) {//希尔排序
    //增量gap,并逐步的缩小增量
    for (int gap = arr.length / 2; gap > 0; gap /= 2) {
        //从第gap个元素,逐个对其所在的组进行直接插入排序
        for (int i = gap; i < arr.length; i++) {
            int j = i;
            int temp = arr[i];

            while (j - gap >= 0 && temp < arr[j - gap]) {
                //移动
                arr[j] = arr[j - gap];
                j -= gap;
            }
            //当退出while后,就给temp找到插入的位置
            arr[j] = temp;
        }
     }
}
  • 应用场景 数据量较小且基本有序时

五、归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

  • 思路原理:把要排序的数组从中间一分为二,分为左边和右边,然后左右分别排序,排好了之后再合并到一起(如果左右两边还能分继续一分为二,递归思想)这样就排好了!

  • 具体步骤: 1)申请一个大小等于左右两个已经排好序的序列之和的空间,用来存放合并后的序列; 2)设定两个指针,最初位置分别指向左右两个已经排好序的序列的起始位置; 3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置; 4)重复步骤 3 直到某一指针达到序列尾; 5)将另一序列剩下的所有元素直接复制到合并序列尾。

  • 复杂度与稳定性 1)平均时间复杂度:O(nlog n) 2)空间复杂度:O(n) 3)稳定性:稳定

  • 过程图示
    alt

  • 核心代码

public class MergeSort {//归并排序--分治思想
    public static void sort(int[] arr) {
        //在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
        int[] temp = new int[arr.length];
        mergeSort(arr, 0, arr.length - 1, temp);
    }
    private static void mergeSort(int[] arr, int left, int right, int[] temp) {
        if (left < right) {
            int mid = (left + right) / 2;
            mergeSort(arr, left, mid, temp);
            mergeSort(arr, mid + 1, right, temp);
            merge(arr, left, mid, right, temp);
        }
    }
    private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        int i = left;//左序列指针
        int j = mid + 1;//右序列指针
        int t = 0;//临时数组指针
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                temp[t++] = arr[i++];
            } else {
                temp[t++] = arr[j++];
            }
        }
        while (i <= mid) {//将左边剩余元素填充进temp中
            temp[t++] = arr[i++];
        }
        while (j <= right) {//将右序列剩余元素填充进temp中
            temp[t++] = arr[j++];
        }
        t = 0;
        //将temp中的元素全部拷贝到原数组中
        while (left <= right) {
            arr[left++] = temp[t++];
        }
     }
}
  • 应用场景 数据量较大且要求排序稳定时

六、快速排序

快速排序又是一种分而治之思想在排序算法上的典型应用,它使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。 快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

  • 思路原理: 1)先从数列中取出一个数作为基准数(每一轮次排序后都可以保证基数左边的值都小于等于它本身,基数右边的值大于等于它本身); 2)分区过程,将比这个数大的数全放在它的右边,小于或者等于它的数全放在它的左边; 3)再对左右区间重复第二步(递归),直到各区间只有一个数。
  • 复杂度与稳定性 1)平均时间复杂度:O(nlog n) 2)空间复杂度:O(log n) 3)稳定性:不稳定
  • 过程图示 原始数组 70 50 30 20 10 70 40 60 第一轮次 60 50 30 20 10 70 40 70 第二轮次 40 50 30 20 10 60 70 70 第三轮次 10 20 30 40 50 60 70 70
  • 核心代码
public class QuickSort {//快速排序
    private static void quickSort(int[] arr, int left, int right) {
        if (left < right) {
            int partitionIndex = partition(arr, left, right);
            quickSort(arr, left, partitionIndex - 1);
            quickSort(arr, partitionIndex + 1, right);
        }
    }
    private static int partition(int[] arr, int left, int right) {
        int pivot = left;// 设定基准值(pivot)
        int index = pivot + 1;
        for (int i = index; i <= right; i++) {
            if (arr[i] < arr[pivot]) {
                swap(arr, i, index);
                index++;
            }
        }
        swap(arr, pivot, index - 1);
        return index - 1;
    }
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
     }
}
  • 应用场景 快速排序适合处理大量数据排序时的场景

七、堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列; 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列; 一般升序采用大顶堆,降序采用小顶堆

  • 思路原理: 1)将待排序序列构造成一个大顶堆; 2)此时,整个序列的最大值就是堆顶的根节点; 3)将其与末尾元素进行交换,此时末尾就为最大值; 4)将剩余n - 1 个元素重新构造成一个堆,这样就会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
  • 复杂度与稳定性 1)平均时间复杂度:O(nlog n) 2)空间复杂度:O(1) 3)稳定性:不稳定
  • 过程图示 https://segmentfault.com/a/1190000021197502?utm_source=sf-similar-article
  • 核心代码
public class HeapSort {
    public void HeapAdjust(int[] arr, int parent, int length) {
        int temp = arr[parent]; // temp保存当前父节点
        int child = 2 * parent + 1; // 先获得左孩子
        while (child < length) {
            // 如果有右孩子结点,并且右孩子结点的值大于左孩子结点,则选取右孩子结点
            if (child + 1 < length && arr[child] < arr[child + 1]) {
                child++;
            }
            // 如果父结点的值已经大于孩子结点的值,则直接结束
            if (temp >= arr[child])
                break;
            // 把孩子结点的值赋给父结点
            arr[parent] = arr[child];
            // 选取孩子结点的左孩子结点,继续向下筛选
            parent = child;
            child = 2 * child + 1;
        }
        arr[parent] = temp;
    }

    public void heapSort(int[] list) {
        // 循环建立初始堆
        for (int i = list.length / 2; i >= 0; i--) {
            HeapAdjust(list, i, list.length);
        }
        // 进行n-1次循环,完成排序
        for (int i = list.length - 1; i > 0; i--) {
            // 最后一个元素和第一元素进行交换
            int temp = list[i];
            list[i] = list[0];
            list[0] = temp;
            HeapAdjust(list, 0, i);
        }
     }
}
  • 应用场景 堆排序适合处理数据量大的情况,数据呈流式输入时用堆排序也很方便

八、计数排序

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数且要求所有元素都非负。 计数排序算法不是基于元素比较,而是利用数组下标来确定元素的正确位置。 它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k),快于任何比较排序算法。 当然这是一种牺牲空间换取时间的做法,而且当 O(k)>O(n*log(n)) 的时候其效率反而不如基于比较的排序。

  • 思路原理: 1)找出待排序的数组中最大和最小的元素 2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项 3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加) 4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
  • 复杂度与稳定性 1)平均时间复杂度:O(n + k) 2)空间复杂度:O(k) 3)稳定性:稳定
  • 过程图示 参照:https://segmentfault.com/a/1190000021197502?utm_source=sf-similar-article
  • 核心代码
public class CountingSort {
    private int[] countingSort(int[] arr) {
        int maxValue = getMaxValue(arr);
        int bucketLen = maxValue + 1;
        int[] bucket = new int[bucketLen];
        for (int value : arr) {
            bucket[value]++;
        }
        int sortedIndex = 0;
        for (int j = 0; j < bucketLen; j++) {
            while (bucket[j] > 0) {
                arr[sortedIndex++] = j;
                bucket[j]--;
            }
        }
        return arr;
    }

    private int getMaxValue(int[] arr) {
        int maxValue = arr[0];
        for (int value : arr) {
            if (maxValue < value) {
                maxValue = value;
            }
        }
        return maxValue;
    }
    @Test
    public void testCountingSort() {
        int[] arr = {106, 18, 1, 34, 2, 56, 23, 2, 0, 3};
        countingSort(arr);
        System.out.println("排序后的数组为:" + Arrays.toString(arr));
    }
}
  • 应用场景 计数排序虽然时间复杂度较低,但需要满足的条件较多,如果能满足限制条件与空间需求,计数排序自然很快

九、桶排序(也叫箱排序)

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点: 1)在额外空间充足的情况下,尽量增大桶的数量 2)使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中 同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

  • 思路原理: 1)根据待排序集合中最大元素和最小元素的差值范围和映射规则,确定申请的桶个数; 2)遍历待排序集合,将每一个元素移动到对应的桶中; 3)对每一个桶中元素进行排序,并移动到已排序集合中。
  • 复杂度与稳定性 1)平均时间复杂度:O(n + k) 2)空间复杂度:O(n + k) 3)稳定性:稳定
  • 过程图示 图片说明
  • 核心代码
public class BucketSort {
    public static void bucketSort(int[] arr){
        // 计算最大值与最小值
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for(int i = 0; i < arr.length; i++){
            max = Math.max(max, arr[i]);
            min = Math.min(min, arr[i]);
        }
        // 计算桶的数量
        int bucketNum = (max - min) / arr.length + 1;
        ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
        for(int i = 0; i < bucketNum; i++){
            bucketArr.add(new ArrayList<Integer>());
        }
        // 将每个元素放入桶
        for(int i = 0; i < arr.length; i++){
            int num = (arr[i] - min) / (arr.length);
            bucketArr.get(num).add(arr[i]);
        }
        // 对每个桶进行排序
        for(int i = 0; i < bucketArr.size(); i++){
            Collections.sort(bucketArr.get(i));
        }
        // 将桶中的元素赋值到原序列
        int index = 0;
        for(int i = 0; i < bucketArr.size(); i++){
            for(int j = 0; j < bucketArr.get(i).size(); j++){
                arr[index++] = bucketArr.get(i).get(j);
            }
        }
    }
    @Test
    public void testBucketSort() {
        int[] arr = {106, -18, 1, 34, 2, -56, 23, 2, 0, 3};
        bucketSort(arr);
        System.out.println("排序后的数组为:" + Arrays.toString(arr));
    }
}
  • 应用场景 如果满足桶排序的假设条件,那么桶排序的速度是非常快的

十、基数排序

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

  • 思路原理 1)我们先根据序列的个位数的数字来进行分类,将其分到指定的桶中。 2)分类后,我们在从各个桶中,将这些数按照从编号0到编号9的顺序依次将所有数取出来。 3)得到的序列就是个位数上呈递增趋势的序列。 4)按照上图个位数排序: {50, 30, 0, 100, 11, 2, 123, 543, 187, 49} 。 5)接下来对十位数、百位数也按照这种方法进行排序,最后就能得到排序完成的序列。
  • 复杂度与稳定性 1)平均时间复杂度:O(n * k) 2)空间复杂度:O(n + k) 3)稳定性:稳定
  • 过程图示 图片说明
  • 核心代码
public static void radixSort(int[] arr) {//基数排序方法
    //1. 得到数组中最大的数的位数
    int max = arr[0]; //假设第一数就是最大数
    for(int i = 1; i < arr.length; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    //得到最大数是几位数
    int maxLength = (max + "").length();
    //定义一个二维数组,表示10个桶, 每个桶就是一个一维数组
    //说明
    //1. 二维数组包含10个一维数组
    //2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length
    //3. 名明确,基数排序是使用空间换时间的经典算法
    int[][] bucket = new int[10][arr.length];

    //为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
    //可以这里理解
    //比如:bucketElementCounts[0] , 记录的就是  bucket[0] 桶的放入数据个数
    int[] bucketElementCounts = new int[10];
    //这里我们使用循环将代码处理
    for(int i = 0 , n = 1; i < maxLength; i++, n *= 10) {
        //(针对每个元素的对应位进行排序处理), 第一次是个位,第二次是十位,第三次是百位..
        for(int j = 0; j < arr.length; j++) {
            //取出每个元素的对应位的值
            int digitOfElement = arr[j] / n % 10;
            //放入到对应的桶中
            bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
            bucketElementCounts[digitOfElement]++;
        }
        //按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
        int index = 0;
        //遍历每一桶,并将桶中是数据,放入到原数组
        for(int k = 0; k < bucketElementCounts.length; k++) {
            //如果桶中,有数据,我们才放入到原数组
            if(bucketElementCounts[k] != 0) {
                //循环该桶即第k个桶(即第k个一维数组), 放入
                for(int l = 0; l < bucketElementCounts[k]; l++) {
                    //取出元素放入到arr
                    arr[index++] = bucket[k][l];
                }
            }
            //第i+1轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
            bucketElementCounts[k] = 0;
        }
        //System.out.println("第"+(i+1)+"轮,对个位的排序处理 arr =" + Arrays.toString(arr));
     }
}
  • 应用场景 同计数排序

参考文献: https://blog.csdn.net/Hairy_Monsters/article/details/80154391?utm_term=%E5%90%84%E7%A7%8D%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%E7%9A%84%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF&utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~sobaiduweb~default-0-80154391&spm=3001.4430 https://segmentfault.com/a/1190000021197502?utm_source=sf-similar-article https://www.sohu.com/a/423777892_505803 https://blog.csdn.net/qq_27124771/article/details/87651495