面试必备:深刻了解冒泡、选择和插入排序的优缺点

前言

相信排序对于每个程序员来讲都不会陌生,极可能你学的第一个算法就是排序,尤为冒泡排序你们可能都是信手拈来,可是当从学校走入了职场以后,这些经典的排序已经慢慢淡出了咱们的视线,由于在平常开发中,高级语言已经帮咱们封装好了排序算法,好比 C 语言中 qsort(),C++ STL 中的 sort()、stable_sort(),还有 Java 语言中的 Collections.sort(),这些算法无疑是写得很是好的,可是咱们做为有追求的程序员,对这些经典的排序算法仍是有必要了解得更透彻一点。程序员

本章,咱们一块儿来探讨一下三个经典排序算法:冒泡、选择和插入排序。算法

思考

咱们都知道,在分析一个算法的好坏的时候,咱们第一反应就是分析它们的时间复杂度,好的算法时间复杂度天然会低,此外,空间复杂度也是衡量它们好坏的标准,好的算法的确也会在空间复杂度上作的比较好。数组

诚如上述,时间复杂度、空间复杂度基本是衡量算法的标准,可是对于排序算法来讲,咱们须要考虑更多,那么还有什么因素会影响排序算法的好坏呢?缓存

影响因素

带着问题,下面咱们一块儿从如下方面探讨排序算法的优点和劣势。bash

1.时间复杂度

最好、最坏、平均时间复杂度性能

通常咱们所说的时间复杂度都是平均时间复杂度,可是若是须要细分,时间复杂度还分最好状况、最坏状况和平均状况的时间复杂度,因此咱们须要经过这三种状况来分析排序算法的执行效率分别是什么。优化

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

咱们知道(平均)时间复杂度反应的是当n很是大的时候的一个增加趋势,这时候它的系数、常数、低阶每每都会被咱们忽略掉。可是当咱们排序的数据只有100、1000或者10000时,它们就不能被咱们忽略掉了。spa

比较次数和交换次数3d

进行排序时,咱们每每须要进行比较大小和交换位置,当咱们比较执行效率的时候,这些数据也须要考虑进去。

2.空间复杂度

算法的内存消耗能够经过空间复杂度来表示,这里有个概念,咱们称空间复杂度为O(1)的排序算法为原地排序算法

3.排序算法的稳定性

