【排序算法】六大比较类排序算法——插入排序、选择排序、冒泡排序、希尔排序、快速排序、归并排序【详解】

文章目录

  • 六大比较类排序算法(插入排序、选择排序、冒泡排序、希尔排序、快速排序、归并排序)
  • 前言
  • 1. 插入排序
    • 算法描述
    • 代码示例
    • 算法分析
  • 2. 选择排序
    • 算法描述
    • 优化
    • 代码示例
    • 算法分析
  • 3. 冒泡排序
    • 算法描述
    • 代码示例
    • 算法分析
    • 与插入排序对比
  • 4. 希尔排序
    • 算法描述
    • 代码示例
    • 算法分析
  • 5. 快速排序
    • 5.1 挖坑法
      • 算法描述
      • 单轮操作
      • 代码示例
      • 算法分析
      • 优化(三数取中,小区间优化)
    • 5.2 左右指针法
      • 算法描述
      • 代码示例
    • 5.3 前后指针法
      • 算法描述
      • 代码示例
    • 非递归写法
  • 6. 归并排序
    • 算法描述
    • 代码示例
  • 总结对比

六大比较类排序算法(插入排序、选择排序、冒泡排序、希尔排序、快速排序、归并排序)

前言

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减地排列起来地操作。

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

内部排序:数据元素全部放在内存中的排序。(我们下面讲的排序都是属于内部排序)

外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1. 插入排序

  • 算法描述

直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。

插入排序的原理很好理解,打过扑克牌的人应该都能懂这种思想。

当我们在插入第 i (i>=1) 个元素时,前面的 array[0], array[1],…, array[i-1] 已经排好序(初始时,已排序部分只包含第一个元素,未排序部分包含剩余元素),此时用 array[i] 的排序码依次与它前面的 array[i-1], array[i-2],… 的排序码顺序进行比较,找到插入位置时就就将 array[i] 插入。原来位置上的元素顺序后移一位。

请添加图片描述

下面是动图演示:

请添加图片描述

  • 代码示例

// 插入排序
void InsertSort(int* a, int n) {  // n表示数组的大小for (int i = 0; i < n - 1; i++) {int end = i;  int tmp = a[end + 1];  // 待排序元素while (end >= 0) {if (a[end] > tmp) {  // 依次向前比较a[end + 1] = a[end];  // 向后移移位--end;}else {  break;}}a[end + 1] = tmp;  // 插入操作}
}
  • 算法分析

  1. 元素集合越接近有序,直接插入排序算法的时间复杂度效率越高

  2. 时间复杂度 O ( N 2 ) O(N^2) O(N2)

    最坏情况下为 O ( N 2 ) O(N^2) O(N2),此时待排序列为逆序,或者说接近逆序
    最好情况下为 O ( N ) O(N) O(N),此时待排序列为升序,或者说接近升序。

  3. 空间复杂度 O ( 1 ) O(1) O(1)

  4. 稳定性:稳定

2. 选择排序

  • 算法描述

选择排序的基本思想是每次从待排序列中选出一个最小值(或最大值),然后放在序列的起始(或末尾)位置,直到全部待排数据排完即可。

下面是动图演示:

请添加图片描述

  • 优化

实际上,我们可以一趟选出两个值,一个最大值一个最小值,然后将其放在序列开头和末尾,这样可以使选择排序的效率快一倍。

  • 代码示例

void SelectSort(int* a, int n) {int begin = 0, end = n - 1;  // 保存开头和末尾的下标while (begin < end) {int mini = begin, maxi = begin;  // 记录区间内最小值和最大值的下标for (int i = begin; i <= end; ++i) {  // 该循环用于找到区间内的最大和最小值if (a[i] < a[mini]) {mini = i; }if (a[i] > a[maxi]) {maxi = i;}}// Swap函数在外部需要自己写一下Swap(&a[begin], &a[mini]);  // 把最小的放在开头Swap(&a[end], &a[maxi]);  // 把最大的放在末尾++begin;--end;}
}

这就完了吗?当然没有,还有一点小瑕疵,我们来看这两行:

