算法与数据结构-排序

文章目录

  • 一、如何分析一个排序算法
    • 1.1 排序算法的执行效率
      • 1.1.1 最好情况、最坏情况、平均情况时间复杂度
        • 1.1.1.1 最好、最坏情况分析
        • 1.1.1.2 平均情况分析
      • 1.1.2 时间复杂度的系数、常数 、低阶
      • 1.1.3 比较次数和交换(或移动)次数
    • 1.2 排序算法的内存消耗
    • 1.3 排序算法的稳定性
  • 二、排序算法分析
    • 2.1 冒泡排序
      • 2.1.1 算法代码
      • 2.1.2 算法分析
    • 2.2 插入排序
      • 2.2.1 算法代码
      • 2.2.2 算法分析
    • 2.3 选择排序
      • 2.3.1 算法代码
      • 2.3.2 算法分析
    • 2.4 归并排序
      • 2.4.1 算法代码
      • 2.4.2 算法分析
    • 2.5 快速排序
      • 2.5.1 算法代码
      • 2.5.2 算法分析
    • 2.6 桶排序
      • 2.6.1 算法代码
      • 2.6.2 算法分析
    • 2.7 计数排序
      • 2.7.1 算法代码
      • 2.7.2 算法分析


一、如何分析一个排序算法

1.1 排序算法的执行效率

1.1.1 最好情况、最坏情况、平均情况时间复杂度

1.1.1.1 最好、最坏情况分析

  我们在分析排序算法的时间复杂度时,要分别给出最好情况、最坏情况、平均情况下的时间复杂度。除此之外,你还要说出最好、最坏时间复杂度对应的要排序的原始数据是什么样的。

  为什么要区分这三种时间复杂度呢?第一,有些排序算法会区分,为了好对比,所以我们最好都做一下区分。第二,对于要排序的数据,有的接近有序,有的完全无序。有序度不同的数据,对于排序的执行时间肯定是有影响的,我们要知道排序算法在不同数据下的性能表现。

1.1.1.2 平均情况分析

  最好、最坏情况下的时间复杂度很容易分析,那平均情况下的时间复杂是多少呢?我们前面讲过,平均时间复杂度就是加权平均期望时间复杂度,分析的时候要结合概率论的知识。

  对于包含 n 个数据的数组,这 n 个数据就有 n! 种排列方式。不同的排列方式,冒泡排序执行的时间肯定是不同的。比如我们前面举的那两个例子,其中一个要进行 6 次冒泡,而另一个只需要 4 次。如果用概率论方法定量分析平均时间复杂度,涉及的数学推理和计算就会很复杂。我这里还有一种思路,通过“有序度”和“逆序度”这两个概念来进行分析。
在这里插入图片描述
  同理,对于一个倒序排列的数组,比如 6,5,4,3,2,1,有序度是 0;对于一个完全有序的数组,比如 1,2,3,4,5,6,有序度就是 n(n-1)/2*,也就是 15。我们把这种完全有序的数组的有序度叫作满有序度

  逆序度的定义正好跟有序度相反(默认从小到大为有序)。所以我们可以推导出一个公式:

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

我们排序的过程就是一种增加有序度,减少逆序度的过程,最后达到满有序度,就说明排序完成了。

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

  我们知道,时间复杂度反映的是数据规模 n 很大的时候的一个增长趋势,所以它表示的时候会忽略系数、常数、低阶。但是实际的软件开发中,我们排序的可能是 10 个、100 个、1000 个这样规模很小的数据,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们就要把系数、常数、低阶也考虑进来。

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

  基于比较的排序算法的执行过程,会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。所以,如果我们在分析排序算法的执行效率的时候,应该把比较次数和交换(或移动)次数也考虑进去。

1.2 排序算法的内存消耗

  算法的内存消耗可以通过空间复杂度来衡量,排序算法也不例外。不过,针对排序算法的空间复杂度,我们还引入了一个新的概念,原地排序(Sorted in place)。原地排序算法,就是特指空间复杂度是 O(1) 的排序算法。我们今天讲的三种排序算法,都是原地排序算法。

1.3 排序算法的稳定性

  针对排序算法,我们还有一个重要的度量指标,稳定性。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。举个例子,如果我们有个电商系统,先将订单按照ID排序,再使用相同的算法按照订单金额排序。经过两次排序后,如果金额相同的订单ID顺序保持不变则认为该算法稳定,否则认为该算法不稳定。

二、排序算法分析

2.1 冒泡排序

2.1.1 算法代码

    public static void bubbleSort(int[] arr) {int size = arr.length;for (int i = 0; i < size; i++) {boolean changeFlag = false;for (int j = 0; j < size - i - 1; j++) {if (arr[j] > arr[j + 1]) {int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;changeFlag = true;}}if (!changeFlag) {break;}}}

2.1.2 算法分析

在这里插入图片描述

  • 第一,冒泡排序是原地排序算法吗?
      冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个原地排序算法。

  • 第二,冒泡排序是稳定的排序算法吗?
      在冒泡排序中,只有交换才可以改变两个元素的前后顺序。为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法。

  • 第三,冒泡排序的时间复杂度是多少?
      最好情况下,要排序的数据已经是有序的了,我们只需要进行一次冒泡操作,就可以结束了,所以最好情况时间复杂度是 O(n)。而最坏的情况是,要排序的数据刚好是倒序排列的,我们需要进行 n 次冒泡操作,所以最坏情况时间复杂度为 O(n²)。
    在这里插入图片描述
      最坏情况下,初始状态的有序度是 0,所以要进行 n*(n-1)/2 次交换。最好情况下,初始状态的有序度是 n*(n-1)/2,就不需要进行交换。我们可以取个中间值 n*(n-1)/4,来表示初始有序度既不是很高也不是很低的平均情况。 换句话说,平均情况下,需要 n*(n-1)/4 次交换操作,比较操作肯定要比交换操作多,而复杂度的上限是 O(n²),所以平均情况下的时间复杂度就是 O(n²)。

2.2 插入排序

2.2.1 算法代码

    public static void insertSort(int[] arr) {int size = arr.length;for (int i = 1; i < size; i++) {int tmp = arr[i];int j = i - 1;for (; j >= 0; j--) {if (tmp < arr[j]) {arr[j + 1] = arr[j];} else {break;}}arr[j + 1] = tmp;}}

2.2.2 算法分析

在这里插入图片描述

  • 第一,插入排序是原地排序算法吗?
      从实现过程可以很明显地看出,插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1),也就是说,这是一个原地排序算法。

  • 第二,插入排序是稳定的排序算法吗?
      在插入排序中,对于值相同的元素,我们可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法。

  • 第三,插入排序的时间复杂度是多少?
      如果要排序的数据已经是有序的,我们并不需要搬移任何数据。如果我们从尾到头在有序数据组里面查找插入位置,每次只需要比较一个数据就能确定插入的位置。所以这种情况下,最好是时间复杂度为 O(n)。注意,这里是从尾到头遍历已经有序的数据。
      如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,所以需要移动大量的数据,所以最坏情况时间复杂度为 O(n²)。
      还记得我们在数组中插入一个数据的平均时间复杂度是多少吗?没错,是 O(n)。所以,对于插入排序来说,每次插入操作都相当于在数组中插入一个数据,循环执行 n 次插入操作,所以平均时间复杂度为 O(n²)。

2.3 选择排序

2.3.1 算法代码

    public static void selectSort(int[] arr) {int size = arr.length;for (int i = 0; i < size - 1; i++) {int minIndex = i;for (int j = i + 1; j < size; j++) {if (arr[j] < arr[minIndex]) {minIndex = j;}}int tmp = arr[i];arr[i] = arr[minIndex];arr[minIndex] = tmp;}}

2.3.2 算法分析

在这里插入图片描述

  • 第一,选择排序是原地排序算法吗?
      选择排序空间复杂度为 O(1),是一种原地排序算法。

  • 第二,选择排序是稳定的排序算法吗?
      选择排序是一种不稳定的排序算法,选择排序每次都要找剩余未排序元素中的最小值,并和前面的元素交换位置,这样破坏了稳定性。

  • 第三,选择排序的时间复杂度是多少?
      选择排序的最好情况时间复杂度、最坏情况和平均情况时间复杂度都为 O(n²)。

2.4 归并排序

2.4.1 算法代码

    public static void mergeSort(int[] arr, int startIndex, int endIndex) {if (startIndex >= endIndex) {return;}int middle = (startIndex + endIndex) / 2;mergeSort(arr, startIndex, middle);mergeSort(arr, middle + 1, endIndex);merge(arr, startIndex, endIndex);}public static void merge(int[] arr, int startIndex, int endIndex) {int[] arrNew = new int[endIndex - startIndex + 1];int middle = (startIndex + endIndex) / 2;int k = 0;int i = startIndex;int j = middle + 1;while (i <= middle && j <= endIndex) {if (arr[i] < arr[j]) {arrNew[k++] = arr[i++];} else {arrNew[k++] = arr[j++];}}int start = i;int end = middle;if (j <= endIndex) {start = j;end = endIndex;}while (start <= end) {arrNew[k++] = arr[start++];}for (int m = 0; m < arrNew.length; m++) {arr[startIndex++] = arrNew[m];}}

