排序算法 - C++11实现

原文链接

一、前言

排序算法一般是所有算法书函授的第一类算法。

本文旨在用C++11来实现主流的排序算法:插入排序、冒泡排序、归并排序、快速排序、堆排序、选择排序。并设计单元测试和代码覆盖率直观比较排序算法性能异同。

说明:本文代码需支持C++11标准编译器编译,单元测试仅支持googletest

项目地址:github.com/Pipapa/algorithm

二、函数声明

函数格式声明如下

// iterator_traits<It>::value_tpye 用于获取模版中It的类型
template<class It>
using value_type_t = typename std::iterator_traits<It>::value_type;

// 所有排序函数声明如下
// 参数传入为迭代器起始位置、结束位置,比较函数,迭代器遵循标准左开右闭
// 默认比较函数std::less<>
template<class It, class Compare = std::less<value_type_t<It>>>
void XxxSort(It begin, It end, Compare cmp = Compare());

三、排序算法实现

3.1.插入排序

将会用到的库函数:

std::next(it)

  • 说明:获取it迭代器的下一个迭代器。

std::upper_bound(first, last, value, cmp)

  • 说明:在有序区间[first, last),查找第一个满足!cmp(valut, *it)迭代器。
  • 复杂度:随机访问迭代器为logn(二分查找),否则为n(遍历)。

std::rotate(first, n_first, last)

  • 说明:左旋区间,在区间[first, last)中,让迭代器n_first成为第一个元素,n_first - 1成为最后一个元素。即:旋转前[first, ..., n_first, ... , last),旋转后:[n_first, ..., first, ..., last)

插入排序思想:

将排序数组分为两部分:已排序、待排序。

依次遍历待排序数组元素并插入到已排序数组中。

template<class It, class Compare = std::less<value_type_t<It>>>
void InsertionSort(It begin, It end, Compare cmp = Compare()) {
    for(auto it = begin; it != end; it = std::next(it)) {
        auto const insertion = std::upper_bound(begin, it, *it, cmp);
        std::rotate(insertion, it, std::next(it));
    }
}

3.2.冒泡排序

将会用到的库函数:

std::iter_swap(a, b)

  • 说明:交换迭代器a,b所指向的值。

冒泡排序思想:

将排序数组分为两部分:待排序、已排序。

依次遍历待排序数组元素,调整元素使两两之间满足大小关系,每一次调整,都使最大(小)元素在后面的位置。

最后会使待排序区中最大(小)元素位于待排序区最后位置,归入已排序区即可。

冒泡排序优化:

如果不发生元素交换,证明数组有序,可结束排序。

如果元素在某区域不发生交换,证明该区域已有序,更新已排序区开始位置为最后一次发生交换的位置。

template<class It, class Compare = std::less<value_type_t<It>>>
void BubbleSort(It begin, It end, Compare cmp = Compare()) {
    for(auto it = end, sorted = false; it != begin && !sorted;) {
        sorted = true;
        auto nit = std::prev(it);
        for(auto fwdit = std::next(begin); fwdit != it; fwdit = std::next(fwdit)) {
            if(cmp(*fwdit, *std::prev(fwdit))) {
                std::iter_swap(fwdit, std::prev(fwdit));
                sorted = false;
                nit = fwdit;
            }
        }
        it = nit;
    }
}

3.3.选择排序

将会用到的库函数:

std::min_element(first, end, cmp)

  • 说明:在[first, end)中查找满足cmp(it, others)的迭代器。

选择排序思想:

将排序数组分为两部分:已排序、待排序。

不同于插入排序,将待排序元素插入已排序区域,而是在待排序区域查找满足大(小)于已排序区域最后一个元素,让其成为已排序区的最后元素。

template<class It, class Compare = std::less<value_type_t<It>>>
void SelectionSort(It begin, It end, Compare cmp = Compare()) {
    for(auto it = begin; it != end; it = std::next(it)) {
        std::iter_swap(it, std::min_element(it, end, cmp));
    }
}

3.4.归并排序

将会用到的库函数:

std::distance(first, last)

  • 说明:计算迭代器firstlast之间有多少元素。
  • 复杂度:随机访问迭代器在常数时间内完成(地址相减),其余在n时间内完成(遍历)。