Swap(&a[begin], &a[mini]);  // 把最小的放在开头
Swap(&a[end], &a[maxi]);  // 把最大的放在末尾

这里万一我最大的数就是开头的数呢?就有可能在第一次交换的时候和最小值交换走,那么下一行交换的时候 a[maxi] 就不是最大值了,所以在第一次交换的时候我们需要判断一下 beginmaxi 的位置是否重叠。完整代码如下:

void SelectSort(int* a, int n) {int begin = 0, end = n - 1;while (begin < end) {int mini = begin, maxi = begin;for (int i = begin; i <= end; ++i) {if (a[i] < a[mini]) {mini = i;}if (a[i] > a[maxi]) {maxi = i;}}Swap(&a[begin], &a[mini]);  // 把最小的放在开头// 如果 begin 和 maxi 的位置重叠,maxi 的位置要修正一下if (begin == maxi) {maxi = mini;  // 也就是说值换了那么对应的下标也得改}Swap(&a[end], &a[maxi]);  // 把最大的放在末尾++begin;--end;}
}
  • 算法分析

时间复杂度:最坏情况: O ( N 2 ) O(N^2) O(N2)
      最好情况: O ( N 2 ) O(N^2) O(N2)
空间复杂度: O ( 1 ) O(1) O(1)

稳定性:不稳定

3. 冒泡排序

  • 算法描述

冒泡排序通过比较相邻的元素并交换它们的位置来实现排序。即从列表的第一个元素开始,比较相邻的两个元素。如果前一个元素比后一个元素大,则交换它们的位置。对列表中的每一对相邻元素重复上述步骤,直到列表的末尾。这样,最大的元素会被 “冒泡” 到列表的最后。忽略已经排序好的最后一个元素,重复上述步骤,直到整个列表排序完成。

下面是动图演示:

请添加图片描述

  • 代码示例

void BubbleSort(int* a, int n) {for (int i = 0; i < n - 1; i++) {int flag = 0;for (int j = 1; j < n - i; j++) {if (a[j - 1] > a[j]) {Swap(&a[j - 1], &a[j]);  // 前面比后面大就交换flag = 1;}}// 没有发生交换说明已经有序,不需要再比较了if (flag == 0) {break;}}
}
  • 算法分析

时间复杂度:最坏情况: O ( N 2 ) O(N^2) O(N2)
      最好情况: O ( N ) O(N) O(N)(通过设置一个变量 flag 来实现)
空间复杂度: O ( 1 ) O(1) O(1)

稳定性:稳定

  • 与插入排序对比

当一个数组很接近有序的时候,比如 [1, 2, 3, 5, 4, 6] ,这个时候采用插入排序循环的执行次数只需要 N N N 次, 而冒泡排序需要 ( N − 1 ) + ( N − 2 ) (N-1)+(N-2) (N1)+(N2) 次。可见,对于接近有序的数组,插入排序比冒泡排序的局部适应性更强

4. 希尔排序

  • 算法描述

希尔排序又称缩小增量法,它的基本思想是:将待排序的元素分为多个子序列,然后对每个子序列进行插入排序。这些子序列是原始序列中相隔一定增量的元素组成的。然后逐渐减小增量,重复这个过程,最终将增量减小到 1,完成最后一轮的插入排序,此时序列已经基本有序,只需进行少量的比较和交换操作,大大提高了排序效率。

请添加图片描述

也就是说,我们需要指定一个 gap(间隔),从第一个数开始每各一个这个 gap 的距离直到最后的这些数为一组,在同一个组内的数它们进行插入排序的操作。然后进行了这一轮 “预排序” 之后再逐渐缩小这个 gap,直到 gap 为 1,最后这个数组就有序了。而我们会发现:

  • gap 越大,大的数可以越快地到后面,小的数可以越快地到前面。但是这样预排序完数组也越不接近有序。
  • gap 越小,排完之后地数组越接近有序。
  • 特别地,当 gap 为 1 的时候,这就变成了一个普通的插入排序。

而刚刚我们也提到过说插入排序对接近有序的数组它的效率就越高,那么这个希尔排序就可以看作是插入排序的升级版,它通过设置一个 gap 先预排序,让数组接近有序,然后再插入排序,这样就能提高效率。

还有一个问题:这个 gap 到底该怎么设计呢?通常我们可以让初始的 gap 设置为数组长度的一半这样会更好,然后每次预排序完之后让 gap 整除 2(或者 3 也不是不行),但最后都要保证 gap 能到 1,因为这样最后排完的数组才能够是有序的。

下面是它的动图演示:

请添加图片描述

  • 代码示例

void ShellSort(int* a, int n) {int gap = n;  // 一开始设置成n,这样一进循环就是长度的一半了while (gap > 1) {gap /= 2;        // 可以保证 gap 最后一次为 1// gap /= 3 + 1;  // gap 整除 3 不一定能到 1,所以要加 1// 把间隔为 gap 的同时排for (int i = 0; i < n - gap; ++i) {int end = i;int tmp = a[end + gap];while (end >= 0) {if (a[end] > tmp) {a[end + gap] = a[end];end -= gap;}else {break;}}a[end + gap] = tmp;}}
}
  • 算法分析

  • 希尔排序实际上是直接插入排序的优化:
  1. 先进行预排序,让数组接近有序
  2. 直接插入插入排序
  • gap > 1​ 时,都是预排序,让数组接近有序。

    gap == 1​ 时,就是直接插入排序。

  • 时间复杂度:

第一层的 while 循环,时间复杂度: O ( l o g 2 N ) O(log_2N) O(log2N) 或者 O ( l o g 3 N ) O(log_3N) O(log3N)

第二层的 for 循环,当 gap​ 很大时,下面的预排序时间复杂度: O ( N ) O(N) O(N),当 gap​ 很小时,数组已经接近有序了,这时候差不多也是 O ( n ) O(n) O(n)

综合计算,有数据得出平均的时间复杂度为 O ( N 1.3 ) O(N^{1.3}) O(N1.3)

  • 空间复杂度: O ( 1 ) O(1) O(1)
  • 稳定性:不稳定

5. 快速排序

快速排序的基本思想是:选择一个基准元素,将数组划分为两个子数组,比基准元素小的元素放在左边,比基准元素大的元素放在右边,然后分别对左右子数组进行排序,最终实现整个数组的有序排列。

而实现上面所说的操作,我们可以有以下几种方法:

5.1 挖坑法

  • 算法描述

  1. 我们把数组的最左边的数看作为一个 “坑位”,把这个值保存在一个变量 key 中。

  2. 定义两个变量 leftright 起始位置分别是数组的开头和结尾,这个时候最左边的 left 就是 “坑位”。让 right 先往左走,每次走一个单位去寻找比 key 更小的单位,找到了就停下。

  3. 然后把这个值赋给 “坑位” 上,也就是 left 对应的位置,此时 right 变成新的 “坑位”。

  4. 这时再让 left 往右走去寻找比 key 更大的值,找到了就停下。

  5. 然后把这个值赋给 “坑位”,也就是 right 所对应的位置,此时 left 形成新的 “坑位”。

  6. 再让 right 往左走…,就这样循环往复直到 leftright 相遇,此时相遇点一定是 “坑位”,最后把 key 的值赋给坑位上就算完成了第一趟排序。

注意:如果将最左边的数看作是 “坑位” 的话,那么需要让 right 先走;如果最右边的数是 “坑位” 的话,那么需要最左边的数先走。

下面是动图演示:

请添加图片描述

  • 单轮操作

void PartQuickSort(int* a, int n) {int begin = 0, end = n - 1;int pivot = begin;  // 最左边的作为坑位int key = a[begin];  // 将坑位上的值保存在 key 中while (begin < end) {// 右边找小,放到左边while (begin < end && a[end] >= key) {--end;}// 小的放到左边的坑,自己形成了新的坑位a[pivot] = a[end];pivot = end;// 左边找大,放到右边while (begin < end && a[begin] <= key) {++begin;}// 大的放到右边的坑,自己形成了新的坑位a[pivot] = a[begin];pivot = begin;}pivot = begin;a[pivot] = key;
}

在完成第一轮操作之后,我们会发现 key 的左边全是比 key 小的值,而 key 的右边全是比 key 大的值。而我们要让整个数组有序,我们希望 key 的左右两部分都是有序的。

这就可以用到分治的思想——去递归地调用自身,让 key 的左右两个子区间进行同样的操作直到区间只有一个数或者没有了为止。这样一来 key 左右两个区间就是有序的,那么整个数组就有序了。

  • 代码示例

void QuickSort(int* a, int left, int right) {  // 由于之后需要递归调用子区间,所以要改一下形参if (left >= right) {return;}  // 递归结束条件int begin = left, end = right;int pivot = begin;int key = a[begin];while (begin < end) {// 右边找小,放到左边while (begin < end && a[end] >= key) {  // 注意这里左边的条件是为了防止两变量错过--end;}// 小的放到左边的坑,自己形成了新的坑位a[pivot] = a[end];pivot = end;// 左边找大,放到右边while (begin < end && a[begin] <= key) {++begin;}// 大的放到右边的坑,自己形成了新的坑位a[pivot] = a[begin];pivot = begin;}pivot = begin;a[pivot] = key;// [left, pivot - 1] pivot [pivot + 1, right]QuickSort(a, left, pivot - 1);  // 递归左边QuickSort(a, pivot + 1, right);  // 递归右边
}
  • 算法分析

  • 时间复杂度

快速排序的时间复杂度为 O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N),其中 N N N 为待排序数组的长度。最坏情况下,快速排序的时间复杂度为 O ( N 2 ) O(N^2) O(N2),但这种情况出现的概率很小,可以通过一些优化措施来避免。

  • 空间复杂度

快速排序的空间复杂度取决于递归栈的深度,在最坏情况下,递归栈的深度为 O ( N ) O(N) O(N),因此快速排序的空间复杂度为 O ( N ) O(N) O(N)。但是,在一些实现中,可以使用非递归的方式来实现快速排序,从而避免递归栈带来的空间开销。

请添加图片描述

  • 稳定性

快速排序是一种不稳定的排序算法。因为在排序过程中,可能会交换相同元素的位置,从而导致相同元素的相对顺序被改变。例如,对于数组 [3, 2, 2, 1],如果选择第一个元素 3 作为基准元素,那么经过第一次划分后,数组变成了 [1, 2, 2, 3],其中两个 2 的相对顺序被改变了,因此是不稳定的。

  • 优化(三数取中,小区间优化)

上面说到快速排序在最坏的情况下时间复杂度会下降到 O ( N 2 ) O(N^2) O(N2),那是什么时候是最坏的呢?

结论:快速排序在**有序(不论顺序还是逆序)**的情况下最坏,时间复杂度为 O ( N 2 ) O(N^2) O(N2)

因为在有序的情况下,这个所谓的 “坑位” 并不会移动至靠近中间的位置,也就是说这样的话每次做完单趟排序的时候也只会让一个数变得有序。如下图所示:

请添加图片描述

为了解决这个问题,提出了一个解决方法——三数取中

即比较左中右三个数的大小,让值最中间的数作为坑,这样的话就避免了坑在最左边或者最右边的极端情况。

int GetMidIndex(int* a, int left, int right) {int mid = (left + right) >> 1;  // 这里是右移运算符,也相当于整除2if (a[left] < a[mid]) {if (a[mid] < a[right]) {return mid;}else if (a[left] > a[right]) {return left;}else {return right;}}else {if (a[mid] > a[right]) {return mid;}else if (a[left] < a[right]) {return left;}else {return right;}}
}void QuickSort(int* a, int left, int right) {if (left >= right) {return;}int index = GetMidIndex(a, left, right);Swap(&a[left], &a[index]);// ...下同
}

还有一种优化——小区间优化

就是说当递归的时候这个区间已经很小了,这个时候再去递归调用的效率就可能比不上其他的排序方法了。这里以 10 10 10 为例,当区间小于 10 10 10 的时候,我们就采用直接插入排序,如果区间大于 10 10 10 我们就正常递归即可。

void QuickSort(int* a, int left, int right) {// ...上同// [left, pivot - 1] pivot [pivot + 1, right]// QuickSort(a, left, pivot - 1);// QuickSort(a, pivot + 1, right);if (pivot - 1 - left > 10) {QuickSort(a, left, pivot - 1);}else {InsertSort(a + left, pivot - 1 - left + 1);}if (pivot - 1 - left > 10) {QuickSort(a, pivot + 1, right);}else {InsertSort(a + pivot + 1, right - (pivot + 1) + 1);}
}

5.2 左右指针法

  • 算法描述

同样我们可以定义数组最左边的值为 key,再定义两个变量 leftright 起始位置分别是数组的开头和结尾,右边的 right 先走去找比 key 更小的值,找到了就停下。然后左边的 left 去找比 key 更大的值,找到了然后停下。

这时交换 leftright 对应的值。

然后 right 再走…,直到 leftright 相遇,此时将相遇的位置对应的值与 key 位置的值交换。这样一来,key 左边的值也都是比 key 小的,右边的是比 key 大的。、

注意:同样的,如果定义对左边的数为 key,那么右边的 right 先走,反之 left 先走。

下面是动图演示:

请添加图片描述

  • 代码示例

void QuickSort2(int* a, int left, int right) {if (left >= right) {return;}int index = GetMidIndex(a, left, right);Swap(&a[left], &a[index]);int begin = left, end = right;int keyi = begin;while (begin < end) {// 找小while (begin < end && a[end] >= a[keyi]) {--end;}// 找大while (begin < end && a[begin] <= a[keyi]) {++begin;}Swap(&a[begin], &a[end]);}Swap(&a[begin], &a[keyi]);QuickSort2(a, begin, keyi - 1);QuickSort2(a, keyi + 1, end);
}

这个方法和挖坑法的区别就在于第一趟排序排完之后左右序列的顺序不同

时间复杂度: O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N)

5.3 前后指针法

  • 算法描述

  1. 还是选出一个最左边或最右边的数作为 key
  2. 定义两个变量 prevcur。起始时, prev 指向开头,cur 指向 prev+1,也就是开头的后一位。
  3. cur 指向的内容小于 key,则 prev 向后移动一位,然后交换 prevcur 所指向的内容,然后 cur++;若 cur 所指向的内容大于 key,则 cur 直接 ++。如此进行下去,直到 cur 到达 end 位置。此时 key++prev 所指向的内容交换即可。

这样也能使得 key 左序列的值小于 key,右边序列的值大于 key。之后再递归调用即可。

下面时动图演示:

请添加图片描述

  • 代码示例

void QuickSort3(int* a, int left, int right) {if (left >= right) {return;}int index = GetMidIndex(a, left, right);Swap(&a[left], &a[index]);int keyi = left;int prev = left, cur = left + 1;while (cur <= right) {if (a[cur] < a[keyi] && ++prev != cur){Swap(&a[prev], &a[cur]);}++cur;}Swap(&a[prev], &a[keyi]);QuickSort3(a, left, prev - 1);QuickSort3(a, prev + 1, right);
}

时间复杂度: O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N)

非递归写法

  • 递归的缺陷
  1. 效率低(这一点其实已经不是主要的因素了)。
  2. 极端情况下会导致栈溢出

针对第二点,举个例子:

// 用递归实现 1+2+3+...+n
int sum(int n){return n == 1 ? 1 : sum(n - 1) + n;
}

比如你让 n = 10000,这个时候递归的深度过深,很可能就会出现栈溢出的情况:

请添加图片描述

当你 n = 10000 时,这个时候递归就要建立 10000 层栈帧,这个时候栈的空间就不够了。

  • 非递归写法

思路 1:直接改循环(简单)

思路 2:借助数据结构模拟递归过程(较复杂)

这里以挖坑法为例,我们利用前面挖坑法所写的单趟排序的操作加以修改获取到坑位的数据。接着利用这个函数得到的坑位来划分区间进行非递归的实现。

int PartQuickSort(int* a, int left, int right) {int begin = left, end = right;int pivot = begin;int key = a[begin];while (begin < end) {// 右边找小,放到左边while (begin < end && a[end] >= key) {  // 注意这里左边的条件是为了防止两变量错过--end;}// 小的放到左边的坑,自己形成了新的坑位a[pivot] = a[end];pivot = end;// 左边找大,放到右边while (begin < end && a[begin] <= key) {++begin;}// 大的放到右边的坑,自己形成了新的坑位a[pivot] = a[begin];pivot = begin;}pivot = begin;a[pivot] = key;return pivot;
}// 非递归写法
void QuickSort(int* a, int n) {ST st;StackInit(&st);StackPush(&st, n - 1);StackPush(&st, 0);while (!StackEmpty(&st)) {int left = StackTop(&st);StackPop(&st);int right = StackTop(&st);StackPop(&st);int keyIndex = PartQuickSort(a, left, right);// [left, ketIndex - 1] keyIndex [keyIndex + 1, right];if (keyIndex + 1 < right) {StackPush(&st, right);StackPush(&st, keyIndex + 1);}if (left < keyIndex - 1) {StackPush(&st, keyIndex - 1);StackPush(&st, left);}}StackDestroy(&st);
}

6. 归并排序

  • 算法描述

归并排序的核心思想是将一个大问题分解成若干个小问题,分别解决这些小问题,然后将结果合并起来,最终得到整个问题的解。具体到排序问题,归并排序的步骤如下:

  1. 分解:将待排序的数组分成两个子数组,每个子数组包含大约一半的元素。
  2. 解决:递归地对每个子数组进行排序。
  3. 合并:将两个已排序的子数组合并成一个有序的数组。

通过不断地分解和合并,最终整个数组将被排序。

请添加图片描述

这样看可能不直观,下面是动图演示:

请添加图片描述

  • 代码示例

注意这里需要用到两个函数,因为要用到开辟新的内存,所以不能用在递归中,需要单独提出来。

void _MergeSort(int* a, int left, int right, int* tmp) {  // 此函数用来递归if (left >= right) {return;}int mid = (left + right) >> 1;_MergeSort(a, left, mid, tmp);_MergeSort(a, mid + 1, right, tmp);int begin1 = left, end1 = mid;int begin2 = mid + 1, end2 = right;int index = left;while (begin1 <= end1 && begin2 <= end2) {if (a[begin1] < a[begin2]) {tmp[index++] = a[begin1++];}else {tmp[index++] = a[begin2++];}}// 当区间没走完,就把剩下的拷贝进数组while (begin1 <= end1) {tmp[index++] = a[begin1++];}while (begin2 <= end2) {tmp[index++] = a[begin2++];}for (int i = left; i <= right; i++) {a[i] = tmp[i];}
}void MergeSort(int* a, int n) {int* tmp = (int*)malloc(sizeof(int) * n);_MergeSort(a, 0, n - 1, tmp);free(tmp);
}

时间复杂度: O ( N l o g 2 N ) O(Nlog_2N) O(Nlog2N)

稳定性:稳定

总结对比

排序方法平均情况最好情况最坏情况辅助空间稳定性
插入排序 O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)稳定
选择排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)不稳定
冒泡排序 O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)稳定
希尔排序 O ( n log ⁡ n ) O(n\operatorname{log}n) O(nlogn) ~ O ( n 2 ) O(n^2) O(n2) O ( n 1.3 ) O(n^{1.3}) O(n1.3) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)不稳定
快速排序 O ( n log ⁡ n ) O(n\operatorname{log}n) O(nlogn) O ( n log ⁡ n ) O(n\operatorname{log}n) O(nlogn) O ( n 2 ) O(n^2) O(n2) O ( log ⁡ n ) O(\operatorname{log}n) O(logn) ~ O ( n ) O(n) O(n)不稳定
归并排序 O ( n log ⁡ n ) O(n\operatorname{log}n) O(nlogn) O ( n log ⁡ n ) O(n\operatorname{log}n) O(nlogn) O ( n log ⁡ n ) O(n\operatorname{log}n) O(nlogn) O ( n ) O(n) O(n)稳定

