排序对于任何一个程序员来说,可能都不会陌生。我学的第一个算法就是冒泡排序。大部分编程语言中,也都提供了排序函数,如Java语言在Jdk1.8版本里Arrays.sort里使用排序算法根据不同数据量使用不同的算法,在数组元素小于47的时候用插入排序,大于47小于286用双轴快排,大于286用timsort归并排序;
比较经典和常用的就是以下八大排序。

推荐一个数据结构可视化的页面: https://visualgo.net/en
排序算法的执行效率
对于排序算法执行效率的分析,我们一般会从这几个方面来衡量:
排序算法的内存消耗
算法的内存消耗可以通过空间复杂度来衡量,排序算法也不例外。不过,针对排序算法的空间复杂度,有一个新的概念原地排序(Sorted in place)。原地排序算法,就是特指空间复杂度是O(1)的排序算法,原地排序就是指不申请多余的空间来进行的排序,就是在原来的排序数据中比较和交换的排序,像归并排序和
属于原地排序算法:希尔排序、冒泡排序、直接插入排序、直接选择排序、堆排序、快速排序
排序算法的稳定性
仅仅用执行效率和内存消耗来衡量排序算法的好坏是不够的。针对排序算法,我们还有一个重要的度量指标,稳定性。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。
例如我们有一组数据1,5,6,7,8,6,按照大小排序之后就是1,5,6,6,7,8。
这组数据里有两个6。经过某种排序算法排序之后,如果两个6的前后顺序没有改变,那我们就把这种排序算法叫作稳定的排序算法;如果前后顺序发生变化,那对应的排序算法就叫作不稳定的排序算法。
基本思想
直接插入排序(Insertion Sort) 对于少量元素的排序,它是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动。
例如我们在打牌玩斗地主的时候摸完牌然后整理顺序是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置。在计算机的实现中,为了要给插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位。
算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(n²) | O(n²) | O(n²) | O(1) |
基于Java语言实现代码如下
public static void sort(int nums[]) {for (int i = 1; i < nums.length; i++) {//若第 i 个元素大于 i-1 元素则直接插入;反之,需要找到适当的插入位置后在插入。if (nums[i] < nums[i - 1]) {int j = i - 1;int x = nums[i];//采用顺序查找方式找到插入的位置,在查找的同时,将数组中的元素进行后移操作,给插入元素腾出空间while (j > -1 && x < nums[j]) {nums[j + 1] = nums[j];j--;}//插入到正确位置nums[j + 1] = x;}}}//测试public static void main(String[] args) {int nums[] = new int[]{3, 44, 38, 5, 47, 15, 36, 26, 27,2,46,4,19,50,48};sort(nums);System.out.println(Arrays.toString(nums));}

总结: 插入排序所需的时间取决于输入元素的初始顺序。例如对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比随机顺序的数组或是逆序数组进行排序要快得多。
基本思想
希尔排序(SheII Sort),也称 递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是 非稳定排序算法。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
希尔排序是先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次再将gap折半减小,循环上述操作;当gap=1时,利用直接插入,完成排序。
可以看到步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。一般来说最简单的步长取值是初次取数组长度的一半为增量,之后每次再减半,直到增量为1。更好的步长序列取值可以参考维基百科。
算法描述

| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(nlog2 n) | O(nlog2 n) | O(nlog2 n) | O(1) |
基于Java语言实现代码如下
public static void sort(int nums[]) {int gap = nums.length;while (true) {//增量每次减半gap /= 2; for (int i = 0; i < gap; i++) {//下面循环是一个插入排序for (int j = i + gap; j < nums.length; j += gap) {int k = j - gap;while (k >= 0 && nums[k] > nums[k + gap]) {int temp = nums[k];nums[k] = nums[k + gap];nums[k + gap] = temp;k -= gap;}}}if (gap == 1) {break;}}}public static void main(String[] args) {int nums[] = {86, 11, 54, 34, 53,12,45,81,19,65};sort(nums);System.out.println(Arrays.toString(nums));}
总结: 希尔排序更高效的原因是它权衡了子数组的规模和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序。

基本思想
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 n个元素的表进行排序总共进行至多 n-1 次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
算法描述

| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(n²) | O(n²) | O(n²) | O(1) |
基于Java语言实现代码如下
public static void sort(int[] nums) {for (int i = 0; i < nums.length; i++) {int min = i;//选出之后待排序中值最小的位置for (int j = i + 1; j < nums.length; j++) {if (nums[j] < nums[min]) {min = j;}}//最小值不等于当前值时进行交换if (min != i) {int temp = nums[i];nums[i] = nums[min];nums[min] = temp;}}}public static void main(String[] args) {int nums[] = new int[]{3, 44, 38, 5, 47, 15, 36, 26, 27,2,46,4,19,50,48};sort(nums);System.out.println(Arrays.toString(nums));}

总结:选择排序的简单和直观名副其实,这也造就了它”出了名的慢性子”,无论是哪种情况,哪怕原数组已排序完成,它也将花费将近n²/2次遍历来确认一遍。即便是这样,它的排序结果也还是不稳定的。 唯一值得高兴的是,它并不耗费额外的内存空间。
堆(HeapHeapHeap)一种特殊的树,堆这种数据结构的应用场景非常多,最经典的莫过于堆排序了。堆排序是一种原地的排序算法。
符合以下两点的树就是堆(二叉堆):
第一点: 堆必须是一个完全二叉树,完全二叉树要求除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。
第二点: 堆中的每个节点的值必须大于等于(或者小于等于)其子树中每个节点的值。实际上,我们还可以换一种说法,堆中每个节点的值都大于等于(或者小
于等于)其左右子节点的值。这两种表述是等价的。
下图中其中第1个和第2是大顶堆,第3个是小顶堆,第4个不是堆,一般在应用中大顶堆用的比较多,可用参考这个数据结构可视化网站二叉堆: https://visualgo.net/zh/heap

完全二叉树比较适合用数组来存储。用数组来存储完全二叉树是非常节省存储空间的。因为我们不需要存储左右子节点的指针,单纯地通过数组的下标,就可以找到一个节点的左右子节点和父节点。
以下用数组存储堆的例子

基本思想
此处以大顶堆为例,堆排序的过程就是将待排序的序列构造成一个堆,选出堆中最大的移走,再把剩余的元素调整成堆,找出最大的再移走,重复直至有序。
算法描述

| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) |
代码实现
从算法描述来看,堆排序需要两个过程,一是建立堆,二是堆顶与堆的最后一个元素交换位置。所以堆排序有两个函数组成。一是建堆函数,二是反复调用建堆函数以选择出剩余未排元素中最大的数来实现排序的函数。
创建堆和排序:
对于堆节点的访问:
public static void sort(int[] nums) {for (int i = nums.length - 1; i > 0; i--) {max_heapify(nums, i);//堆顶元素(第一个元素)与Kn交换int temp = nums[0];nums[0] = nums[i];nums[i] = temp;}}/***** 将数组堆化* i = 第一个非叶子节点。* 从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。* 叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。*/public static void max_heapify(int[] nums, int n) {int child;for (int i = (n - 1) / 2; i >= 0; i--) {//左子节点位置child = 2 * i + 1;//右子节点存在且大于左子节点,child变成右子节点if (child != n && nums[child] < nums[child + 1]) {child++;}//交换父节点与左右子节点中的最大值if (nums[i] < nums[child]) {int temp = nums[i];nums[i] = nums[child];nums[child] = temp;}}}public static void main(String[] args) {int[] nums = new int[]{91, 60, 96, 13, 35, 65, 46, 65, 10, 30, 20, 31, 77, 81, 22};sort(nums);System.out.println(Arrays.toString(nums));}

总结:由于堆排序中初始化堆的过程比较次数较多, 因此它不太适用于小序列。 同时由于多次任意下标相互交换位置, 相同元素之间原本相对的顺序被破坏了, 因此, 它是不稳定的排序。
基本思想
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法描述
比较相邻的元素。如果第一个比第二个大,就交换他们两个。对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。针对所有的元素重复以上的步骤,除了最后一个。持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(n²) | O(n) | O(n²) | O(1) |
基于Java语言实现代码如下
public static void sort(int nums[]) {int temp;for (int i = 0; i < nums.length; i++) {for (int j = i + 1; j < nums.length; j++) {if (nums[i] > nums[j]) {temp = nums[i];nums[i] = nums[j];nums[j] = temp;}}}}public static void main(String[] args) {int[] nums = new int[]{3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};sort(nums);System.out.println(Arrays.toString(nums));}
总结:由于冒泡排序只在相邻元素大小不符合要求时才调换他们的位置, 它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法。
基本思想
快速排序(Quick Sort)的基本思想:挖坑填数+分治法。快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好。
算法描述
快速排序使用分治策略来把一个序列(list)分为两个子序列(sub-lists)。步骤为:
递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(nlog₂n) | O(nlog₂n) | O(n²) | O(1)(原地分区递归版) |
基于Java语言实现代码如下
/*** @param nums * @param low 起始* @param high 结束*/public static void sort(int[] nums, int low, int high) {//已经排完if (low >= high) {return;}int left = low;int right = high;//保存基准值int pivot = nums[left];while (left < right) {//从后向前找到比基准小的元素while (left < right && nums[right] >= pivot) {right--;}nums[left] = nums[right];//从前往后找到比基准大的元素while (left < right && nums[left] <= pivot) {left++;}nums[right] = nums[left];}// 放置基准值,准备分治递归快排nums[left] = pivot;sort(nums, low, left - 1);sort(nums, left + 1, high);}public static void main(String[] args) {int[] nums = new int[]{2, 5, 4, 3, 7, 1, 6, 2, 10};sort(nums,0,nums.length-1);System.out.println(Arrays.toString(nums));}

总结:快速排序和归并排序是两种稍微复杂的排序算法,它们用的都是分治的思想,代码都通过递归来实现,过程非常相似。归并排序算法是一种在任何情况下时间复杂度都比较稳定的排序算法,这也使它存在致命的缺点,即归并排序不是原地排序算法,空间复杂度比较高,是O(n)。正因为此,它也没有快排应用广泛。
基本思想
归并排序(Merge Sort)是建立在归并操作上的一种有效的排序算法,1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
归并排序算法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列
算法描述
归并排序可通过两种方式实现:
递归法(假设序列共有n个元素):

| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(n) |
基于Java语言实现代码如下
public static void sort(int [] nums,int lo,int hi){//判断是否是最后一个元素if(lo>=hi){return;}//重中间将数组分为两个部分int mid=lo+(hi-lo)/2;//分别递归将左右两半排好序sort(nums,lo,mid);sort(nums,mid+1,hi);//将排好序的左右两半合并merge(nums,lo,mid,hi);}/**** @param nums* @param low 右偏移的数* @param mid 中位数 * @param high 查找的范围*/private static void merge(int[] nums,int low,int mid,int high){//复制原来的数组int [] copy=nums.clone();//定义一个k指定表示重什么位置开始修改原来的数组,i指针左半边的位置,j表示右半边的位置int k=low,i=low,j=mid+1;while (k<=high){if(i>mid){// 左半边的数据处理完成,将右半边的数copy就行nums[k++]=copy[j++];}else if(j>high){// 右半边的数据处理完成,将左半边的数copy就行nums[k++]=copy[i++];}else if(copy[j]//右边的数小于左边的数,将右边的数拷贝到合适的位置,j指针往前移动一位nums[k++]=copy[j++];}else{//左边的数小于右边的数,将左边的数拷贝到合适的位置,i指针往前移动一位nums[k++]=copy[i++];}}}public static void main(String[] args) {int[] nums=new int[]{1,3,4,5,11,6,7,5,28};sort(nums,0,nums.length-1);System.out.println(Arrays.toString(nums));}

总结: 从效率上看,归并排序可算是排序算法中的”佼佼者”. 假设数组长度为n,那么拆分数组共需logn,, 又每步都是一个普通的合并子数组的过程, 时间复杂度为O(n), 故其综合时间复杂度为O(nlogn)。另一方面, 归并排序多次递归过程中拆分的子数组需要保存在内存空间, 其空间复杂度为O(n)。
基本思想
基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
基数排序将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
基数排序按照优先从高位或低位来排序有两种实现方案:
MSD(Most significant digital) 从最左侧高位开始进行排序。先按k1排序分组, 同一组中记录, 关键码k1相等, 再对各组按k2排序分成子组, 之后, 对后面的关键码继续这样的排序分组, 直到按最次位关键码kd对各子组排序后. 再将各组连接起来, 便得到一个有序序列。MSD方式适用于位数多的序列。
LSD (Least significant digital)从最右侧低位开始进行排序。先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。LSD方式适用于位数少的序列。
算法描述
我们以LSD为例,从最低位开始,具体算法描述如下:
1.取得数组中的最大数,并取得位数;
2.arr为原始数组,从最低位开始取每个位组成radix数组;
3.对radix进行计数排序(利用计数排序适用于小范围数的特点);

| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(d*(n+r)) | O(d*(n+r)) | O(d*(n+r)) | O(n+r) |
基于Java语言实现代码如下
public static void sort(int[] nums) {if (nums.length <= 1) return;//取得数组中的最大数,并取得位数int max = 0;for (int i = 0; i < nums.length; i++) {if (max < nums[i]) {max = nums[i];}}int maxDigit = 1;while (max / 10 > 0) {maxDigit++;max = max / 10;}//申请一个桶空间int[][] buckets = new int[10][nums.length];int base = 10;//从低位到高位,对每一位遍历,将所有元素分配到桶中for (int i = 0; i < maxDigit; i++) {//存储各个桶中存储元素的数量int[] bktLen = new int[10];//分配:将所有元素分配到桶中for (int j = 0; j < nums.length; j++) {int whichBucket = (nums[j] % base) / (base / 10);buckets[whichBucket][bktLen[whichBucket]] = nums[j];bktLen[whichBucket]++;}//收集:将不同桶里数据挨个捞出来,为下一轮高位排序做准备,由于靠近桶底的元素排名靠前,因此从桶底先捞int k = 0;for (int b = 0; b < buckets.length; b++) {for (int p = 0; p < bktLen[b]; p++) {nums[k++] = buckets[b][p];}}base *= 10;}}public static void main(String[] args) {int[] nums = new int[]{3, 44, 38, 5, 47, 15, 36, 26, 27,2,46,4,19,50,48};sort(nums);System.out.println(Arrays.toString(nums));}

各种排序性能对比如下:
| 排序类型 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
| 插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
| 希尔排序 | O(n^1.3) | O(nlogn) | O(n²) | O(1) | 不稳定 |
| 归并排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(n) | 稳定 |
| 快速排序 | O(nlog₂n) | O(nlog₂n) | O(n²) | O(nlog₂n) | 不稳定 |
| 堆排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(1) | 不稳定 |
| 基数排序 | O(d(n+k)) | O(d(n+k)) | O(d(n+kd)) | O(n+kd) | 稳定 |
从时间复杂度来说:
论是否有序的影响:
