十大排序算法及Java中的排序算法

文章目录

  • 一、简介
  • 二、时间复杂度
  • 三、非线性时间比较类排序
    • 冒泡排序(Bubble Sort)
      • 排序过程
      • 代码实现
      • 步骤拆解演示
      • 复杂度
    • 选择排序(Selection Sort)
      • 排序过程
      • 代码实现
      • 步骤拆解演示
      • 复杂度
    • 插入排序(Insertion Sort)
      • 排序过程
      • 代码实现
      • 步骤拆解演示
      • 复杂度
      • 二分插入排序(Binary Insertion Sort)
        • 代码实现
    • 希尔排序(Shell Sort)
      • 排序过程
      • 代码实现
      • 步骤拆解演示
      • 复杂度
    • 归并排序(Merging Sort)
      • 排序过程
      • 代码实现
      • 步骤拆解演示
      • 复杂度
    • 快速排序(Quick Sort)
      • 排序过程
      • 代码实现
      • 步骤拆解演示
      • 复杂度
    • 堆排序(Heap Sort)
      • 排序过程
      • 代码实现
      • 步骤拆解演示
      • 复杂度
  • 四、线性时间非比较类排序
    • 基数排序(Radix Sort)
      • 排序过程
      • 代码实现
      • 步骤拆解演示
      • 复杂度
    • 计数排序(Counting Sort)
      • 排序过程
      • 代码实现
      • 步骤拆解演示
      • 复杂度
    • 桶排序(Bucket Sort)
      • 排序过程
      • 代码实现
      • 步骤拆解演示
      • 复杂度
  • 五、Java中Arrays.sort()用的什么排序算法
    • Dual-Pivot Quick Sort(双轴快速排序)
      • 流程图
    • Tim Sort
      • run
      • minRun
      • 压栈
      • 合并
      • 流程总结
      • 复杂度

一、简介

常见的排序算法有十种,可以分为以下两大类:

  • 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(n log n),因此称为非线性时间比较类排序

  • 线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序

二、时间复杂度

时间复杂度从小到大:

O(1) < O(log n) < O(n) < O(n log n) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)

前四个效率比较高,中间两个一般般,后三个比较差

三、非线性时间比较类排序

冒泡排序(Bubble Sort)

循环遍历多次,每次从前往后把大元素往后调,每次确定一个最大(最小)元素,循环多次后达到排序效果。因为在排序过程中,较大(或较小)的元素会逐渐向列表的一端"冒泡",所以得名:冒泡排序

排序过程

  1. 比较相邻元素,如果前者大于后者,就交换两个元素的位置。对每一对相邻元素都做这样的操作,从开始第一对到结尾的最后一对。例如:一个长度是n的数组,先比对1、2两个位置,再比对2、3两个位置,直到比对完n-1、n两个位置,这时候,我们认为完成了一次冒泡,最后一个元素一定是最大的。
  2. 然后不断重复第1步的操作,因为最后一个元素一定是最大的,不需要再操作最后一个元素了,可以当最后一个元素不存在,数组长度变成了n-1,每一次冒泡,结尾都会排好一个元素,下一次冒泡需要操作的元素就少一个,一直重复直到完成排序

代码实现

