排序算法——冒泡排序VS插入排序

一、概述

       最经典、最常用的排序算法有:冒泡排序、插入排序、选择排序、归并排序、快速排序、计数排序、基数排序、按照时间复杂度分为三类。如下图所示

                                                     排序算法——冒泡排序VS插入排序_第1张图片

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

       (1)排序算法的执行效率

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

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

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

       (2)排序算法的内存消耗

        算法的内存消耗可以通过空间复杂度来衡量,排序算法也不例外。针对排序算法的空间复杂度,引入了新概念,原地排序

原地排序算法,就是特指空间复杂度为O(1)的排序算法

       (3)排序算法的稳定性

         针对排序算法,还有一个重要的衡量指标:稳定性。所谓稳定性就是说如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。经过排序算法之后,如果值相等的前后顺序没有改变,那我们把这种排序算法叫做稳定的排序算法

三、冒泡排序

   冒泡排序的英文Bubble Sort,是一种最基础的交换排序。冒泡排序的思想是,相邻的元素两两比较,根据大小来交换元素的位置。

       原始的冒泡排序是稳当排序,冒泡排序在每一轮只把一个元素冒泡到数列的一端,也就是有序区。由于该排序算法的每一轮都要遍历所有元素轮转的次数和元素数量相当,所以时间复杂度是O(N²)。

 

public class BubbleSort {
    //从小到大排序
    private static void sort(int array[]) {
        for (int i = 0; i < array.length; i++) {
            for (int j = 0; j < array.length - i - 1; j++) {
                if (array[j] > array[j + 1]) {
                    int tmp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = tmp;
                }
            }
        }
    }

    public static void main(String[] args) {
        int [] array=new int[]{5,8,6,3,9,2,1,7};
        sort(array);
        System.out.println(Arrays.toString(array));
    }
}

      ①使用双重循环来进行排序。外部循环控制所有的回合,内部循环代表每一轮的冒泡处理。先进行元素比较,再进行元素交换

四、冒泡排序总结

       1、第一,冒泡排序是原地排序算法吗?

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

       2、第二,冒泡排序是稳定的排序算法吗?

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

       3、第三,冒泡排序的时间复杂度是多少?

        最好情况下,要排序的数据已经是有序的了,我们只需要进行1次冒泡操作,就可以结束了,所以最好情况时间复杂度是O(n)。而最坏情况是,要排序的数据是倒序排列的,我们需要进行n次冒泡操作,所以最坏情况时间复杂度是O(n²)。那么平均时间复杂度是多少呢?平均时间复杂度就是加权平均期望时间复杂度。

        如果用概率论方法定量分析平均时间复杂度,涉及的数学推理和计算很复杂。所以通过“有序度”和“逆序度”来进行分析。

        有序度:数组中具有有序关系的元素对的个数。有序元素对用数学表达式就是这样:

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

                                排序算法——冒泡排序VS插入排序_第2张图片

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

逆序度的定义正好和有序度相反(默认从小到大为有序

逆序度=满有序度-有序度。我们排序的过程就是一种增加有序度,减少逆序度,最终达到满有序度的过程

       要排序的数组初始状态是4,5,6,3,2,1。其中,有序元素对有(4,5)(4,6)(5,6)。所以有序度是3.n=6,所以满有序度为n*(n-1)/2=15。

                                                   排序算法——冒泡排序VS插入排序_第3张图片

       冒泡排序包含两个原子操作:比较交换。每交换一次,有序度就加1。不管算法怎么改进,交换次数总是确定的,即为逆序度,也就是n*(n-1)/2-初始有序度。此例中就是15-3=12.要进行12次交换操作

五、插入排序

       插入排序的英文是Insertion Sort。首先来看一个问题。一个有序的数组,我们往里面添加一个新的数据后,如何继续保持数据有序呢?很简单,我们只要遍历数组,找到数据应该插入的位置将其插入即可。示意图如下

                                               排序算法——冒泡排序VS插入排序_第4张图片

插入排序如何借助上面的思想来实现排序的呢?

       首先,我们将数组中的数据分为两个区间,有序区无序区。初始有序区只有1个元素,就是数组的第一个元素。插入算法的核心思想是取无序区中的元素,在有序区中找到合适的插入位置将其插入,并保证有序区数据一直有序。重复这个过程,直到无序区中元素为空,算法结束

       如图所示,要排序的数据是4,5,6,1,3,2,其中左侧为有序区,右侧为无序区。

                                  排序算法——冒泡排序VS插入排序_第5张图片

插入排序代码如下:

public class InsertionSort {
    //插入排序,a表示数组,n表示数组大小
    public static void insertionSort(int[] a) {
        int n = a.length;
        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;//插入数据
        }
    }

    public static void main(String[] args) {
        int[] array = new int[]{5, 8, 6, 3, 9, 2, 1, 7};
        //sort(array);
        insertionSort(array);
        System.out.println(Arrays.toString(array));
    }
}

六、选择排序

      选择排序的英文是Selection Sort

七、为什么插入排序比冒泡排序更受欢迎

       冒泡排序和插入排序的时间复杂度都是O(n²),都是原地排序算法,为什么插入排序要比冒泡排序更受欢迎呢?

       上文分析冒泡排序和插入排序的时候讲到,冒泡排序不管怎么优化,元素交换的次数是一个固定值。是原始数据的逆序度。插入排序是同样的,不管怎么优化,元素移动的次数也等于原始数据的逆序度

       但是,从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要3个赋值操作,而插入排序只需要1个。我们来看这段操作。

冒泡排序中数据的交换操作:
if (a[j] > a[j+1]) { // 交换
   int tmp = a[j];
   a[j] = a[j+1];
   a[j+1] = tmp;
}

插入排序中数据的移动操作:
if (a[j] > value) {
  a[j+1] = a[j];  // 数据移动
} else {
  break;
}

 

你可能感兴趣的:(数据结构与算法,决战面试)