0. 前言

大家好,我是多选参数的程序锅,一个正在捣鼓操作系统、学数据结构和算法以及 Java 的失业人员。数据结构和算法我已经学了有一段日子了,最近也开始在刷 LeetCode 上面的题目了,但是自己感觉在算法上还是 0 ,还得猛补啊。

今天这篇基于之前的 8 大排序算法基础之上,增加一个堆排序,也就是这么 9 个排序算法:冒泡排序、插入排序、选择排序、归并排序、快速排序、堆排序、桶排序、计数排序、基数排序。它们对应的时间复杂度如下所示:

排序算法时间复杂度是否基于比较
冒泡、插入、选择O(n^2)
快排、归并、堆排序O(nlogn)
桶、计数、基数O(n)×

整篇文章的主要知识提纲如图所示,本篇相关的代码都可以从 https://github.com/DawnGuoDev/algorithm 获取,另外,该仓库除了包含了基础的数据结构和算法实现之外,还会有数据结构和算法的笔记、LeetCode 刷题记录(多种解法、Java 实现) 、一些优质书籍整理。

本文的图很多都是从极客时间王争老师专栏那边拷贝过来或者截图过来的,少部分图是自己重新画的。为什么不全都换成自己画的图?主要是我比较懒,我觉得图能将自己要阐述的点解释清楚,或者说和自己整理过后的文字结合的不错,我觉得这个图就没必要重新画了,人家的画已经很好看了,也很清晰了,你将它重新画,其实也是差不多,可能就是换个样式而已,核心的东西还是没有变。但是,在有些地方,我觉得别人的图跟我阐述的内容不符合,或者不能很好地阐述我想表达的东西,又或者这个地方需要一个图,那么我会画一个。

1. 排序算法分析

学习排序算法除了学习它的算法原理、代码实现之外,最重要的是学会如何评价、分析一个排序算法。分析一个排序算法通常从以下几点出发。

1.1. 执行效率

而对执行效率的分析,一般从这几个方面来衡量:

  • 最好情况、最坏情况、平均情况

    除了需要给出这三种情况下的时间复杂度还要给出对应的要排序的原始数据是怎么样的。

  • 时间复杂度的系数、常数、低阶

    大 O 时间复杂度反应的是算法时间随 n 的一个增长趋势,比如 O(n^2) 表示算法时间随 n 的增加,呈现的是平方的增长趋势。这种情况下往往会忽略掉系数、常数、低阶等。但是实际开发过程中,排序的数据往往是 10 个、100 个、1000 个这样规模很小的数据,所以在比较同阶复杂度的排序算法时,这些系数、常数、低阶不能省略。

  • 比较次数和交换(或移动)次数

    在基于比较的算法中,会涉及到元素比较和元素交换等操作。所以分析的时候,还需要对比较次数和交换次数进行分析。

1.2. 内存消耗

内存消耗其实就是空间复杂度。针对排序算法来说,如果该排序算法的空间复杂度为 O(1),那么这个排序算法又称为原地排序。

1.3. 稳定性

是什么

稳定性是指待排序的序列中存在值相等的元素。在排序之后,相等元素的前后顺序跟排序之前的是一样的。

为什么

我们将排序的原理和实现排序时用的大部分都是整数,但是实际开发过程中要排序的往往是一组对象,而我们只是按照对象中的某个 key 来进行排序。

比如一个对象有两个属性,下单时间和订单金额。在存入到数据库的时候,这些对象已经按照时间先后的顺序存入了。但是我们现在要以订单金额为主要 key,在订单金额相同的时候,以下单时间为 key。那么在采用稳定的算法之后,只需要按照订单金额进行一次排序即可。比如有这么三个数据,第一个数据是下单时间、第二数据是订单金额:(20200515、20)、(20200516、10)、(20200517、30)、(20200518、20)。在采用稳定的算法之后,排序的情况如下:(20200516、10)、(20200515、20)、(20200518、20)、(20200517、30)可以发现在订单金额相同的情况下是按订单时间进行排序的。

2. 经典的常用排序算法

2.1. 冒泡排序

冒泡排序就是依次对两个相邻的元素进行比较,然后在不满足大小条件的情况下进行元素交换。一趟冒泡排序下来至少会让一个元素排好序(元素排序好的区域相当于有序区,因此冒泡排序中相当于待排序数组分成了两个已排序区间和未排序区间)。因此为了将 n 个元素排好序,需要 n-1 趟冒泡排序(第 n 趟的时候就不需要)。

下面用冒泡排序对这么一组数据4、5、6、3、2、1,从小到大进行排序。第一次排序情况如下:

可以看出,经过一次冒泡操作之后,6 这个元素已经存储在正确的位置上了,要想完成有所有数据的排序,我们其实只需要 5 次这样的冒泡排序就行了。图中给出的是带第 6 次了的,但是第 6 次其实没必要。

2.1.1. 优化

使用冒泡排序的过程中,如果有一趟冒泡过程中元素之间没有发生交换,那么就说明已经排序好了,可以直接退出不再继续执行后续的冒泡操作了。

2.1.2. 实现

下面的冒泡排序实现是优化之后的:

  1. /**
  2. * 冒泡排序:
  3. * 以升序为例,就是比较相邻两个数,如果逆序就交换,类似于冒泡;
  4. * 一次冒泡确定一个数的位置,因为要确定 n-1 个数,因此需要 n-1
  5. * 次冒泡;
  6. * 冒泡排序时,其实相当于把整个待排序序列分为未排序区和已排序区
  7. */
  8. public void bubbleSort(int[] arr, int len) {
  9. // len-1 趟
  10. for (int j = 0; j < len-1; j++) {
  11. int sortedFlag = 0;
  12. // 一趟冒泡
  13. for (int i = 0; i < len-1-j; i++) {
  14. if (arr[i] > arr[i+1]) {
  15. int temp = arr[i];
  16. arr[i] = arr[i+1];
  17. arr[i+1] = temp;
  18. sortedFlag = 1;
  19. }
  20. }
  21. // 该趟排序中没有发生,表示已经有序
  22. if (0 == sortedFlag) {
  23. break;
  24. }
  25. }
  26. }

2.1.3. 算法分析

  • 冒泡排序是原地排序。因为冒泡过程中只涉及到相邻数据的交换,相当于只需要开辟一个内存空间用来完成相邻的数据交换即可。

  • 在元素大小相等的时候,不进行交换,那么冒泡排序就是稳定的排序算法。

  • 冒泡排序的时间复杂度。

    • 当元素已经是排序好了的,那么最好情况的时间复杂度是 O(n)。因为只需要跑一趟,然后发现已经排好序了,那么就可以退出了。

    • 当元素正好是倒序排列的,那么需要进行 n-1 趟排序,最坏情况复杂度为 O(n^2)。

    • 一般情况下,平均时间复杂度是 O(n^2)。使用有序度和逆序度的方法来求时间复杂度,冒泡排序过程中主要是两个操作:比较和交换。每交换一次,有序度就增加一,因此有序度增加的次数就是交换的次数。又因为有序度需要增加的次数等于逆序度,所以交换的次数其实就等于逆序度

      因此当要对包含 n 个数据的数组进行冒泡排序时。最坏情况下,有序度为 0 ,那么需要进行 n*(n-1)/2 次交换;最好情况下,不需要进行交换。我们取中间值 n*(n-1)/4,来表示初始有序度不是很高也不是很低的平均情况。由于平均情况下需要进行 n*(n-1)/4 次交换,比较操作肯定比交换操作要多。但是时间复杂度的上限是 O(n^2),所以平均情况下的时间复杂度就是 O(n^2)。

      这种方法虽然不严格,但是很实用。主要是因为概率的定量分析太复杂,不实用。(PS:我就喜欢这种的)

2.2. 插入排序

**插入排序中将数组中的元素分成两个区间:已排序区间和未排序区间(最开始的时候已排序区间的元素只有数组的第一个元素),插入排序就是将未排序区间的元素依次插入到已排序区间(需要保持已排序区间的有序)。最终整个数组都是已排序区间,即排序好了。**假设要对 n 个元素进行排序,那么未排序区间的元素个数为 n-1,因此需要 n-1 次插入。插入位置的查找可以从尾到头遍历已排序区间也可以从头到尾遍历已排序区间。

如图所示,假设要对 4、5、6、1、3、2进行排序。左侧橙红色表示的是已排序区间,右侧黄色的表示未排序区间。整个插入排序过程如下所示

2.2.1. 优化

  • 采用希尔排序的方式。

  • **使用哨兵机制。**比如要排序的数组是[2、1、3、4],为了使用哨兵机制,首先需要将数组的第 0 位空出来,然后数组元素全都往后移动一格,变成[0、2、1、3、4]。那么数组 0 的位置用来存放要插入的数据,这样一来,判断条件就少了一个,不用再判断 j >= 0 这个条件了,只需要使用 arr[j] > arr[0] 的条件就可以了。因为就算遍历到下标为 0 的位置,由于 0 处这个值跟要插入的值是一样的,所以会退出循环,不会出现越界的问题。

2.2.2. 实现

