为了账号安全,请及时绑定邮箱和手机立即绑定

C++模板实现常用排序算法

标签:
C++

本文利用模板实现通用性好的排序算法,包括冒泡排序,选择排序,直接插入排序,快速排序,堆排序,希尔排序,归并排序和桶排序,并且给出排序算法的比较。

#include <iostream>
#include <algorithm>
#include <stdexcept>
using namespace std;

/*
冒泡排序思想就是通过与相邻元素的比较和交换来把小的数交换到最前面。
冒泡排序的时间复杂度为O(n^2)
*/
template <typename T> void BubbleSort(T *array, const int length)
{
    if(array == NULL){ 
        throw invalid_argument("Array must not be empty");
    }   
    if(length<=0) 
        return;

    bool flag = false;
    for(auto i=0; i<length-1; ++i){
        for(auto j=length-1; j>=i; --j){
            if(array[j+1]<array[j]){
                T tmp = array[j+1];
                array[j+1] = array[j];
                array[j] = tmp;
                flag = true;
            }
        }
    }
    if(!flag) return;
}

/*
直接选择排序都是在一次排序后把最小的元素放到最前面
直接选择排序的时间复杂度为O(n^2)
*/
template <typename T> void SelectSort(T *array, const int length)
{
    if(array == NULL){ 
        throw invalid_argument("Array must not be empty");
    }   
    if(length<=0) 
        return;

    for(auto i=0; i<length-1; ++i){
        auto k = i;
        for(auto j=i+1; j<length; ++j){
            if(array[j]<array[k]){
                k = j;
            }
        }
        if(k != i){
            T tmp = array[i];
            array[i] = array[k];
            array[k] = tmp;
        }
    }
} 

/*
插入排序不是通过交换位置而是通过比较找到合适的位置插入元素来达到排序的目的。
1.从第一个元素开始,该元素可以认为已被排序;
2.取出下一个元素,在已经排序的元素序列中从后向前扫描;
3.如果该元素(已排序)大于新元素,将该元素移到下一个位置;
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
5.将新元素插入到该位置后,重复2~5
插入排序的时间复杂度为O(n^2)
*/
template <typename T> void InsertSort(T *array, const int length)
{
    if(array == NULL){ 
        throw invalid_argument("Array must not be empty");
    }   
    if(length<=0) 
        return;
    for(auto j=1; j<length; ++j){
        T tmp = array[j];
        auto i = j-1;
        while(i>=0 && array[i] > tmp){
            array[i+1] = array[i];
            --i;
        }
        array[i+1] = tmp;
    }           
}

/*
快速排序步骤
1.i=L; j=R; 将基准数挖出形成第一个坑a[i]。
2.j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
快速排序不稳定,时间复杂度为是O(nlgn)
*/
template <typename T> void QuickSort(T *array, int l, int r)
{   
    if(l<r){
        int i = l;
        int j = r;
        T tmp = array[l];
        while(i<j){
            while(i<j&&array[j]>=tmp) --j;
            if(i<j){
                array[i++] = array[j];              
            }
            while(i<j&&array[i]<tmp) ++i;
            if(i<j){
                array[j--] = array[i];
            }

        }
        array[i] = tmp;
        QuickSort(array, l, i-1);
        QuickSort(array, i+1, r);
    }
}
template <typename T> void QuickSort(T *array, const int length)
{
    if(array == NULL){ 
        throw invalid_argument("Array must not be empty");
    }   
    if(length<=0) 
        return;
    QuickSort(array, 0, length-1);
}

/*
堆排序
最差时间复杂度:O(nlogn)
最优时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
稳定性:不稳定
堆排序,利用堆这种数据结构设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足性质:即子节点的键值或索引总是小于(或者大于)它的父节点。
通常堆是通过一维数组来实现的,在起始数组为0的情形中,对于节点i:
其左子节点的下标为 (2*i+1);
其右子节点的下标为 (2*i+2);
其父节点的下标为 floor((i-1)/2)。
在堆的数据结构中,堆中的最大值总是位于根节点。堆中定义一下三个操作:
1.最大堆调整(Max Heapify):在假定节点i的左右子节点为根的两棵二叉树都是最大堆的前提下,确保父节点大于子节点,否则下降原父节点,最终使以i为根的子树成为最大堆。
2.创建最大堆(Build Max Heap):将堆所有数据重新排序,对所有非叶子节点调用一次Max Heapify。
3.堆排序(Heap Sort):首先创建最大堆,然后依次将堆的根节点与末节点交换、剔除末节点、对根节点进行最大堆调整,直到堆中的节点数为1,排序结束。
*/
template <typename T> void MaxHeapify(T *array, int i, int heapSize)
{
    int l = 2*i+1;
    int r = 2*i+2;
    int tmp = i;
    if(l<=heapSize && array[l]>array[i]){
        tmp = l;
    }else{
        tmp = i;
    }

    if(r<=heapSize && array[r]>array[tmp]){
        tmp = r;
    }

    if(tmp != i){
        swap(array[i], array[tmp]);
        MaxHeapify(array, tmp, heapSize);
    }

}
template <typename T> void HeapSort(T *array, const int length)
{
    if(array == NULL){ 
        throw invalid_argument("Array must not be empty");
    }   
    if(length<=0) 
        return;
    for(auto i = length/2; i>=0; --i){ //构建最大堆
        MaxHeapify(array, i, length-1);
    }
    for(auto i = length-1; i>=0; --i){
        swap(array[0], array[i]);
        MaxHeapify(array, 0, i-1);
    }
}

