排序算法总结1

下面介绍的所有排序默认都为非递减排序!所有算法和测试用例见github

1.简单排序

简单排序算法主要包括冒泡排序、选择排序、插入排序以及由插入排序改进而来的希尔排序,其时间复杂度都为 n l o g n

1.1 冒泡排序

算法思想:
属于暴力解法
从第一个元素开始,两两比较大小,如果后者 < 前者,则交换两者顺序,以此类推,直到最大的元素“冒泡”到它的最终位置
这里写图片描述

代码如下:

#include <iostream>
#include <algorithm>

template <typename T>
void bubbleSort (T arr[], int n){
    for (int i = 0 ; i < n - 1; i++){
        for (int j = 0; j < n - 1 - i; j++){
            if (arr[j + 1] < arr[j])
                std::swap(arr[j + 1],arr[j]);
        }
    }
}

1.2 选择排序

算法思想:
属于暴力解法
每次循环遍历是,选择排序默认第一个元素即为本次循环最小的元素,然后依次与后面的元素进行比较,不断更新该次循环中的最小值,最终将得到的最小元素和默认的初始元素交换位置即可,以此类推不断进行比较。
思想和冒泡有点类似,不同点在于冒泡排序时前后元素两两比较,而选择排序是初始元素与后续所有元素进行比较。
这里写图片描述
代码如下:

#include <iostream>
#include <algorithm>

template <typename T>
void selectionSort(T arr[],int n){
    int minIndex = 0;
    for (int i = 0; i < n - 1;i++){
        //这里只需要记录最小值的索引即可,最后交换arr[minIndex]和arr[i]即可,无需其他冗余的交换操作
        minIndex = i;
        for (int j = i + 1; j < n ;j++){
            if (arr[j] < arr[minIndex])
                minIndex = j;
        }
        std::swap(arr[minIndex],arr[i]);
    }
}

1.3 插入排序

算法思想:
自下而上的减治算法
假设数组前面几个元素已经有序,插入排序的思想为,把未排序的第一个元素从后向前依次和已排序的元素进行比较,如果遇到比本身小元素,将未排序元素插入该元素后一个位置,以此类推……
这里写图片描述
代码如下:

#include <iostream>