这边查找插入位置的方式采用从尾到头遍历已排序区间,也没有使用哨兵。

  1. /**
  2. * 插入排序:
  3. * 插入排序也相当于把待排序序列分成已排序区和未排序区;
  4. * 每趟排序都将从未排序区选择一个元素插入到已排序合适的位置;
  5. * 假设第一个元素属于已排序区,那么还需要插入 len-1 趟;
  6. */
  7. public void insertSort(int[] arr, int len) {
  8. // len-1 趟
  9. for (int i = 1; i < len; i++) {
  10. // 一趟排序
  11. int temp = arr[i];
  12. int j;
  13. for (j = i-1; j >= 0; j--) {
  14. if (arr[j] > temp) {
  15. arr[j+1] = arr[j];
  16. } else {
  17. break;
  18. }
  19. }
  20. arr[j+1] = temp;
  21. }
  22. }

2.2.3. 算法分析

  • 插入排序是原地算法。因为只需要开辟一个额外的存储空间来临时存储元素。

  • 当比较元素时发现元素相等,那么插入到相等元素的后面,此时就是稳定排序。也就是说只有当有序区间中的元素大于要插入的元素时才移到到后面的位置,不大于(小于等于)了的话直接插入。

  • 插入排序的时间复杂度。

    • 待排序的数据是有序的情况下,不需要搬移任何数据。那么采用从尾到头在已排序区间中查找插入位置的方式,最好时间复杂度是 O(n)。

    • 待排序的数据是倒序的情况,需要依次移动 1、2、3、...、n-1 个数据,因此最坏时间复杂度是 O(n^2)。

    • 平均时间复杂度是 O(n^2)。因此将一个数据插入到一个有序数组中的平均时间度是 O(n),那么需要插入 n-1 个数据,因此平均时间复杂度是 O(n^2)

      最好的情况是在这个数组中的末尾插入元素的话,不需要移动数组,时间复杂度是 O(1),假如在数组开头插入数据的话,那么所有的数据都需要依次往后移动一位,所以时间复杂度是 O(n)。往数组第 k 个位置插入的话,那么 k~n 这部分的元素都需要往后移动一位。因此此时插入的平均时间复杂度是 O(n)

2.2.4. VS 冒泡排序

冒泡排序和插入排序的时间复杂度都是 O(n^2),都是原地稳定排序。而且冒泡排序不管怎么优化,元素交换的次数是一个固定值,是原始数据的逆序度。插入排序是同样的,不管怎么优化,元素移动的次数也等于原始数据的逆序度。但是,从代码的实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 3 个赋值操作,而插入排序只需要一个赋值操作。所以,虽然冒泡排序和插入排序在时间复杂度上都是 O(n^2),但是如果希望把性能做到极致,首选插入排序。其实该点分析的主要出发点就是在同阶复杂度下,需要考虑系数、常数、低阶等。

2.3. 选择排序

选择排序也分为已排序区间和未排序区间(刚开始的已排序区间没有数据),选择排序每趟都会从未排序区间中找到最小的值(从小到大排序的话)放到已排序区间的末尾。

2.3.1. 实现

  1. /**
  2. * 选择排序:
  3. * 选择排序将待排序序列分成未排序区和已排序区;
  4. * 第一趟排序的时候整个待排序序列是未排序区;
  5. * 每一趟排序其实就是从未排序区选择一个最值,放到已排序区;
  6. * 跑 len-1 趟就好
  7. */
  8. public void switchSort(int[] arr, int len) {
  9. // len-1 趟,0-i 为已排序区
  10. for (int i = 0; i < len-1; i++) {
  11. int minIndex = i;
  12. for (int j = i+1; j < len; j++) {
  13. if (arr[j] < arr[minIndex]) {
  14. minIndex = j;
  15. }
  16. }
  17. if (minIndex != i) {
  18. int temp = arr[i];
  19. arr[i] = arr[minIndex];
  20. arr[minIndex] = temp;
  21. }
  22. }
  23. }

2.3.2. 算法分析

  • 选择排序是原地排序,因为只需要用来存储最小值所处位置的额外空间和交换时所需的额外空间。

  • 选择排序不是一个稳定的算法。因为选择排序是从未排序区间中找一个最小值,并且和前面的元素交换位置,这会破坏稳定性。比如 1、5、5、2 这样一组数据中,使用排序算法的话。当找到 2 为 5、5、2 当前未排序区间最小的元素时,2 会与第一个 5 交换位置,那么两个 5 的顺序就变了,就破坏了稳定性。

  • 时间复杂度分析。最好、最坏、平均都是 O(n^2),因为无论待排序数组情况怎么样,就算是已经有序了,都是需要依次遍历完未排序区间,需要比较的次数依次是 n-1、n-2,所以时间复杂度是 O(n^2)。

2.4. 归并排序(Merge Sort)

**归并排序的核心思想就是我要对一个数组进行排序:首先将数组分成前后两部分,然后对两部分分别进行排序,排序好之后再将两部分合在一起,那整个数组就是有序的了。对于分出的两部分可以采用相同的方式进行排序。**这个思想就是分治的思想,就是先将大问题分解成小的子问题来解决,子问题解决之后,大问题也就解决了。而对于子问题的求解也是一样的套路。这个套路有点类似于递归的方式,所以分治算法一般使用递归来实现。分治是一种解决问题的处理思想,而递归是一种实现它的编程方法。

2.4.1. 实现

下面使用递归的方式来实现归并排序。递归的递推公式是:merge_sort(p...r) = merge(merge_sort(p...q), merge_sort(q+1...r)),终止条件是 p>=r,不再递归下去了。整个实现过程是先调用 __mergeSort() 函数将两部分分别排好序,之后再使用数组合并的方式将两个排序好的部分进行合并。

  1. /**
  2. * 归并排序
  3. */
  4. public void mergeSort(int[] arr, int len) {
  5. __mergerSort(arr, 0, len-1);
  6. }
  7. private void __mergerSort(int[] arr, int begin, int end) {
  8. if (begin == end){
  9. return;
  10. }
  11. __mergerSort(arr, begin, (begin+end)/2);
  12. __mergerSort(arr, (begin+end)/2 + 1, end);
  13. merge(arr, begin, end);
  14. return;
  15. }
  16. private void merge(int[] arr, int begin, int end) {
  17. int[] copyArr = new int[end-begin+1];
  18. System.arraycopy(arr, begin, copyArr, 0, end-begin+1);
  19. int mid = (end - begin + 1)/2;
  20. int i = 0; // begin - mid 的指针
  21. int j = mid; // mid - end 的指针
  22. int count = begin; // 合并之后数组的指针
  23. while (i <= mid-1 && j <= end - begin) {
  24. arr[count++] = copyArr[i] < copyArr[j] ? copyArr[i++] : copyArr[j++];
  25. }
  26. while (i <= mid-1) {
  27. arr[count++] = copyArr[i++];
  28. }
  29. while (j <= end - begin) {
  30. arr[count++] = copyArr[j++];
  31. }
  32. }

2.4.2. 算法分析

  • 归并排序可以是稳定的排序算法,只要确保合并时,如果遇到两个相等值的,前半部分那个相等的值是在后半部分那个相等的值的前面即可保证是稳定的排序算法。

  • 归并排序的时间复杂度为 O(nlogn),无论是最好、最坏还是平均情况都一样。

    归并的时间复杂度分析则是递归代码的时间复杂度的分析。假设求解问题 a 可以分为对 b、c 两个子问题的求解。那么问题 a 的时间是 T(a) 、求解 b、c 的时间分别是 T(b) 和 T(c),那么 T(a) = T(b) +T(c) + K。k 等于将 b、c 两个子问题的结果合并问题 a 所消耗的时间。

    套用上述的套路,假设对 n 个元素进行归并排序需要的时间是 T(n),子问题归并排序的时间是 T(n/2),合并操作的时间复杂度是 O(n)。所以,T(n) =2 * T(n/2) +O(n),T(1) = C。最终得到:

    1. T(n)= 2*T(n/2) + n
    2. = 2*(2*T(n/4)+ n/2)+n = 2^2*T(n/4) + 2*n
    3. = 2^2*(2*T(n/8)+n/4) + 2*n = 2^3*T(n/8) + 3*n
    4. = ....
    5. = 2^k*T(n/2^K) + k*n
    6. = ....
    7. = 2^(log_2^n)*T(1) + log_2^n*n

    最终得到  ,使用大 O 时间复杂表示 T(n)=O(nlogn)。

    归并排序中,无论待排数列是有序还是倒序,最终递归的层次都是到只有一个数组为主,所以归并排序跟待排序列没有什么关系,最好、最坏、平均的时间复杂度都是 O(nlogn)。

  • 归并排序并不是原地排序,因为在归并排序的合并函数中,还需要额外的存储空间,这个存储空间是 O(n)。递归过程中,空间复杂度并不能像时间复杂度那样累加。因为在每次递归下去的过程中,虽然合并操作都会申请额外的内存空间,但是合并之后,这些申请的内存空间就会被释放掉。因此其实主要考虑最大问题合并时所需的空间复杂度即可,该空间复杂度为 O(n)。

