1.简介

        排序算法可以分为:
  • 内部排序:数据记录在内存中进行排序。
  • 外部排序:因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存
        常见的内部排序算法有:插入排序希尔排序选择排序冒泡排序归并排序快速排序堆排序基数排序等。用一张图概括:
        
图中名词解释:
  • n:数据规模
  • k:“桶” 的个数
  • In-place占用常数内存不占用额外内存
  • Out-place:占用额外内存,比如需要开辟一个新数组
术语说明:
  • 稳定:相等的数据排序后相对顺序保持不变,如果 A 原本在 B 前面,而且 A=B,排序之后 A 仍然在 B 的前面。
  • 不稳定相等的数据排序后相对顺序可能改变如果 A 原本在 B 的前面,而且 A=B,排序之后 A 可能会出现在 B 的后面。
    不稳定的有:选择、希尔、快速、堆排序。(投机取巧地记一下:选择希尔快速怼)
  • 内排序:所有排序操作都在内存中完成
  • 外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行。
  • 最好:原序列有序。
  • :原序列逆序。

2.冒泡排序(Bubble Sort)

(1)算法步骤

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个
  • 重复步骤 1~3,直到排序完成。
        

(2)代码实现

  • 未优化写法:
  • 优化:可以在每次循环中加一个标志位判断,如果原序列有序,就不用比较了,可以将O(n²)降为O(n)

(3)算法分析

  • 稳定性:稳定
  • 时间复杂度 :最佳:O(n) ,最差:O(n2), 平均:O(n2)
  • 空间复杂度 :O(1)
  • 排序方式 :In-place

3.选择排序 (Selection Sort)

        无论什么样的序列使用选择排序都是O(n²)的时间复杂度,所以选择排序适用于数据规模少的情况,唯一的好处可能就是不占用额外的内存空间了吧。

(1)算法步骤

  • 每次都从未排序序列中寻找最小(大)元素,然后放到已排序序列的末尾,直到所有元素均排序完毕。
    相当于每次从未排序序列中选择最小的元素,放到排序序列的最后,所以叫选择排序。
        

(2)代码实现

        
【tips】用minIndex记录最小元素的下标,在遍历未排序序列时只需要更新minIndex即可,遍历完了进行一次交换就行了。我自己写的没有用到minIndex(如下),但是在遍历未排序序列时,遇到更小的就要交换一次,不如人家写的。
        

(3)算法分析

  • 稳定性不稳定,比如[33,1],选择排序后变成了[1,33],两个"3"的相对顺序改变了。
  • 时间复杂度 :最佳:O(n2) ,最差:O(n2), 平均:O(n2)
  • 空间复杂度 :O(1)
  • 排序方式 :In-place

4.插入排序 (Insertion Sort)

(1)算法步骤

        插入排序跟打扑克摸牌是一个道理,先把手里的牌排好,摸到新牌再插入合适位置。
  • 从第一个元素开始,该元素可以认为已经有序(手里的第一张牌本身就有序了);
  • 取出下一个元素(摸牌),从后向前扫描有序序列
  • 如果该元素(已排序)大于新元素,将该元素后移
  • 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤 2~5。
        

(2)代码实现

        

(3)算法分析

  • 稳定性:稳定
  • 时间复杂度 :最佳:O(n) ,最差:O(n2), 平均:O(n2)
  • 空间复杂度 :O(1)
  • 排序方式 :In-place

5.希尔排序 (Shell Sort)

(1)算法步骤

        希尔排序也是一种插入排序,它的基本思想是:先将整个待排序序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录 “基本有序” 时,再对整个序列进行直接插入排序。
        

(2)代码实现

        希尔排序是基于插入排序做了改进,用步长gap先来划分成若干子序列,子序列进行插入排序(红框),然后再缩小gap(每次/2),重复对子序列进行插入排序,直到gap=0了,说明不能再分组了,序列排序结束了。
        
        从希尔排序和插入排序的对比也可以发现,希尔排序中用到的插入排序,只不过把"1"换成了步长"gap"。

(3)算法分析

  • 稳定性:不稳定
  • 时间复杂度 :最佳:O(nlogn), 最差:O(n2) ,平均:O(nlogn)
  • 空间复杂度 :O(1)

6.★归并排序 (Merge Sort) 

(1)算法步骤

        归并排序的思想是即先使每个子序列有序,再使子序列之间有序,最终达到整个序列有序(分治思想)。若将两个有序表合并成一个有序表,称为 2 - 路归并
        归并排序算法是一个递归过程,边界条件为当输入序列仅有一个元素时,直接返回,具体过程如下:
  • 如果输入内只有一个元素,则直接返回,否则将长度为n的输入序列分成两个长度为n/2的子序列
  • 分别对这两个子序列进行归并排序,使子序列变为有序状态;
  • 设定两个指针,分别指向两个已经排序子序列的起始位置
  • 比较两个指针所指向的元素,选择相对小的元素放入到合并空间(用于存放排序结果),并移动指针到下一位置;
  • 重复步骤 3 ~4 直到某一指针达到序列尾;
  • 将另一序列剩下的所有元素直接复制到合并序列尾。
        

(2)2路-归并排序代码实现

        