注:快速排序加了三数取中之后基本不会出现最坏的情况。

有任何问题,还请大佬们指出!

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

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

相关文章

纠错检索增广生成论文

一、摘要 动机&#xff1a;RAG严重依赖于检索文档的相关性&#xff0c;如果检索出错&#xff0c;那么LLM的输出结果也会出现问题 解决方案&#xff1a;提出纠正性检索增强生成&#xff08;CRAG&#xff09;即设计一个轻量级的检索评估器&#xff0c;用来评估针对某个查询检索…

Java NIO与传统IO性能对比分析

Java NIO与传统IO性能对比分析 在Java中&#xff0c;I/O&#xff08;输入输出&#xff09;操作是开发中最常见的任务之一。传统的I/O方式基于阻塞模型&#xff0c;而Java NIO&#xff08;New I/O&#xff09;引入了非阻塞和基于通道&#xff08;Channel&#xff09;和缓冲区&a…

easelog(1)基础C++日志功能实现

EaseLog(1)基础C日志功能实现 Author: Once Day Date: 2025年2月22日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 注&#xff1a;本简易日志组件代码实现参考了Google …

Vue面试2

1.跨域问题以及如何解决跨域 跨域问题&#xff08;Cross-Origin Resource Sharing, CORS&#xff09;是指在浏览器中&#xff0c;当一个资源试图从一个不同的源请求另一个资源时所遇到的限制。这种限制是浏览器为了保护用户安全而实施的一种同源策略&#xff08;Same-origin p…