2.5. 快速排序(Quick Sort)

快速排序利用的也是分治思想,核心思想是从待排数组中选择一个元素,然后将待排数组划分成两个部分:左边部分的元素都小于该元素的值,右边部分的元素都大于该元素的值,中间是该元素的值。然后对左右两个部分套用相同的处理方法,也就是将左边部分的元素再划分成左右两部分,右边部分的元素也再划分成左右两部分。以此类推,当递归到只有一个元素的时候,就说明此时数组是有序了的。

2.5.1. 实现

首先要对下标从 begin 到 end 之间的数据进行分区,可以选择 begin 到 end 之间的任意一个数据作为 pivot(分区点),一般是最后一个数据作为分区点。之后遍历 begin 到 end 之间的数据,将小于 pivot 的放在左边,大于的 pivot 的放在右边,将pivot 放在中间(位置 p)。经过这一操作之后,数组 begin 到 end 之间的数据就被分成了三个部分:begin 到 p-1、p、p+1 到 end。最后,返回 pivot 的下标。那么这个过程一般有三种方式:

  • 首先说明这种方法不可取。在不考虑空间消耗的情况下,分区操作可以非常简单。使用两个临时数组 X 和 Y,遍历 begin 到 end 之间的数据,将小于 pivot 的数据都放到数组 X 中,将大于 pivot 的数据都放到数组 Y 中,最后将数组 X 拷贝到原数组中,然后再放入 pivot,最后再放入数组 Y。但是采用这种方式之后,快排就不是原地排序算法了,因此可以采用以下两种方法在原数组的基础之上完成分区操作。

  • 第一种方法还是使用两个指针:i 和 j,i 和 j 一开始都放置在 begin 初。之后 j 指针开始遍历,如果 j 指针所指的元素小于等于 pivot,那么则将 j 指针的元素放到 i 指针的处,i  指针的元素放置于 j 处,然后 i 后移,j 后移。如果 j 指针所指的元素大于 pivot 那么 j 后移即可。首先个人觉得其实整个数组被分成三个区域:0-i-1 的为小于等于 pivot 的区域,i-j-1 为大于 pivot 的区域,j 之后的区域是未排序的区域。

  • 第二种方法还是使用两个指针:i 和 j,i 从 begin 处开始,j 从 end 处开始。首先 j 从 end 开始往前遍历,当遇到小于 pivot 的时候停下来,然后此时 i 从 begin 开始往后遍历,当遇到大于 pivot 的时候停下来,此时交换 i 和 j 处的元素。之后 j 继续移动,重复上述过程,直至 i >= j。

在返回 pivot 的下标 q 之后,再根据分治的思想,将 begin 到 q-1 之间的数据和下标 q+1 到 end 之间的数据进行递归。这边一定要 q-1 和 q+1 而不能是 q 和 q+1 是因为:考虑数据已经有序的极端情况,一开始是对 begin 到 end;当分区之后 q 的位置还是 end 的位置,那么相当于死循环了。最终,当区间缩小至 1 时,说明所有的数据都有序了。

如果用递推公式来描述上述的过程的话,递推公式:quick_sort(begin...end) = quick_sort(begin...q-1) + quick_sort(q+1...end),终止条件是:begin >= end。将这两个公式转化为代码之后,如下所示:

  1. /**
  2. * 快速排序
  3. */
  4. public void quickSort(int[] arr, int len) {
  5. __quickSort(arr, 0, len-1);
  6. }
  7. // 注意边界条件
  8. private void __quickSort(int[] arr, int begin, int end) {
  9. if (begin >= end) {
  10. return;
  11. }
  12. // 一定要是 p-1!
  13. int p = partition(arr, begin, end); // 先进行大致排序,并获取区分点
  14. __quickSort(arr, begin, p-1);
  15. __quickSort(arr, p+1, end);
  16. }
  17. private int partition(int[] arr, int begin, int end) {
  18. int pValue = arr[end];
  19. // 整两个指针,两个指针都从头开始
  20. // begin --- i-1(含 i-1):小于 pValue 的区
  21. // i --- j-1(含 j-1):大于 pValue 的区
  22. // j --- end:未排序区
  23. int i = begin;
  24. int j = begin;
  25. while (j <= end) {
  26. if (arr[j] <= pValue) {
  27. int temp = arr[j];
  28. arr[j] = arr[i];
  29. arr[i] = temp;
  30. i++;
  31. j++;
  32. } else {
  33. j++;
  34. }
  35. }
  36. return i-1;
  37. }

2.5.2. 优化

  • 由于分区点很重要(为什么重要见算法分析),因此可以想方法寻找一个好的分区点来使得被分区点分开的两个分区中,数据的数量差不多。下面介绍两种比较常见的算法:

    • **三数取中法。就是从区间的首、尾、中间分别取出一个数,然后对比大小,取这 3 个数的中间值作为分区点。**但是,如果排序的数组比较大,那“三数取中”可能不够了,可能就要“五数取中”或者“十数取中”,也就是间隔某个固定的长度,取数据进行比较,然后选择中间值最为分区点。

    • 随机法。随机法就是从排序的区间中,随机选择一个元素作为分区点。随机法不能保证每次分区点都是比较好的,但是从概率的角度来看,也不太可能出现每次分区点都很差的情况。所以平均情况下,随机法取分区点还是比较好的。

  • 递归可能会栈溢出,最好的方式是使用非递归的方式;

2.5.3. 算法分析

  • 快排不是一个稳定的排序算法。因为分区的过程涉及到交换操作,原本在前面的元素可能会被交换到后面去。比如 6、8、7、6、3、5、9、4 这个数组中。在经过第一次分区操作之后,两个 6 的顺序就会发生改变。

  • 快排是一种原地的排序算法。

  • 快排的最坏时间复杂度是 O(n^2),最好时间复杂度是O(nlogn),平均时间复杂度是 O(nlogn)。

    快排也是使用递归来实现,那么递归代码的时间复杂度处理方式和前面类似。

    快排的时间复杂度取决于 pivot 的选择,通过合理地选择 pivot 来使得算法的时间复杂度尽可能不出现 O(n^2) 的情况。

    • 假设每次分区操作都可以把数组分成大小接近相等的两个小区间,那么快排的时间复杂度和归并排序一样,都是 O(nlogn)。

    • 但是分区操作不一定都能把数组分成大小接近相等的两个小区间。极端情况如数组中的数组已经有序了,如果还是取最后一个元素作为分割点,左边区间是 n-1 个数,右边区间没有任何数。此时, T(n)=T(n-1)+n,最终时间复杂度退化为 O(n^2)。大部分情况下,采用递归树的方法可得到时间复杂度是 O(nlogn)。由于极端情况是少数,因此平均时间复杂度是 O(nlogn)。

2.5.4. VS 归并排序

首先从思想上来看:归并排序的处理过程是由下到上的,先处理子问题,然后对子问题的解再合并;而快排正好相反,处理过程是由上到下的,先分区,再处理子问题。

从性能上来看:归并是一个稳定的、时间复杂度为 O(nlogn) 的排序算法,但是归并并不是一个原地排序算法(所以归并没有快排应用广泛)。而快速排序算法时间复杂度不一定是 O(nlogn),最坏情况下是 O(n^2),而且不是一个稳定的算法,但是通过设计可以让快速排序成为一个原地排序算法。

2.6. 堆排序

堆是一种特殊的树。只要满足以下两个条件就是一个堆。

  • 堆是一个完全二叉树。既然是完全二叉树,那么使用数组存储的方式将会很方便。

  • 堆中的每个节点的值都必须大于等于(或小于等于)其子节点的值。对于大于等于子节点的堆又被称为“大顶堆”;小于等于子节点的堆又被称为“小顶堆”。

由于”堆是一个完全二叉树“,因此一般堆使用数组来存储,一是节省空间,二是通过数组的下标就可以找到父节点、左右子节点(数组下标最好从 1 开始,该节的代码实现都将从数组下标为 1 的地方开始)。那么,借助堆这种数据结构实现的排序被称为堆排序。堆排序是一种原地的、时间复杂度为 O(nlogn) 且不稳定的排序算法。

2.6.1. 实现

整个堆排序的实现分为建堆和排序两个步骤。

建堆

