数据结构和算法-O(n2)-排序算法

217 阅读3分钟

前言

平均时间复杂度为O(n2)的经典排序算法有以下三个

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

冒泡排序

实现

    public static void bubbleSort(int[] numbers) {
        for(int i=0; i<numbers.length; i++) {
            boolean flag = false;
            for(int j=0; j<numbers.length-i-1; j++) {
                if(numbers[j] > numbers[j+1]) {
                    int tmp = numbers[j];
                    numbers[j] = numbers[j+1];
                    numbers[j+1] = tmp;
                    flag = true;
                }
            }
            // 有序提前结束
            if(!flag) {
                break;
            }
        }
    }

时间复杂度

  • 最好:O(n)
  • 最坏:O(n2)
  • 平均:O(n2)

空间复杂度

原地排序:O(1)

稳定性

冒泡排序是稳定的,排序之后相同元素的先后顺序保持不变。

选择排序

实现

    public static void selectSort(int[] numbers) {
        for(int i=0; i<numbers.length; i++){
            int index = i;
            for(int j=index; j<numbers.length; j++){
                if(numbers[j] < numbers[index]) {
                    index = j;
                }
            }
            int tmp = numbers[i];
            numbers[i] = numbers[index];
            numbers[index] = tmp;
        }
    }

时间复杂度

  • 最好:O(n2)
  • 最坏:O(n2)
  • 平均:O(n2)

空间复杂度

原地排序:O(1)

稳定性

选择排序是不稳定的,排序之后相同元素的先后顺序有可能发生改变。

插入排序

实现

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

时间复杂度

  • 最好:O(n)
  • 最坏:O(n2)
  • 平均:O(n2)

空间复杂度

原地排序:O(1)

稳定性

选择排序是稳定的,排序之后相同元素的先后顺序保持不变。

性能测试比较

样本量为10,使用这3种算法分别对长度为10,100,1000,5000,10000的乱序数组进行排序

测试环境

> java -version
openjdk version "11.0.12" 2021-07-20 LTS
OpenJDK Runtime Environment Corretto-11.0.12.7.2 (build 11.0.12+7-LTS)
OpenJDK 64-Bit Server VM Corretto-11.0.12.7.2 (build 11.0.12+7-LTS, mixed mode)
Hardware:
    Hardware Overview:
      Processor Name: Quad-Core Intel Core i5
      Processor Speed: 2 GHz
      Number of Processors: 1
      Total Number of Cores: 4
      L2 Cache (per Core): 512 KB
      L3 Cache: 6 MB
      Hyper-Threading Technology: Enabled
      Memory: 16 GB

报告

样本数据:10, 数组长度:10
冒泡排序-avg-cost:0.2 ms
选择排序-avg-cost:0.0 ms
插入排序-avg-cost:0.0 ms

样本数据:10, 数组长度:100
冒泡排序-avg-cost:0.2 ms
选择排序-avg-cost:0.1 ms
插入排序-avg-cost:0.1 ms

样本数据:10, 数组长度:1000
冒泡排序-avg-cost:1.3 ms
选择排序-avg-cost:0.9 ms
插入排序-avg-cost:0.8 ms

样本数据:10, 数组长度:5000
冒泡排序-avg-cost:15.9 ms
选择排序-avg-cost:6.4 ms
插入排序-avg-cost:2.0 ms

样本数据:10, 数组长度:10000
冒泡排序-avg-cost:90.5 ms
选择排序-avg-cost:24.7 ms
插入排序-avg-cost:7.2 ms

从测试结果可以看出,虽然三种排序算法的平均时间复杂度均为O(n2),但实际的执行效率是 插入排序 > 选择排序 > 冒泡排序,且随着数据规模的扩大,这种性能差异越大。

拓展问题

算法的复杂度分析是否可以替代性能测试?

复杂度分析提供了一个粗略的分析模型,能让我们对一个算法的效率有一个感性的认识;但是在不同的平台、不同的数据集、不同的数据量大小下,实际的性能不一定和复杂度分析的一致;因此,复杂度分析和性能测试是相辅相成的,复杂度分析可以指导性能测试用例编写,性能测试结果又反过来验证和优化复杂度分析。

为什么实际的性能测试结果插入排序要快于冒泡排序?

插入排序的交换指令少于冒泡排序

//插入排序
if(numbers[j] > val){
    numbers[j+1] = numbers[j];
} else {
    break;
}

//冒泡排序
if(numbers[j] > numbers[j+1]) {
    int tmp = numbers[j];
    numbers[j] = numbers[j+1];
    numbers[j+1] = tmp;
    flag = true;
}

与选择排序的性能差异比较也一样