MongoDB应用设计调优

应用范式设计 什么是范式 数据库范式概念是数据库技术的基本理论&#xff0c;几乎是伴随着数据库软件产品的推出而产生的。在传统关系型数据库领域&#xff0c;应用开发中遵循范式是最基本的要求。但随着互联网行业的发展&#xff0c;NoSQL开始变得非常流行&#xff0c;在许多…

Mac安装配置Tomcat 8

一、官网下载 Index of /disthttps://archive.apache.org/dist/ 1、进入界面如下&#xff1a; 2、我们找到Tomcat文件夹并进入 3、找到Tomcat 8并打开 4、找到对应的版本打开 5、打开bin 6、找到“apache-tomcat-8.5.99.tar.gz”并下载 二、配置Tomcat 1、解压已经下载好的…

【论文精读】VLM-AD:通过视觉-语言模型监督实现端到端自动驾驶

论文地址&#xff1a; VLM-AD: End-to-End Autonomous Driving through Vision-Language Model Supervision 摘要 人类驾驶员依赖常识推理来应对复杂多变的真实世界驾驶场景。现有的端到端&#xff08;E2E&#xff09;自动驾驶&#xff08;AD&#xff09;模型通常被优化以模仿…

百度搜索,能否将DeepSeek变成“内功”?