std::inplace_merge(first, mid, last)

  • 合并两个有序区间[first, mid)[mid, last)

归并排序思想:

将两个有序数组合并为一个有序数组,使用递归或多路归并保障合并的两数组皆有序。

template<class It, class Compare = std::less<value_type_t<It>>>
void MergeSort(It begin, It end, Compare cmp = Compare()) {
    const auto N = std::distance(begin, end);
    if(N <= 1) {
        return;
    }
    const auto mid = std::next(begin, N/2);
    MergeSort(begin, mid, cmp);
    MergeSort(mid, end, cmp);
    std::inplace_merge(begin, mid, end);
}

3.5.堆排序

堆排序思路:

将排序数组分为两部分:待排序、已排序。

在待排序区建立大(小)根堆,根堆满足其根节点均大(小)于所有子节点。

每次迭代交换根节点到已排序区,待排序区再重新调整根堆。

// 仅给出适用于随机存储的迭代器实现,详细参考github
// 堆调整
template<class It, class Compare = std::less<value_type_t<It>>>                                                                   
void RandomAccessAdjustHeap(It begin, It end, It root, Compare cmp = Compare()) {                                                 
    auto son = (root - begin) * 2 + 1 + begin;
    while(son < end) {           
        if(son + 1 < end && cmp(*son, *(son+1))) {
            son++;                  
        }             
        if(cmp(*son, *root)) return;   
        std::iter_swap(son, root);
        root = son;                            
        son = (root - begin) * 2 + 1 + begin;          
    }                                                  
}
// 堆构建
template<class It, class Compare = std::less<value_type_t<It>>>
void RandomAccessMakeHeap(It begin, It end, Compare cmp = Compare()) {
    const auto N = std::distance(begin, end);
    if(N <= 1) {
        return;
    }
    for(auto root = std::next(begin, (N-2)/2); root >= begin; --root) {
        RandomAccessAdjustHeap(begin, end, root, cmp);
    }
}
// 堆排序
template<class It, class Compare = std::less<value_type_t<It>>>
void RandomAccessSortHeap(It begin, It end, Compare cmp = Compare()) {
    const auto N = std::distance(begin, end);
    if(N <= 1) {
        return;
    }
    auto last = std::prev(end);
    while(begin != last) {
        std::iter_swap(begin, last);
        RandomAccessAdjustHeap(begin, last, begin, cmp);
        last = std::prev(last);
    }
}
template<class It, class Compare = std::less<value_type_t<It>>>
void HeapSort(It begin, It end, Compare cmp = Compare()) {
    RandomAccessMakeHeap(begin, end, cmp);
    RandomAccessSortHeap(begin, end, cmp);
}

3.6.快速排序

将会用到的库函数:

std::partition(first, last, p)

  • 说明:将区间[first, last)分为两部分,第一部分满足条件p,第二部分不满足条件p,返回第一个不满足条件p的迭代器。

快速排序思路:

将数组分为三部分,第一部分所有元素小(大)于pivot值,第二部分所有元素等于pivot值,第三部分所有元素大(小)于pivot值。

递归排序第一、第三部分直到不可划分,即排序完成。

template<class It, class Compare = std::less<value_type_t<It>>>
void QuickSort(It begin, It end, Compare cmp = Compare()) {
    auto const N = std::distance(begin, end);
    if(N <= 1) {
        return;
    }
    auto const pivot = *std::next(begin, N/2);
    auto const midl = std::partition(begin, end, [=](value_type_t<It> const& elem) {
        return cmp(elem, pivot);
    });
    auto const midg = std::partition(midl, end, [=](value_type_t<It> const &elem) {
        return !cmp(pivot, elem);
    });
    QuickSort(begin, midl, cmp);
    QuickSort(midg, end, cmp);
}

四、单元测试设计

  • 随机生成1000组长度为[0, 1000]的数组,其值为[0, 1000]之间,排序该数组所有值。
  • 随机生成1000组长度为[0, 1000]的数组,其值为[0, 1000]之间,排序该数组区间[l, r]间值(保障区间合法)。

总结:

  • 2000组测试用例,分别测试区间全排序和合法部分区间[l, r]排序。

测试结果执行如下:

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269