2.4.2 算法分析

在这里插入图片描述

  • 第一,归并排序是原地排序算法吗?
      通过代码可以看到,在合并数组时,需要开辟额外的数组空间来保证顺序,所以归并排序不是原地排序算法。

  • 第二,归并排序是稳定的排序算法吗?
      归并排序稳不稳定关键要看 merge() 函数,也就是两个有序子数组合并成一个有序数组的那部分代码。 在合并的过程中,如果 A[p…q]和 A[q+1…r]之间有值相同的元素,那我们可以像伪代码中那样,先把 A[p…q]中的元素放入 tmp 数组。这样就保证了值相同的元素,在合并前后的先后顺序不变。所以,归并排序是一个稳定的排序算法。

  • 第三,归并排序的时间复杂度是多少?
      可以看到,归并排序主要就是通过递归的方式,即那求解问题 a 就可以分解为求解问题 b、c。问题 b、c 解决之后,我们再把 b、c 的结果合并成 a 的结果。
      
      如果我们定义求解问题 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)。我们知道,merge() 函数合并两个有序子数组的时间复杂度是 O(n)。所以,套用前面的公式,归并排序的时间复杂度的计算公式就是:T(n) = 2*T(n/2) + n(当n=1时,T(1) = C)
      
      通过这个公式,如何来求解 T(n) 呢?还不够直观?那我们再进一步分解一下计算过程。
      T(n) = 2*T(n/2) + n
         = 2(2T(n/4) + n/2) + n = 4T(n/4) + 2n
         = 4(2T(n/8) + n/4) + 2*n = 8T(n/8) + 3n
         = 8(2T(n/16) + n/8) + 3*n = 16T(n/16) + 4n
        
         = 2^k * T(n/2^k) + k * n
        
      通过这样一步一步分解推导,我们可以得到 T(n) = 2kT(n/2k)+kn。当 T(n/2k)=T(1) 时,也就是 n/2^k=1,我们得到 k=log2n 。我们将 k 值代入上面的公式,得到 T(n)=Cn+nlog2n 。如果我们用大 O 标记法来表示的话,T(n) 就等于 O(nlogn)。所以归并排序的时间复杂度是 O(nlogn)。
      
      从我们的原理分析和代码可以看出,归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。

2.5 快速排序

2.5.1 算法代码

    public static void quickSort(int[] arr, int startIndex, int endIndex) {if (startIndex >= endIndex) {return;}int part = part(arr, startIndex, endIndex);quickSort(arr, startIndex, part - 1);quickSort(arr, part + 1, endIndex);}public static int part(int[] arr, int startIndex, int endIndex) {int midValue = arr[endIndex];int i = startIndex;int j = startIndex;for (; i < endIndex; i++) {if (arr[i] < midValue) {if (j == i) {j++;} else {int tmp = arr[i];arr[i] = arr[j];arr[j] = tmp;j++;}}}arr[endIndex] = arr[j];arr[j] = midValue;return j;}

2.5.2 算法分析

在这里插入图片描述

  • 第一,快速排序是原地排序算法吗?
      通过代码可以看到,快速排序不需要开辟单独的空间,所以快速排序是原地排序算法。

  • 第二,快速排序是稳定的排序算法吗?
      通过代码可以看出来,快速排序是基于“分而治之”思想的数据交换,所以快速排序不是稳定的排序算法。

  • 第三,快速排序的时间复杂度是多少?
      快排也是用递归来实现的。对于递归代码的时间复杂度,我前面总结的公式,这里也还是适用的。如果每次分区操作,都能正好把数组分成大小接近相等的两个小区间,那快排的时间复杂度递推求解公式跟归并是相同的。所以,快排的时间复杂度也是 O(nlogn)。
      
      但是,公式成立的前提是每次分区操作,我们选择的 pivot 都很合适,正好能将大区间对等地一分为二。但实际上这种情况是很难实现的。
      
      我举一个比较极端的例子。如果数组中的数据原来已经是有序的了,比如 1,3,5,6,8。如果我们每次选择最后一个元素作为 pivot,那每次分区得到的两个区间都是不均等的。我们需要进行大约 n 次分区操作,才能完成快排的整个过程。每次分区我们平均要扫描大约 n/2 个元素,这种情况下,快排的时间复杂度就从 O(nlogn) 退化成了 O(n2)。
      
      但是我们可以通过首中尾三点取中法来尽量避免取到极限值的情况。首中尾三点取中法的意思就是取第一个节点、中间的节点、最后一个节点三者进行比较,取中间值作为分界点。