最近&#xff0c;所有的云平台和主流APP都在努力接入DeepSeek。其中&#xff0c;搜索类APP与搜索引擎更是“战况激烈”。那么问题来了&#xff0c;接入DeepSeek已经变成了标准配置&#xff0c;到底应该如何做出差异化&#xff1f;接入DeepSeek这件事能不能实现11大于2的效果&am…

Flask实现高效日志记录模块

目录 一. 简介&#xff1a; 1. 为什么需要请求日志 二. 日志模块组成 1. 对应日志表创建&#xff08;包含日志记录的关键字段&#xff09; 2. 编写日志记录静态方法 3. 在Flask中捕获请求日志 4. 捕获异常并记录错误日志 5. 编写日志接口数据展示 6. 写入数据展…

25轻化工程研究生复试面试问题汇总 轻化工程专业知识问题很全! 轻化工程复试全流程攻略 轻化工程考研复试真题汇总

轻化工程复试心里没谱&#xff1f;学姐带你玩转面试准备&#xff01; 是不是总觉得老师会问些刁钻问题&#xff1f;别焦虑&#xff01;其实轻化工程复试套路就那些&#xff0c;看完这篇攻略直接掌握复试通关密码&#xff01;文中有重点面试题可直接背~ 目录 一、这些行为赶紧避…