排序算法的稳定性是指,在排序过程当中,值相同的元素间的相对位置跟排序前的相对位置是同样的。举个例子,排序前一个数组为{3, 2, 1, 2', 4},咱们用2'来区分第二个2和第一个2,假如是稳定的排序算法,它的结果必定是这样{1, 2, 2', 3, 4},而若是不稳定的算法,它的结果有多是这样{1, 2', 2, 3, 4}。

为何咱们要强调稳定性呢?举个例子,假如咱们须要排序一个订单,须要按照时间和价格进行升序排序,首先会先将全部订单按时间升序排序,而后再进行价格的升序排序,假如价格排序不是一个稳定的排序,那么订单的时间就有可能不会按升序排列,因此在特定状况下,排序算法的稳定性是一个比较重要的考虑因素。

冒泡排序

咱们先从冒泡排序开始分析。

冒泡排序原理是让相邻的两个元素进行比较,看是否知足大小关系,若是不知足则交换位置,每一次冒泡会让一个元素放到属于它的位置,而后进行n轮冒泡,即完成冒泡排序。

举个例子,假如咱们须要对数组{5,4,3,1,8,2}进行升序排序,那么第一轮冒泡后,8就冒泡到了最后一个位置,由于它是最大值。

image

如此重复6轮,以下图所示,便可将数组的每个元素冒泡到本身的位置,从而完成排序。

image

固然,这是没有通过优化的冒泡排序,事实上当数组再也不进行数据交换时,咱们就能够直接退出,由于此时已是有序数组。具体代码以下:

public void bubbleSort(int[] a, int n) {
  if (n <= 1) return;
 
 for (int i = 0; i < n; ++i) {
    // 退出冒泡的标志
    boolean flag = false;
    for (int j = 0; j < n - i - 1; ++j) {
      if (a[j] > a[j+1]) { 
        // 交换
        int tmp = a[j];
        a[j] = a[j+1];
        a[j+1] = tmp;
        // 表示有数据交换    
        flag = true;   
      }
    }
    // 没有数据交换,提早退出
    if (!flag) 
        break; 
  }
}
复制代码

结合上述代码,咱们总结冒泡排序的特性。

1.时间复杂度

当数组是有序时,那么它只要遍历它一次便可,因此最好时间复杂度是O(n);若是数据是逆序时,这时就是最坏时间复杂度O(n^2),那么平均时间复杂度怎么算呢?

计算平均时间复杂度会比较麻烦,由于涉及到几率论定量分析,因此能够用如下方法来计算,以上面排序数组为例子,先来理解一下下面的概念:

  • 有序度:有序度是指一个数组有序数对的个数,例如上面数组为排序前有序数对为(4,5),(4,8),(3,5),(3,8),(1,5),(1,2),(1,8),(5,8),(2,8),因此有序度是9。
  • 满有序度:满有序度是指一个数组处于有序状态时的有序数对,仍是上面数组排序完成后的有序数对有15个,用等差数列求和,得出求满有序度的通用公式为n(n-1)/2。
  • 逆序度:逆序度正好和有序度相反,因此上面数组对应的值是15-9=6,即逆序度=满有序度-有序度。

理解了这三个概念后,咱们就能够知道,其实将数组排序就是有序度增长,逆序度减少的过程,因此咱们排序的过程其实也是求逆序度大小的过程。

那么咱们就能够计算冒泡排序的平均时间复杂度了,最好状况下,逆序度是0,最坏状况下,逆序度是n(n-1)/2,那么平均时间复杂度就取中间值,即n(n-1)/4,因此简化掉系数、常数和低阶后,平均时间复杂度为O(n^2)。

2.空间复杂度

因为冒泡排序只须要常量级的临时空间,因此空间复杂度为O(1),是一个原地排序算法。

3.稳定性

在冒泡排序中,只有当两个元素不知足条件的时候才会须要交换,因此只有后一个元素大于前一个元素时才进行交换,这时的冒泡排序是一个稳定的排序算法。

选择排序

选择排序的原理是从未排序部分选择最小的一个元素放到已排序部分的后一个位置,最后使得数组有序的过程。

跟冒泡排序同样,咱们分析选择排序时也是对数组{5,4,3,1,8,2}进行排序,整个排序过程以下图所示。

image

通过6次循环,完成了数组排序,具体实现代码以下:

public static int[] SelectSort(int[] array) {
	for(int i = 0;i < array.length; i++) {
		int index = i;
		for(int j = i; j < array.length ;j++) {
		    // 找出每一轮的最小值
			if(array[j] < array[index]) {
				index = j;
			}
		}
		// 和已排序部分的后一个位置进行交换
		int temp = array[i];
		array[i] = array[index];
		array[index] = temp;
	}
    return array;
}
复制代码

经过上面的排序过程图和代码对选择排序进行分析。

1.时间复杂度

选择排序的最好状况时间复杂度、最坏状况和平均状况时间复杂度都是O(n^2)。由于原数组不管是否有序,进行排序时都是须要每个元素对进行比较,当比较完成后还要进行一次交换,因此每一种状况的时间复杂度都是O(n^2)。

2.空间复杂度

由于只须要用到临时空间,因此是一个原地排序算法,空间复杂度为O(1)。

3.稳定性

以下图所示,咱们排序{5,5',1},得知排序后两个5的位置已经交换,因此不是稳定排序。

image

插入排序

先想一下,当咱们往有序数组里面插入一个元素,而且使得它插入后保持数组的有序性,咱们应该如何操做呢?以下图。

image

插入排序就是上面图示的操做,从无序数组中拿到一个元素,而后往有序数组里面寻找它的位置,而后插入,有序数组元素加一,无序数组减一,直至无序数组的长度为0。

同上两个排序算法同样,咱们使用插入排序对数组{5,4,3,1,8,2}进行排序,流程以下图所示。

image

如上述流程能够知道,每一次从无序数组中抽第一个元素,缓存起来,而后在从已排序的最后一个元素开始比较,当该元素大于已排序元素,已排序元素后移一位,直到遇到比他小的元素,而后在比他小的元素的后一个位置插入缓存起来的元素,这样已排序数组增长一个元素,未排序数组减小一个元素,直至结束。

综上,能够得知算法以下:

public void insertSort(int[] arr, int n) {
	if (n <= 0) {
		return;
	}

	for (int i = 0; i < n; i++) {
		int temp = arr[i];
		// 从有序数组的最后一个元素开始往前找
		int j=i-1;
		while(j>=0) {
			if (arr[j] > temp) {
			    // 若是当前元素大于temp,则后移
				arr[j+1] = arr[j];
				j--;	
			} else {
			    // 若是当前元素小于temp,说明temp比前面全部都要大
				break;
			}
		} 
		// 插入
		arr[j+1] = temp;
	}
}
复制代码

经过上面的排序过程图和代码对插入排序进行分析。

1.时间复杂度

最好时间复杂度为当数组为有序时,为O(n);最坏时间复杂度为当数组逆序时,为O(n^2)。已知,往一个有序数组插入一个元素的平均时间复杂度为O(n),那么进行了n次操做,因此平均时间复杂度为O(n^2)。

2.空间复杂度

插入排序为原地排序,因此空间复杂度为O(1)。

3.稳定性

插入排序每一次插入的位置都会大于或者等于前一个元素,因此为稳定排序。

三者比较

通过上述对三个时间复杂度均为O(n^2)的算法进行分析,能够知道它们的差别以下表所示:

image

经过对比能够看到,冒泡和插入排序都是优于选择排序的,可是插入排序每每会比冒泡排序更容易被选择使用,这是为何呢?

虽说冒泡和插入在时间复杂度、空间复杂度、稳定上表现都是同样的,可是咱们别忽略了一个事情,咱们求出来的时间复杂度是忽略了系数、常数和低阶参数的,回看代码咱们知道,冒泡和插入在进行交换元素的时候分别是以下这样的,假如一次赋值操做耗时为K,那么冒泡排序执行一次交换元素就须要3K时间,而插入排序只须要K。

// 冒泡排序交换元素
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
复制代码
// 插入排序交换元素
arr[j+1] = arr[j];
复制代码

固然,这只是理论分析,当我用我本身的机器,分别用冒泡和插入算法对10000个一样的数组(每一个数组200个元素),进行排序,冒泡排序耗时490ms,插入排序耗时8ms,因此可见插入排序因为每次比较的耗时比较短,因此总体来讲耗时也会比冒泡要少。

总结

虽然这三种排序的复杂度较高,达到O(n^2),冒泡和选择排序也不可能使用到应用实践中去,都只能停留在理论研究上,而插入排序仍是以其原地排序和稳定性可以在某些特定的场景中使用。

相关文章
相关标签/搜索