算法学习笔记(2) 四种排序算法速度比较

导言

在之前介绍快速排序时提到了快速排序的时间复杂度为,然而这样的时间复杂度究竟是什么概念,依然没有做出详细的解释。这次将根据插入排序、冒泡排序、选择排序和快速排序四种算法在不同数据规模情况下的排序时间,来对算法时间复杂度的意义进行一定的剖析。

1.1插入排序

插入排序的原理是:对于一个已经从小到大排好序的序列,若往其中插入一个数,要让新序列依然有序,就要从原序列的第一个数开始与插入的数对比,若发现了一个数比新数大,则把新数插入到该数前。

只有一个数的序列必定是从小到大有序排列的,再按上述方法插入一个数后形成的两个数的新序列也是有序的,再按上述方法插入一个数后形成的三个数的新序列也是有序的……以此类推直到原序列中所有数都被插入新序列,则从小到大的插入排序完毕。

实际实现时,只需另外开辟一个数组,将原数组中的元素按照上述步骤全部插入新数组即可。

public static int[] InsertSort(int[] list)
{
    int[] list2 = new int[list.Length];
    for(int i=0;i list[i])
                break;
        }
        for (int k = i; k > j; k--)
            list2[k] = list2[k - 1];
        list2[j] = list[i];
    }
    return list2;
}

插入排序对于原数组的遍历需要n次操作,遍历到第m个元素时,新数组有m-1个元素,对于在x位置的插入,对比需要x次操作,插入需要将该位置及之后的(m-x)个元素后移,外加1次插入操作。因此总计需要次操作,看成一个从2到n+1、公差为1的等差数列求和,操作次数为,时间复杂度是。

1.2冒泡排序

要将数组中的数据用冒泡排序的方式排成从小到大有序,就要将位于数组前部的较大数据像冒泡一样逐渐“冒”到数组后部。我们将数组一啊开始视为均无序,其具体做法是:

  1. 选择数组中的第一个数
  2. 如果该数比数组中的下一个数大,就交换该数与下一个数
  3. 选择数组中的下一个数
  4. 重复2、3步,直到选择数组无序部分的最后一个数
  5. 此时选择的最后一个数一定是无序部分最大的数,因为一旦无序部分最大的数被选择,它一定每一次执行步骤2时都会被交换到后面,然后在步骤3中被选择,就像一个大气泡一样浮到碳酸饮料的表层。而有序部分的数也都是经过刚才的冒泡操作被交换到数组后部的,同理一定大于无序部分的最后一个数,因此有序部分的长度加一,无序部分的长度减一,从步骤1开始重新执行上述过程
  6. 当无序部分长度为零时,结束排序
public static void BubbleSort(int[] list)
{
    int tmp;
    for (int i = 0; i < list.Length-1; i++)
    {
        for(int j=0;jlist[j+1])
            {
                tmp = list[j+1];
                list[j + 1] = list[j];
                list[j] = tmp;
            }
        }
    }
}   

由于冒泡排序每一趟遍历所需要的对比数据的次数减一,因此冒泡排序需要的对比次数是从1到n-1、公差为1的等差数列求和,即。交换次数最少需要0次,最多需要与对比次数等同的次数,其实际和数列中的逆数对数目相同。随机生成的数组中逆数对个数平均值为,而每次交换需要的操作次数为3,因此操作次数为,时间复杂度为。

1.3选择排序

选择排序相当容易理解,将大小为n的数组从小到大排序仅需要遍历数组n次,每次选择最小的一个数放进一个新数组,把该数从原数组中删除就行。

实现时,用一个bool型数组来记录原数组中对应位置上的数是否已经被放入新数组。

public static int[] SelectSort(int[] list)
{
    int[] list2 = new int[list.Length];
    bool[] selected = new bool[list.Length];
    int length = 0;
    while(length< list.Length)
    {
        int i = 0;
        while (selected[i]) { i++; }
        int min = list[i],id=i;
        for (int j = i; j < list.Length; j++)
        {
            if (selected[j])
                continue;
            if (min > list[j])
            {
                min = list[j];
                id = j;
            }
        }
        selected[id] = true;
        list2[length] = min;
        length++;
    }
    return list2;
}

可见,选择排序需要n次遍历大小为n的数组,其时间复杂度为。

1.4快速排序

public static void QuickSort(int[] numbers, int s, int e)
{
    if (s >= e)
        return;
    int mid = numbers[s], start = s, end = e;
    while (s < e)
    {
        while (s < e)
        {
            if (numbers[e] < mid)
            {
                numbers[s++] = numbers[e];
                break;
            }
            e--;
        }
        while (s < e)
        {
            if (numbers[s] > mid)
            {
                numbers[e--] = numbers[s];
                break;
            }
            s++;
        }
    }
    numbers[s] = mid;
    QuickSort(numbers,start, s - 1);
    QuickSort(numbers,s + 1, end);
}

根据之前文章中的分析,快速排序的时间复杂度为。

2.时间统计

我们随机生成10、20、50、100、500、1000、5000、、、数据规模的数组,每次生成的数组均用4种排序算法进行排序,以系统的ticks为单位计算排序时间,绘制曲线图。

在这里插入图片描述

其中插入排序、冒泡排序、选择排序都可以用二次函数很完美地拟合,而二次项前的系数则和算法在实现上使用的操作次数有关。比如根据拟合结果,冒泡排序的二次项系数是插入排序的2.5倍,这与我们之前预测的结果相同。可以看见,在十万级数据的情况下,系数的差别对时间的影响非常可怕,因此在实现设计大量重复操作的算法时,尽量减少比较、赋值、运算的次数可以让算法得到很大程度的优化,这也是我们常说的底层优化。

底层优化之所以是底层,在于它不能对算法的本质产生影响。上述前三个算法,纵使系数不尽相同,本质依然是平方级算法,和快速排序相比,在十万级的数据规模下,它们的运算速度差别达到了5000倍左右,(可见,logn是一个很小的因子,在十万级数据规模下快速排序的时间复杂度依然可以用线性函数较好地拟合)这与同一级别算法之间个位数倍数的时间差距相比,可以说是有了质的飞跃,而这正是计算机程序设计艺术的体现,也是程序员思(gong)维(zi)差距的体现。

因此,对于一名出色的算法工程师来说,不仅要学会编写简洁的代码,还要学会分析问题模型,尽可能地去寻找时间复杂度级别较低的算法。如果能够开创性地设计出一种时间复杂度级别比现有算法更低的算法,那么恭喜,你已经从一名码农变成一名计算机科学家了!

附上一张不同时间复杂度级别下,数据规模与运算时间的对照表:

在这里插入图片描述

一般来说,级别的算法足以高效应付常规规模的数据处理了。

你可能感兴趣的:(算法学习笔记(2) 四种排序算法速度比较)