当前位置: 首页 > news >正文

这可能是你看过最详细的 [八大排序算法]

排序算法

  • 前置知识 [排序稳定性]
  • 一、直接插入排序
  • 二、希尔排序
  • 三、直接选择排序
  • 四、堆排序
  • 五、冒泡排序
  • 六、快速排序
  • 七、归并排序
  • 八、计数排序(非比较排序)
  • 排序复杂度和稳定性总结

前置知识 [排序稳定性]

假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的。

前要说明[以下所有排序均以升序排序为例]

一、直接插入排序

直接插入排序(Insertion Sort)是一种简单直观的排序算法,它的工作原理类似于我们 整理一组扑克牌的 方法。该算法的特点是 排第 i 个的时候说明前 i-1 个已经有序了 该算法逐个将待排序的元素插入到已排序序列中的正确位置,从而逐步构建有序序列。

实现思路:

  1. 从第二个元素开始(默认第一个元素本身是有序的),将它与已排序序列进行比较,寻找插入位置。
  2. 如果 待插入元素 小于前一个元素,将前一个元素后移一位(腾位置)。如果 待插入元素 大于前一个元素,则找到了插入位置。
  3. 如果 待插入元素 小于所有已排序序列,则插入到第一个位置。
  4. 重复步骤 1、2、3,直到所有元素都被插入到正确的位置为止。
public static void insertSort(int[] array) {// 从 1 下标开始,向前插入for (int i = 1; i < array.length; i++) {// 记录待插入下标 i 的值int tmp = array[i];// 将 i 下标对应值分别和 i 下标之前的元素比较int j = i - 1;for (; j >= 0; j--) {if (tmp < array[j]) {// 升序排序:如果 value(i) 小于之前元素,该元素向后挪动array[j + 1] = array[j];} else {// 如果 value(i) > 前面的某一个元素值,即找到插入位置,跳出循环break;}}// 代码走到这里表示:// 1.break跳出循环,即在[0,i]下标之间找到小于value(i)的元素// 2.value(i) 在 [0,i]下标之间是最小的array[j + 1] = tmp;}
}

插入排序特性分析:

  1. 时间复杂度
    最坏情况:对于上述插入排序,最坏情况下的时间复杂度是一个等差数列 O ( 1 + 2 + . . . + N ) = O ( N 2 ) O(1+2+...+N)=O(N^2) O(1+2+...+N)=O(N2)
    最好情况:最好的情况即待排序列本身就是有序的,插入排序算法仅遍历比较一次,时间复杂度为 O ( N ) O(N) O(N)
    因此对于插入排序来说:元素集合越接近有序,直接插入排序算法的时间效率越高
  2. 空间复杂度
    由于没有使用额外空间,故空间复杂度为 O ( 1 ) O(1) O(1)
  3. 稳定性
    上述插入排序算法是稳定的。但是如果将 tmp < array[j] 改为 tmp <= array[j]那么排序就不在稳定了。因为得出结论:如果一个排序时稳定的,那么可以实现为不稳定;如果一个排序本身就不稳定,无法实现为稳定排序。

二、希尔排序

希尔排序法又称缩小增量法。希尔排序法的基本思想是:选定一个整数增量 g a p gap gap,把待排序序列中所有记录分成多个组,所有距离为增量 g a p gap gap 的记录分在同一组内,并对每一组内的记录进行排序。然后,减小增量,重复上述分组和排序的工作。当增量 g a p gap gap 缩小至 1 时,所有记录在统一组内排好序。

希尔排序,当 g a p > 1 gap > 1 gap>1 时都是预排序,目的是让数组更接近于有序。对于插入排序来说,元素趋于有序,插入排序时间复杂度就趋于 O(N)。所以希尔排序是对直接插入排序的优化。

实现思路:

  1. 首先,确定初始的增量 gap,可以选择将数组长度的一半作为初始值。
  2. 对每个增量进行分组(代码控制)和排序操作(插入排序)。
  3. 每次增量减少一半,即 gap = gap / 2,直到 gap 缩小至 1 时,执行最后一次分组和排序操作。
