掌握 STL 中的分区技术:std::partition 优化 C++ 代码性能

一、简介

对集合进行分区(Partitioning )就是重新排列它,使得满足给定谓词的元素移到最前面,而不满足该谓词的元素移到其后面。第一个不满足谓词的元素称为分区点。这也是满足谓词的子范围的结束点:
在这里插入图片描述
分区是一种常见的数据操作,它将一个集合或范围中的元素按照某个条件重新排列,使得满足给定条件的元素都聚集在前面,而不满足条件的元素聚集在后面。这样做的目的是为了后续的其他操作提供便利,比如查找、删除等。分区的关键点在于确定一个"分区点",它是第一个不满足给定条件的元素的位置。通过分区,可以将原始范围划分为两个子范围:一个包含满足条件的元素,另一个包含不满足条件的元素。

二、使用 std::partition 执行分区

std::partition接受一个范围和一个谓词,并对范围中的元素重新排序,使它们按照这个谓词进行分区:

template<typename ForwardIterator, typename Predicate>
ForwardIterator partition(ForwardIterator first, ForwardIterator last, Predicate p);

std::partition 函数返回一个指向被重新排序的范围的分区点的迭代器。它的复杂度为 O(n)。
std::partition 不能保证满足(或不满足)谓词条件的元素的顺序。如果需要这种保证,可以使用 std::stable_partitionstd::stable_partition 也会返回一个指向重新排序范围分区点的迭代器。

template< class BidirIt, class UnaryPredicate >
BidirIt stable_partition( BidirIt first, BidirIt last, UnaryPredicate p );

与其他算法相反,std::stable_partition允许分配临时缓冲区。如果有足够的额外内存来分配,那么它的复杂度是O(n),否则是O(n*log(n))

使用示例:

#include <iostream>
#include <vector>
#include <algorithm>

bool isEven(int n) {
    return n % 2 == 0;
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    auto it = std::stable_partition(numbers.begin(), numbers.end(), isEven);

    std::cout << "Partition point: " << *it << std::endl;

    std::cout << "Partitioned elements: ";
    for (auto iter = numbers.begin(); iter != it; ++iter) {
        std::cout << *iter << " ";
    }
    std::cout << std::endl;

    std::cout << "Remaining elements: ";
    for (auto iter = it; iter != numbers.end(); ++iter) {
        std::cout << *iter << " ";
    }
    std::cout << std::endl;

    return 0;
}

输出:

Partition point: 5
Partitioned elements: 2 4 6 8 10
Remaining elements: 1 3 5 7 9

如果需要保持范围不变,并将输出放在其他地方,可以使用std::partition_copy。它在两个范围中写入输出:第一个范围包含满足谓词的元素,第二个范围包含不满足谓词的元素。std::partition_copy返回一个指向两个输出范围末尾的迭代器对。函数原型:

template<typename InputIt, typename OutputIt1, typename OutputIt2, typename Predicate>
std::pair<OutputIt1, OutputIt2>
        partition_copy(InputIt first, InputIt last,
                       OutputIt first_true, OutputIt first_false,
                       Predicate p);

三、检查范围的分区属性

使用std::is_partitioned检查是否根据某个谓词对一个范围进行了分区。函数原型:

template<typename InputIt, typename Predicate>
bool is_partitioned(InputIt first, InputIterator last, Predicate p);

要获取分区范围的分区点,使用std::partition_point

template<typename ForwardIterator, typename Predicate>
ForwardIterator partition_point(ForwardIterator first,
                                ForwardIterator last,
                                Predicate p);

与在STL中的排序算法中看到的std::is_sorted_until类似,Boost添加了一个is_partitioned_until函数。该算法接受一个范围和一个谓词,并返回从该范围不再被分区的首个位置开始的迭代器。

四、分区的应用示例

4.1、lower_bound、upper_bound 和 equal_range

std::lower_bound可以通过使用分区算法来实现。在给定值a的范围下界之前的每个元素x都满足谓词x < a。下界是第一个不满足该谓词的元素,因此a的下界实际上是谓词x < a的划分点。

因此,std::lower_bound的一种可能实现是:

template<typename ForwardIt, typename T>
ForwardIterator lower_bound(ForwardIt first, ForwardIt last, const T& value)
{
    return std::partition_point(first, last, [value](const auto& x){return x < value;});
}

这同样适用于std::upper_bound,使用谓词!(a < x)lower_boundupper_bound本身可以用来实现std::equal_range

4.2、收集

如何将一个范围内满足条件的元素聚集到给定位置?即:
在这里插入图片描述

变成:
在这里插入图片描述
使用std::stable_partition可以很容易地实现这一点。

一个可行的想法是将初始范围视为2部分:[begin, position][position, end]

  • [begin, position]上应用一个稳定分区,将满足谓词的所有元素放在末尾(用谓词的否定来划分)。
  • [position, end]上应用一个稳定分区,拉出满足范围元素的所有元素。

每次调用std::stable_partition都会返回相应的分区点,分别是收集范围的开始和结束。这个范围可以从函数返回。

template<typename BidirIterator, typename Predicate>
Range<BidirIterator> gather(BidirIterator first, BidirIterator last,
                            BidirIterator position, Predicate p)
{
    return { std::stable_partition(first, position, std::not_fn(p)),
             std::stable_partition(position, last, p) };
}

注意,C++ 17中的std::not_fn函数取代了旧的std::not1来否定函数。

Range是一个类,可以用两个表示开始和结束的迭代器进行初始化,例如boost::iterator_rangeRange -v3中的迭代器。也可以使用std::pair的迭代器,就像std::equal_range一样,但是显得比较笨拙。

注意,在boost中,可以通过boost::algorithm::gather函数使用gather算法,该函数返回一对迭代器。

五、总结

知道如何用STL实现分区是很有用的,因为这个概念出现在很多情况下。它是C++工具箱中的另一个重要工具。

学会使用STL中的分区相关算法是很有帮助的,因为这些算法广泛应用于各种数据处理场景。掌握好分区的概念和使用方法,可以让我们在面对需要对元素进行重新排列的问题时,有更加灵活和高效的解决方案。无论是基本的std::partitionstd::stable_partition,还是一些扩展算法如std::is_partitionedstd::partition_point,都是我们工具箱中不可或缺的重要组件。

在这里插入图片描述

  • 25
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lion Long

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值