STL之partial_sort算法源码讲解

本文首发于个人CSDN博客: https://blog.csdn.net/ggq89/article/details/88817085

假设有一个容器,保存了 100 万个数值,但我们只对其中最小的 100 个元素的顺序感兴趣。可以对容器的全部内容排序,然后选择前 100 个元素,但这样处理会使效率低。这时候需要使用部分排序 partial_sort,高效的获得这些数中的前100个最小数的顺序。

1. partial_sort 接口说明……

partial_sort有两个版本,一个默认以小于(less-than操作符)作为比较规则,出来的顺序为递增排列。另一个可以传入一个比较函数,即调用者指定一个比较规则。

partial_sort(beg,mid,end)
partial_sort(beg,mid,end,comp)

对容器中的(mid-beg)个元素进行排序,partial_sort 完成之后,从beg到mid(但不包括mid)范围内的元素时有序的,且大于(或者指定的comp函数)mid之后的元素。未排序元素之间的次序是不确定的,即partial_port不是稳定排序算法。

2. partial_sort 用法举例

下面这段代码演示了 partial_sort() 算法的工作方式:

size_t count {5}; // Number of elements to be sorted
std::vector<int> numbers {22, 7, 93, 45, 19, 56, 88, 12, 8, 7, 15, 10};
std::partial_sort(std::begin(numbers), std::begin(numbers) + count, std::end(numbers));

执行partial__sort()后的效果如下图所示:

partial_sort() 算法的操作

需要注意的是,不能保持未排序元素的原始顺序。在执行 partial_sort() 后后面元素的顺序是不确定的,这取决于具体的实现。

也可以指定不同的比较函数。例如:

std::partial_sort(std::begin(numbers), std::begin(numbers) + count, std:: end (numbers) , std::greater<>());

现在,number 中最大的 count 个元素会是容器开始处的一个降序序列。以上语句的输出结果为:

93 88 56 45 22 7 19 12 8 7 15 10

同样的,不能保持 numbers 中未排序元素的原始顺序。

3. partial_sort 原理概述

那么 partial_sort 的原理是什么呢?是堆排序!

partial_sort的原理:

  1. 对原始容器内区间为[first, middle)的元素执行 make_heap() 操作构造一个大顶堆,
  2. 然后遍历剩余区间[middle, last)中的元素,剩余区间的每个元素均与大顶堆的堆顶元素进行比较(大顶堆的堆顶元素为最大元素,该元素为第一个元素,很容易获得),若堆顶元素较小,则交换堆顶元素和遍历得到的元素值(pop_heap ),并重新调整该大顶堆以维持该堆为大顶堆(adjust_heap)。
  3. 遍历结束后,[first, middle)区间内的元素便是排名在前的m个元素,再对该堆做一次堆排序 sort_heap() 便可得到最后的结果。

下图为partial_sort 算法的步骤详解:

partial_sort() 算法步骤详解

4. partial_sort 源码讲解

下面是STL中 partial_sort 的源码讲解,考虑到篇幅,这里只列出第一个版本的。

// 版本一
template <class RandomAccessIterator>
inline void partial_sort(RandomAccessIterator first,
    RandomAccessIterator middle,
    RandomAccessIterator last) {
        __partial_sort(first, middle, last, value_type(first));
}
 
template <class RandomAccessIterator, class T>
void __partial_sort(RandomAccessIterator first, RandomAccessIterator middle,
    RandomAccessIterator last, T*) {
        make_heap(first, middle); //将区间[first, middle)构造为一个堆结构
        for (RandomAccessIterator i = middle; i < last; ++i)
            if (*i < *first)    // 遍历堆以外的元素,并将更优的元素放入堆中
                __pop_heap(first, middle, i, T(*i), distance_type(first)); // first值放i中,i的原值融入heap并调整
        sort_heap(first, middle); // 对最终的堆进行排序
}

先来分析 make_heap 算法, 该算法将一段指定的数据排列出max-heap

template <class RandomAccessIterator>
inline void make_heap(RandomAccessIterator first, RandomAccessIterator last) {
    __make_heap(first, last, value_type(first), distance_type(first));
}
 