首先是将待排序数组建立成一个堆,秉着能不借助额外数组则不借助的原则,我们可以直接在原数组上直接操作。这样,建堆有两个方法:

  • 第一种方法类似于上述堆的操作中“往堆中插入一个元素”的思想。刚开始的时候假设堆中只有一个元素,也就是下标为 1 的元素。然后,将下标为 2 的元素插入堆中,并对堆进行调整。以此类推,将下标从 2 到 n 的元素依次插入到堆中。这种建堆方式相当于将待排序数组分成“堆区”和“待插入堆区”。

    如图所示,我们将对待排序数据 7、5、19、8、4 进行建堆(大顶堆)。可以看到初始化堆就一个元素 7。之后将指针移到下标为 2 的位置,将 5 这个元素添加到堆中并从下往上进行堆化。之后,再将指针移到下标为 3 的位置,将 19 这个元素添加到堆中并从下往上进行堆化。依次类推。

  • 第二种方法是将整个待排序数组都当成一个“堆”,但是这个“堆”不一定满足堆的两个条件,因此我们需要对其进行整体调整。那么,调整的时候是从数组的最后开始,依次往前调整。调整的时候,只需要调整该节点及其子树满足堆的要求,并且是从上往下的方式进行调整。由于,叶子节点没有子树,所以叶子节点没必要调整,我们只需要从第一个非叶子节点开始调整(这边的第一是从后往前数的第一个)。那么第一个非叶子节点的下标为 n/2,因此我们只需要对 n/2 到 1 的数组元素进行从上往下堆化即可(下标从 n/2 到 1 的数组元素所在节点都是非叶子节点,下标从 n/2+1 到 n 的数组元素所在节点都是叶子节点)。

    如图所示,我们将对待排序数据 7、5、19、8、4、1、20、13、16 进行建堆(大顶堆)。可以看到整个过程是从 8 这个元素开始进行堆化。在对 8 进行堆化的时候,仅对 8 及其子树进行堆化。在对 5 进行堆化的时候,仅对 5 及其子树进行堆化。

我们将第二种思路实现成如下代码段所示:

  1. public void buildHeap(int[] datas, int len) {
  2. this.heap = datas;
  3. this.capacity = len - 1;
  4. this.count = len - 1;
  5. for (int i = this.count/2; i >=1; i--) {
  6. heapifyFromTop(i);
  7. }
  8. }
  9. public void heapifyFromTop(int begin) {
  10. while (true) {
  11. int i = begin; // i 是节点及其左右子节点中较大值的那个节点的下标
  12. /* 就是在节点及其左右子节点中选择一个最大的值,与节点所处的位置进行;
  13. 但是,需要注意的是假如这个值正好是节点本身,那么直接退出循环;
  14. 否则需要进行交换,然后从交换之后的节点开始继续堆化 */
  15. if (begin * 2 <= this.count && this.heap[begin] < this.heap[2 * begin]) {
  16. i = 2 * begin;
  17. }
  18. if ((2 * begin + 1) <= this.count && this.heap[i] < this.heap[2 * begin + 1]) {
  19. i = 2 * begin + 1;
  20. }
  21. if (i == begin) {
  22. break;
  23. }
  24. swap(begin, i);
  25. begin = i;
  26. }
  27. }

为什么下标从 n/2 到 1 的数组元素所在节点都是非叶子节点,下标从 n/2+1 到 n 的数组元素所在节点都是叶子节点?这个算是完全二叉树的一个特性。严格的证明暂时没有,不严谨的证明还是有的。这里采用反证法,假如 n/2 + 1 不是叶子节点,那么它的左子节点下标应该为 n+2,但是整个完全二叉树最大的节点的下标为 n。所以 n/2 + 1 不是叶子节点不成立,即 n/2 + 1 是叶子节点。那么同理可证 n/2 + 1 到 n 也是如此。而对于下标为 n/2 的节点来说,它的左子节点有的话下标应该为 n,n 在数组中有元素,因此 n/2 有左子节点,即 n/2 不是叶子节点。同理可证 1 到 n/2 都不是叶子节点。

排序

建完堆(大顶堆)之后,接下去的步骤是排序。那么具体该怎么实现排序呢?

此时,我们可以发现,堆顶的元素是最大的,即数组中的第一个元素是最大的。实现排序的过程大致如下:我们把它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。此时堆顶元素不是最大,因此需要进行堆化。采用从上而下的堆化方法(参考删除堆顶元素的堆化方法),将剩下的 n-1 个数据构建成堆。最后一个数据因为已知是最大了,所以不用参与堆化了。n-1 个数据构建成堆之后,再将堆顶的元素(下标为 1 的元素)和下标为 n-1 的元素进行交换。一直重复这个过程,直至堆中只剩一个元素,此时排序工作完成。如图所示,这是整个过程的示意图。

下面将排序的过程使用 Java 实现,如下所示。那么讲建堆和排序的过程结合在一起之后就是完整的堆排序了。

  1. public void heapSort() {
  2. while (this.count > 1) {
  3. swap(this.count, 1);
  4. this.count--;
  5. heapifyFromTop(1);
  6. }
  7. }

详细的代码看文章开头给出的 Github 地址。

2.6.2. 算法分析

时间复杂度

堆排序的时间复杂度是由建堆和排序两个步骤的时间复杂度叠加而成。

  • 建堆的时间复杂度

在采用第二方式建堆时,从粗略的角度来看,每个节点进行堆化的时间复杂度是 O(logn),那么 n/2 个节点堆化的总时间复杂度为 O(nlogn)。但是这此时粗略的计算,更加精确的计算结果不是 O(nlogn),而是 O(n)

因为叶子节点不需要进行堆化,所以需要堆化的节点从倒数第二层开始。每个节点需要堆化的过程中,需要比较和交换的次数,跟这个节点的高度 k 成正比。那么所有节点的高度之和,就是所有节点堆化的时间复杂度。假设堆顶节点对应的高度为 h ,那么整个节点对应的高度如图所示(以满二叉树为例,最后一层叶子节点不考虑)。

那么将每个非叶子节点的高度求和为

求解这个公式可将两边同时乘以 2 得到 S2,

然后再减去 S1,从而就得到 S1

由于

所以最终时间复杂度为 O(2n-logn),也就是 O(n)。

  • 排序的时间复杂度

排序过程中,我们需要进行 (n-1) 次堆化,每次堆化的时间复杂度是 O(logn),那么排序阶段的时间复杂度为 O(nlogn)。

  • 总的时间复杂度

那么,整个总的时间复杂度为 O(nlogn)

不对建堆过程的时间复杂度进行精确计算,也就是建堆以 O(nlogn) 的时间复杂度算的话,那么总的时间复杂度还是 O(nlogn)。

稳定与否

堆排序不是稳定的排序算法。因为在排序阶段,存在将堆的最后一个节点跟堆顶点进行互换的操作,所以有可能会改变相同数据的原始相对顺序。比如下面这样一组待排序 20、16、13、13 ,在排序时,第二个 13 会跟 20 交换,从而更换了两个 13 的相对顺序。

是否原地

堆排序是原地排序算法,因为堆排序的过程中的两个步骤中都只需要极个别临时存储空间。

2.6.3. 总结

在实际开发中,为什么快速排序要比堆排序性能要好?

  1. 对于同样的数据,在排序过程中,堆排序算法的数据交换次数要多于快速排序

    对于基于比较的排序算法来说,整个排序过程就是由比较和交换这两个操作组成。快速排序中,交换的次数不会比逆序度多。但是堆排序的过程,第一步是建堆,这个过程存在大量的比较交换操作,并且很有可能会打乱数据原有的相对先后顺序,导致原数据的有序度降低。比如,在对一组已经按从小到大的顺序排列的数据进行堆排序时,那么建堆过程会将这组数据构建成大顶堆,而这一操作将会让数据变得更加无序。而采用快速排序的方法时,只需要比较而不需要交换。

    最直接的方式就是做个试验看一下,对交换次数进行统计。

  2. 堆排序的访问方式没有快速排序友好

    快速排序来说,数据是顺序访问的。而堆排序,数据是跳着访问的。访问的数据量如何很大的话,那么堆排序可能对 CPU 缓存不太友好。

2.7. 桶排序

**桶排序的核心思想就是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。**桶内排序完成之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。一般步骤是:

  • 先确定要排序的数据的范围;

  • 然后根据范围将数据分到桶中(可以选择桶的数量固定,也可以选择桶的大小固定);

  • 之后对每个桶进行排序;

  • 之后将桶中的数据进行合并;

2.7.1. 实现

  1. public void buckerSort(int[] arr, int len, int bucketCount) {
  2. // 确定数据的范围
  3. int minVal = arr[0];
  4. int maxVal = arr[0];
  5. for (int i = 1; i < len; ++i) {
  6. if (arr[i] < minVal) {
  7. minVal = arr[i];
  8. } else if (arr[i] > maxVal){
  9. maxVal = arr[i];
  10. }
  11. }
  12. // 确认每个桶的所表示的范围
  13. bucketCount = (maxVal - minVal + 1) < bucketCount ? (maxVal - minVal + 1) : bucketCount;
  14. int bucketSize = (maxVal - minVal + 1) / bucketCount;
  15. bucketCount = (maxVal - minVal + 1) % bucketCount == 0 ? bucketCount : bucketCount + 1;
  16. int[][] buckets = new int[bucketCount][bucketSize];
  17. int[] indexArr = new int[bucketCount]; // 数组位置记录
  18. // 将数据依次放入桶中
  19. for (int i = 0; i < len; i++) {
  20. int bucketIndex = (arr[i] - minVal) / bucketSize;
  21. if (indexArr[bucketIndex] == buckets[bucketIndex].length) {
  22. expandCapacity(buckets, bucketIndex);
  23. }
  24. buckets[bucketIndex][indexArr[bucketIndex]++] = arr[i];
  25. }
  26. // 桶内排序
  27. for (int i = 0; i < bucketCount; ++i) {
  28. if (indexArr[i] != 0) {
  29. quickSort(buckets[i], 0, indexArr[i] - 1);
  30. }
  31. }
  32. // 桶内数据依次取出
  33. int index = 0;
  34. for (int i = 0; i < bucketCount; ++i) {
  35. for (int j = 0; j < indexArr[i]; ++j) {
  36. arr[index++] = buckets[i][j];
  37. }
  38. }
  39. // 打印
  40. for (int i = 0; i < len; ++i) {
  41. System.out.print(arr[i] + " ");
  42. }
  43. System.out.println();
  44. }
  45. // 对数组进行扩容
  46. public void expandCapacity(int[][] buckets, int bucketIndex) {
  47. int[] newArr = new int[buckets[bucketIndex].length * 2];
  48. System.arraycopy(buckets[bucketIndex], 0, newArr, 0, buckets[bucketIndex].length);
  49. buckets[bucketIndex] = newArr;
  50. }