/*
希尔排序是插入排序的改进版,注意这里的每组元素并非连续的,而是隔了一个gap,比如奇数位是一组偶数位是一组的话gap就是1; 
核心思想是 
1、将序列分组,并且每次分的组越来越小直至只剩一个一组; 
2、然后分好的组各自进行直接插入排序; 
3、当分成一个一组的时候,就是完完全全的插入排序了, 
4、但是由于先前已进行了一定的排序,所以这个完全的插入排序交换次数很少; 
5、所以希尔排序在交换次数和遍历次数上下了功夫,在插入排序的基础上提升了效率降低了复杂度
*/
template <typename T> void ShellSort(T *array, const int length)
{
    if(array == NULL){ 
        throw invalid_argument("Array must not be empty");
    }   
    if(length<=0) 
        return;

    int gap = length/2;
    while(gap){
        T tmp;
        for(int i=gap; i<length; ++i){
            tmp = array[i];
            int j = i;
            while(j>=gap && tmp<array[j-gap]){
                array[j] = array[j-gap];
                j-=gap;
            }
            array[j] = tmp;
        }
        gap/=2;
    }
}

/*
归并排序,分治策略
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
先递归划分子问题,然后合并结果。把待排序列看成两个有序的子序列,然后合并两个子序列,然后把子序列看成两个有序序列;
倒着来看,其实就是先两两合并,然后四四合并,最终形成有序序列。
空间复杂度为O(n),时间复杂度为O(nlogn)。
*/
template <typename T> void Merge(T *array, int l, int mid, int r)
{
    T *tmp = new T[r-l+1];
    int i = l;
    int j = mid + 1;
    int k = 0;
    while(i<=mid && j<=r){
        if(array[i]<array[j]){
            tmp[k++] = array[i++];
        }else{
            tmp[k++] = array[j++];
        }
    } 
    while(i<=mid){
        tmp[k++] = array[i++];
    }
    while(j<=r){
        tmp[k++] = array[j++];
    }
    for(i=l, k=0; i<=r; ){
        array[i++] = tmp[k++];
    }
    delete []tmp;
}
template <typename T> void subSort(T *array, int l, int r)
{
    if(l<r){
        int mid = (l+r)/2;
        subSort(array, l, mid);
        subSort(array, mid+1, r);
        Merge(array, l, mid, r);
    }
}
template <typename T> void MergeSort(T *array, const int length)
{
    if(array == NULL){ 
        throw invalid_argument("Array must not be empty");
    }   
    if(length<=0) 
        return;
    subSort(array, 0, length-1);
}

/*
桶排序(参考http://www.tuicool.com/articles/3emMVz)
假设有一组长度为N的待排关键字序列K[1....n]。首先将这个序列划分成M个的子区间(桶) 。
然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i) ,
那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为N/M的序列)。
接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。
然后依次枚举输出B[0]....B[M]中的全部内容即是一个有序序列。
对N个关键字进行桶排序的时间复杂度分为两个部分:
(1) 循环计算每个关键字的桶映射函数,这个时间复杂度是O(N)。
(2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为  ∑ O(Ni*logNi) 。其中Ni为第i个桶的数据量。 
*/

template <typename T> void BucketSort(T *array, const int length)
{
    if(array == NULL){ 
        throw invalid_argument("Array must not be empty");
    }   
    if(length<=0) 
        return;
    T maxV = array[0];
    for(int i=0; i<length; ++i){
        if(array[i] > maxV)
            maxV = array[i];
    }

    int tempArrLength = maxV+1;
    T tempArr[tempArrLength];
    for(int i=0; i<tempArrLength; ++i){ //空桶初始化
        tempArr[i] = 0;
    }

    for(int i=0; i<length; ++i){ //遍历序列,并且把元素一个一个放到对应的桶子去。
        tempArr[array[i]]++;
    }

    for(int i=0,j=0; i<tempArrLength; ++i){
        while(tempArr[i]!=0){
            array[j++] = i;
            tempArr[i]--;
        }
    }
}

int main()
{
    int arr[] = {4,2,5,8,3};
    const int len = sizeof(arr) / sizeof(arr[0]);

    //double arr[] = {4.5, 2.3,6.7, 3.5, 1.1};
    //const int len = sizeof(arr) / sizeof(arr[0]);
    //BubbleSort(arr, len);
    //SelectSort(arr, len);
    //InsertSort(arr, len);
    //QuickSort(arr, len);
    //HeapSort(arr, len);
    //ShellSort(arr, len);
    //MergeSort(arr, len);
    BucketSort(arr, len); //double类型不可以
    for(auto v : arr){
        cout << v << " ";
    }
    cout << endl;
}

下表是个人对排序算法的总结:
排序算法比较

点击查看更多内容
11人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消