冒泡排序、插入排序、选择排序

本文详细介绍了排序算法中的冒泡排序、插入排序和选择排序,包括它们的执行效率、内存消耗和稳定性。冒泡排序在最好情况下时间复杂度为O(n),最坏情况为O(n^2),是稳定的排序算法;插入排序在最好情况下时间复杂度为O(n),最坏情况和平均情况均为O(n^2),也是稳定的排序算法;而选择排序在所有情况下时间复杂度均为O(n^2),且是不稳定的排序算法。
摘要由CSDN通过智能技术生成

在平时的项目中,我们遇到最多的算法应该就是排序了。其中最经典、最常用的算法有:冒泡排序、插入排序、选择排序、快速排序、归并排序、基数排序等。

1. 评判排序算法的标准

排序算法有很多种,那么我们该如何评判一个排序算法呢?一般情况下,我们可以从排序算法的执行效率、排序算法的内存消耗和排序算法的稳定性去考虑。

1.1 执行效率

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

在要排序的数据中,执行效率与原始数据是否有序有关,有的原始数据接近有序,有的原始数据完全无序,对于有序度不同的数据,去排序执行的时间肯定有影响,因此我们有必要知道最好情况、最坏情况和平均情况时间复杂度下的执行效率。

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

通过前面的学习,我们知道:时间复杂度反应的是数据规模n趋向无穷大时的一个增长趋势,在理想情况下我们会忽略系数、常数、低阶。但在实际开发过程中,我们排序的可能是10个、100个、1000个这样规模很小的数据,所以,在对同一阶时间复杂度的排序算法性能对比的时候,我们需要将系数、常数、低阶也考虑进去。

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

在基于比较排序算法的执行过程中,一般是进行比较元素大小或元素交换或移动。因此我们在分析排序算法的执行效率的时候,应该将比较次数和交换(移动)次数考虑进去。

1.2 内存消耗

算法的内存消耗可以用空间复杂度来衡量。在排序算法的空间复杂度中,有一个概念叫"原地排序":是指空间复杂度为O(1)的排序算法。

1.3 稳定性

排序算法中的稳定性是指:在待排序的数据中存在相等的元素,经过排序后,相等元素之间原有的先后顺序不变。

例如有如下一组数据:

2 6 3 8 9 3 7 

在上面的数据中有两个3。

经过某种排序算法之后,如果两个3的前后顺序没有改变,那么这个排序算法就叫做"稳定的排序算法";如果前后顺序发生变化,那么对应的排序算法就叫做"不稳定的排序算法"。

2. 冒泡排序
  • 冒泡排序操作的是相邻的两个数据

  • 每次冒泡操作都会对相邻的两个元素进行比较,看看是否满足大小关系要求,如果不满足就让它俩互换

  • 一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作

举例说明:

为了更好的理解冒泡排序,这里我们举一个例子:对4,5,6,3,2,1这六个数字从小到大进行排序。

第一次冒泡操作的详细过程如下:

在这里插入图片描述

通过上面的照片我们可以看到:经过第一次冒泡操作后,6这个元素已经存储在正确的位置上了。

如果想要完成全部数据的排序,那么我们就需要进行六次这样的冒泡操作,每次冒泡操作后的如下图所示:

在这里插入图片描述

实际上,在上面冒泡的过程中可以进行优化:当某次冒泡操作已经没有数据交换时,说明已经达到完全有序了,就不用在继续执行后续的冒泡操作。

举例说明:

这里给定6个元素,只需要进行4次冒泡操作就可以了,如下图所示:

在这里插入图片描述

示例代码:

结合前面的分析,我们可以得出冒泡排序的代码实现:

// 冒泡排序,a 表示数组,n 表示数组大小
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)。

最坏情况:

如果要排序的数据刚好是倒序的,那么我们就需要进行n次冒泡操作,这是最坏情况,此时的时间复杂度是O(n^2)。

平均情况:

平均时间复杂度就是加权平均期望时间复杂度,分析的时候要结合概率论的知识。

对于包含n个数据的数组,这n个数据就有n阶乘中排列方式。不同的排列方式,冒泡排序执行的时间肯定不同。

如果用概率论方法定量分析平均时间复杂度,涉及到数学推理和计算就会很复杂。不过有另外一种分析思路,通过“有序度”和“逆序度”这两个概念来分析。

有序度是数组中具有有序关系的元素对的个数。

数学表达式如下:

有序元素对:a[i] <= a[j], 如果 i < j。

举例说明:

在这里插入图片描述

同理,对于一个倒序排列的数组,比如6,5,4,3,2,1,有序度就是0;对于一个完全有序的数组,比如1,2,3,4,5,6,有序度就是n*(n-1)/2,也就是15。我们把这种完全有序的数组的有序度叫作 满有序度

**逆序度:**是数组中不具有有序关系的元素对的个数。

逆序度的定义正好跟有序度相反(默认从小到大为有序),数学表达式如下:

逆序元素对:a[i] > a[j], 如果 i < j。

上面讲了有序度、满有序度和逆序度的概念,它们之间有如下关系:

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

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