2.7.2. 算法分析

  • 最好时间复杂度为 O(n),最坏时间复杂度为 O(nlogn),平均时间复杂度为 O(n)。

    如果要排序的数据为 n 个,把这些数据均匀地分到 m 个桶内,每个桶就有 k=n/m 个元素。每个桶使用快速排序,时间复杂度为 O(k.logk)。m 个 桶的时间复杂度就是 O(m*k*logk),转换的时间复杂度就是 O(n*log(n/m))。当桶的数量 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。

    如果数据经过桶的划分之后,每个桶的数据很不平均,比如一个桶中包含了所有数据,那么桶排序就退化为 O(nlogn) 的排序算法了。

    这边的平均时间复杂度为 O(n) 没有经过严格运算,只是采用粗略的方式得出的。因为桶排序大部分情况下,都能将数据进行大致均分,而极少情况出现所有的数据都在一个桶里。

  • 非原地算法

    因为桶排序的过程中,需要创建 m 个桶这个的空间复杂度就肯定不是 O(1) 了。在桶内采用快速排序的情况下,桶排序的空间复杂度应该是 O(n)。

  • 桶排序的稳定与否,主要看两块:1.将数据放入桶中的时候是否按照顺序放入;2.桶内采用的排序算法。所以将数据放入桶中是按照顺序的,并且桶内也采用稳定的排序算法的话,那么整个桶排序则是稳定的。既然能稳定的话,那么一般算稳定的。

2.7.3. 总结

  • 桶排序对要排序的数据的要求是非常苛刻的。

    • 首先,要排序的数据需要很容易被划分到 m 个桶。并且,桶与桶之间有着天然的大小顺序,这样子每个桶内的数据都排序完之后,桶与桶之间的数据不需要再进行排序;

    • 其次,数据在各个桶中的分布是比较均匀的。如果数据经过桶的划分之后,每个桶的数据很不平均,比如一个桶中包含了所有数据,那么桶排序就退化为 O(nlogn) 的排序算法了。

  • **桶排序适合应用在外部排序中。**比如要排序的数据有 10 GB 的订单数据,但是内存只有几百 MB,无法一次性把  10GB 的数据全都加载到内存中。这个时候,就可以先扫描 10GB 的订单数据,然后确定一下订单数据的所处的范围,比如订单的范围位于 1~10 万元之间,那么可以将所有的数据划分到 100 个桶里。再依次扫描 10GB 的订单数据,把 1~1000 元之内的订单存放到第一个桶中,1001~2000 元之内的订单数据存放到第二个桶中,每个桶对应一个文件,文件的命名按照金额范围的大小顺序编号如 00、01,即第一个桶的数据输出到文件 00 中。

    理想情况下,如果订单数据是均匀分布的话,每个文件的数据大约是 100MB,依次将这些文件的数据读取到内存中,利用快排来排序,再将排序好的数据存放回文件中。最后只要按照文件顺序依次读取文件中的数据,并将这些数据写入到一个文件中,那么这个文件中的数据就是排序好了的。

    但是,订单数据不一定是均匀分布的。划分之后可能还会存在比较大的文件,那就继续划分。比如订单金额在 1~1000 元之间的比较多,那就将这个区间继续划分为 10 个小区间,1~100、101~200 等等。如果划分之后还是很大,那么继续划分,直到所有的文件都能读入内存。

    外部排序就是数据存储在磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。

2.8. 计数排序

计数排序跟桶排序类似,可以说计数排序其实是桶排序的一种特殊情况。**当要排序的 n 个数据,所处的范围并不大的时候,比如最大值是 K,那么就可以把数据划分到 K 个桶,每个桶内的数据值都是相同的,**从而省掉了桶内排序的时间。可以说计数排序和桶排序的区别其实也就在于桶的大小粒度不一样。

下面通过举例子的方式来看一下计数排序的过程。假设数组 A 中有 8 个数据,值在 0 到 5 之间,分别是:2、5、3、0、2、3、0、3。

  • 首先使用大小为 6 的数组 C[6] 来存储每个值的个数,下标对应具体值。从而得到,C[6] 的情况为:2、0、2、3、0、1。

  • 那么,值为 3 分的数据个数有 3 个,小于 3 分的数据个数有 4 个,所以值为 3 的数据在有序数组 R 中所处的位置应该是 4、5、6。为了快速计算出位置,对 C[6] 这个数组进行变化,C[k] 里存储小于等于值 k 的数据个数。变化之后的数组为 2、2、4、7、7、8。

  • 之后我们从后往前依次扫描数据 A(从后往前是为了稳定),比如扫描到 3 的时候,从数据 C 中取出下标为 3 的值,是7(也就说到目前为止,包含自己在内,值小于等于 3 的数据个数有 7 个),那么 3 就是数组 R 中第 7 个元素,也就是下标为 6。当然 3 放入到数组 R 中后,C[3] 要减 1,变成 6,表示此时未排序的数据中小于等于 3 的数据个数有 6 个。

  • 以此类推,当扫描到第 2 个值为 3 的数据的时候,就会将这个数据放入到 R 中下标为 5 的位置。当扫描完整个数组 A 后,数组 R 内的数据就是按照值从小到大的有序排列了。

2.8.1. 实现

  1. /**
  2. * 计数排序,暂时只能处理整数(包括整数和负数)
  3. * @param arr
  4. * @param len
  5. */
  6. public void countingSort(int[] arr, int len) {
  7. // 确定范围
  8. int minVal = arr[0];
  9. int maxVal = arr[0];
  10. for (int i = 1; i < len; ++i) {
  11. if (maxVal < arr[i]) {
  12. maxVal = arr[i];
  13. } else if (arr[i] < minVal) {
  14. minVal = arr[i];
  15. }
  16. }
  17. // 对数据进行处理
  18. for (int i = 0; i < len; ++i) {
  19. arr[i] = arr[i] - minVal;
  20. }
  21. maxVal = maxVal - minVal;
  22. // 遍历数据数组,求得计数数组的个数
  23. int[] count = new int[maxVal + 1];
  24. for (int i = 0; i < len; ++i) {
  25. count[arr[i]] ++;
  26. }
  27. printAll(count, maxVal + 1);
  28. // 对计数数组进行优化
  29. for (int i = 1; i < maxVal + 1; ++i) {
  30. count[i] = count[i - 1] + count[i];
  31. }
  32. printAll(count, maxVal + 1);
  33. // 进行排序,从后往前遍历(为了稳定)
  34. int[] sort = new int[len];
  35. for (int i = len - 1; i >= 0; --i) {
  36. sort[count[arr[i]] - 1] = arr[i] + minVal;
  37. count[arr[i]]--;
  38. }
  39. printAll(sort, len);
  40. }

2.8.2. 算法分析

  • 非原地算法

    计数排序相当于桶排序的特例一样。计数排序需要额外的 k 个内存空间和 n 个新的内存空间存放排序之后的数组。

  • 稳定算法

    前面也提到了,假如采用从后往前遍历的方式话,那么是稳定算法。

  • 时间复杂度

    最好、最坏、平均时间复杂度都是一样,为 O(n+k),k 为数据范围。这个从代码的实现可以看出,无论待排数组的情况怎么样,都是要循环同样的次数。

2.8.3. 总结

  • 计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。

  • 计数排序只能直接对非负整数进行排序,如果要排序的数据是其他类型的,需要在不改变相对大小的情况下,转化为非负整数。比如当要排序的数是精确到小数点后一位时,就需要将所有的数据的值都先乘以 10,转换为整数。再比如排序的数据中有负数时,数据的范围是[-1000,1000],那么就需要先将每个数据加上 1000,转换为非负整数。

2.9. 基数排序

桶排序和计数排序都适合范围不是特别大的情况(请注意是范围),但是桶排序的范围可以比计数排序的范围稍微大一点。假如数据的范围很大很大,比如对手机号这种的,桶排序和技术排序显然不适合,因为需要的桶的数量也是十分巨大的。此时,可以使用基数排序。**基数排序的思想就是将要排序的数据拆分成位,然后逐位按照先后顺序进行比较。**比如手机号中就可以从后往前,先按照手机号最后一位来进行排序,之后再按照倒数第二位来进行排序,以此类推。当按照第一位重新排序之后,整个排序就算完成了。