(3)算法分析

  • 稳定性:稳定
  • 时间复杂度 :最佳:O(nlogn), 最差:O(nlogn), 平均:O(nlogn)
  • 空间复杂度 :O(n),额外创建了一个辅助数组sortedArr

7.快速排序 (Quick Sort)

(1)算法步骤

  • 首先设定一个分界点(pivot),通过分界点将数组分成左右两部分;
  • 将小于分界点的数据都放在到数组左边,将大于分界点的数据都放在数组的右边,与分界点相同的数据放哪边都可以;
  • 然后,分界点左边的子数组和右边的子数组分别重复1~2步(递归);
  • 当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
        

(2)代码实现——双指针法

        

(3)算法分析

  • 稳定性 :不稳定
  • 时间复杂度 :最佳:O(nlogn), 最差:O(nlogn),平均:O(nlogn)
    最好:最好的情况就是分割点始终是中间元素。
    最差:当分界点是最大或最小的元素时最差,这种情况导致中心元素位于已排序数组的最末端,一个子数组始终为空,而另一个子数组包含元素。

  • 空间复杂度 :尽管快速排序只需要一个元素的辅助空间,但快速排序需要一个栈空间来实现递归,因此是O(logn)。

8.堆排序 (Heap Sort)

(1)前置知识

        堆排序(升序)用到了大顶堆(升序大顶堆、降序小顶堆),这就涉及到将原序列转化为大顶堆(建堆)。建堆时,先从第一个有孩子的节点(下图无序堆中下标为4的节点4)开始维护大顶堆。
        
        我们一般用一个数组按照从上到下、从左到右的顺序来存储大顶堆,那么建堆的过程就变成了构造右边这个数组了。比如下面这个大顶堆可以用右边这个数组存储。
        
        那么如何用数组保存大顶堆呢?我们可以根据节点间下标的关系,根节点下标是0,那么大顶堆中节点的下标关系是:
  • 下标为 i 的节点的父节点的下标:(i - 1) / 2
  • 下标为 i 的节点的左孩子的下标:i * 2 + 1
  • 下标为 i 的节点的右孩子的下标:i * 2 + 2

(2)算法步骤

  • 将原序列转换成大顶堆(建堆)
  • 进行堆排序,每次将堆顶元素与最后一个元素交换,然后断开最后一个元素(因为已排好序),再对剩下的元素维护大顶堆
  • 重复上一步。

(3)代码实现

        

(4)算法分析

  • 稳定性 :不稳定
  • 时间复杂度 :最佳:O(nlogn), 最差:O(nlogn), 平均:O(nlogn)
  • 空间复杂度 :O(1)

9.计数排序 (Counting Sort)

(1)算法步骤

        计数排序使用一个额外的数组 C,其中第 i 个元素是待排序数组 A 中值等于 i 的元素的个数。然后根据数组 C 来将 A 中的元素排到正确的位置。它只能对整数进行排序
  • 找出数组中的最大值max、最小值min;
  • 创建一个新数组C,其长度是max-min+1,其元素默认值都为 0;
  • 遍历原数组A中的元素A[i],以A[i]-min作为C数组的索引,以A[i]的值在A中元素出现次数作为C[A[i]-min]的值;
  • 对C数组变形,新元素的值是该元素与前一个元素值的和,即当i>1时C[i] = C[i] + C[i-1];
  • 创建结果数组R,长度和原始数组一样。
  • 从后向前遍历原始数组A中的元素A[i],使用A[i]减去最小值min作为索引,在计数数组C中找到对应的值C[A[i]-min],C[A[i]-min]-1就是A[i]在结果数组R中的位置,做完上述这些操作,将count[A[i]-min]减小 1。
        

(2)代码实现

//获取最大值和最小值
private static int[] getMinAndMax(int[] arr) {
    int maxValue = arr[0];
    int minValue = arr[0];
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] > maxValue) {
            maxValue = arr[i];
        } else if (arr[i] < minValue) {
            minValue = arr[i];
        }
    }
    return new int[] { minValue, maxValue };
}
//排序
public static int[] countingSort(int[] arr) {
    if (arr.length < 2) {
        return arr;
    }
    int[] extremum = getMinAndMax(arr);
    int minValue = extremum[0];
    int maxValue = extremum[1];
    int[] countArr = new int[maxValue - minValue + 1];
    int[] result = new int[arr.length];

    for (int i = 0; i < arr.length; i++) {
        countArr[arr[i] - minValue] += 1;
    }
    for (int i = 1; i < countArr.length; i++) {
        countArr[i] += countArr[i - 1];
    }
    for (int i = arr.length - 1; i >= 0; i--) {
        int idx = countArr[arr[i] - minValue] - 1;
        result[idx] = arr[i];
        countArr[arr[i] - minValue] -= 1;
    }
    return result;
}

(3)算法分析

        当输入的元素是n个0到k之间的整数时,它的运行时间是O(n+k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上 1),这使得计数排序对于数据范围很大的数组,需要大量额外内存空间
  • 稳定性 :稳定
  • 时间复杂度 :最佳:O(n+k) 最差:O(n+k) 平均:O(n+k)
  • 空间复杂度 :O(k)

10.桶排序

11.基数排序