public static void shellSort(int[] array) {// 确定增量 gapint gap = array.length/2;while (gap > 1) {shell(array, gap);// 每次增量减少gap = gap / 2;}// 增量为1时再排一次shell(array, gap);}// 这段代码,利用 下标 i、j 和 gap 之间的关系,实现每一组的交替插入排序:
private static void shell(int[] array, int gap) {// 初始时 i = gap ,使下标 i 指向第 1 组的第 2 个元素// 便于对每一组执行插入排序for (int i = gap; i < array.length; i++) {// 记录待插入下标 i 的值int tmp = array[i];// 下标 j 指向每组的已排序序列int j = i - gap;// 寻找每组 value(i) 的插入位置for (; j >= 0; j -= gap) {if (array[j] > tmp) {array[j + gap] = array[j];} else {break;}}// 代码走到这里表示:找到插入位置array[j + gap] = tmp;}
}

希尔排序特性分析:

  1. 时间复杂度
    希尔排序的时间复杂度涉及数学上尚未解决的难题,因此我们暂时无法得出希尔排序准确的时间复杂度,这里给出一个范围: O ( N 1.3 ) ∼ O ( N 1.5 ) O(N^{1.3}) \sim O(N^{1.5}) O(N1.3)O(N1.5)
  2. 空间复杂度
    由于排序过程没有使用额外的空间,因此空间复杂度为 O ( 1 ) O(1) O(1)
  3. 稳定性
    在希尔排序中,元素按照增量分组并进行插入排序,这可能导致相等的元素在排序后的相对顺序发生变化,因此希尔排序是一个不稳定的排序算法

三、直接选择排序

直接选择排序思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在待排序列的起始位置,直到全部待排序的数据元素排完 。

实现思路:

  1. 记录待排序列起始下标 i,遍历待排序列,每次找到待排元素中的最小值下标 minIndex
  2. 交换下标值,将最小值放到待排序列的起始位置,并且 i++ 缩小待排序列范围
  3. 重复步骤1、2 直到排序完待排序列,序列即为有序
// 交换函数:swap()
public static void swap(int[] array, int i, int j) {int tmp = array[i];array[i] = array[j];array[j] = tmp;
}public static void selectSort(int[] array) {// 待排序列起始下标 ifor (int i = 0; i < array.length; i++) {// 初始化最小值下标int minIndex = i;// 寻找待排元素中最小值for (int j = i + 1; j < array.length; j++) {if (array[j] < array[minIndex]) {//更新最小值下标minIndex = j;}}// 此时minIndex放的是最小元素的下标swap(array, minIndex, i);}
}

直接选择排序变种
上述直接选择排序的思路是,每次寻找最小值,放到待排序列起始位置,从而实现排序。这里还有另外一种实现思路:

  1. 同时记录待排序列的起始下标 left 和终止下标 right,即限制待排序列范围 [left,right]
  2. 每次遍历待排序列,同时记录最大值下标 maxIndex 和最小值下标 minIndex
  3. 交换下标值,将最大值放到待排序列的终止位置,最小位置放到待排序列的起始位置
  4. 每次交换缩小待排序列范围 left ++right --,直到待排序列范围为 0,即 left == right
public static void selectSort2(int[] array) {// 待排序列起始下标int left = 0;// 待排序列终止下标int right = array.length - 1;// 遍历待排序列while (left < right) {// 初识值,假定最小最大值下标都在最左边int maxIndex = left;int minIndex = left;//每次找到当前范围内的最大值下标:maxIndex;最小值下标:minIndexfor (int i = left + 1; i <= right; i++) {if (array[i] < array[minIndex]) {minIndex = i;}if (array[i] > array[maxIndex]) {maxIndex = i;}}// 确定最小值位置swap(array, left, minIndex);// 如果最大值对应待排序列的起始下标,需要特殊处理,// 因为上面的交换会将最大值换到下标 minIndex 处if (maxIndex == left) {maxIndex = minIndex;}// 确定最大值位置swap(array, right, maxIndex);// 缩小待排序列范围left++;right--;}
}

直接选择排序特性分析:

  1. 时间复杂度
    无论是 直接选择排序 还 是直接选择排序 的变种,作为一种非常“暴力”的排序,不管待排序列本身是否有序,每一趟都会遍历待排序列,时间复杂度都是一个等差数列相加,即 O ( N 2 ) O(N^2) O(N2)

  2. 空间复杂度
    由于没有使用额外空间,故空间复杂度为 O ( 1 ) O(1) O(1)

  3. 稳定性
    在选择排序中,每次会选择未排序序列中的最小(或最大)元素,然后将其与未排序序列的第一个位置交换。这个操作可能会破坏相等元素之间的相对顺序,导致排序后它们的相对位置发生改变。因此直接选择排序是不稳定的。

四、堆排序

堆排序(Heapsort)是指利用堆,这种数据结构所设计的一种排序算法。它的原理是,利用堆的性质,即堆顶元素为堆中的最大值(最小值)的特性,每次确定一个有序序列元素的位置,逐渐构建有序序列。

注意:排升序,需要构建大根堆;排降序需要构建小根堆。


实现原理:

  1. 构建源待排序列为大顶堆。
  2. 将堆顶元素和待排序列的最后一个元素交换。
  3. 重新确定待排序列范围,并将新待排序列使用向下调整算法,构建为大根堆。
  4. 重复上述步骤2、3直到序列有序。
public static void heapSort(int[] array) {// 将源序列构建为大根堆creatHeap(array);// 进行堆排序// 从后向前调整待排序列int end = array.length - 1;while (end > 0) {swap(array, 0, end);shiftDown(array, 0, end);end--;}
}//这里是建立大根堆->升序排序
private static void creatHeap(int[] array) {for (int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {shiftDown(array, parent, array.length);}
}
// 向下调整算法
private static void shiftDown(int[] array, int parent, int len) {int child = 2 * parent + 1;//一定有左孩子while (child < len) {//有左孩子和右孩子if (child + 1 < len && array[child] < array[child + 1]) {child++;}// 此时child拿到最大值孩子下标if (array[child] > array[parent]) {swap(array, child, parent);parent = child;child = 2 * parent + 1;} else {break;}}
}

堆排序特性分析:

  1. 时间复杂度
    在构建大根堆(creatHeap)的过程中,需要对每个非叶子节点进行向下调整操作(shiftDown),这部分的时间复杂度为 O ( n ) O(n) O(n)。在堆排序过程中,需要将堆顶元素与当前待排序的最后一个元素交换,并对堆顶元素进行向下调整操作。重复这个过程直到所有元素都被排序,这部分的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。并且无论输入序列的初始状态如何,堆排序都需要进行完整的堆构建和元素交换操作。因此,整个堆排序的时间复杂度为 O ( n + n l o g n ) O(n + nlogn) O(n+nlogn),即 O ( n l o g n ) O(nlogn) O(nlogn)

  2. 空间复杂度
    堆排序是一种原地排序算法,不需要额外的辅助空间,空间复杂度为 O ( 1 ) O(1) O(1)

  3. 稳定性
    在堆排序中,交换节点的操作可能会改变具有相同值的元素之间的相对顺序,因此堆排序是一个不稳定的排序算法。

五、冒泡排序

冒泡排序是一种简单的排序算法,它重复地比较相邻的两个元素,并按照升序(降序)交换位置,直到整个序列有序。冒泡排序的过程类似于气泡不断往上升的过程,较大的元素会像气泡一样逐渐“浮”到序列的末尾,因此得名冒泡排序。

实现思路:

  1. 从序列的第一个元素开始,依次比较相邻的两个元素。
  2. 如果当前元素大于后一个元素,则交换这两个元素的位置,直到完成一轮比较。
  3. 重复步骤 2 ,直到整个序列有序。因为每一轮比较都会将本轮最大的元素移动到末尾,故每次比较的元素个数减少 1(优化)。
public static void bubbleSort(int[] array) {// 外层循环控制比较的轮数for (int i = 0; i < array.length - 1; i++) {// 判断是否有序的标志boolean flag = true;// 内层循环进行相邻元素的比较和交换,每一轮次比较个数少1(优化1)for (int j = 0; j < array.length - 1 - i; j++) {// 这里是升序if (array[j] > array[j + 1]) {swap(array, j, j + 1);// 只要进入比较就说明还不一定有序flag = false;}}//如果在一趟比较中一次都不比较说明已经有序,不需要继续遍历(优化2)if (flag) {break;}}
}

冒泡排序特性分析:

  1. 时间复杂度
    加入两个优化后,最坏情况下,上述冒泡排序的时间复杂度为 O ( n − 1 + n − 2 + . . . 1 ) = O ( n 2 ) O(n-1+n-2+...1)=O(n^2) O(n1+n2+...1)=O(n2)。最好情况下即序列本身有序,此时仅遍历序列 1 次,时间复杂度为 O ( 1 ) O(1) O(1).
    不加入优化的情况下,最好情况和最坏情况的时间复杂度均为 O ( n 2 ) O(n^2) O(n2).

  2. 空间复杂度
    冒泡排序是一种原地排序算法,不需要额外的辅助空间,空间复杂度为 O ( 1 ) O(1) O(1).

  3. 稳定性
    冒泡排序是一种稳定的排序算法,保持了相等元素的相对顺序。

六、快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元
素作为基准值,按照该基准值将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有
元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

注意:快速排序,每次划分都可以确定序列中一个元素的有序位置。

实现思路:

  1. 首先根据基准值,对待排序列进行划分,使左子序列小于基准值;右子序列大于基准值。
  2. 然后递归排左子序列和右子序列 直到子序列为空 或者 子序列仅剩1个元素(默认有序)。
public static void quickSort(int[] array) {quick(array,0,array.length-1);
}
// 为了接口的统一抽象出一个方法
private static void quick(int[] array,int start,int end) {// 递归终止条件:// start > end 表示子序列为空;// start == end 表示子序列仅剩1个元素if (start >= end) {return;}// 按照基准值对array数组的 [left, right)区间中的元素进行划分// 返回基准值下标,便于递归左右子序列int pivotIndex = partition(array,start, end);// 划分成功后以div为边界形成了左右两部分 [pivot, div) 和 [pivot+1, right)// 递归排[left, pivotIndex)quick(array,start,pivotIndex-1);// 递归排[pivotIndex+1, right)quick(array,pivotIndex+1,end);
}

快排的一个核心函数是划分 partition() ,根据按照基准值划分区间的方式,主要有以下 3 种实现方法:

1. Hoare版
Hoare 划分实现思想:

  1. 首先确定基准值 pivot,一般选取序列区间 [left,right] 最左边元素或最右边元素为基准值。这里选择value(left) 为基准值。
  2. left 指针向右走,直到找到大于基准值 pivot 的值;right 指针向左走,直到找到小于基准值 pivot 的值。分别找到之后,让left、right下标值交换,使比基准值小的在 left 下标处,比基准值大的在 right 下标处。
  3. 当某一时刻,left == right时,此时 left 和 right 下标均指向小于基准值的下标,此时将基准值 pivot 和 left/right 下标对应值交换,即完成该序列划分。

private static int partition(int[] array, int left, int right) {// 记录初始时pivot下标int i = left;// 记录基准值int pivot = array[left];while (left < right) {// 注意取等,否则可能会出现左右横跳的死循环![left和right相等时]// 注意取左边为基准值就右边 right 先走// 1.寻找右边小于piovt的值while (left < right && array[right] >= pivot) {right--;}// 2.寻找左边大于pivot的值while (left < right && array[left] <= pivot) {left++;}// 交换swap(array, left, right);}// left == right 时,让下标值交换swap(array, left, i);// 此时 left 和 right 均指向pivot基准值,返回即可return left;
}

(1)为什么以序列最左为基准值时,需要 right 先走,left 后走?

这里我们假设每次 right 先走,right 后走,此时当 left == right 时,指向的下标对应大于基准值 pivot 的元素,如果此时将下标交换,就会导致,大于基准值的元素出现在 pivot 的左边,这是一个隐蔽的 bug,我们应该避免:


(2)为什么 array[right] >= pivot 寻找小于基准值 和 array[left] <= pivot 寻找大于基准值时必须取等?

2. 挖坑法
挖坑法 划分实现思想:

  1. 首先保存基准值 pivot ,pivot对应下标为第一个坑位。这里假设在区间 [left,right]value(left) 为基准值,则坑位对应下标为 left.
  2. right 开始向前移动,找到大于 pivot 的的位置,找到后将该位置值放入坑位中,该位置形成新的坑位。
  3. left 开始向后移动,找到小于 pivot 的位置,找到后将该位置值放入到坑位中,该位置形成新的坑位。
  4. 重复步骤2、3直到 left == right 时,将保存的 pivot 值放到当前坑位,划分结束。

private static int partition2(int[] array, int left, int right) {// 保存基准值int pivot = array[left];while (left < right) {//注意条件取等(原因同 hoare)// 1.寻找右边小于piovt的值while (left < right && array[right] >= pivot) {right--;}// 入坑array[left] = array[right];// 2.寻找左边大于piovt的值while (left < right && array[left] <= pivot) {left++;}// 入坑array[right] = array[left];}// pivot 值入坑array[left] = pivot;// 返回pivot值对应下标return left;
}

3. 前后指针(了解)
前后指针 划分实现思想:

  1. 设置两个指针 curprev,初始时,prev 指向序列开头,cur 指向 prev 的后一个位置。
  2. 判断 cur 指针指向数据是否大于基准值 pivot ,如果大于,cur向后移动,直到找到小于基准值 pivot 的位置。
  3. 通过步骤2,此时,cur指针指向数据小于基准值 pivot 。prev 向后移动 1 位 ,并判断此时指针 cur 是否等于 prev,如果 cur != prev,则说明 prev 和 cur 之间存在大于 pivot 的值,并且prev此时指向大于pivot的值,然后让value(prev) 和 value(cur) 交换即可;如果 cur == prev,说明prev和cur之间不存在大于 pivot 的值,即 prev 和 cur 相邻,不进行交换,cur 继续向后移动。
  4. 直到 cur 越界,将 基准值 pivot 同 value(prev) 交换。完成划分。

前后指针法,非常巧妙,通过控制cur 指针 和 prev 指针的位置,实现序列划分:

private static int partition3(int[] array,int left,int right) {// 初始化指针位置int prev= left;int cur = left+1;while(cur<= right) {// 条件限制,保证不相邻时 prev 下标记录的是左边小于基准值的最后一个if (array[cur]<array[left] && array[++prev]!=array[cur]) {swap(array,prev,cur);}cur++;}// 上面走完了需要把基准值放到 prev 下标下swap(array,prev,left);// 返回基准值下标return prev;
}

快速排序的非递归实现

  1. 快排的非递归实际上是使用来模拟递归操作。因此首先需要创建一个栈。
  2. 将待排序序列的起始位置 left 和结束位置 right 压入栈中。
  3. 循环出栈,判断栈是否为空,如果不为空,从栈中弹出 right 和 left,并调用 partition 函数对当前范围内的子序列进行划分,得到基准元素的正确位置 pivotIndex。
  4. 如果 pivotIndex 左边的范围仍然有两个以上的元素,则将左边的起始位置 left 和 pivotIndex - 1 压入栈中,下一轮循环将对其进行划分。
  5. 如果 pivotIndex 右边的范围仍然有两个以上的元素,则将 pivotIndex + 1 和右边的结束位置 right 压入栈中,下一轮循环将对其进行划分。
  6. 循环结束后,所有序列划分完毕,排序完成。

public static void quickSort2(int[] array) {// 创建栈Deque<Integer> stack = new LinkedList<>();// 将待排序列起始和终止位置压栈int left = 0;int right = array.length - 1;// 初始时如果left>=right,直接返回if (left < right) {stack.push(left);stack.push(right);} else {return;}// 循环出栈while (!stack.isEmpty()) {right = stack.pop();left = stack.pop();// 对子序列进行划分int pivotIndex = partition(array,left,right);// 判断左子序列是否存在两个以上元素if (pivotIndex > left + 1) {stack.push(left);stack.push(pivotIndex - 1);}// 判断右子序列是否存在两个以上元素if (pivotIndex < right - 1) {stack.push(pivotIndex + 1);stack.push(right);}}
}

快速排序特性分析:

  1. 时间复杂度
    最好情况:最好情况下,即每次都能均衡地划分序列,将序列划分成两个子序列,最终递归的高度为一颗完全二叉树的高度,为 l o g 2 ( N ) log_2(N) log2(N),每一层需要比较交换 n 次,故时间复杂度为 O ( N . l o g 2 ( N ) ) O(N.log_2(N)) O(N.log2(N)).

    最坏情况:当序列本身为升序或逆序的情况下,每次划分,只有右子序列或左子序列,则最终递归的高度为一颗n层高的单分支二叉树的高度,第一层需比较交换 n 次,第二层比较交换 n-1 次……,故时间复杂度为 O ( N 2 ) O(N^2) O(N2)

  2. 空间复杂度
    最好情况下(同上),递归的高度为 l o g 2 ( N ) log_2(N) log2(N),即空间复杂度为 O ( l o g 2 ( N ) ) O(log_2(N)) O(log2(N)).
    最坏情况下(同上),递归的高度为 N N N,即空间复杂度为 O ( N ) O(N) O(N).

  3. 稳定性
    由于在划分的过程中,相等的元素之间可能会发生位置的交换,导致原本相等的元素的相对顺序发生改变。因此快速排序是不稳定的。

(1)快速排序优化一:三数取中法确定基准值

(2)快速排序优化二:后期递归改用直接插入排序
通过上面的分析我们已知,平均情况下,快速排序的递归类似于一颗完全二叉树,而对于一颗完全二叉树来说,它的最后几层结点的个数基本上占据了整个二叉树结点的 3 / 4 3/4 3/4,也就是说快速排序递归深度越深,递归的次数就越多。再者对于快速排序来说,当递归来到后几层时,子序列序列已经基本上趋于有序了。

因此为了减少快速排序后期的递归的次数,同时利用插入排序特性:插入排序适用于小规模或近乎有序的序列,在小规模序列上运行时有着较好的性能。所以,当划分得到的子序列长度小于等于一个阈值时,可以选择直接使用插入排序来对子序列进行排序。

经过上述 2 2 2 次优化,最终快速排序可实现为:

	public static void quickSort(int[] array) {quick(array,0,array.length-1);}// 为了接口的统一抽象出一个方法private static void quick(int[] array,int start,int end) {// 递归终止条件:// start > end 表示子序列为空;// start == end 表示子序列仅剩1个元素if (start >= end) {return;}// 优化二:递归到小的子区间时,可以考虑使用直接插入排序if (end-start+1<=16) {quickInsert(array,start,end);return;}// 优化一:三数取中法选 pivot ,防止单枝树int index = midThree(array,start,end);// 每次将 pivot 值换到 start 位置swap(array,start,index);// 按照基准值对array数组的 [left, right)区间中的元素进行划分// 返回基准值下标,便于递归左右子序列int pivotIndex = partition(array,start, end);// 划分成功后以div为边界形成了左右两部分 [pivot, div) 和 [pivot+1, right)// 递归排[left, pivotIndex)quick(array,start,pivotIndex-1);// 递归排[pivotIndex+1, right)quick(array,pivotIndex+1,end);}/*优化*/// 1.三数取中法选 pivot,防止单枝树private static int midThree(int[] array,int left,int right) {int mid = (left+right)/2;if (array[left]<array[right]) {if (array[mid]>array[right]) {return right;} else if (array[mid]<array[left]) {return left;} else {return mid;}} else {//array[left]>=array[right]的情况if (array[mid]>array[left]) {return left;} else if (array[mid]<array[right]) {return right;} else {return mid;}}}// 2. 递归到小的子区间时,可以考虑使用插入排序[利用越有序越快的特点]public static void quickInsert(int[] array,int left,int right) {for (int i = left+1; i <=right; i++) {int tmp = array[i];//将i下标对应值分别和i下标后元素比较int j = i - 1;for (; j >= left; j--) {//升序排序:如果小于其后元素,向后挪动if (array[j] > tmp) {array[j + 1] = array[j];} else {break;}}array[j + 1] = tmp;}}

七、归并排序

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列。过程主要包括分解、合并排序。

归并排序递归实现

  1. 将待排序列递归分解,每次分解为左子序列 [left,mid];右子序列 [mid+1,right]
  2. 分解到每个子序列仅剩1个元素时,开始合并排序的回溯过程,每次合并,将两个有序序列,合并为1个有序序列。
public static void mergeSort(int[] array) {// 为了接口的统一性,这里将归并排序抽象出一个方法mergeSortFunc(array,0,array.length-1);
}
// 归并排序
private static void mergeSortFunc(int[] array, int start,int end){// 当元素个数小于等于1时,分解停止if (start >= end) {return;}// 递归分解int mid = (start+end)/2;// 左子序列[left,mid]mergeSortFunc(array,start,mid);// 右子序列[mid+1,right]mergeSortFunc(array,mid+1,end);// 合并排序merge(array,start,end,mid);
}// 递归完成后的合并排序过程
private static void merge(int[] array,int left,int right,int mid) {// 定义两个变量分别指向两个子序列的头int s1 = left;int s2 = mid+1;// 定义一个临时数组用来存储“和并排序”的数据int[] tmp = new int[right-left+1];// 临时数组下标int k = 0;// 进行“合并排序”的条件是两个子数组均不越界while (s1<=mid && s2<=right) {if (array[s1]<array[s2]) {tmp[k++] = array[s1++];} else {tmp[k++] = array[s2++];}}// 将还没走完的数组全部排入临时数组中while (s1<=mid) {tmp[k++] = array[s1++];}while (s2<=right) {tmp[k++] = array[s2++];}// 将排好的数据放入原来的数组中->注意: i+left找到原数组对应下标for (int i = 0; i < tmp.length; i++) {array[i+left] = tmp[i];}
}

归并排序非递归实现

  1. 记录每组有序个数gap,省去递归分解过程,直接从每组1个元素开始合并排序(当每组仅有1个元素时,默认有序)。
  2. 通过确定每两组有序序列的 leftmidright 下标,调用 merge 方法,两组两组进行合并排序。
  3. 每趟排序过后,每组有序个数乘 2.
  4. 重复步骤2、3 ,直到序列有序。
public static void mergeSort2(int[] array) {// gap 表示当前每组多少个有序元素int gap = 1;// 合并过程// 因为每组最多为 array.length 个,所以 gap < array.lengthwhile (gap < array.length) {// 两组两组的合并序列// i += gap * 2 表示去合并另外两组有序序列 for (int left = 0; left < array.length; left += gap * 2) {int mid = left+gap-1;// 有可能会越界,处理越界情况if(mid >= array.length) {mid = array.length-1;}int right = mid+gap;// 有可能会越界,处理越界情况if(right>= array.length) {right = array.length-1;}// 进行合并排序merge(array,left,right,mid);}// 当前为每2个一组有序,下次变成4个一组有序gap *= 2;}
}

这里需要注意的是,midright 下标可能会越界,需要处理越界情况。例如,如下情况就会越界:

快速排序特性分析:

  1. 时间复杂度
    在分解时,需要对序列进行二分,对于长度为n的序列,需要进行 l o g 2 ( n ) log_2(n) log2(n) 次划分。在每一次合并排序操作中,需要每一次分解的元素进行比较和合并,时间复杂度为 O ( n ) O(n) O(n)。总的时间复杂度为 O ( n l o g 2 ( n ) ) O(nlog_2(n)) O(nlog2(n)).

  2. 空间复杂度
    归并排序需要额外的空间来存储临时的合并结果,因此空间复杂度为 O(n).

  3. 稳定性
    归并排序保持了相等元素的相对顺序,是稳定的排序。

归并排序的应用场景
归并排序可以通过外部排序的方式处理海量数据的排序问题。外部排序是一种针对无法一次性载入内存的大规模数据进行排序的技术。

  1. 将海量数据划分为多个能够载入内存的小块。
  2. 对每个小块,因为内存已经可以放的下,所以任意排序方式都可以。将结果生成有序的子文件。
  3. 将每个子文件归并成一个更大的有序文件。
  4. 如果仍然无法一次性载入内存,重复步骤2和步骤3,直到所有的子文件都被归并成一个完整的有序文件。

八、计数排序(非比较排序)

计数排序是一种非比较排序算法,其基本思想是统计待排序序列中每个元素的出现次数,然后根据这些统计信息将元素放置到正确的位置上,从而达到排序的目的。

计数排序的主要实现思想如下:

  1. 找出待排序序列 arr 中的最大值 max 和最小值 min,并确定计数数组 count 的长度 len = max - min +1。计数数组用于存储每个元素的出现次数。
  2. 遍历待排序序列 arr,统计每个元素出现的次数,并在计数数组中相应的位置 arr[i] - min 进行累加。
  3. 根据计数数组中统计的信息,重新构建排序后的序列。具体方法是遍历计数数组,根据元素的累加次数,将对应元素放入待排序列中。
public static void countSort(int[] array) {//1. 遍历数组 找到最大值 和 最小值int min =array[0];int max =array[0];for (int i = 1; i < array.length; i++) {if (array[i]<min) {min = array[i];}if (array[i]>max) {max = array[i];}}//2. 根据范围 确定计数数组的长度int len = max - min + 1;int[] count = new int[len];//3.遍历数组,在计数数组当中 统计每个元素出现的次数for (int i = 0; i < array.length; i++) {// 下标值 i - min 表示相对位置int index = array[i]-min;count[index]++;}int k=0;//4.遍历计数数组,根据元素的累加次数,将对应元素放入待排序列中for (int i = 0; i < count.length; i++) {while (count[i]!=0) {// 下标值 i + min 表示真实的数据array[k++] = i+min;count[i]--;}}
}

计数排序特性分析:

  1. 时间复杂度
    计数排序的时间复杂度为 O ( n + k ) O(n+k) O(n+k),其中 n 表示待排序序列的长度,k 表示计数数组的长度,也就是待排序列的范围。
  2. 空间复杂度
    计数排序的空间复杂度为O(k),k表示计数数组的长度,即待排序列的范围。
  3. 稳定性
    当前上面这种实现方式下,没有稳定性可言。
  4. 适用场景
    对于计数排序来说,当取值范围较大时,或者数据不集中,需要耗费较大的空间来创建计数数组。因此对于计数排序来说,它适用于 已知一定范围内相对集中的整数排序

排序复杂度和稳定性总结

排序方法最好情况时间复杂度平均情况时间复杂度最坏情况时间复杂度空间复杂度稳定性
冒泡排序O(n)O(n^2)O(n^2)O(1)稳定
插入排序O(n)O(n^2)O(n^2)O(1)稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
希尔排序O(n)O(n^1.3)O(n^2)O(1)不稳定
堆排序O(n * log(n))O(n * log(n))O(n * log(n))O(1)不稳定
快速排序O(n * log(n))O(n * log(n))O(n^2)O(log(n)) ~ O(n)不稳定
归并排序O(n * log(n))O(n * log(n))O(n * log(n))O(n)稳定

相关文章:

这可能是你看过最详细的 [八大排序算法]

排序算法 前置知识 [排序稳定性]一、直接插入排序二、希尔排序三、直接选择排序四、堆排序五、冒泡排序六、快速排序七、归并排序八、计数排序&#xff08;非比较排序&#xff09;排序复杂度和稳定性总结 前置知识 [排序稳定性] 假定在待排序的记录序列中&#xff0c;存在多个…...

docker的安装

CentOS7 安装 Docker 安装需要的软件包&#xff0c; yum-util 提供yum-config-manager功能&#xff0c;另两个是devicemapper驱动依赖 yum install -y yum-utils device-mapper-persistent-data lvm2 添加下载源 yum-config-manager --add-repo http://mirrors.aliyun.com/…...

【业务功能篇75】微服务项目环境搭建docker-mysql-redisSpringCloudAlibaba

项目环境准备 1.虚拟机环境 我们可以通过VMWare来安装&#xff0c;但是通过VMWare安装大家经常会碰到网络ip连接问题&#xff0c;为了减少额外的环境因素影响&#xff0c;Docker内容的讲解我们会通过VirtualBox结合Vagrant来安装虚拟机。 VirtualBox官网&#xff1a;https:/…...

学习笔记|认识数码管|控制原理|数码管实现0-9的显示|段码跟位码|STC32G单片机视频开发教程(冲哥)|第九集:数码管静态显示

文章目录 1.认识数码管2.控制原理十进制转换为任意进制其它进制转十进制 3.数码管实现0-9的显示1.用数组定义0-9的内码段码跟位码的区别2.尝试用延时实现0-9的循环显示3.用按键控制数字的加或者减。 总结课后练习&#xff1a; 1.认识数码管 数码管按段数可分为七段数码管和八段…...

CentOS 7/8 firewall 转发端口

#开启系统路由模式功能 echo net.ipv4.ip_forward1>>/etc/sysctl.conf sysctl -p #开启firewalld systemctl start firewalld 打开防火墙伪装IP # 检查是否允许伪装IP&#xff0c;返回no表示没开启&#xff0c;反之开启伪装IP firewall-cmd --query-masquerade #设置…...

mysql自动备份脚本

备份脚本 #!/bin/bash #author cheng #mysql数据自动备份 mysql_user“root” mysql_password“passwprd” mysql_host“localhost” mysql_port“3306” mysql_charset“utf8mb4” #备份文件存放地址(根据实际情况填写) backup_location/usr/cheng/msg_manager/sql #是否删…...

VUE笔记(九)vuex

一、vuex的简介 1、回顾组件之间的通讯 父组件向子组件通讯&#xff1a;通过props实现 子组件向父组件通讯&#xff1a;通过自定义事件($emit)方式来实现 兄弟组件之间的通讯&#xff1a;事件总线&#xff08;$eventBus&#xff09;、订阅与发布方式来实现 跨级组件的通讯…...

Webpack高频面试题

Webpack高频面试题 1 谈谈你对webpack的看法 现在的前端网页功能丰富&#xff0c;特别是SPA&#xff08;single page web application 单页应用&#xff09;技术流行后&#xff0c;JavaScript的复杂度增加和需要一大堆依赖包&#xff0c;还需要解决Scss&#xff0c;Less……新…...

数字基带传输系统

文章目录 前言一、数字基带系统基本组成二、基本码型1、数字基带信号2、6 种基本码型 三、数字基带信号的频谱特性四、数字基带信号选码1、原则2、常用的传输码型①、AMI 码&#xff08;传号交替反转码&#xff09;②、 H D B 3 HDB_3 HDB3​ 码&#xff08;3 阶高密度双极性码…...

FPGA使用MIG调用SODIMM内存条接口教程,提供vivado工程源码和技术支持

目录 1、前言免责声明 2、SODIMM内存条简介3、设计思路框架视频输入视频缓存MIG配置调用SODIMM内存条VGA时序视频输出 4、vivado工程详解5、上板调试验证6、福利&#xff1a;工程代码的获取 1、前言 FPGA应用中&#xff0c;数据缓存是一大重点&#xff0c;不管是图像处理还是A…...

深度学习数据预处理

参考文章&#xff1a;深度学习中的数据预处理方法总结 在深度学习中&#xff0c;数据预处理&#xff08;preprocessing&#xff09;的重要性体现在以下几个方面&#xff1a; 1、数据质量&#xff1a; 原始数据通常包含错误、缺失值、异常值和噪声。预处理能够检测和处理这些问…...

[C++] STL_vector 迭代器失效问题

文章目录 1、前言2、情况一&#xff1a;底层空间改变的操作3、情况二&#xff1a;指定位置元素的删除操作4、g编译器对迭代器失效检测4.1 扩容4.2 erase删除任意位置&#xff08;非尾删&#xff09;4.3 erase尾删 5、总结 1、前言 **迭代器的主要作用就是让算法能够不用关心底…...

C语言暑假刷题冲刺篇——day5

目录 一、选择题 二、编程题 &#x1f388;个人主页&#xff1a;库库的里昂 &#x1f390;CSDN新晋作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏✨收录专栏&#xff1a;C语言每日一练✨相关专栏&#xff1a;代码小游戏、C语言初阶、C语言进阶&#x1f91d;希望作者…...

若依Cloud集成Flowable6.7.2

项目简介 基于若依Cloud的Jove-Fast微服务项目&#xff0c;集成工作流flowable(接上篇文章) 若依Cloud集成积木报表 项目地址&#xff1a;https://gitee.com/wxjstudy/jove-fast 后端 新建模块 目录结构如下: 引入依赖 前提:引入依赖之前先配置好maven的setting.xml &…...

动态不确定性的动态S过程(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

LoadRunner操作教程

日升时奋斗&#xff0c;日落时自省 目录 1、Virtual User Generator &#xff08;VUG&#xff09; 1.1、WebTours系统 1.1.1、WebTours启动 1.1.2、WebTours配置 1.2、脚本录制 1.3、编译 1.4、脚本运行 1.5、加强脚本 1.5.1、事务插入 1.5.2、插入集合点 1.5.3、参…...

.NET Core 实现日志打印输出在控制台应用程序中

在本文中&#xff0c;我们将探讨如何在 .NET Core 应用程序中将日志消息输出到控制台&#xff0c;从而更好地了解应用程序的运行状况。 .NET Core 实现日志打印输出在控制台应用程序中 在 .NET Core 中&#xff0c;日志输出打印是使用 Microsoft.Extensions.Logging 命名空间…...

Nginx正向代理与反向代理及Minio反向代理实操(三)

本文是对: Nginx安装及Minio集群反向动态代理配置(二) 文的进一步完善: 多台服务器间免密登录|免密拷贝 Cenos7 搭建Minio集群部署服务器(一) Cenos7 搭建Minio集群Nginx统一访问入口|反向动态代理(二) Spring Boot 与Minio整合实现文件上传与下载(三) CentOS7的journa…...

Xmake v2.8.2 发布,官方包仓库数量突破 1k

Xmake 是一个基于 Lua 的轻量级跨平台构建工具。 它非常的轻量&#xff0c;没有任何依赖&#xff0c;因为它内置了 Lua 运行时。 它使用 xmake.lua 维护项目构建&#xff0c;相比 makefile/CMakeLists.txt&#xff0c;配置语法更加简洁直观&#xff0c;对新手非常友好&#x…...

加油站抽烟烟火智能识别算法

加油站抽烟烟火智能识别系统通过yoloopencv网络模型图像识别分析技术&#xff0c;加油站抽烟烟火智能识别算法识别出抽烟和燃放烟火的情况&#xff0c;并发出预警信号以提醒相关人员&#xff0c;减少火灾风险。OpenCV基于C实现&#xff0c;同时提供python, Ruby, Matlab等语言的…...

web前端开发中的响应式布局设计是什么意思?

响应式布局是指网页设计和开发中的一种技术方法&#xff0c;旨在使网页能够在不同大小的屏幕和设备上都能良好地显示和交互。这种方法使得网页可以自动适应不同的屏幕尺寸&#xff0c;包括桌面电脑、平板电脑和手机等。 在Web前端开发中&#xff0c;响应式布局通常使用CSS&…...

【LeetCode-面试经典150题-day14】

目录 19.删除链表的倒数第N个结点 82.删除排序链表中的重复元素Ⅱ 61. 旋转链表 86.分隔链表 146.LRU缓存 19.删除链表的倒数第N个结点 题意&#xff1a; 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 【输入样例】head [1,2,3,4,5…...

【算法系列总结之分组循环篇】

【算法系列总结之分组循环篇】 分组循环1446.连续字符1869.哪种连续子字符串更长1957.删除字符使字符串变好2038.如果相邻两个颜色均相同则删除当前颜色1759.统计同质子字符串的数目2110.股票平滑下跌阶段的数目1578.使绳子变成彩色的最短时间1839.所有元音按顺序排布的最长子字…...

汽车摩托车零部件出口管理ERP解决方案

近年来&#xff0c;随着全球经济的发展&#xff0c;人们对交通工具的需求增加&#xff0c;国内汽车、摩托车市场的不断扩大&#xff0c;以及国内制造技术的不断提高&#xff0c;中国汽车、摩托车零部件出口业务迎来了广阔的发展前景&#xff0c;带动了汽车配件和摩托车配件市场…...

NPM 管理组织包

目录 1、关于组织范围和包 1.1 管理无作用域的包 2、使用组织设置配置npm客户端 2.1 配置您的npm客户端以使用您组织的范围 为所有新包设置组织范围 为单个包设置组织范围 2.2 将默认包可见性更改为public 将单个包的包可见性设置为public 将所有包的包可见性设置为pu…...

蓝桥杯上岸每日N题 (修剪灌木)

大家好 我是寸铁 希望这篇题解对你有用&#xff0c;麻烦动动手指点个赞或关注&#xff0c;感谢您的关注 不清楚蓝桥杯考什么的点点下方&#x1f447; 考点秘籍 想背纯享模版的伙伴们点点下方&#x1f447; 蓝桥杯省一你一定不能错过的模板大全(第一期) 蓝桥杯省一你一定不…...

docker harbor私有库

目录 一.Harbor介绍 二.Harbor的特性 三.Harbor的构成 四.Harbor构建Docker私有仓库 4.2在Server主机上部署Harbor服务&#xff08;192.168.158.25&#xff09; 4.2.1 这时候这边就可以去查看192.168.158.25网页 4.3此时可真机访问serverIP 4.4通过127.0.0.1来登陆和推送镜…...

strcmp 的使用和模拟

目录 函数介绍&#xff1a; 头文件&#xff1a; 语法&#xff1a; 代码演示&#xff1a; 函数模拟&#xff1a; 函数介绍&#xff1a; strcmp是比较大小的函数。从字符串开始进行比较&#xff0c;如果两个相同位置的字符相同&#xff0c;那么继续往下进行比较&#xff0c;…...

军用加固计算机

军用加固计算机是为满足军事应用需求而设计的一种高性能、高安全性的计算机。与普通计算机相比&#xff0c;它具有以下特点&#xff1a; 加固材料&#xff1a;军用加固计算机通常采用钢板、铝合金等材料进行加固&#xff0c;能够承受较大的冲击和振动&#xff0c;保证在恶劣环境…...

block层:5. 请求分配

请求相关 源码基于5.10 1. 分配请求 static struct request *__blk_mq_alloc_request(struct blk_mq_alloc_data *data) {// 请求队列struct request_queue *q data->q;// 电梯struct elevator_queue *e q->elevator;u64 alloc_time_ns 0;unsigned int tag;// 判断…...