需要注意的是**,按照每位排序的过程需要稳定的**,因为假如后一次的排序不稳定,前一次的排序结果将功亏一篑。比如,第一次对个位进行排序结果为 21、11、42、22、62,此时 21 在 22 前面;第二次对十位的排序假如是不稳定的话,22 可能跑到 21 前面去了。那么整个排序就错了,对个位的排序也就相当于白费了。

下面举个字符串的例子,整个基数排序的过程如下图所示:

2.9.1. 实现

  1. /**
  2. * 基数排序
  3. * @param arr
  4. * @param len
  5. */
  6. public void radixSort(int[] arr, int len, int bitCount) {
  7. int exp = 1;
  8. for (int i = 0; i < bitCount; ++i) {
  9. countingSort(arr, len, exp);
  10. exp = exp * 10;
  11. }
  12. }
  13. public int getBit(int value, int exp) {
  14. return (value / exp) % 10;
  15. }
  16. /**
  17. * 计数排序,暂时只能处理整数(包括整数和负数)
  18. * @param arr
  19. * @param len
  20. */
  21. public void countingSort(int[] arr, int len, int exp) {
  22. // 确定范围
  23. int maxVal = getBit(arr[0], exp);
  24. for (int i = 1; i < len; ++i) {
  25. if (maxVal < getBit(arr[i], exp)) {
  26. maxVal = getBit(arr[i], exp);
  27. }
  28. }
  29. // 遍历数据数组,求得计数数组的个数
  30. int[] count = new int[maxVal + 1];
  31. for (int i = 0; i < len; ++i) {
  32. count[getBit(arr[i], exp)] ++;
  33. }
  34. // 对计数数组进行优化
  35. for (int i = 1; i < maxVal + 1; ++i) {
  36. count[i] = count[i - 1] + count[i];
  37. }
  38. // 进行排序,从后往前遍历(为了稳定)
  39. int[] sort = new int[len];
  40. for (int i = len - 1; i >= 0; --i) {
  41. sort[count[getBit(arr[i], exp)] - 1] = arr[i];
  42. count[getBit(arr[i], exp)]--;
  43. }
  44. System.arraycopy(sort, 0, arr, 0, len);
  45. printAll(sort, len);
  46. }

2.9.2. 算法分析

  • 非原地算法

    是不是原地算法其实看针对每一位排序时所使用的算法。为了确保基数排序的时间复杂度以及每一位的稳定性,一般采用计数排序,计数排序是非原地算法,所以可以把基数排序当成非原地排序。

  • 稳定算法

    因为基数排序需要确保每一位进行排序时都是稳定的,所以整个基数排序时稳定的。

  • 时间复杂度是 O(kn),k 是数组的位数

    最好、最坏、平均的时间复杂度都是 O(n)。因为无论待排数组的情况怎么样,基数排序其实都是遍历每一位,对每一位进行排序。假如每一位排序的过程中使用计数排序,时间复杂度为 O(n)。假如有 k 位的话,那么则需要 k 次桶排序或者计数排序。因此总的时间复杂度是 O(kn),当 k 不大时,比如手机号是 11 位,那么基数排序的时间复杂度就近似于 O(n)。也可以从代码中看出。

2.9.3. 总结

  • 基数排序的一个要求是排序的数据要是等长的。当不等长时候可以在前面或者后面补 0,比如字符串排序的话,就可以在后面补 0,因为 ASCII 码中所有的字母都大于 “0”,所以补 “0” 不会影响到原有的大小排序。

  • 基数排序的另一个要求就是数据可以分割出独立的 “位” 来比较,而且位之间存在递进关系:如果 a 数据的高位比 b 数据大,那么剩下的低位就不用比较了。

  • 除此之外,每一个位的数据范围不能太大,要能用线性排序算法来排序,否则,基数排序时间复杂度无法达到 O(n)。

3. 排序函数

几乎所有编程语言都会提供排序函数,比如 C 语言中 qsort()、C++ STL 中的 sort()/stable_sort()、Java 中的 Collections.sort()。这些排序函数,并不会只采用一种排序算法,而是多种排序算法的结合。当然主要使用的排序算法都是 O(nlogn) 的。

  • glibc 的 qsort() 排序函数。qsort() 会优先使用归并排序算法。当排序的数据量很大时,会使用快速排序。使用排序算法的时候也会进行优化,如使用 “三数取中法”、在堆上手动实现一个栈来模拟递归来解决。在快排的过程中,如果排序的区间的元素个数小于等于 4 时,则使用插入排序。而且在插入排序中还用到了哨兵机制,减少了一次判断。

    在小规模数据面前 O(n^2) 时间复杂度的算法并不一定比 O(nlogn)的算法执行时间长。主要是因为时间复杂度会将系数和低阶去掉。

  • Array.sort() 排序函数,使用 TimSort 算法。TimSort 算法是一种归并算法和插入排序算法混合的排序算法。基本工作过程就是:

    整个排序过程,分段选择策略可以保证 O(nlogn) 的时间复杂度。TimSort 主要利用了待排序列中可能有些片段已经基本有序的特性。之后,对于小片段采用插入算法进行合并,合并成大片段。最后,再使用归并排序的方式进行合并,从而完成排序工作。

    • 扫描数组,确定其中的单调上升段和单调下降段,将严格下降段反转;

    • 定义最小基本片段长度,长度不满足的单调片段通过插入排序的方式形成满足长度的单调片段(就是长度大于等于所要求的最小基本片段长度)

    • 反复归并一些相邻片段,过程中避免归并长度相差很大的片段,直至整个排序完成。

4. 附加知识

4.1. 有序度、逆序度

在以从小到大为有序的情况中,有序度是数组中有序关系的元素对的个数,用数学公式表示如下所示。

如果 i < j,那么 a[i] < a[j]

比如 2、4、3、1、5、6 这组数据的有序度是 11;倒序排列的数组,有序度是 0;一个完全有序的数组,有序度为满有序度,为 n*(n-1)/2,比如1、2、3、4、5、6,有序度就是 15。

逆序度的定义正好跟有序度的定义相反

如果 i < j,那么 a[i] > a[j]

关于逆序度、有序度、满有序度满足如下公式

逆序度 = 满有序度 - 有序度

排序的过程其实就是减少逆序度,增加有序度的过程,如果待排序序列达到满有序度了,那么此时的序列就是有序了

5. 总结

  • 冒泡排序、选择排序可能就停留在理论的层面,实际开发应用中不多,但是插入排序还是挺有用的,有些排序算法优化的时候就会用到插入排序,比如在排序数据量小的时候会先选择插入排序。

  • 冒泡、选择、插入三者的时间复杂度一般都是按 n^2 来算。**并且这三者都有一个共同特点,那就是都会将排序数列分成已排序和未排序两部分。**外层循环一次,其实是让有序部分增加一个,因此外层循环相当于对有序部分和未排序部分进行分割。而外层循环次数就是待排序的数据的个数;内层循环则主要负责处理未排序部分的元素。

  • 快排的分区过程和分区思想其实特别好用,在解决很多非排序的问题上都会遇到。比如如何在 O(n) 的时间复杂度内查找一个 k 最值的问题(还用到分治,更多是分区这种方式);比如将一串字符串划分成字母和数字两部分(其实就是分区,所以需要注意分区过程的应用)。以后看到类似分区什么的,可以想想快排分区过程的操作。

  • 快排和归并使用都是分治的思想,都可使用递归的方式实现。只是归并是从下往上的处理过程,是先进行子问题处理,然后再合并;而快排是从上往下的处理过程,是先进行分区,而后再进行子问题处理。

  • 桶排序、计数排序、基数排序的时间复杂度是线性的,所以这类排序算法叫做线性排序。之所以这能做到线性排序,主要是因为这三种算法都不是基于比较的排序算法,不涉及到元素之间的比较操作。但是这三种算法对排序的数据要求很苛刻。如果数据特征比较符合这些排序算法的要求,这些算法的复杂度可以达到 O(n)。

  • 桶排序、计数排序针对范围不大的数据是可行的,它们的基本思想都是将数据划分为不同的桶来实现排序。

  • 各种算法比较

    排序算法平均时间复杂度最好时间复杂度最坏时间复杂度是否是原地排序是否稳定
    冒泡O(n^2)O(n)O(n^2)
    插入O(n^2)O(n)O(n^2)
    选择O(n^2)O(n^2)O(n^2)×
    归并O(nlogn)O(nlogn)O(nlogn)×  O(n)
    快排O(nlogn)O(nlogn)O(n^2)×
    堆排序O(nlogn)O(nlogn)O(nlogn)×
    桶排序O(n)O(n)O(nlogn)×
    计数排序O(n+k)O(n+k)O(n+k)×
    基数排序O(kn)O(kn)O(kn)×

6. 巨人的肩膀

  1. 极客时间,《数据结构与算法之美》,王争

  2. 《算法图解》

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