查看已经安装的Python库,高效管理自己的Python开发环境

在日常的Python开发中&#xff0c;掌握如何管理和查看已经安装的库是非常重要的。这不仅能帮助你了解当前项目的依赖关系&#xff0c;还能避免出现版本冲突等问题。在这篇文章中&#xff0c;我们将详细介绍查看已安装Python库的方法&#xff0c;并提供一些实用的工具和技巧。 …

Selenium实战案例1:论文pdf自动下载

在上一篇文章中&#xff0c;我们介绍了Selenium的基础用法和一些常见技巧。今天&#xff0c;我们将通过中国科学&#xff1a;信息科学网站内当前目录论文下载这一实战案例来进一步展示Selenium的web自动化流程。 目录 中国科学&#xff1a;信息科学当期目录论文下载 1.网页内…

Visual Studio Code 2025 安装与高效配置教程

一、软件简介与下载 1. Visual Studio Code 是什么&#xff1f; Visual Studio Code&#xff08;简称VS Code&#xff09;是微软推出的免费开源代码编辑器&#xff0c;支持 智能代码补全、Git集成、插件扩展 等功能&#xff0c;适用于前端开发、Python、Java等多种编程场景。…

工业路由器和工业交换机,打造高效稳定的工业网络?

工业路由器和工业交换机各有千秋&#xff0c;但如何将它们完美结合&#xff0c;构建稳定高效的工业网络&#xff1f;答案就在这里&#xff01; 工业物联网&#xff08;IIoT&#xff09;是高效、稳定的工业网络成为智慧工厂、工业自动化和远程监控等场景的基础支撑。工业路由器…