继续前面冒泡排序的例子来说明:要排序的数组的初始状态是4,5,6,3,2,1,其中有序元素对有(4,5)(4,6)(5,6),所以有序度是3。而n=6,所以排序完成之后终态的满有序度为n*(n-1)/2=15。

在这里插入图片描述

冒泡排序包含两个操作原子,比较交换

每交换一次,有序度就加1。不管算法怎么改进,交换次数总是确定的,即为逆序度,也就是n*(n-1)/2 - 初始有序度。上面的例子就是15-3=12,即要进行12次交换操作。

对于包含n个数据的数组进行冒泡排序:

最坏情况下,初始状态的有序度是0,所以要进行n*(n-1)/2次交换;

最好情况下,初始状态的有序度是n*(n-1)/2,就不需要进行交换。

这里我们可以取个中间值n*(n-1)/4,来表示初始有序度既不是很高也不是很低的平均情况。此时就需要n*(n-1)/4次交换操作,比较操作肯定要比交换操作多,而复杂度的上限是O(n2),所以平均情况下的时间复杂度就是O(n2)。

这种计算平均复杂度的过程并不严格,但相比于概率论的定量分析还是比较实用的!

2. 冒泡排序的内存消耗如何?

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

3. 冒泡排序稳定吗?

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

3. 插入排序

在插入排序中,我们将数组中的数据分为两个区间:已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组中的第一个元素。

插入排序算法的核心思想就是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。

举例说明:

要排序的初始数据是4,5,6,1,3,2,其中左侧为排序区间,右侧是未排序区间。

在这里插入图片描述

插入排序主要包含元素的比较和元素的移动两种操作。当我们需要将一个数据a插入到已排序区间时,需要将a与已经排序区间的元素依次比较大小,找到合适的插入位置。找到插入点后,还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素a插入。

对于不同的查找插入点方法(从头到尾、从尾到头),元素的比较次数是有区别的。但对于一个给定的初始序列,移动操作的次数是固定的,等于逆序度。

示例代码:

// 插入排序,a 表示数组,n 表示数组大小
public void insertionSort(int[] a, int n) {
  if (n <= 1) return;

  for (int i = 1; i < n; ++i) {
    int value = a[i];
    int j = i - 1;
    // 查找插入的位置
    for (; j >= 0; --j) {
      if (a[j] > value) {
        a[j+1] = a[j];  // 数据移动
      } else {
        break;
      }
    }
    a[j+1] = value; // 插入数据
  }
}

结合前面的三个算法评判标准来看看插入排序:

1. 插入排序的时间复杂度?

最好情况:

如果要排序的数据已经是有序的,那么我们就不需要移动任何数据。

如果在从尾到头的有序数据中找插入位置,每次只需要比较一个数据就能确定插入的位置,这是最好的情况,此时的时间复杂度是O(n)。

最坏情况:

如果数组是倒序的,每次插入都相当于在数组的第一个位置插入新的数据,这是最坏的情况,此时的时间复杂度是O(n^2)。

平均情况:

前面我们说了在数组中插入一个数据的平均时间复杂度是O(n),所以,对于插入排序来说,每次插入操作相当于在数组中插入一个数据,循环执行n次插入操作,所以平均情况的时间复杂度是O(n^2)。

2. 插入排序的内存消耗如何?

从上面的示例代码可以看出,插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是O(1),即插入排序是一个原地排序算法。

3. 插入排序稳定吗?

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

4. 选择排序

选择排序算法的实现思路也分已排序区间和未排序区间。选择排序是每次都会从未排序区间中找到最小的元素,然后再将其放到已排序区间的末尾。

举例说明:

要排序的初始数据是4,5,6,3,2,1,其中默认第一个数为排序区间,右侧其它的是未排序区间。

在这里插入图片描述

示例代码:

// 选择排序,a表示数组,n表示数组大小
public static void selectionSort(int[] a, int n) {
    if (n <= 1) return;

    for (int i = 0; i < n - 1; ++i) {
        // 查找最小值
        int minIndex = i;
        for (int j = i + 1; j < n; ++j) {
            if (a[j] < a[minIndex]) {
                minIndex = j;
            }
        }

        // 交换
        int tmp = a[i];
        a[i] = a[minIndex];
        a[minIndex] = tmp;
    }
}

结合前面的三个算法评判标准来看看插入排序:

1. 选择排序的时间复杂度?

选择排序的最好情况、最坏情况和平均情况的时间复杂度都是O(n^2)。

2. 选择排序的内存消耗如何?

选择排序的运行并不需要额外的存储空间,所以空间复杂度是O(1),即选择排序是一个原地排序算法。

3. 选择排序稳定吗?

选择排序每次都要从未排序元素中找最小值,并和前面的元素交换位置,这样就是破坏了稳定性。

例如5,8,5,2,9这样一组数据,使用选择排序算法排序的时候,第一次找到最小元素2,与第一个5交换位置,那第一个5和中间的5顺序就变了。所以选择排序不稳定。

非常感谢您的耐心阅读,希望我的文章对您有帮助。欢迎点评、转发或分享给您的朋友或技术群。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值