排序算法学习——O(n^2)型

前言

排序算法作为最基础,也是最经典的算法,应用于我们日常生活的方方面面。而其中最为基础的三大算法——选择排序、插入排序、冒泡排序,是我们最一开始接触的排序算法。虽然当时C语言学完没有什么感觉,但现在回过头来学习算法的时候才发现其中另有一番玄机。

三大排序算法

这三大排序都是时间复杂度为O(n^2)的排序算法,排序效率不是很高,但在各种应用场景上仍然能发挥其作用。

选择排序

原理

Selection Sort的原理很简单:顺序向后查找剩下的元素中最小的元素,找到之后与起始位进行交换。
选择排序图片

代码实现

这里附上C++实现代码:

//这里使用了C++的模板,其实可以看成通用类型(后面不作注释)
template<typename T>
void selectionSort(T arr[], int n){

    for(int i = 0 ; i < n ; i ++){
        // 寻找[i,n)区间的最小值
        int minIndex = i;
        for( int j = i + 1 ; j < n ; j ++ )
            if( arr[j] < arr[minIndex] )
                minIndex = j;

        swap( arr[i] , arr[minIndex] );
    }
}

插入排序

原理

Insertion Sort的原理就是你打扑克牌的时候,摸牌插牌的原理:首先前面一部分是有序的表,后面一部分是无序表,无序表依次和有序表中的各个数进行比较,并插入有序表中。
插入排序图片

实现代码

首先是1.0版本,按照原理,我们每次比较都进行一次交换,代码量比教科书上的减少了很多……

void insertionSort(T arr[], int n) {

    for (int i = 1; i < n; i++) {
        //寻找元素arr[i]合适的插入位置
        for(int j = i; j > 0; j--) {
            if(arr[j] < arr[j-1])
                swap(arr[j], arr[j-1]);
            else
                break;
        }
}

通过代码我们发现,其实if判断语句可以写在for循环里面,for循环内部代码改写2.0版:

for(int j = i; j > 0 && arr[j] < arr[j-1]; j--) {
    swap(arr[j], arr[j-1]);
}

优势

通过对比我们可以发现,插入排序相比于选择排序不一定需要完成整个第二层循环,因此在最优的情况(数组本来就是有序的)下,它的时间复杂度仅仅为O(n)。所以在一些特定的情况下,插入排序甚至表现得比其他高级的排序方式更好,因此插入排序会常常最为一个高级排序的子过程。
但是经过测试我们发现,我们上面的代码在进行10000个随机数排序时的效率竟然比选择排序高!!What??大家可以试一下。
经过冷静分析,我们可以看到,是我们的代码不够优化。上面的插入排序代码,每经过一次比较就交换一次,而交换需要进行三次赋值,代价可比单纯作比较要高,这也是性能低于选择排序的原因。
最终我们进行了优化,既然问题出在了交换上面,我们就减少交换。根据原理,我们先将要插入的元素保存起来,进行比较,如果前一个元素大于该元素,则证明该元素不在正确位置,前一个元素向后移动一位,直到找到位置,再插入。代码3.0如下:

    T e = arr[i];
    int j; //j保存元素e应该插入的位置
    for (j = i; j > 0 && arr[j-1] > e; j--) {
        arr[j] = arr[j-1];
    }

    arr[j] = e;

最后经过测试发现,在几乎有序的情况下,插入排序的效率是很惊人的。

希尔排序

原理

Shell Sort其实是插入排序的升级版,他的时间复杂度并不是O(n^2),但这里因为扯到了插入排序就顺便提一下。
摘自百度百科:
希尔排序是基于插入排序的以下两点性质而提出改进方法的:

  1. 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
  2. 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

因此希尔排序是以步长为单位进行排序的,首先它先以较大的步长进行插入排序,比如4,那么先对arr[0]、arr[4]、arr[8]等进行排序,再对arr[1]、arr[5]、arr[9]进行排序。结束排序后,不断减小步长,当减小步长为1时,它就成为了一个插入排序,经过之前的很多次排序,这个数组其实已经接近有序了,所以希尔排序就是利用了插入排序在接近有序的情况下效率高的特点进行排序的,进行多次分组排序形成一个接近有序的数组最终完成步长为1的插入排序。
当然,希尔排序的时间复杂度是由步长的变化方式决定的,步长变化方式不同,时间复杂度也不同,因此有千千万万种希尔排序,下面给出几种:
步长

代码实现

这里参照的是liuyubobobo给出的序列

template<typename T>
void shellSort(T arr[], int n){

    // 计算 increment sequence: 1, 4, 13, 40, 121, 364, 1093...
    int h = 1;
    while( h < n/3 )
        h = 3 * h + 1;

    while( h >= 1 ){

        // h-sort the array
        for( int i = h ; i < n ; i ++ ){

            // 对 arr[i], arr[i-h], arr[i-2*h]... 使用插入排序
            T e = arr[i];
            int j;
            for( j = i ; j >= h && e < arr[j-h] ; j -= h )
                arr[j] = arr[j-h];
            arr[j] = e;
        }

        h /= 3;
    }
}

冒泡排序

原理

Bubble Sort的原理也很简单,顾名思义,就像冒泡一样,从后往前,一个个的找到无序表中的最大值,然后置于末尾,一个个冒出来……
冒泡排序

代码实现

template<typename T>
void bubbleSort(T arr[], int n) {

    for (int i = n - 1; i > 0; i--)
        for (int j = i; j > 0; j--)
            if (arr[j] < arr[j - 1])
                swap(arr[j], arr[j - 1]);
}

图片引用百度图片
代码实现参照liuyubobobo慕课网教程

猜你喜欢

转载自blog.csdn.net/blueblueskyz/article/details/79305019