DeepSeek 助力 Vue 开发:打造丝滑的二维码生成(QR Code)

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 Deep…

TSMaster【第七篇:千机百变——面板设计艺术】

武侠场景导入&#xff1a;唐门暗器阁的启示 江湖传言&#xff0c;唐门暗器之所以独步天下&#xff0c;全凭其「千机匣」中七十二种机关变化。TSMaster面板设计恰似打造暗器机关——控件如同飞镖、机簧、毒针&#xff0c;组合方式不同则威力迥异。昔日某新势力车型因仪表盘刷新…

提升 AI 服务的稳定性:Higress AI 网关的降级功能介绍

在使用 LLM 服务时&#xff0c;服务的稳定性和可用性至关重要。然而&#xff0c;由于网络问题、服务器故障或其他不可控因素&#xff0c;LLM 服务可能会暂时不可用。为了保障用户体验和业务连续性&#xff0c;Higress AI 网关提供了强大的模型降级和令牌降级功能。本文将介绍这…

提升C++项目编译速度

目录 一、问题背景 二、代码规范方面的解决方案 2.1 拆分头文件 2.2 拆分巨型类 2.3 使用前置声明 2.4 避免在头文件中包含实现 2.5 避免头文件重复包含 2.6 将常用且变动较少的独立到一个文件 三、代码业务重构方面经验 3.1 使用PIMPL&#xff08;Pointer to Imple…

【学术投稿-第四届材料工程与应用力学国际学术会议(ICMEAAE 2025】材料工程与应用力学的探讨

重要信息 官网&#xff1a;www.icmeaae.com 时间&#xff1a;2025年3月7-9日 地点&#xff1a;中国西安 简介 第四届材料工程与应用力学&#xff08;ICMEAAE 2025&#xff09;将于2025年3月7日至9日在中国西安召开。本次会议将重点讨论材料科学、应用力学等领域的最新研究进…

抓包工具(三)Wireshark代理抓包Java程序的HTTPS请求

目录 一、需求背景二、操作步骤2.1 jSSLKeyLog 工具下载2.2 jSSLKeyLog工具使用2.3 将sslkeylog导入Wireshark2.4 测试Demo2.5 测试结果1&#xff09;使用工具解密HTTPS前&#xff1a;2&#xff09;实用工具解密HTTPS后&#xff1a; 三、补充&#xff1a;如果出现未解密成功的情…