相关文章

  1. 前端开发学习笔记3-列表/表单

    HTML5之列表标签 列表即是用于布局的&#xff0c;特点是整齐&#xff0c;有序 列表分为三大类&#xff1a;无序列表&#xff0c;有序列表&#xff0c;自定义列表 无序列表 ul:unordered lists的缩写&#xff0c;即无序列表。 用ul包含&#xff0c; 列表项用li表示<ul>&…...

    2024/4/21 8:03:40
  2. 前阿里大佬干货分享,0基础小白,转行必看Python学习笔记(五)

    Python学习笔记5 面向对象 类和对象的创建属相相关方法相关元类内置的特殊属性内置的特殊方法 面向对象 类和对象的创建 类 # 经典类 没有继承 object的类 # 新式类 继承了 object的类class Money: # 2.x中默认是经典类&#xff0c;3.x中是新式类passclass Money(object…...

    2024/4/21 4:42:26
  3. PAT甲级1043 Is It a Binary Search Tree (25分)|C++实现

    一、题目描述 原题链接 Input Specification: ​​Output Specification: Sample Input 1: 7 8 6 5 7 10 8 11 Sample Output 1: YES 5 7 6 8 11 10 8 Sample Input 2: 7 8 10 11 8 6 7 5 Sample Output 2: YES 11 8 10 7 5 6 8 Sample Input 3: 7 8 6 8 5 10 9 11…...

    2024/4/21 15:08:50
  4. 故障再思考-预防故障清单

    目录 一、引言 二、故障的分类 三、故障的3大主题 四、故障预防清单 1、 为什么用清单...

    2024/4/22 7:26:05
  5. css--伪类选择器

    伪类&#xff08;不存在的类&#xff0c;特殊的类&#xff09;- 伪类用来描述一个元素的特殊状态比如&#xff1a;第一个子元素、被点击的元素、鼠标移入的元素...- 伪类一般情况下都是使用:开头:first-child 第一个子元素:last-child 最后一个子元素:nth-child() 选中第n个子元…...

    2024/5/2 5:16:28
  6. E. Egor in the Republic of Dagestan(DAG最短路+dp)详解

    https://codeforces.com/contest/1407/problem/E 题意&#xff1a;给一个有向图&#xff0c;每条边有一个类型&#xff08;0或者1&#xff09;&#xff0c;什么类型的城市就只能走什么类型的边&#xff0c;你需要给每个城市指定类型&#xff08;0/1&#xff09;&#xff0c;使得…...

    2024/4/13 2:20:17
  7. Django中session 保留登录状态

    1. 在settings.py中配置&#xff1a; # 默认不保持登录状态&#xff0c;如果改False保持2个月不用登录 SESSION_EXPIRE_AT_BROWSER_CLOSE False SESSION_COOKIE_AGE 60*60*24*30*2 SESSION_SAVE_EVERY_REQUEST True...

    2024/4/22 4:57:25
  8. win10安装配置CUDA+cuDNN+Tensorflow2.0

    转载请注明作者和出处&#xff1a;http://blog.csdn.net/john_bh/ 文章目录1. 概念说明2. CUDA3. cuDNN1. 概念说明 CUDA&#xff08;Compute Unified Device Architecture&#xff0c;统一计算架构&#xff09;是由NVIDIA所推出通用并行计算架构&#xff0c;该架构使GPU能够解…...

    2024/5/3 6:10:57
  9. Ubuntu18.04屏幕亮度调节

    之前用的Ubuntu16.04&#xff0c;屏幕太亮&#xff0c;看得眼睛疼&#xff0c;找了好久调节亮度的方法一直没搞好&#xff0c;现在有转到18.04了&#xff0c;又搞了好久&#xff0c;终于找到一中奏效的方法&#xff1a; 终端输入&#xff1a;sudo gedit /etc/default/grub&…...

    2024/4/10 4:31:29
  10. OJ常用代码块(Python语言)[ 1 ]

    1.计算频数 普通遍历 #假设查找某个字母出现的频数 # < s: list >for i in set(s):count 0 #count设置为计数器并初始化for j in s:if i j:count 1resultlist.append(tuple([i,count]))字典查频 利用字典的键值对来进行频数计算&#xff1a; #假设查找某个字母出…...

    2024/4/20 22:25:12
  11. 简单讲解一下http2的多路复用 每日一题系列(八)

    多路复用 HTTP2采用二进制格式传输&#xff0c;取代了HTTP1.x的文本格式&#xff0c;二进制格式解析更高效。 多路复用代替了HTTP1.x的序列和阻塞机制&#xff0c;所有的相同域名请求都通过同一个TCP连接并发完成。在HTTP1.x中&#xff0c;并发多个请求需要多个TCP连接&#x…...

    2024/4/25 0:57:43
  12. G1-SATB、CMS-增量更新(文章记录)

    https://blog.csdn.net/sai739295732/article/details/107358199 https://blog.csdn.net/RaymondCoder/article/details/106078984 https://blog.csdn.net/qq_36697880/article/details/105206385 里健康的哥们儿二面的时候问我对垃圾回收算法的理解&#xff0c;我通过给他分…...

    2024/4/1 18:13:37
  13. VS2013编译生成BOOSTC++静态链接库

    vs工具进入x64命令行终端&#xff0c;cmd进入不一定能生成x64的链接库进入boostC 目录 cd D:\Library\boost_1_73_0运行 bootstrap.bat 文件生成 b2.exe静态库生成 b2 --build-typecomplete toolsetmsvc-12.0 threadingmulti linkstatic address-model64 动态库生成 b2 --build…...

    2024/4/27 0:55:00
  14. Error: Redirected when going from “/newbtn“ to “/user“ via a navigation guar问题

    方案一 在router下的index.js最后添加以下代码 // 解决Vue-Router升级导致的Uncaught(in promise) navigation guard问题 const originalPush VueRouter.prototype.push VueRouter.prototype.push function push (location, onResolve, onReject) {if (onResolve || onRejec…...

    2024/4/1 16:51:38
  15. 架构师梳理4万字长篇PDF:程序员必备核心知识点,进入名企不是梦

    小编最近收集整理到一份非常全面的学习进阶资料&#xff0c;就迫不及待来与大家分享了&#xff0c;大概有四万字&#xff0c;篇幅太长不利于文章阅读&#xff0c;下面将是以图片形式进行一一展示。 这份资料覆盖了&#xff1a; JVM、Java集合、JAVA多线程并发、JAVA基础、Spri…...

    2024/4/21 14:42:47
  16. Unity Android由scheme导致APP图标消失的问题

    Unity Android项目通过集成aar插件引入新的Activity&#xff0c;并用WebView填充该Activity来展示Web版的支付SDK。玩家在Unity侧点击支付则弹出新Activity&#xff0c;支付成功后的WebView页面有一个按钮&#xff0c;需求是点击该按钮&#xff0c;关闭WebView界面&#xff0c;…...

    2024/4/28 2:45:46
  17. Log4j2进阶使用(Pattern Layout详细设置)

    1.进阶说明 通过配置Layout打印格式化的日志&#xff0c; Log4j2支持很多的Layouts&#xff1a; CSV GELF HTML JSON Pattern Serialized Syslog XML YAML 本文仅介绍Pattern Layouts的详细使用。 本文基于Log4j2基本使用入门。 请先参考上面的基本使用入门。 2.Pattern Layout…...

    2024/4/30 14:19:57
  18. python滑块验证(打码)+pillow裁剪图片

    用的图鉴的接口 缺口图背景图的接口识别很准&#xff0c;但要传2张图 背景图带滑块&#xff0c;需要裁调滑块部分 # x,y:426x248 from PIL import Image as image img image.open("D:\lpt_img\\img_bgk.png") # 按缩放比例 算出要裁剪的距离左边的x值 x_1int(2…...

    2024/4/1 18:13:34
  19. JAVA(五)————向上造型,方法的重写,重写与重载

    1.向上造型 1)超类型的引用指向派生类的对象 2)能点出来什么&#xff0c;看引用类型 2.方法的重写(override):重新写、覆盖 1)发生在父子类中&#xff0c;方法名相同&#xff0c;参数列表相同&#xff0c;方法体不同 2)重写方法被调用时&#xff0c;看对象的类型 3)重写方法…...

    2024/4/1 18:13:33
  20. 二十不惑的年纪,我简直走了狗屎运(4面拿字节跳动offer)

    文字内容太长&#xff0c;请耐心看完&#xff0c;或许对迷茫的你有所帮助&#xff0c;文章重点在后半部分。 前言 二十岁的年纪&#xff0c;青春张扬&#xff0c;无拘无束&#xff0c;这种状态自然是好事&#xff0c;不过在某种意义上&#xff0c;也并不能太过乐观。实际上&am…...

    2024/4/25 16:16:06