public static void bubbleSort(int[] arr) {if (arr == null || arr.length == 0) {return;}for (int i = 0; i < arr.length - 1; i++) {//这里减i的原因是,每次外循环会在最后排好一个元素,下次循环时就可以少比较一个元素for (int j = 0; j < arr.length - 1 - i; j++) {//如果前者大于后者,交换两者位置if (arr[j] > arr[j + 1]) {int temp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = temp;}}}
}

步骤拆解演示

我们以 [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48] 为例,初始如下图所示:

初始

从第一个元素开始比较大小,如果前者大于后者,就调换两个位置,先比较3和44,发现3小于44,不动,再比较44和38,发现44大于38,所以调换两者位置,因为44和38换了位置,所以下面比较44和5,发现44大于5,再调换两者位置,一直比对到最后,如下图所示:

冒泡排序1

这样就完成一次冒泡,50这个元素就排好了,下面再以同样的方法,只不过排到倒数第二个元素,每重复一次,排好一个元素,15次冒泡排序后就完成排序了,如下图所示:

冒泡排序2

其实在第9次冒泡排序的时候,就已经完成所有的排序,所以冒泡排序还可以优化一下,比如增加一个标识来记录每次冒泡有没有元素交换,如果没有,就说明已经排好了,可以直接结束排序了。或者记录上一次最后交换的位置,作为下一次比较的终点,因为在这个位置之后的元素已经是有序的,不需要再进行比较了(比如第二次冒泡的最后一次交换是在倒数第4个位置,第三次冒泡的时候,比较到这个位置就可以了)。

复杂度

平均时间复杂度最好情况最坏情况空间复杂度
O(n^2)O(n)O(n^2)O(1)

最好的情况下,一趟扫描就可以完成排序,复杂度为O(n),最坏的情况就是每次都需要交换,复杂度为O(n2),总体平均复杂度为O(n2),因为没有占用额外空间,所以空间复杂度是O(1)。

选择排序(Selection Sort)

每次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余未排序的元素中继续寻找最小(或最大)的元素,放到已排序序列的末尾。以此类推,直到所有元素都排序完毕

排序过程

  1. 先排第1个元素,拿第1个元素和后面所有的元素比较,如果找到比第1个元素小且最小的那个元素,就和第1个元素交换,这样第1个元素就是最小的
  2. 因为第1步已经排好了第1个元素,所以可以跳过第1个元素了,拿第2个元素和后面所有元素比较,找到最小的元素和第2个交换,这样第2个元素就排好了,下面是第3个元素…,一直重复这个操作,直到完成排序

代码实现

public static void selectSort(int[] arr) {if (arr == null || arr.length == 0) {return;}for (int i = 0; i < arr.length - 1; i++) {//设定当前位置为最小值int min = i;//遍历剩余未排序的部分,如果找到比当前最小值更小的元素,则更新最小值的位置for (int j = i + 1; j < arr.length; j++) {if (arr[min] > arr[j]) {min = j;}}if (min != i) {//将最小值与当前位置交换int temp = arr[min];arr[min] = arr[i];arr[i] = temp;}}
}

步骤拆解演示

我们还是以 [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48] 为例,初始如下图所示:

初始

初始状态下,所有元素都是待排序元素,在其中找到最小的元素(也就是2),和待排序序列的第一个元素(也就是3)交换,这样第一个元素就是最小的,也是排好序的,这样剩下的元素就是下次的待排序元素,第2个元素就成了待排序序列的第一个元素,再从这个待排序序列里找最小元素,和这个第2个元素交换,如此重复就能排好整个序列,具体如下图所示:

选择排序1

复杂度

平均时间复杂度最好情况最坏情况空间复杂度
O(n^2)O(n^2)O(n^2)O(1)

最好的情况下,已经有序,最坏情况交换 n - 1 次,逆序交换 n/2 次,即使原数组已排序完成,它也将花费将近 n²/2 次遍历来确认一遍,所以其复杂度为O(n^2),不过不用花费额外空间,空间复杂度是O(1)。

插入排序(Insertion Sort)

对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入

排序过程

  1. 从第一个元素开始,认为该元素已经是有序序列。
  2. 取出下一个元素,在已排序的序列中从后向前扫描。
  3. 如果该元素(已排序)大于新元素,则将该元素移到下一位置。
  4. 重复步骤3,直到找到已排序的元素小于或等于新元素的位置。
  5. 将新元素插入到该位置后。
  6. 重复步骤2~5,直到所有元素都排序完毕。

代码实现

public static void insertionSort(int[] arr) {if (arr == null || arr.length == 0) {return;}//默认第1个元素是有序的,从第2个开始遍历for (int i = 1; i < arr.length; i++) {//取出当前元素tempint temp = arr[i];//循环前面已经排好序的元素for (int j = i; j >= 0; j--) {//如果temp小于扫描的元素,就将扫描的元素挪至下一个位置if (j > 0 && arr[j - 1] > temp) {arr[j] = arr[j - 1];} else {//如果不小于扫描的元素,就插入到该元素的下一个arr[j] = temp;break;}}}
}

步骤拆解演示

我们还是以 [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48] 为例,初始如下图所示:

初始

我们默认第一个元素3是排好的,取第二个元素44和前面的比较,44大于3,不动,然后取第三个元素38和前面的比较,发现38小于44大于3,就把44往后挪,然后把38放置在44的位置上,下面再取第四个元素,重复之前的操作,直到完成排序

插入排序1

复杂度

平均时间复杂度最好情况最坏情况空间复杂度
O(n^2)O(n)O(n^2)O(1)

最好情况是已经有序,只需当前数跟前一个数比较一下就可以了,这时一共需要比较 n- 1次,复杂度为O(n),最坏情况就是逆序,这时比较次数最多,总次数 = 1+2+…+ (n - 1),所以最坏情况复杂度是O(n2),所以平均复杂度是O(n2),因为不花费额外空间,所以空间复杂度是O(1)。

二分插入排序(Binary Insertion Sort)

我们发现插入排序,在往前扫描找合适插入位置的时候,是逐个扫描的,因为前面是已经排好序的,所以可以使用二分查找的方式提高一点效率,于是就有了二分插入排序(Binary Insertion Sort)

排序过程:

  1. 从第一个元素开始,认为该元素已经是有序序列。
  2. 取出下一个元素,在已排序的序列中使用二分查找的方式找到合适的位置索引。
  3. 将大于该元素的所有元素后移,腾出合适的位置。
  4. 将新元素插入到该位置。
  5. 重复步骤2~4,直到所有元素都排序完毕。

代码实现

public static void binaryInsertionSort(int[] arr) {if (arr == null || arr.length == 0) {return;}for (int i = 1; i < arr.length; ++i) {int temp = arr[i];int left = 0, right = i - 1;// 使用二分查找找到合适的位置索引while (left <= right) {int mid = (left + right) / 2;if (arr[mid] > temp) {right = mid - 1;} else {left = mid + 1;}}// 将大于该元素的所有元素后移,腾出合适的位置for (int j = i - 1; j >= left; --j) {arr[j + 1] = arr[j];}arr[left] = temp;}
}

虽然在插入排序的基础上优化了,但是在最坏的情况下,其时间复杂度仍然是 O(n^2),没有使用额外空间,所以空间复杂度是 O(1)

希尔排序(Shell Sort)

将待排序的数组分割成若干个较小的子数组进行插入排序,最后再对整个数组进行一次插入排序

29560c0a0cb64cb19c31c4ab6f942546

排序过程

  1. 先选定一个小于数组长度n的整数gap(一般情况下是将n/2作为gap)作为第一增量,然后将所有距离为gap的元素分为一组,并对每一组进行插入排序
  2. 重复步骤1,每次将gap缩减一半,直到gap等于1停止,这时整个序列被分到了一组,进行一次直接插入排序,排序完成

代码实现

public static void shellSort(int[] arr) {if (arr == null || arr.length == 0) {return;}//初始间隔设置为数组长度的一半int gap = arr.length / 2;while (gap > 0) {//因为插入排序默认第一个是排好序的,所以每组的第一个元素全部跳过,从第2个元素开始循环,和前面的元素比较for (int i = gap; i < arr.length; i++) {//先取出这个元素int temp = arr[i];//标记自己组的元素下标int j = i;//找自己组的前一个元素,并比较大小,如果比自己大,就把那个元素挪动至自己的位置,并且把下标往前挪,标记前一个同组元素while (j >= gap && arr[j - gap] > temp) {arr[j] = arr[j - gap];j -= gap;}arr[j] = temp;}//缩小增量gap /= 2;}
}

步骤拆解演示

我们还是以 [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48] 为例,初始如下图所示:

初始

因为数组长度是15,所以初始增量为 15/2 = 7,然后我们将元素分组,间隔7的元素为1组,3、26、48一组,44、27一组…,如下图所示,相同颜色的为一组:

希尔排序1

然后对每组使用插入排序,排序后如下图所示:

希尔排序2

这时候增量需要重新计算,7/2 = 3,然后我们将元素分组,间隔3的元素为1组,3、5、36、38、19一组,27、4、26、46、50一组…,如下图所示,相同颜色的为一组:

希尔排序3

然后对每组使用插入排序,排序后如下图所示:

希尔排序4

最后,增量再除以2,3/2 = 1,最后所有数是一组,完成一次插入排序,就得到最终排序好的序列

完成

复杂度

平均时间复杂度最好情况最坏情况空间复杂度
O(n log^2 n)O(n log^2 n)O(n^2)O(1)

希尔排序的时间复杂度不易准确地表示为一个函数,因为它取决于增量序列的选择,总体我们认为是 O(n log n) 到 O(n^2) 之间,不额外花费空间,所以空间复杂度为O(1)。

归并排序(Merging Sort)

通过将待排序的序列逐步划分为更小的子序列,并对这些子序列进行排序,最后再将已排序的子序列合并成一个有序序列

归并排序

排序过程

  1. 将待排序的数组递归地分解成若干个小的子数组,直到每个子数组只包含一个元素或为空
  2. 将相邻的子数组进行合并,得到更大的有序子数组。合并操作从最小的子数组开始,逐步合并直到整个数组排序完成

代码实现

public static void mergingSort(int[] arr) {if (arr == null || arr.length == 0) {return;}doMergingSort(arr, 0, arr.length - 1);
}private static void doMergingSort(int[] arr, int left, int right) {if (left >= right) {return;}int mid = (left + right) / 2;//递归归并左边doMergingSort(arr, left, mid);//递归归并右边doMergingSort(arr, mid + 1, right);//开始归并//创建数组用于放归并后的元素int[] temp = new int[right - left + 1];//标记左边数组的起始下标int lIndex = left;//标记右边数组的起始下标int rIndex = mid + 1;//标记归并后的新数组下标int index = 0;// 把较小的数先移到新数组中while (lIndex <= mid && rIndex <= right) {//对比两个数组的元素大小,小的就放入新数组,下标++if (arr[lIndex] < arr[rIndex]) {temp[index++] = arr[lIndex++];} else {temp[index++] = arr[rIndex++];}}//做完上面的操作,最后左边数据或者右边数组还有剩余(只可能一个数组有剩余,不可能两个都剩),移入到新数组里//把左边数组剩余的数移入数组while (lIndex <= mid) {temp[index++] = arr[lIndex++];}//把右边剩余的数移入数组while (rIndex <= right) {temp[index++] = arr[rIndex++];}//将新数组的值赋给原数组for (int i = 0; i < temp.length; i++) {arr[i + left] = temp[i];}
}

步骤拆解演示

我们还是以 [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48] 为例,初始如下图所示:

初始

我们先把数组进行分割,分割成若干个小的子数组,然后再将相邻的子数组合并,如下图所示:

归并排序1

复杂度

平均时间复杂度最好情况最坏情况空间复杂度
O(n log n)O(n log n)O(n log n)O(n)

分割过程的时间复杂度是O(log n),因为每次都将数组分割成两半。合并过程的时间复杂度是O(n),因为需要将两个有序数组合并成一个有序数组。因此,总的时间复杂度可以表示为O(n log n),其复杂度是稳定的,无论是在最好情况、平均情况还是最坏情况下,都是O(n log n),因为需要创建临时数组来存储合并过程的中间结果,这个临时数组的长度与待排序数组的长度相同,所以空间复杂度是O(n)。

快速排序(Quick Sort)

基于分治思想的排序算法,它通过在数组中选择一个基准元素,将数组分成两个子数组,其中一个子数组的所有元素都小于基准元素,另一个子数组的所有元素都大于基准元素,然后递归地对两个子数组进行排序,最终达到整个序列有序的目的

快速排序

排序过程

  1. 从数列中挑出一个元素,称为 “基准”(pivot)
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置
  3. 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序

代码实现

public static void quickSort(int[] arr) {if (arr == null || arr.length == 0) {return;}doQuickSort(arr, 0, arr.length - 1);
}public static void doQuickSort(int[] arr, int left, int right) {if (left >= right) {return;}//分区int partitionIndex = partition(arr, left, right);//递归左分区doQuickSort(arr, left, partitionIndex - 1);//递归右分区doQuickSort(arr, partitionIndex + 1, right);
}//分区
public static int partition(int[] arr, int left, int right) {//基准值int pivot = arr[left];//mark标记初始下标int mark = left;//循环基准值之后的所有元素for (int i = left + 1; i <= right; i++) {if (arr[i] < pivot) {//如果有小于基准值的元素,把他挪动至标记的下一位(相当于标记+1,然后交换位置)mark++;int temp = arr[mark];arr[mark] = arr[i];arr[i] = temp;}}//最后把基准值和标记位的值互换(目的是为了把基准值放置到正确的位置)arr[left] = arr[mark];arr[mark] = pivot;return mark;
}

步骤拆解演示

我们还是以 [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48] 为例,初始如下图所示:

初始

首先取数组第一个元素为基准值,也就是3,然后把所有小于3的值挪动至3后面(就是和3后面的元素交换),这里只有一个2比3小,所以挪动后,如下图所示:

快速排序1

然后再把基准值3和最后一次调换的元素2交换,如下图所示:

快速排序2

这时候认为3就排好了,然后以3为中心,把数组分割成左右两块,再递归执行上面的操作,因为左侧只有一个元素2,不用操作,对右侧操作即可,右侧数组的第一个元素是38,所以以38为基准值,把小于38的元素挪动到38之后,如下图所示:

快速排序3

然后再把基准值38和最后一次调换的元素19交换,如下图所示:

快速排序4

这时候38这个元素是排好的了,然后再以38为中心,把数组分割成左右两块,[19, 5, 15, 36, 26, 27, 4] 和 [46, 47, 44, 50 48] ,分别对左右两个数组重复上面的操作,(找基准值,把比基准值小的挪至基准值后面,最后调换基准值和最后一次交换的元素),执行完之后,对左右两个数组分别再分割,直至完成排序

快速排序5

复杂度

平均时间复杂度最好情况最坏情况空间复杂度
O(n log n)O(n log n)O(n^2)O(1)

最好的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过 log n 趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为 O(n log n),最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过 n 趟划分,使得整个排序算法的时间复杂度为O(n^2)。所以平均复杂度是O(n log n),不额外花费空间,所以空间复杂度是O(1)

堆排序(Heap Sort)

堆排序利用了二叉堆的性质,将待排序的数组构建成一个最大堆(或最小堆),然后将堆顶元素与当前未排序区的最后一个元素交换位置,并对交换后的堆进行调整,直到所有元素都排好序为止

堆排序

排序过程

  1. 先将待排序的数组构建成最大堆(或最小堆)
  2. 将堆顶元素和未排序的最后一个元素交换位置
  3. 将剩余元素组成的堆调整
  4. 重复操作2、3步骤直至排序完成

代码实现

public static int[] heapSort(int[] arr) {if (arr == null || arr.length == 0) {return;}//构建一个最大堆(这边演示正序排序,如果是倒序就构建一个最小堆)int length = arr.length;//从最后一个非叶子节点开始向上调整for (int i = length / 2 - 1; i >= 0; i--) {adjustHeap(arr, i, length);}//上面构建好了最大堆,下面开始排序//将最大的节点放在堆尾,然后从根节点重新调整for (int j = arr.length - 1; j > 0; j--) {//因为最大堆的根节点是最大的,把它和最后一个节点交换,相当于把最大元素排好了int temp = arr[j];arr[j] = arr[0];arr[0] = temp;//然后再以根节点调整最大堆adjustHeap(arr, 0, j);}return arr;
}private static void adjustHeap(int[] arr, int i, int length) {//当前的节点i是父节点,我们先认为它是最大的int largest = i;//i节点的左子节点int left = 2 * i + 1;//i节点的右子节点int right = 2 * i + 2;//比较左子节点和父节点if (left < length && arr[left] > arr[largest]) {//如果子节点大于父节点,改标记largest = left;}//比较右子节点和父节点if (right < length && arr[right] > arr[largest]) {//如果子节点大于父节点,改标记largest = right;}//如果父节点不是最大值,则交换父节点和最大值,因为调整后可能会导致子树不满足最大堆,所以通过递归调整交换后的子树if (largest != i) {//交换int temp = arr[i];arr[i] = arr[largest];arr[largest] = temp;//调整子树adjustHeap(arr, largest, length);}
}

步骤拆解演示

我们还是以 [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48] 为例,初始如下图所示:

初始

第一步,我们要把数组构建成一个最大堆(父节点大于等于子节点的特殊完全二叉树),首先可以把这个数组看成是一个完全二叉树,如下图所示:

堆排序1

数组的第一个元素(索引为0)将表示树的根节点,对于任意位置 i 的节点,它的左子节点位置为 2 * i + 1,右子节点位置为 2 * i + 2,现在我们需要将这个完全二叉树调整为最大堆,首先找到最后一个非叶子节点(也就是36,位置是数组长度除以2再减1),然后和它的两个子节点比较,找到最大的那个和自己交换,所以就是36和50交换,如下图所示:

堆排序2

然后按照这个操作,从后往前循环调整前面的所有非叶子节点(15、47、5、38、44、3),调整完15、47、5这三个非叶子节点后,如下图所示:

堆排序3

下面调整38、44的时候,需要注意调整后需要递归调整子树,因为调整后可能会导致子树不满足最大堆,比如38在和50交换之后,38还比子节点48小,还需要和48交换,交换之后,如下图所示:

堆排序4

44、3也按这样操作后,就会得到最大堆,如下图所示:

堆排序5

下面就是开始排序,将堆顶元素(最大元素)和未排序的最后一个元素交换位置,也就是50和3交换位置,交换后50在最后一个位置,我们认为他是排好序的(将它从树上挪走,其实它只是在数组的最后一位),如下图所示:

堆排序6

然后剩下的元素,以根节点调整为最大堆(就是拿根节点3和它的子节点47、48比,发现48最大,然后把48和3交换,再比较交换后3的子节点19和38,发现38最大,把38和3交换,再比较交换后3的子节点36,发现36大,再交换36和3),调整后,如下图所示:

堆排序7

这时候再重复之前的操作,把堆顶和最后的元素交换,48交换3,那么48就排好序了,再调整堆为最大堆,如此重复直到排好序

复杂度

平均时间复杂度最好情况最坏情况空间复杂度
O(n log n)O(n log n)O(n log n)O(1)

堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(n log n),所以,堆排序整体的时间复杂度是 O(n log n),因为没有占用额外空间,所以空间复杂度是O(1)。

四、线性时间非比较类排序

基数排序(Radix Sort)

按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。需要注意的是,只能排大于等于0的数

基数排序

排序过程

  1. 找到数组里最大的数,以及它的位数
  2. 按照位数从低位到高位,对元素进行排序,放置于桶中,然后再从桶里收集数据

代码实现

public static void radixSort(int[] arr) {if (arr == null || arr.length == 0) {return;}//找出数组中最大值,int max = Arrays.stream(arr).max().getAsInt();//确定最大值的位数int maxDigits = (int) Math.log10(max) + 1;//创建10个桶用于分配元素int[][] buckets = new int[10][arr.length];//记录每个桶里的元素个数int[] bucketSizes = new int[10];//进行maxDigits轮分配和收集for (int digit = 0; digit < maxDigits; digit++) {//分配元素到桶中for (int num : arr) {int bucketIndex = (num / (int) Math.pow(10, digit)) % 10;//将元素放到对应的桶内buckets[bucketIndex][bucketSizes[bucketIndex]] = num;//对应桶里的元素数量+1bucketSizes[bucketIndex]++;}//收集桶中的元素int index = 0;for (int i = 0; i < 10; i++) {for (int j = 0; j < bucketSizes[i]; j++) {arr[index] = buckets[i][j];index++;}//清空桶计数器bucketSizes[i] = 0;}}
}

步骤拆解演示

我们还是以 [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48] 为例,初始如下图所示:

初始

首先确定最大值以及它的位数,最大值是50,位数是2位,下面创建10个桶,按个位数的数字摆放这些元素到桶里,比如27个位数是7就放到7那个桶里,如下图所示:

基数排序

然后从低到高收集每个桶里的元素,如下图所示:

基数排序2

再按十位数的数字摆放这些元素到桶里,比如36十位数是3就放到3那个桶里,如下图所示

基数排序3

然后从低到高收集每个桶里的元素,就会得到排好序的序列,如下图所示:

完成

复杂度

平均时间复杂度最好情况最坏情况空间复杂度
O(d*(n+r))O(d*(n+r))O(d*(n+r))O(n+r)

其中,d 为位数,r 为基数,n 为原数组个数。在基数排序中,因为没有比较操作,所以在复杂上,最好的情况与最坏的情况在时间上是一致的。

计数排序(Counting Sort)

将输入的数据值转化为键存储在额外开辟的数组空间中,需要排序的元素必须是整数,并且里面的元素取值要在一定范围内且比较集中

排序过程

  1. 找出待排序的数组中最大和最小的元素
  2. 统计数组中每个值为i的元素出现的次数,存入数组count的第i项
  3. 对所有的计数累加(从count中的第一个元素开始,每一项和前一项相加)
  4. 反向填充目标数组:将每个元素i放在新数组的第count(i)项,每放一个元素就将count(i)减去1

代码实现

public static int[] countingSort(int[] arr) {if (arr == null || arr.length <= 1) {return arr;}//找出数组中的最大值、最小值int max = Integer.MIN_VALUE;int min = Integer.MAX_VALUE;for (int num : arr) {max = Math.max(max, num);min = Math.min(min, num);}//初始化计数数组count,长度为最大值减最小值加1int[] count = new int[max - min + 1];for (int num : arr) {//数组中的元素要减去最小值作为新索引count[num - min]++;}//计数数组变形,新元素的值是前面元素累加之和的值int sum = 0;for (int i = 0; i < count.length; i++) {sum += count[i];count[i] = sum;}//倒序遍历原始数组,从统计数组找到正确位置,输出到结果数组int[] sortedArr = new int[arr.length];for (int i = arr.length - 1; i >= 0; i--) {sortedArr[count[arr[i] - min] - 1] = arr[i];count[arr[i] - min]--;}return sortedArr;
}

步骤拆解演示

我们还是以 [2, 3, 8, 7, 1, 2, 2, 2, 7, 3, 9, 8, 2, 1, 4, 2, 4, 6, 9, 2] 为例,先找到最大值和最小值,分别为 9 和 1,然后我们建一个 count 数组,长度是 max - min + 1,所以是 9,然后把待排序数组里的值减去最小值就得到 count 数组的下标位置,每有一个,count 加 1,比如元素 7 减去最小值 1 得到 6,就是 7 这个元素在 count 数组里的下标

计数排序1

复杂度

平均时间复杂度最好情况最坏情况空间复杂度
O(n + k)O(n + k)O(n + k)O(k)

n表示的是数组的个数,k表示的 max - min + 1 的大小。

桶排序(Bucket Sort)

将待排序的元素分配到有限数量的桶中,然后对每个桶中的元素进行排序,最后按照桶的顺序将各个桶中的元素依次取出,适用于数据分布范围已知,在分布范围内均匀分布,并且数据量不能过大的场景

排序过程

  1. 根据待排序序列的最大值和最小值,确定需要的桶的数量
  2. 遍历待排序序列,根据元素的大小将其分配到对应的桶中
  3. 对每个桶中的元素进行排序(可以使用其他排序算法,如插入排序、快速排序等)
  4. 按照桶的顺序,依次将每个桶中排好序的元素取出,就可以得到有序序列

代码实现

public static int[] bucketSort(int[] arr) {if (arr == null || arr.length <= 1) {return arr;}//找出数组中的最大值、最小值int max = Integer.MIN_VALUE;int min = Integer.MAX_VALUE;for (int value : arr) {max = Math.max(max, value);min = Math.min(min, value);}//计算桶的数量int bucketNum = (max - min) / arr.length + 1;//创建桶List<List<Integer>> bucketArr = new ArrayList<>(bucketNum);for (int i = 0; i < bucketNum; i++) {bucketArr.add(new ArrayList<>());}//将每个元素放入桶for (int value : arr) {//算出元素应该放的桶int num = (value - min) / (arr.length);bucketArr.get(num).add(value);}//对每个桶进行排序(可以使用其他排序算法,如插入排序、快速排序等),这里用的Collections.sort()for (List<Integer> bucket : bucketArr) {Collections.sort(bucket);}//将桶中的元素赋值到原序列int index = 0;for (List<Integer> bucket : bucketArr) {for (Integer i : bucket) {arr[index++] = i;}}return arr;
}

步骤拆解演示

我们还是以 [3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48] 为例,初始如下图所示:

初始

第一步,根据待排序序列的最大值和最小值,确定需要的桶的数量,计算方式是最大值减最小值,然后除以数组长度,最后再加1,int bucketNum = (max - min) / arr.length + 1;这里最大是50,最小是2,数组长度15,算出来是桶的数量是4,然后根据每个元素的值减去最小值之后,再除以数组长度得到需要放置的桶,int num = (value - min) / (arr.length);,按顺序一个一个放到桶里,就得到下图:

桶排序1

然后针对每个桶内部做排序,排序后,如下图:

桶排序2

然后再从前往后,把每个桶的元素取出,就可以得到排好序的序列

完成

复杂度

平均时间复杂度最好情况最坏情况空间复杂度
O(n + c)O(n)O(n log n)O(n + m)

有n个元素进行排序的话,其时间复杂度可以分为两个部分:

  1. 循环计算每个元素所属桶的映射函数,其时间复杂度是O(n)
  2. 对每个桶进行排序,比较算法复杂度的最好情况是O(n log n)

所以尽可能均匀分布元素到每个桶里,并且尽可能增加桶的数量,极限情况下,一个桶就一个元素,就可以避免桶内排序,但是这样会导致空间浪费

对于n个元素,m个桶,平均每个桶 n/m 个数据,可以得到复杂度计算公式:

O(n) + O(m * (n/m) * log(n/m))
=O(n + n * (logn - logm))

极限情况下,一个桶就一个元素,也就是 n = m 时,桶排序的复杂度可以达到最好情况的 O(n),平均复杂度为 O(n + c) ,c = n * (logn - logm),最差情况,所有元素在一个桶里,也就是 m = 1,复杂度为 O(n log n) ,如果桶内排序选择较差的插入排序之类的,复杂度会退化到 O(n^2),空间复杂度是待排序元素加上桶数,也就是 O(n + m)。

五、Java中Arrays.sort()用的什么排序算法

原本学了基础的十大排序算法,就结束了,但是我就好奇 Java 里的排序底层用了什么排序算法,于是去看了 Arrays.sort() 的源码,这一看不得了,发现了新大陆。。。

首先,针对基本数据类型和对象类型,使用了不同的算法,如下图

image-20230912150526362

image-20230912150628638

对于基本数据类型,使用了 DualPivotQuicksort,对于对象类型使用了 TimSort(ps:也可以通过配置改成归并排序),我们逐一来看

Dual-Pivot Quick Sort(双轴快速排序)

Dual-Pivot Quick Sort(双轴快速排序)是一种快速排序算法,是传统快速排序的进阶版,Java里的这个 DualPivotQuicksort.sort() 方法内部并不是直接使用双轴快速排序,而是根据不同的数组长度选择不同的排序算法,可以简单总结如下:

  • 1 <= len < 47:使用插入排序
  • 47 <= len < 286 : 使用快速排序
  • 286 < len 且数组有一定顺序:使用归并排序
  • 286 < len 且数组无顺序:使用快速排序

流程图

Java基本类型排序流程图

下面重点讲下双轴快排:

我们原本的快速排序其实是单轴快排,会选取一个基准点 pivot,双轴快排其实就是选两个基准点 pivot1和 pivot2,这样可以规避单轴快排时,基准点总是选中最大或者最小的值,而导致复杂度退化到O(n^2),双轴通过两个基准点可以将区间划为三部分,这样不仅每次能够确定2个元素,最坏最坏的情况就是左右同大小并且都是最大或者最小,但这样的概率相比一个最大或者最小还是低很多很多,选取到基准点之后排序方式和单轴快排是一样的,所以重点就在于怎么选取两个基准点 pivot1 和 pivot2,具体步骤如下:

  1. 通过数组长度 n / 7 算出轴长,然后算出序列的中间数 e3,
  2. 然后用 e3 往前减1个轴长和2个轴长,得到 e2 和 e1,后面加1个轴长和2个轴长,得到 e4 和 e5,
  3. 然后使用插入排序将这五个位置的数排序,排序后如果5个数相邻的数两两不相同(a[e1] != a[e2] && a[e2] != a[e3] && a[e3] != a[e4] && a[e4] != a[e5]),就取 e2 和 e4 位置的两个数为 pivot1 和 pivot2,使用双轴快排,否则就取 e3 位置的数为 pivot 使用单轴快排。

可以看 Java 源码里的注释:

image-20230912163325514

Tim Sort

Tim Sort 是一种混合排序算法,结合了归并排序(Merge Sort)和插入排序(Insertion Sort)的优点。它由Tim Peters于2002年设计,号称世界上最快的排序算法。

下面直接看源码

static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {assert a != null && lo >= 0 && lo <= hi && hi <= a.length;//计算数组的长度int nRemaining  = hi - lo;//如果数组长度小于2,一定有序,直接返回if (nRemaining < 2)return;//如果数组长度小于32,就是用二分插入排序if (nRemaining < MIN_MERGE) {//找到数组从起始位置开始的第一个run(连续递增或递减的子数组,递减的翻转)的长度int initRunLen = countRunAndMakeAscending(a, lo, hi);//因为第一个run的是已经排序好的,从后面开始使用二分插入排序binarySort(a, lo, hi, lo + initRunLen);return;}//接下来是主要的排序ComparableTimSort ts = new ComparableTimSort(a, work, workBase, workLen);//根据待排序数组长度计算最小run分区大小minRun,介于16-32之间int minRun = minRunLength(nRemaining);do {//计算当前run(连续递增或递减的子数组,递减的翻转)的长度int runLen = countRunAndMakeAscending(a, lo, hi);//如果run的长度小于minRun,则会将其扩展到min(minRun, nRemaining) 的长度if (runLen < minRun) {//nRemaining代表剩余元素大小,如果nRemaining小于minRun,则将剩余元素处理完毕int force = nRemaining <= minRun ? nRemaining : minRun;//使用二分插入排序对指定范围元素排序binarySort(a, lo, lo + force, lo + runLen);runLen = force;}//将run压入待处理的栈中(其实压入的是起始下标和长度)ts.pushRun(lo, runLen);/*** 不满足以下两个条件,就执行merge操作,直到满足以下两个条件(很有意思,可以看看源码)* 1. runLen[i - 3] > runLen[i - 2] + runLen[i - 1]  栈顶第3个元素大于栈顶第2个和第1个元素之和* 2. runLen[i - 2] > runLen[i - 1]                  栈顶第2个元素大于栈顶第1个元素**/ts.mergeCollapse();//更新下一个run的起始位置(lo)以及数组中剩余元素(nRemaining),继续寻找下一个runlo += runLen;nRemaining -= runLen;} while (nRemaining != 0);//最后,使用mergeForceCollapse方法将剩余的所有run进行合并,以完成排序assert lo == hi;ts.mergeForceCollapse();assert ts.stackSize == 1;
}

源码中有几个重点:

run

首先有一个很重要的概念,叫 run,这个 run 是什么呢,可以理解为分区,每一个 run 都是递增或者递减的子数组,比如 [1, 3, 7, 6, 4, 8, 9] 可以拆分成 [1, 3, 7]、[6, 4]、[8, 9]三个 run,而且会把倒序的 run 翻转,以保证所有的 run 都是单向递增的。具体怎么获取 run的长度 以及翻转的,可以看看源码:

/*** 计算待排序数组中从指定位置开始升序或严格降序的run长度,* 如果是run是降序,还需进行反转(确保方法返回时run始终是升序的)** run最长升序条件满足(注意有等于号,相等的数认为是满足升序条件):**    a[lo] <= a[lo + 1] <= a[lo + 2] <= ...** run最长降序条件满足(注意没有等于号,相等的数认为是不满足降序条件):**    a[lo] >  a[lo + 1] >  a[lo + 2] >  ...** 为了保证算法的稳定性,必须严格满足降序条件,以便在进行反转时不破坏稳定性** @param a 待排序的数组* @param lo 计算run分区的起始索引下标* @param hi 计算run分区的结束索引下标* @param c 比较器* @return  返回待排序数组中从指定位置开始升序或严格降序的run长度*/
private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,Comparator<? super T> c) {//必须满足lo < hiassert lo < hi;int runHi = lo + 1;if (runHi == hi)return 1;// Find end of run, and reverse range if descendingif (c.compare(a[runHi++], a[lo]) < 0) { // Descending//开头的两个元素如果是降序,就循环找到结束下标while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)runHi++;//降序需要翻转reverseRange(a, lo, runHi);} else {                              // Ascending//开头的两个元素如果是升序,就循环找到结束下标while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)runHi++;}//计算长度return runHi - lo;
}

minRun

其次会根据数组长度计算一个 run 的最小长度,控制在16~32之间,在后面循环每个 run 的时候,如果该 run 长度太短,就用二分插入排序把 run 后面的元素插入到前面的 run 里面,比如我们假设 run 的最小长度是3(实际不会这么小,会在16~32之间), 数组[1, 3, 7, 6, 4, 5, 9] ,先找到第一个 run [1, 3, 7] 满足长度大于等于3,跳过,再找第二个 run [6, 4] 反转成 [4, 6] 长度只有2,小于3,所以就会把后面的5用二分插入法插入到这个 run 中,使其满足条件,变成 [4, 5, 6]

/*** 如果数组大小为2的N次幂,则返回16(MIN_MERGE / 2)* 其他情况下,逐位向右位移(即除以2),直到找到16-32间的一个数* MIN_MERGE/2 <= minRun <= MIN_MERGE (MIN_MERGE默认32)*/
private static int minRunLength(int n) {assert n >= 0;int r = 0;      // Becomes 1 if any 1 bits are shifted offwhile (n >= MIN_MERGE) { //MIN_MERGE是32r |= (n & 1);n >>= 1;}return n + r;
}

压栈

每划分一个 run 后,就会把这个 run 压栈,然后检查栈顶三个run是否满足合并规则,满足的话就将相邻的run合并

  • 如果栈中 run 的数量等于1,直接跳过合并;
  • 如果栈中 run 的数量等于2,只要满足下面的 run 长度小于等于上面的 run 长度,就会合并;
  • 如果栈中 run 的数量大于2,比较栈顶3个 run 的长度,下面的长度小于等于中间和上面的长度之和,就从下面和上面中选出较短长度的和中间的合并

执行合并后,会再次检查栈顶的3个元素,不满足条件会再次合并,直至满足条件

光说好像不好理解,画个图把,先说明下源码压入栈的不是整个 run,而是 run 的初始位置和 run 的长度,这里我们用 run(n) 来表示 run,用 runLen(n) 来表示 run 的长度

TimSort

可以参考下源码:

/*** 检查栈顶三个run是否满足合并规则,满足的话就将相邻的run合并。* 不满足以下的规则,就会发起合并* *     1. runLen[i - 3] > runLen[i - 2] + runLen[i - 1]*     2. runLen[i - 2] > runLen[i - 1]** 每次有新的run压入栈时就调用这个方法,栈里run的数量必须大于1*/
private void mergeCollapse() {//循环条件,栈内run的数量必须大于1while (stackSize > 1) {int n = stackSize - 2;//stackSize > 2 并且栈顶第三个run个长度小于等于栈顶两个run的长度之和if (n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1]) {//满足上面的条件之后,从栈顶第一个和第三个中选出小的那个,为了下面和第二个合并if (runLen[n - 1] < runLen[n + 1])n--;//合并mergeAt(n);//如果前一条件不满足,当栈顶第二个run的长度小于等于栈顶第一个run时也需要合并} else if (runLen[n] <= runLen[n + 1]) //合并mergeAt(n);} else {//不满足条件退出break; // Invariant is established}}
}

合并

上面讲到满足一些条件就会把两个 run 进行合并,具体怎么合并的呢?

首先查找 run2 的第一个元素在 run1 中的位置。run1 中这个位置之前的元素就可以忽略(因为它们已经就位)。再查找 run1 的最后一个元素在 run2 中的位置。run2 中这个位置之后的元素就可以忽略(因为它们已经就位)。另外,这里的查找使用了加速查找的方式提高效率(内部其实是先用指数搜索确定大致范围,再使用二分查找具体定位)。

然后用 run1 的后半部分和 run2 的前半部分进行合并,合并的时候,是需要开辟额外空间来提高合并效率的,这个开辟的额外空间是多少才能尽可能的节省空间呢,从 run1 的后半部分和 run2 的前半部分中选出较短的长度作为额外空间的大小,去做合并,如下图

TimSort2

然后就是合并了,这个合并也有讲究,我们知道在归并排序算法中合并两个数组其实就是比较每个元素,把较小的放到相应的位置,然后再比较下一个,这种方式有个不好的地方,拿这里的 run1run2 举例,比如 run1 中有连续的好多元素小于 run2 里的某一个元素,归并排序会一直去比较,浪费时间,所以 Tim Sort 就引入了一个 gallop 模式,当 run1 中小于 run2 的元素连续超过一个阈值(minGallop,初始值是MIN_GALLOP,等于7,这个阈值会变化,后面再说)就会进入 gallop 模式,进入 gallop 模式之后,就不是每次拿一个元素了,而是一串元素(找的时候使用了和上面一样的加速查找),进行合并,并且找到的串的长度不能小于阈值(MIN_GALLOP,固定是7),如果小于就会退出 gallop 模式,如果每次 gallop 循环没有退出,就把 minGallop 减1,minGallop 减少会导致退出 gallop 模式之后,更容易再次进入,而退出 gallop 模式之后,又会将 minGallop 加2,minGallop 增加会导致再次进入到 gallop 模式变难,这里其实就是根据实际的情况,去调整阈值,做平衡,因为如果是比较聚集的数组,我们希望通过 gallop 模式处理(减少比对时间),而随机分布的数组,我们更希望使用单个元素比对的方式处理(减少查找时间)

这里我们先看下 mergeAt 的源码:

/*** 合并栈中下标位置是i和i+1的两个run**/
private void mergeAt(int i) {assert stackSize >= 2;assert i >= 0;//i必须是栈顶的第二个或者第三个assert i == stackSize - 2 || i == stackSize - 3;//获取两个run的其实位置和长度int base1 = runBase[i];int len1 = runLen[i];int base2 = runBase[i + 1];int len2 = runLen[i + 1];assert len1 > 0 && len2 > 0;//必须是连续的两个runassert base1 + len1 == base2;/** Record the length of the combined runs; if i is the 3rd-last* run now, also slide over the last run (which isn't involved* in this merge).  The current run (i+1) goes away in any case.*///更新合并后的run长度runLen[i] = len1 + len2;if (i == stackSize - 3) {//当i是栈顶第三个run的时候,就意味着栈顶的run不会被合并(合并第二和第三嘛),所以要把栈顶第二个run的起始位置和长度更新成目前第三个run的runBase[i + 1] = runBase[i + 2];runLen[i + 1] = runLen[i + 2];}//合并会导致run的数量减少1,所以栈的长度减1stackSize--;/** Find where the first element of run2 goes in run1. Prior elements* in run1 can be ignored (because they're already in place).*///查找run2的第一个元素在run1中的位置。这个位置之前的元素在run1中可以忽略int k = gallopRight(a[base2], a, base1, len1, 0, c);assert k >= 0;base1 += k;len1 -= k;//当run2的第一个元素在run1的最后一个,说明两个run已经有序,直接返回if (len1 == 0)return;/** Find where the last element of run1 goes in run2. Subsequent elements* in run2 can be ignored (because they're already in place).*///查找run1的最后一个元素在run2中的位置。这个位置之后的元素在run2中可以忽略len2 = gallopLeft(a[base1 + len1 - 1], a, base2, len2, len2 - 1, c);assert len2 >= 0;//当run1的最后一个元素在run2的第一个,说明两个run已经有序,直接返回if (len2 == 0)return;// Merge remaining runs, using tmp array with min(len1, len2) elements//run1和run2忽略部分元素之后,根据剩余的长度对比,分别调用mergeLo或者mergeHi方法完成合并if (len1 <= len2)mergeLo(base1, len1, base2, len2);elsemergeHi(base1, len1, base2, len2);
}

这里我们看下 mergeLo 的源码:

private void mergeLo(int base1, int len1, int base2, int len2) {assert len1 > 0 && len2 > 0 && base1 + len1 == base2;// Copy first run into temp array//下面这一段是创建一个临时数组tmp,并把较小的去除忽略元素的run拷贝过去T[] a = this.a; // For performanceT[] tmp = ensureCapacity(len1);int cursor1 = tmpBase; // Indexes into tmp arrayint cursor2 = base2;   // Indexes int aint dest = base1;      // Indexes int aSystem.arraycopy(a, base1, tmp, cursor1, len1);// Move first element of second run and deal with degenerate cases//把run2的第一个元素拷贝到原本run1的起始位置a[dest++] = a[cursor2++];//上面那步处理之后,如果run2的元素没有了,就直接把临时数组里的run1拷贝回去,直接完成合并,返回if (--len2 == 0) {System.arraycopy(tmp, cursor1, a, dest, len1);return;}//如果run1的长度是1,就把run2复制到原本run1的位置,再把run1的那个元素复制到最后,直接完成合并,返回if (len1 == 1) {System.arraycopy(a, cursor2, a, dest, len2);a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of mergereturn;}Comparator<? super T> c = this.c;  // Use local variable for performanceint minGallop = this.minGallop;    //  "    "       "     "      "
outer:while (true) {//因为归并排序就是比较两个run中元素的大小,谁小谁就赢了,这里分别用count1和count2来记录run1和run2赢的次数int count1 = 0; // Number of times in a row that first run wonint count2 = 0; // Number of times in a row that second run won/** Do the straightforward thing until (if ever) one run starts* winning consistently.*///这一段就是当(count1 | count2)小于阈值(可变)就执行单个元素比对合并,当不满足了,就会跳过这段do {assert len1 > 1 && len2 > 0;if (c.compare(a[cursor2], tmp[cursor1]) < 0) {a[dest++] = a[cursor2++];count2++;count1 = 0;if (--len2 == 0)break outer;} else {a[dest++] = tmp[cursor1++];count1++;count2 = 0;if (--len1 == 1)break outer;}} while ((count1 | count2) < minGallop);/** One run is winning so consistently that galloping may be a* huge win. So try that, and continue galloping until (if ever)* neither run appears to be winning consistently anymore.*///这里是当大于阈值(固定7)的情况,就进入gallop模式,取多段合并do {assert len1 > 1 && len2 > 0;//gallopRight内部其实是先用指数搜索确定大致范围,再使用二分查找具体定位count1 = gallopRight(a[cursor2], tmp, cursor1, len1, 0, c);if (count1 != 0) {System.arraycopy(tmp, cursor1, a, dest, count1);dest += count1;cursor1 += count1;len1 -= count1;if (len1 <= 1) // len1 == 1 || len1 == 0break outer;}a[dest++] = a[cursor2++];if (--len2 == 0)break outer;count2 = gallopLeft(tmp[cursor1], a, cursor2, len2, 0, c);if (count2 != 0) {System.arraycopy(a, cursor2, a, dest, count2);dest += count2;cursor2 += count2;len2 -= count2;if (len2 == 0)break outer;}a[dest++] = tmp[cursor1++];if (--len1 == 1)break outer;minGallop--;} while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);if (minGallop < 0)minGallop = 0;minGallop += 2;  // Penalize for leaving gallop mode}  // End of "outer" loopthis.minGallop = minGallop < 1 ? 1 : minGallop;  // Write back to fieldif (len1 == 1) {assert len2 > 0;System.arraycopy(a, cursor2, a, dest, len2);a[dest + len2] = tmp[cursor1]; //  Last elt of run 1 to end of merge} else if (len1 == 0) {throw new IllegalArgumentException("Comparison method violates its general contract!");} else {assert len2 == 0;assert len1 > 1;System.arraycopy(tmp, cursor1, a, dest, len1);}
}

流程总结

最后总结下 Tim Sort 的整体流程

  1. 数组长度小于2,直接返回,小于32,使用二分插入排序
  2. 根据数组长度算出 run(连续递增或递减的子数组) 的最小长度 minRun
  3. 开始循环获取 run,得到 run 的长度,如果递减的,将其翻转,如果 run 的长度小于 minRun,就拿其后面的元素使用二分插入补充到这个 run 中,使其满足 minRun 长度
  4. 把这个 run 压入到栈中,并判断栈中的 run 是否满足合并条件,如果满足就合并(合并时,使用了很多提速和节省空间的手段)
  5. 循环完成后,将剩余的所有的 run 合并

复杂度

最后附上和快速排序(Quick Sort)以及归并排序(Merging Sort)的复杂度对比:

算法名称平均时间复杂度最好情况最坏情况空间复杂度
快速排序(Quick Sort)O(n log n)O(n log n)O(n^2)O(1)
归并排序(Merging Sort)O(n log n)O(n log n)O(n log n)O(n)
Tim SortO(n log n)O(n)O(n log n)O(n)

Tim Sort 在处理一些部分排序好的数的时候,需要的比较次数要远小于 n log n,也是远小于相同情况下的归并排序算法需要的比较次数。但是和其他的归并排序算法一样,最坏情况下的时间复杂度是 O(n log n) 。但是在最坏的情况下,Tim Sort 需要的临时存储空间只有 n/2,在最好的情况下,需要的额外空间是常数级别的。所以各个方面都优于需要 O(n) 空间和稳定 O(n log n) 时间的归并算法。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/78189.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux常用命令】

一、防火墙相关 1、查看防火墙状态 systemctl status flrewalld2、如果防火墙是开启状态的&#xff0c;需要关闭 systemctl stop firewalld3、永久行关闭操作&#xff08;禁止开机自启动&#xff09; 因为防火默认是开启状态的&#xff0c;如果只是手动关闭&#xff0c;先次…

【Java】抽奖系统———保姆学习教程

目录 一、抽奖系统介绍 二、代码实现 1、随机生成中奖号码 1.1、中奖号码createNumber方法 1.2、控制判断contains方法 2、用户输入中奖号码 3、判断中奖情况 3.1、判断奖项isWin方法 三、完整代码 一、抽奖系统介绍 抽奖的号码由6个红色球号码和1个蓝色球号码组成。红色…

智慧园区:AI边缘计算技术与视频监控汇聚平台打造智慧园区解决方案

一、行业趋势与背景 智慧园区是现代城市发展的重要组成部分&#xff0c;通过智能化技术提高园区的运营效率、降低成本、增强环境可持续性等具有重要作用。在智慧园区中&#xff0c;人工智能和视频汇聚技术是重要的前置技术。人工智能技术可以实现对数据的智能化处理和分析&…

时序数据库 TimescaleDB 安装与使用

TimescaleDB 是一个时间序列数据库&#xff0c;建立在 PostgreSQL 之上。然而&#xff0c;不仅如此&#xff0c;它还是时间序列的关系数据库。使用 TimescaleDB 的开发人员将受益于专门构建的时间序列数据库以及经典的关系数据库 (PostgreSQL)&#xff0c;所有这些都具有完整的…

2023/9/13 -- C++/QT

作业&#xff1a; 1> 将之前定义的栈类和队列类都实现成模板类 栈&#xff1a; #include <iostream> #define MAX 40 using namespace std;template <typename T> class Stack{ private:T *data;int top; public:Stack();~Stack();Stack(const Stack &ot…

TouchGFX之缓存位图

位图缓存是专用RAM缓冲区&#xff0c;应用可将位图保存&#xff08;或缓存&#xff09;在其中。 如果缓存了位图&#xff0c;在绘制位图时&#xff0c;TouchGFX将自动使用RAM缓存作为像素来源。位图缓存在许多情况下十分有用。 从RAM读取数据通常比从闪存读取要快&#xff08;特…

Linux下Minio分布式存储安装配置(图文详细)

文章目录 Linux下Minio分布式存储安装配置(图文详细)1 资源准备1.1 创建存储目录1.2 获取Minio Server资源1.3 获取Minio Client资源 2 Minio Server安装配置2.1 切换目录2.2 后台启动2.3 查看进程2.4 控制台测试 3 Minio Client安装配置3.1 切换目录3.2 移动mc脚本3.2 运行mc命…

Vue3后台管理系统Element-plus_侧边栏制作_无限递归

在home.view中添加代码 <template><div><div class"common-layout"><el-container><el-header class"common-header flex-float"><div class"flex"><img class"logo" src"../assets/logo…

【Redis】Redis实现分布式锁

【Redis】Redis常见面试题&#xff08;1&#xff09; 文章目录 【Redis】Redis常见面试题&#xff08;1&#xff09;1. 为什么要用分布式锁2. Redis如何实现分布式锁3. Redis接受多个请求模拟演示4. 使用Redis实现分布式锁会存在什么问题4.1 一个锁被长时间占用4.2 锁误删 【Re…

vue2+element-ui批量导入方法并判断上传的文件是否为xls或xlsx

业务需求: 代码结构: <el-dialogtitle"批量导入":close-on-click-modal"true"close"close()":visible"true"width"35%":center"true"><div class"el-dialog-div"><!-- 头部区域布局 -…

【基本数据结构 四】线性数据结构:队列

学习了栈后,再来看看第四种线性表结构,也就是队列,队列和栈一样也是一种受限的线性表结构,和栈后进先出的操作方式不同的是,队列是FIFO的结构,也就是先进先出的操作方式。 队列的定义 队列这个概念非常好理解。可以把它想象成排队买票,先来的先买,后来的人只能站末尾…

软考知识汇总--结构化开发方法

文章目录 1 结构化开发2 耦合3 内聚4 设计原则5 系统文档6 数据流图6.1 数据流图的基本图形元素 7 数据字典 1 结构化开发 结构化方法总的指导思想是自顶向下、逐层分解&#xff0c;它的基本原则是功能的分解与抽象。它是软件工程中最早出现的开发方法&#xff0c;特别适合于数…

「C++程序设计 (面向对象进阶)」学习笔记・二

0、引言 本专栏的系列文章是在学习 北京邮电大学 崔毅东 老师的《C程序设计 (面向对象进阶)》课程过程中整理的。欢迎前往专栏了解更多相关内容~ &#x1f600; 有关于现代 C 的基本介绍&#xff0c;请前往《现代C基本介绍》&#xff01; &#x1f514; 先决条件 本专栏的系列…

定时器+BOM

9.定时器BOM 1.定时器 **概念:**重复执行一个函数 1.1setInterval() setInterval(“代码/函数”,时间,参数),返回定时器的序列号,默认从1开始 clearInterval(序列号)清除定时 <button class"start">开启定时器</button><button class"close…

通过Power Platform自定义D365 CE 业务需求 - 3. 使用Microsoft Power应用程序

Microsoft Power Apps是一个用于开发应用程序的无代码、无代码平台。Power应用程序可以在Dataverse之上配置为数据库。尽管您可以连接Salesforce、OneDrive、Dropbox等多种云源,但Dataverse也可以用作内部数据库来构建应用程序,并通过连接器连接其他数据源进行集成。 Power应…

Java开发之Redis核心内容【面试篇 完结版】

文章目录 前言一、redis使用场景1. 知识分布2. 缓存穿透① 问题引入② 举例说明③ 解决方案④ 实战面试 3. 缓存击穿① 问题引入② 举例说明③ 解决方案④ 实战面试 4. 缓存雪崩① 问题引入② 举例说明③ 解决方案④ 实战面试 5. 缓存-双写一致性① 问题引入② 举例说明③ 解决…

内存管理机制

aCoral内存管理机制 aCoral内存管理机制在伙伴系统基础上&#xff0c;采用了位图法方式提高内存分配和回收速度的确定性&#xff0c;更能满足系统实时性的需求。 aCoral内存管理机制分为两级&#xff0c;上一级采用改进的伙伴系统&#xff0c;负责确定要分配的内存的大小&…

数据分析综述

⭐️⭐️⭐️⭐️⭐️欢迎来到我的博客⭐️⭐️⭐️⭐️⭐️ &#x1f434;作者&#xff1a;秋无之地 &#x1f434;简介&#xff1a;CSDN爬虫、后端、大数据领域创作者。目前从事python爬虫、后端和大数据等相关工作&#xff0c;主要擅长领域有&#xff1a;爬虫、后端、大数据…

C#类与类库调用注意事项

类 创建一个类文件&#xff0c;myfunction.cs //静态类&#xff1a;直接引用、无需实例化 static public int jiafa(int V) //普通类&#xff1a;引用时需要实例化 public int jiafa(int V)using System; using System.Collections.Generic; using System.Diagnostics; using …

ChatGPT追祖寻宗:GPT-2论文要点解读

论文地址&#xff1a;Language Models are Unsupervised Multitask Learners 上篇&#xff1a;GPT-1论文要点解读 在上篇&#xff1a;GPT-1论文要点解读中我们介绍了GPT1论文中的相关要点内容&#xff0c;其实自GPT模型诞生以来&#xff0c;其核心模型架构基本没有太大的改变&a…