template <typename T>
void insertionSort(T arr[],int n){
    T target = 0;
    int j = 0;
    for (int i = 1;i < n;i++){
        target = arr[i];
        j = i - 1;
        while (j >= 0 && arr[j] > target){
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = target;
    }
}

需要说明的是,虽然插入排序的时间复杂度平均情况下为 O ( n 2 ) ,但当待排序数组近乎有序的时候,其时间复杂度退化为 O ( n ) ,效率完全不亚于任何一种 O ( n l o g n ) 的高级排序算法,因此插入排序也常常作为优化子过程用在高级排序算法中,当高级排序算法将数组整理的近乎有序的时候,此时调用插入排序的效果是非常好的,下文高级排序算法中将会涉及到该优化思想。

1.4 希尔排序

算法思想:
希尔排序(由D.L. 希尔发明)是在总结并放大插入排序优点的基础上被创造出来的,可以理解为是插入排序的改进。
由上文知道,插入排序对近乎有序的元素进行排序相当高效,希尔排序就是根据给定的步长对元素进行插入排序,进而构造近乎有序的数组,并不断进行插入排序,显然这个步长必须以1作为结束。因此,该算法的时间复杂度更准确的说是依赖于步长队列的选取的。对于希尔排序,步长1,4,13,40……的效率是最高的,当然,使用的时候需要反过来。
这里写图片描述
代码如下:

#include <iostream>

template <typename T>
void shellSort(T arr[],int n){
    int h = 1;
    T target = 0;
    int j = 0;
    while (h <= n)
        h = 3 * h + 1;
    while (h > 0){
        //下面即为步长为h的插入排序
        for (int i = h;i < n ; i += h){
            target = arr[i];
            j = i - h; 
            while (j >= 0 && arr[j] > target){
                arr[j + h] = arr[j];
                j -= h;
            }
            arr[j + h] = target;
        }
        h /= 3;
    }
}

2 高级排序

高级排序主要包括归并排序、快速排序和堆排序,算法复杂度平均情况下为 O ( n l o g n ) ,但是针对不同类型的数据表现出来的性能可能截然不同,下面将会讨论不同的算法适合的应用场景,以及该算法针对大多情况下的改进。

2.1 归并排序

算法思想:
归并排序是应用分而治之技术的一个完美例子
对于一个需要排序的数组A[0…n-1],归并排序将他们一分为二:A[0…n / 2]和A[n / 2 + 1],并对每个子数组递归排序,然后将两个排好序的子数组合并为一个有序数组。
首先我们考虑将数组分成A和B,并保证A和B都是有序的。这里用到了 递归 的思想,每个数组一分为二,知道分成的数组仅仅包含一个元素,因为我们认为一旦只包含一个元素,该数组即为有序数组。
归并的代码如下:

template <typename T>
void __mergeSort(T arr[],int l,int r){
    //递归终止条件
    if (l >= r){
        return ;
    }
    int mid = (r + l) / 2;
    __mergeSort(arr, l, mid);
    __mergeSort(arr, mid + 1, r);
    //分别排序完成后进行归并
    __merge(arr, l, mid, r);
}

以上逻辑很简单,关键在于如何将两个已排序的数组进行归并。与其他排序算法不同的是,为此我们需要引入一个辅助数组来帮助我们完成归并。
这里写图片描述
我们将新开辟的经过赋值的辅助数组命名为aux(上图的第二个数组),原来的数组为arr(上图的第一个数组),用指针k(索引下标,下同)指向arr的第一个元素,并分别用指针i,j分别指向两个待合并的数组的第一个元素,然后比较这两个元素的大小,将较小的元素添加到arr[k],然后指针k后移,被复制的数组的指针后移。上述操作一直持续到aux两部分中的一个被处理完为止,然后,在未处理完的数组中,剩下的元素直接被复制到arr的尾部。
归并排序代码如下:

template <typename T>

//l为数组第一个元素的下标,mid为待归并数组中的第一个数组的最后一个元素的下标,r为数组最后一个元素的下标
void __merge(T arr[],int l,int mid,int r){
    //声明辅助数组aux,注意元素个数(数组下标为闭区间)
    T aux[r - l + 1];
    //辅助数组赋值,注意arr下标从l开始,aux下标从0开始
    for (int i = l;i <= r;i++){
        aux[i - l] = arr[i];
    }

    //定义待归并数组的首元素指针
    int i = l, j = mid + 1;
    for (int k = l;k <= r;k++){
        //判断指针i是否越过mid
        //如果是,说明aux[l...mid]已经归并完成
        if (i > mid){
            arr[k] = aux[j - l];
            j++;
        }else if (j > r){//同理,检查j是否越过r
            arr[k] =aux[i - l];
            i++;
        }else if (aux[i - l] < aux[j - l]){
            arr[k] = aux[i - l];
            i++;
        }else{
            arr[k] = aux[j - l];
            j++;
        }
    }
}

最后,我们按照上面对简单排序的函数格式,对归并最后进行一下封装

template <typename T>
void mergeSort(T arr[], int n){
    __mergeSort(arr,0,n-1);
}

2.1.1 归并排序性能测试

我们用函数自动顺序生成了100000个元素,并随即对其中10组进行交换,这样我们得到了一组近乎有序的数组,对同一样的样本数据,叫我们分别用插入排序和归并排序进行排序测试,测试结果如下:
这里写图片描述
由此得出,对近乎有序的数组(更极端点,对完全有序的数组),在大数量级的前提下,插入排序的表现要优于归并排序!
这是由于在近乎有序的情况下,插入排序会退化为 O ( n ) ,对已排好序的元素不再进行排序处理。而归并排序依然沿用之前的规则,即使是有序的元素,还是会按照原定的策略进行排序。

2.1.2 归并排序优化

针对这一点,我们引出了归并排序的一点优化,代码如下:

template <typename T>
void __mergeSort(T arr[],int l,int r) {
    //递归终止条件
//  if (l >= r) {
//      return ;
//  }

    //优化2:如果r-l<=15(随便选择),则利用插入排序
    if ( r - l <= 15) {
        insertionSort(arr,l,r);
        return;
    }

    int mid = (r + l) / 2;
    __mergeSort(arr, l, mid);
    __mergeSort(arr, mid + 1, r);
    //分别排序完成后进行归并
    //优化1:如果近乎有序,则加入这个判断会有效提升性能
    if (arr[mid] > arr[mid + 1])
        __merge(arr, l, mid, r);
}

其中,插入排序的代码如下:

template <typename T>
void insertionSort(T arr[],int l, int r){
    T target = 0;
    int j = 0;
    for (int i = l + 1;i <= r;i++){
        target = arr[i];
        j = i - 1;
        while (j >= l && arr[j] > target){
            arr[j + 1] = arr[j];
            j--;
        }
        arr[j + 1] = target;
    }
}

效果如下:
这里写图片描述
虽然还是比插入排序稍微慢一点,但是相比之前的归并排序,速度已经提升了不少

2.1.3 自下而上的归并排序算法

上文中介绍的归并排序属于自顶向下的方法,就是说从整体出发,将整体分成部分,再将部分合成整体。下面我们介绍自下而上的归并排序算法,先从小数组开始归并,然后将归并完成的数组(长度是小数组的2倍)再继续归并,图例如下:
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
自底而上的思想不需要递归,只需要迭代,代码如下:

#include <iostream>
#include <algorithm>

template <typename T>
void mergeSortBU(T arr[],int n){
    for (int h = 1;h <= n;h += h){
        //保证一定有两个数组进行归并,而不是一个
        for(int i = 0;i + h < n;i += h + h){
            __merge(arr,i,i + h - 1,std::min(r,i + h + h - 1));
        }
    }
}

由于较少利用了数组的索引特性,所以自底而上的归并排序可以扩展用来对链表进行排序,时间复杂度 O ( n l o g n )


csdn居然有长度限制,无奈快速排序和堆排序只能分开来写了,详情见**

猜你喜欢

转载自blog.csdn.net/chanmufeng/article/details/80295178