最新文章

  1. pytorch简单神经网络模型训练

    目录 一、导入包 二、数据预处理 三、定义神经网络 四、训练模型和测试模型 五、程序入口 一、导入包 import torch import torch.nn as nn import torch.optim as optim # 导入优化器 from torchvision import datasets, transforms # 导入数据集和数据预处理库 from tor…...

    2024/5/4 15:41:15
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. CSS3 高级- 复杂选择器、内容生成、变形(transform)、过渡(transition)、动画(animation)

    文章目录 一、复杂选择器兄弟选择器:选择平级元素的唯一办法属性选择器:1、通用:基本用不着,太泛了2、自定义:4种伪类选择器:1、目标伪类:2、结构伪类:3、元素状态伪类:4、伪元素选择器:应用于文字,使网页看起来想杂志5、否定伪类:选择器:not([本选择器的条件]) /*…...

    2024/5/3 10:26:16
  4. Kafka入门到实战-第五弹

    Kafka入门到实战 Kafka常见操作官网地址Kafka概述Kafka的基础操作更新计划 Kafka常见操作 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不一定100%复现, 还要以官方信息为准 https://kafka.apache.org/Kafka概述 Apache Kafka 是一个开源的分布式事件流平台&…...

    2024/5/1 13:31:04
  5. 416. 分割等和子集问题(动态规划)

    题目 题解 class Solution:def canPartition(self, nums: List[int]) -> bool:# badcaseif not nums:return True# 不能被2整除if sum(nums) % 2 ! 0:return False# 状态定义&#xff1a;dp[i][j]表示当背包容量为j&#xff0c;用前i个物品是否正好可以将背包填满&#xff…...

    2024/5/4 12:05:22
  6. 【Java】ExcelWriter自适应宽度工具类(支持中文)

    工具类 import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet;/*** Excel工具类** author xiaoming* date 2023/11/17 10:40*/ public class ExcelUti…...

    2024/5/4 11:23:32
  7. Spring cloud负载均衡@LoadBalanced LoadBalancerClient

    LoadBalance vs Ribbon 由于Spring cloud2020之后移除了Ribbon&#xff0c;直接使用Spring Cloud LoadBalancer作为客户端负载均衡组件&#xff0c;我们讨论Spring负载均衡以Spring Cloud2020之后版本为主&#xff0c;学习Spring Cloud LoadBalance&#xff0c;暂不讨论Ribbon…...

    2024/5/4 14:46:16
  8. TSINGSEE青犀AI智能分析+视频监控工业园区周界安全防范方案

    一、背景需求分析 在工业产业园、化工园或生产制造园区中&#xff0c;周界防范意义重大&#xff0c;对园区的安全起到重要的作用。常规的安防方式是采用人员巡查&#xff0c;人力投入成本大而且效率低。周界一旦被破坏或入侵&#xff0c;会影响园区人员和资产安全&#xff0c;…...

    2024/5/3 16:00:51
  9. VB.net WebBrowser网页元素抓取分析方法

    在用WebBrowser编程实现网页操作自动化时&#xff0c;常要分析网页Html&#xff0c;例如网页在加载数据时&#xff0c;常会显示“系统处理中&#xff0c;请稍候..”&#xff0c;我们需要在数据加载完成后才能继续下一步操作&#xff0c;如何抓取这个信息的网页html元素变化&…...

    2024/5/4 12:10:13
  10. 【Objective-C】Objective-C汇总

    方法定义 参考&#xff1a;https://www.yiibai.com/objective_c/objective_c_functions.html Objective-C编程语言中方法定义的一般形式如下 - (return_type) method_name:( argumentType1 )argumentName1 joiningArgument2:( argumentType2 )argumentName2 ... joiningArgu…...

    2024/5/3 21:22:01
  11. 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】

    &#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格…...

    2024/5/3 23:17:01
  12. 【ES6.0】- 扩展运算符(...)

    【ES6.0】- 扩展运算符... 文章目录 【ES6.0】- 扩展运算符...一、概述二、拷贝数组对象三、合并操作四、参数传递五、数组去重六、字符串转字符数组七、NodeList转数组八、解构变量九、打印日志十、总结 一、概述 **扩展运算符(...)**允许一个表达式在期望多个参数&#xff0…...

    2024/5/4 14:46:12
  13. 摩根看好的前智能硬件头部品牌双11交易数据极度异常!——是模式创新还是饮鸩止渴?

    文 | 螳螂观察 作者 | 李燃 双11狂欢已落下帷幕&#xff0c;各大品牌纷纷晒出优异的成绩单&#xff0c;摩根士丹利投资的智能硬件头部品牌凯迪仕也不例外。然而有爆料称&#xff0c;在自媒体平台发布霸榜各大榜单喜讯的凯迪仕智能锁&#xff0c;多个平台数据都表现出极度异常…...

    2024/5/4 14:46:11
  14. Go语言常用命令详解(二)

    文章目录 前言常用命令go bug示例参数说明 go doc示例参数说明 go env示例 go fix示例 go fmt示例 go generate示例 总结写在最后 前言 接着上一篇继续介绍Go语言的常用命令 常用命令 以下是一些常用的Go命令&#xff0c;这些命令可以帮助您在Go开发中进行编译、测试、运行和…...

    2024/5/4 14:46:11
  15. 用欧拉路径判断图同构推出reverse合法性:1116T4

    http://cplusoj.com/d/senior/p/SS231116D 假设我们要把 a a a 变成 b b b&#xff0c;我们在 a i a_i ai​ 和 a i 1 a_{i1} ai1​ 之间连边&#xff0c; b b b 同理&#xff0c;则 a a a 能变成 b b b 的充要条件是两图 A , B A,B A,B 同构。 必要性显然&#xff0…...

    2024/5/4 2:14:16
  16. 【NGINX--1】基础知识

    1、在 Debian/Ubuntu 上安装 NGINX 在 Debian 或 Ubuntu 机器上安装 NGINX 开源版。 更新已配置源的软件包信息&#xff0c;并安装一些有助于配置官方 NGINX 软件包仓库的软件包&#xff1a; apt-get update apt install -y curl gnupg2 ca-certificates lsb-release debian-…...

    2024/5/3 16:23:03
  17. Hive默认分割符、存储格式与数据压缩

    目录 1、Hive默认分割符2、Hive存储格式3、Hive数据压缩 1、Hive默认分割符 Hive创建表时指定的行受限&#xff08;ROW FORMAT&#xff09;配置标准HQL为&#xff1a; ... ROW FORMAT DELIMITED FIELDS TERMINATED BY \u0001 COLLECTION ITEMS TERMINATED BY , MAP KEYS TERMI…...

    2024/5/4 12:39:12
  18. 【论文阅读】MAG:一种用于航天器遥测数据中有效异常检测的新方法

    文章目录 摘要1 引言2 问题描述3 拟议框架4 所提出方法的细节A.数据预处理B.变量相关分析C.MAG模型D.异常分数 5 实验A.数据集和性能指标B.实验设置与平台C.结果和比较 6 结论 摘要 异常检测是保证航天器稳定性的关键。在航天器运行过程中&#xff0c;传感器和控制器产生大量周…...

    2024/5/4 13:16:06
  19. --max-old-space-size=8192报错

    vue项目运行时&#xff0c;如果经常运行慢&#xff0c;崩溃停止服务&#xff0c;报如下错误 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 因为在 Node 中&#xff0c;通过JavaScript使用内存时只能使用部分内存&#xff08;64位系统&…...

    2024/5/3 14:57:24
  20. 基于深度学习的恶意软件检测

    恶意软件是指恶意软件犯罪者用来感染个人计算机或整个组织的网络的软件。 它利用目标系统漏洞&#xff0c;例如可以被劫持的合法软件&#xff08;例如浏览器或 Web 应用程序插件&#xff09;中的错误。 恶意软件渗透可能会造成灾难性的后果&#xff0c;包括数据被盗、勒索或网…...

    2024/5/4 14:46:05
  21. JS原型对象prototype

    让我简单的为大家介绍一下原型对象prototype吧&#xff01; 使用原型实现方法共享 1.构造函数通过原型分配的函数是所有对象所 共享的。 2.JavaScript 规定&#xff0c;每一个构造函数都有一个 prototype 属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象…...

    2024/5/4 2:00:16
  22. C++中只能有一个实例的单例类

    C中只能有一个实例的单例类 前面讨论的 President 类很不错&#xff0c;但存在一个缺陷&#xff1a;无法禁止通过实例化多个对象来创建多名总统&#xff1a; President One, Two, Three; 由于复制构造函数是私有的&#xff0c;其中每个对象都是不可复制的&#xff0c;但您的目…...

    2024/5/3 22:03:11
  23. python django 小程序图书借阅源码

    开发工具&#xff1a; PyCharm&#xff0c;mysql5.7&#xff0c;微信开发者工具 技术说明&#xff1a; python django html 小程序 功能介绍&#xff1a; 用户端&#xff1a; 登录注册&#xff08;含授权登录&#xff09; 首页显示搜索图书&#xff0c;轮播图&#xff0…...

    2024/5/4 9:07:39
  24. 电子学会C/C++编程等级考试2022年03月(一级)真题解析

    C/C++等级考试(1~8级)全部真题・点这里 第1题:双精度浮点数的输入输出 输入一个双精度浮点数,保留8位小数,输出这个浮点数。 时间限制:1000 内存限制:65536输入 只有一行,一个双精度浮点数。输出 一行,保留8位小数的浮点数。样例输入 3.1415926535798932样例输出 3.1…...

    2024/5/4 14:46:02
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57