template <class RandomAccessIterator, class T, class Distance>
void __make_heap(RandomAccessIterator first, RandomAccessIterator last, T*,
    Distance*) {
        if (last - first < 2) return; // 如果长度为0或1,不必重新排列    
        Distance len = last - first;
        // 找出第一个需要重新排列的子树头部(即最后一个子树),以parent标记。由于任何叶子节点都不需要处理。
        // parent 命名不佳, 为 holeIndex 更好
        Distance parent = (len - 2)/2; 
 
        while (true) {
            // 重排以parent为首的子树,以len为操作范围
            __adjust_heap(first, parent, len, T(*(first + parent)));
            if (parent == 0) return; // 走完根节点,结束
            parent--; // 向前排列前一个节点
        }
}

下面来重点分析 __adjust_heap 算法, 该算法调整从first开始的len个元素,洞号为holeIndex,洞值为value, 最终获得一个 max-heap

template <class RandomAccessIterator, class Distance, class T>
void __adjust_heap(RandomAccessIterator first, Distance holeIndex,
    Distance len, T value) {
        Distance topIndex = holeIndex;
        Distance secondChild = 2 * holeIndex + 2; // holeIndex的右子节点
        while (secondChild < len) {
            // 比较holeIndex两个子节点的值,用secondChild代表值较大的子节点
            if (*(first + secondChild) < *(first + (secondChild - 1)))
                secondChild--;   
            // 令较大子值为洞值,注意:原洞值已在函数形参value中得以保存
            *(first + holeIndex) = *(first + secondChild); 
            // 再让洞号下移到左子节点处,
            holeIndex = secondChild;
            // 算出新的洞节点的右子节点
            secondChild = 2 * (secondChild + 1);
        }
        // 如果没有右子节点,只有左子节点
        if (secondChild == len) { 
            // 令左子节点为洞值,然后将洞号下移到左子节点
            *(first + holeIndex) = *(first + (secondChild - 1));
            holeIndex = secondChild - 1;
        }
        // 将原洞值push到新的洞号中。
        // 以下语句的效果类似于: *(first + holeIndex) = value; 
        __push_heap(first, holeIndex, topIndex, value);
}

进一步讲解__adjust_heap 算法 调用的 __push_heap 算法,该算实现将新元素value push到max-heap中topIndex到holeIndex的合适位置中,其中max-heap的起始位置是first。

template <class RandomAccessIterator, class Distance, class T>
void __push_heap(RandomAccessIterator first, Distance holeIndex,
    Distance topIndex, T value) {
        Distance parent = (holeIndex - 1) / 2;  // 找到父节点
        // 当尚未达到顶端, 且父节点的值小于新值(不符合max-heap的次序特性)
        while (holeIndex > topIndex && *(first + parent) < value) {
            *(first + holeIndex) = *(first + parent); // 移动父值到洞号处
            holeIndex = parent; // 调整洞号为父节点
            parent = (holeIndex - 1) / 2; // 新洞的父节点
        }  // 循环到顶端,或者满足max-heap的顺序为止
        *(first + holeIndex) = value; // 将新值放入循环完得到的洞号,完成push操作
}

再回头看 __partial_sort 算法,当*i < *first时,代码中没有互换i所指元素和first所指元素。实际上这一步操作是在 __pop_heap 函数 完成的,在该函数内,先将要first元素放入指定的result,然后用新值value去调整max-heap。

template <class RandomAccessIterator, class T, class Distance>
inline void __pop_heap(RandomAccessIterator first, RandomAccessIterator last,
                       RandomAccessIterator result, T value, Distance*) {
  *result = *first; // 将heap顶端元素值放入 result中
  // 重新调整heap,洞号为0,欲调整的新值为value
  __adjust_heap(first, Distance(0), Distance(last - first), value);
}

最后来看看 sort_heap 算法,该算法将[first, last)中的元素由堆序变为增序排列。每次弹出堆的最大值并放入尾部,然后缩小堆的范围,循环执行弹出操作直至堆只剩下最后一个元素。


template <class RandomAccessIterator>
void sort_heap(RandomAccessIterator first, RandomAccessIterator last) {
    while (last - first > 1)  // 直到只剩一个元素为止
        pop_heap(first, last--); // 每执行一次,范围缩小一格
}

template <class RandomAccessIterator>
inline void pop_heap(RandomAccessIterator first, RandomAccessIterator last) {
    __pop_heap_aux(first, last, value_type(first));
}
 
template <class RandomAccessIterator, class T>
inline void __pop_heap_aux(RandomAccessIterator first,
    RandomAccessIterator last, T*) {
        // 将first元素值(即最大值)放入last-1,然后重调[first, last-1)为max-heap
        __pop_heap(first, last-1, last-1, T(*(last-1)), distance_type(first));
}

参考文章:

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