2.6 桶排序

2.6.1 算法代码

    public static void bucketSort(int[] arr, int bucketSize) {// 取最大最小值int minValue = arr[0];int maxValue = arr[0];int size = arr.length;for (int i = 0; i < size; i++) {if (arr[i] < minValue) {minValue = arr[i];}if (arr[i] > maxValue) {maxValue = arr[i];}}// 计算桶的个数int bucketCount = (maxValue - minValue) / bucketSize + 1;// 定义桶int[][] bucketArr = new int[bucketCount][bucketSize];// 记录每个桶里数据量int[] bucketIndexArr = new int[bucketCount];// 循环将放入桶中for (int i = 0; i < size; i++) {int bucketIndex = (arr[i] - minValue) / bucketSize;bucketArr[bucketIndex][bucketIndexArr[bucketIndex]] = arr[i];bucketIndexArr[bucketIndex]++;}// 为每个桶排序,并将排序好的桶数据重设到数组里int k = 0;for (int i = 0; i < bucketCount; i++) {if (bucketIndexArr[i] == 0) {continue;}// 这里使用的是快速排序,代码省略QuickSort.quickSort(bucketArr[i], 0, bucketIndexArr[i] - 1);for (int j = 0; j < bucketIndexArr[i]; j++) {arr[k++] = bucketArr[i][j];}}}

2.6.2 算法分析

在这里插入图片描述

  • 第一,桶排序是原地排序算法吗?
      通过代码我们可以看到,桶排序需要新的桶(数组)来存储数据,那么桶排序算法不是原地排序算法。

  • 第二,桶排序是稳定的排序算法吗?
      桶排序需要借助其他排序算法,由于我们这里借助的是快速排序(不稳定),所以这里的桶排序算法是不稳定的排序算法。

  • 第三,桶排序的时间复杂度是多少?
      如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k=n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。
      
      桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。

2.7 计数排序

2.7.1 算法代码

// 计数排序,a是数组,n是数组大小。假设数组中存储的都是非负整数。
public void countingSort(int[] a, int n) {if (n <= 1) return;// 查找数组中数据的范围int max = a[0];for (int i = 1; i < n; ++i) {if (max < a[i]) {max = a[i];}}int[] c = new int[max + 1]; // 申请一个计数数组c,下标大小[0,max]for (int i = 0; i <= max; ++i) {c[i] = 0;}// 计算每个元素的个数,放入c中for (int i = 0; i < n; ++i) {c[a[i]]++;}// 依次累加for (int i = 1; i <= max; ++i) {c[i] = c[i-1] + c[i];}// 临时数组r,存储排序之后的结果int[] r = new int[n];// 计算排序的关键步骤,有点难理解for (int i = n - 1; i >= 0; --i) {int index = c[a[i]]-1;r[index] = a[i];c[a[i]]--;}// 将结果拷贝给a数组for (int i = 0; i < n; ++i) {a[i] = r[i];}
}

2.7.2 算法分析

在这里插入图片描述

  • 第一,计数排序是原地排序算法吗?
      通过代码可以看到,我们需要一个单独的数组存储计数信息,所以计数排序不是原地排序算法。

  • 第二,计数排序是稳定的排序算法吗?
      通过代码可以看到,在计数排序中,如果两个元素的计数结果不同,它们的相对顺序就会改变,因此计数排序是不稳定的排序算法。

  • 第三,计数排序的时间复杂度是多少?
      通过代码我们可以看到,计数排序的时间复杂度是O(n)。
      但是计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

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

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

相关文章

【PostgreSQL内核学习(二)—— 查询分析】

查询分析 查询处理查询分析查询处理与查询分析的关系查询分析执行流程Lex和YaccLex&#xff1a;Yacc&#xff1a;词法分析工具Lex语法分析工具Yacc使用Lex和Yacc的案例 词法和语法分析以SELECT语句为例讲解 PostgreSQL中查询语句如何被解析并生成分析树。 语义分析 声明&#x…

图片速览 DCN K-means-friendly Spaces: Simultaneous Deep Learning and Clustering

本文使用了一种交替更新网络参数和聚类中心的方法。在网络更新完成之后&#xff0c;对于固定的网络参数和 M&#xff0c;再更新当前样本的分配向量。然后根据新的分配结果如式子3.8更新聚类中心&#xff1a; 注&#xff1a;文中还有问题是否能进行凸优化的部分 CG https…

什么是70v转12v芯片?

问&#xff1a;什么是70v转12v芯片&#xff1f; 答&#xff1a;70v转12v芯片是一种电子器件&#xff0c;其功能是将输入电压范围在9v至100v之间的电源转换为稳定的12v输出电压。这种芯片通常被用于充电器、车载电池充电器和电源适配器等设备中。 问&#xff1a;这种芯片的最大…

学习babylon.js --- [3] 开启https

babylonjs提供WebVR功能&#xff0c;但是使用这个功能得用https&#xff0c;本文讲述如何使用自签名证书来开启https&#xff0c;基于第二篇文章中搭建的工程。 一 生成自签名证书 首先要安装openssl&#xff0c;这个去网上搜下就行了。安装完之后在终端下输入openssl回车可以…

Python测试框架Pytest的基础入门

Pytest简介 Pytest is a mature full-featured Python testing tool that helps you write better programs.The pytest framework makes it easy to write small tests, yet scales to support complex functional testing for applications and libraries. 通过官方网站介绍…

容器云平台监控告警体系(四)—— Golang应用接入Prometheus

1、概述 目前容器云平台中的容器仅支持获取CPU使用率、内存使用率、网络流入速率和网络流出速率这4个指标&#xff0c;如果想监控应用程序的性能指标或者想更加细粒度的监控应用程序的运行状态指标的话&#xff0c;则需要在应用程序中内置对Prometheus的支持或者部署独立于应用…

OpenCV for Python 学习第四天 :通道的获取与合并

上一篇博客&#xff0c;我们学习了如何通过更快的item()和itemset()的方法访问图片&#xff0c;以及了解了图像的兴趣位置的获取方法&#xff0c;那么今天&#xff0c;我们将学习通道的处理方法&#xff0c;通过通道的拆分和合并的实例&#xff0c;让大家更好的了解咱们有关于B…

架构训练营学习笔记:4-2 存储架构模式之复制架构

高可用的关键指标 问题&#xff1a;分为故障跟灾难。不是有了多活架构就不在用复制架构 &#xff0c;还是之前的合适原则&#xff1a;多活架构的技术复杂度 跟成本都比复制架构高。 高可用的关键指标 恢复时间目标(RecoveryTimeObjective&#xff0c;RTO)指为避免在灾难发生后…

EasyCVR录像阈值配置未生效,是什么原因?

有用户反馈&#xff0c;在平台中设置了录像阈值不生效&#xff0c;导致磁盘爆满。针对该反馈&#xff0c;我们立即进行了排查。 EasyCVR基于云边端一体化架构&#xff0c;可支持多协议、多类型设备接入&#xff0c;在视频能力上&#xff0c;平台可实现视频直播、录像、回放、检…

软件测试测试用例

等价类&#xff1a;把输入的数据可以分为有效的数据和无效的数据 被测试的对象输入的数据&#xff1a; 1、有效的数据 2、无效的数据 测试一个产品&#xff0c;需要考虑它的正确场景&#xff0c;也需要考虑它的异常场景 边界值:边界值测试用例是针对等价类测试用例方法的补…

Stable Diffusion学习笔记

一些零散笔记 灰常好的模型网站 LiblibAI哩布哩布AI-中国领先原创AI模型分享社区 出图效率倍增&#xff01;47个高质量的 Stable Diffusion 常用模型推荐 - 优设网 - 学设计上优设 关键词Prompt顺序 画质 风格 主体 外表、描述 表情、情绪 姿势 背景 杂项 同时可以…

星火认知大模型,让我感受到了国产AI的崛起

文章目录 一、申请和测试代码二、实测GPT4.0和星火认知大模型的对比2.1 测试网站2.2 经典问题提问对比2.3 代码问题提问对比2.4 论文问题对比2.5 评价 一、申请和测试代码 在我之前的一篇文章中&#xff0c;我分享了如何申请星火认知大模型的内测&#xff0c;并提供了一份可以…