【问题标题】:What STL algorithm can determine if exactly one item in a container satisfies a predicate?什么 STL 算法可以确定容器中的一项是否满足谓词?
【发布时间】:2019-10-23 08:04:15
【问题描述】:

我需要一个 STL 算法,它接受一个谓词和一个集合,如果集合中只有一个成员满足谓词,则返回 true,否则返回 false

我将如何使用 STL 算法做到这一点?

例如,用 STL 算法代码替换下面的代码来表达相同的返回值。

int count = 0;

for( auto itr = c.begin(); itr != c.end(); ++itr ) {
    if ( predicate( *itr ) ) {
      if ( ++count > 1 ) {
        break;
      }
    }
}

return 1 == count;

【问题讨论】:

  • count_if 处理算法部分。您仍然需要检查 ==1
  • 范围排序了吗?
  • @JesperJuhl std::any_of() 返回是否至少有 1 个元素满足谓词。可能不止 1 个。std::any_of() 不返回 EXACTLY 1 个元素是否满足谓词,这正是 OP 想要的。
  • 在我看来,您开始使用的代码比您接受的答案要清晰得多。为什么要对您尝试对库调用执行的操作进行编码显然只是为了简洁?
  • @imallett 我很难反对,因为答案是我的,但同样的道理,你可以说使用任何算法都是毫无意义的,因为它们与手写循环的作用相同。不管我的回答如何,我都不同意手写更清楚。这两个代码的作用是:找到一个元素,然后找到另一个元素。在算法版本中,人们可以理解,通过阅读代码从头到尾,而在带有循环的版本中,您必须将代码作为一个整体来考虑,看看发生了什么

标签: c++ algorithm c++11 counting c++-standard-library


【解决方案1】:

你可以使用std::count_if计数,如果是1则返回。

例如:

#include <iostream>
#include <algorithm> // std::count_if
#include <vector>    // std::vector
#include <ios>       // std::boolalpha

template<class Iterator, class UnaryPredicate>
constexpr bool is_count_one(Iterator begin, const Iterator end, UnaryPredicate pred)
{
    return std::count_if(begin, end, pred) == 1;
}

int main()
{
    std::vector<int> vec{ 2, 4, 3 };
    // true: if only one Odd element present in the container
    std::cout << std::boolalpha
              << is_count_one(vec.cbegin(), vec.cend(),
                  [](const int ele) constexpr noexcept -> bool { return ele & 1; });
    return 0;
}

更新:但是,std::count_if 计算容器中的整个元素,这不如问题中给出的算法好。 @formerlyknownas_463035818 的回答中提到了使用标准算法集合的最佳方法。

话虽如此,OP 的方法也与上述最佳标准方法一样好,当count 达到2 时会发生短路。如果有人对 OP 方法的非标准算法模板函数感兴趣,这里就是。

#include <iostream>
#include <vector>    // std::vector
#include <ios>       // std::boolalpha
#include <iterator>  // std::iterator_traits

template<class Iterator, class UnaryPredicate>
bool is_count_one(Iterator begin, const Iterator end, UnaryPredicate pred)
{
    typename std::iterator_traits<Iterator>::difference_type count{ 0 };
    for (; begin != end; ++begin) {
        if (pred(*begin) && ++count > 1) return false;
    }
    return count == 1;
}

int main()
{
    std::vector<int> vec{ 2, 3, 4, 2 };
    // true: if only one Odd element present in the container
    std::cout << std::boolalpha
              << is_count_one(vec.cbegin(), vec.cend(),
                  [](const int ele) constexpr noexcept -> bool { return ele & 1; });
    return 0;
}

现在可以概括,通过再提供一个参数,N 元素的数量必须/必须在容器中找到。

template<typename Iterator>
using diff_type = typename std::iterator_traits<Iterator>::difference_type;

template<class Iterator, class UnaryPredicate>
bool has_exactly_n(Iterator begin, const Iterator end, UnaryPredicate pred, diff_type<Iterator> N = 1)
{
    diff_type<Iterator> count{ 0 };
    for (; begin != end; ++begin) {
        if (pred(*begin) && ++count > N) return false;
    }
    return count == N;
}

【讨论】:

  • 是的,但这错过了不计算过去 2 的关键要求。
  • 我的意思是一旦count达到2,就不需要继续计算谓词了。
  • @WilliamKF 可耻地同意这一点。另一个答案显示了更好的选择。如果您有兴趣通过将显示的算法打包到模板函数中来将它们转换为更好的版本,这里有一个示例:wandbox.org/permlink/zej1d4L0J7RoS5PH,它将像您的代码一样短路。
【解决方案2】:

我想到了两件事:

std::count_if 然后将结果与1 进行比较。

为了避免遍历整个容器,例如前两个元素已经与谓词匹配,我将使用两个调用来寻找匹配的元素。类似的东西

auto it = std::find_if(begin,end,predicate);
if (it == end) return false;
++it;
return std::none_of(it,end,predicate);

或者如果你更喜欢它更紧凑:

auto it = std::find_if(begin,end,predicate); 
return (it != end) && std::none_of(std::next(it),end,predicate);

感谢 Remy Lebeau 用于压缩,Deduplicator 用于去括号和 Blastfurnance 意识到我们也可以使用none_of 标准算法。

【讨论】:

  • 我喜欢第二个选项的短路。
  • @NathanOliver 我总是努力尽可能明确地说明我在代码中实际想要做什么,实际上并不是为了提高效率,而是主要是为了代码的清晰度。没有人要求对整个容器进行计数,所以为什么要让代码撒谎呢。不幸的是,它并不总是像这个例子那样简单
  • 有人可能会在口头上不同意我的观点,但我只是觉得不得不说出来。正是这样的答案让我想起了有时可以在标准算法库中找到的美丽。
【解决方案3】:

使用std::not_fn 否定谓词

作为这个问题算法的核心(正如在the accepted answer 中结合std::find_ifstd::none_of 优雅地涵盖的那样),在失败时短路,是扫描容器以查找一元谓词和,当遇到时,继续扫描容器的其余部分以查找谓词的 否定,我还会提到 C++17 中引入的否定符 std::not_fn,替换不太有用的 std::not1 和 @ 987654331@ 构造。

我们可以使用std::not_fn 来实现与接受的答案相同的谓词逻辑(std::find_if 有条件地后跟std::none_of),但语义有所不同,将后面的步骤(std::none_of)替换为std::all_of超过第一步中使用的一元谓词的否定 (std::find_if)。例如:

// C++17
#include <algorithm>   // std::find_if
#include <functional>  // std::not_fn
#include <ios>         // std::boolalpha
#include <iostream>
#include <iterator>  // std::next
#include <vector>

template <class InputIt, class UnaryPredicate>
constexpr bool one_of(InputIt first, InputIt last, UnaryPredicate p) {
  auto it = std::find_if(first, last, p);
  return (it != last) && std::all_of(std::next(it), last, std::not_fn(p));
}

int main() {
  const std::vector<int> v{1, 3, 5, 6, 7};
  std::cout << std::boolalpha << "Exactly one even number : "
            << one_of(v.begin(), v.end(), [](const int n) {
                 return n % 2 == 0;
               });  // Exactly one even number : true
}

静态尺寸容器的参数包方法

由于我已经将此答案限制为 C++14(及更高版本),因此我将包含一种用于静态大小容器的替代方法(这里特别适用于 std::array),结合使用 std::index_sequence带参数包扩展:

#include <array>
#include <ios>         // std::boolalpha
#include <iostream>
#include <utility>     // std::(make_)index_sequence

namespace detail {
template <typename Array, typename UnaryPredicate, std::size_t... I>
bool one_of_impl(const Array& arr, const UnaryPredicate& p,
                 std::index_sequence<I...>) {
  bool found = false;
  auto keep_searching = [&](const int n){
      const bool p_res = found != p(n);
      found = found || p_res;
      return !found || p_res;
  };
  return (keep_searching(arr[I]) && ...) && found;
}
}  // namespace detail

template <typename T, typename UnaryPredicate, std::size_t N,
          typename Indices = std::make_index_sequence<N>>
auto one_of(const std::array<T, N>& arr,
            const UnaryPredicate& p) {
  return detail::one_of_impl(arr, p, Indices{});
}

int main() {
  const std::array<int, 5> a{1, 3, 5, 6, 7};
  std::cout << std::boolalpha << "Exactly one even number : "
            << one_of(a, [](const int n) {
                 return n % 2 == 0;
               });  // Exactly one even number : true
}

这也会在早期失败时短路(“发现不止一个”),但将包含比上述方法更简单的布尔比较。

然而,请注意,这种方法可能有其缺点,特别是对于具有许多元素的容器输入的优化代码,正如@PeterCordes 在下面的评论中指出的那样。引用评论(因为不能保证 cmets 会随着时间的推移而持续存在):

仅仅因为大小是静态的并不意味着使用模板完全展开循环是个好主意。在生成的 asm 中,每次迭代都需要一个分支才能在找到时停止,所以这也可能是一个循环分支。 CPU 擅长运行循环(代码缓存、环回缓冲区)。编译器将根据启发式完全展开静态大小的循环,但如果a 很大,则可能不会回滚。所以你的第一个 one_of 实现已经两全其美了,假设一个普通的现代编译器,比如 gcc 或 clang,或者可能是 MSVC

【讨论】:

  • 仅仅因为大小是静态的并不意味着使用模板完全展开循环是一个好主意。在生成的 asm 中,每次迭代都需要一个分支才能在找到时停止,所以这也可能是一个循环分支。 CPU 擅长运行循环(代码缓存、环回缓冲区)。编译器将根据启发式完全展开静态大小的循环,但如果a 很大,则可能不会 将其回滚。所以你的第一个 one_of 实现已经两全其美了,假设一个普通的现代编译器,比如 gcc 或 clang,或者可能是 MSVC。
  • @PeterCordes 感谢您的反馈,好点!由于我目前无法在我的工作中使用现代 C++ 特性,我担心我的一些答案可能会盲目地偏向于使用我觉得可以练习的特定“新”(非传统......)核心语言特性。如果可以的话,我会在我的答案末尾添加您的评论作为引用?因为 cmets 可能不会随着时间的推移而持续存在。
【解决方案4】:

formerlyknownas_463035818's answer 开始,这可以概括为查看容器是否恰好有满足谓词的n 项。为什么?因为这是 C++,直到我们可以在编译时阅读电子邮件才会满足。

template<typename Iterator, typename Predicate>
bool has_exactly_n(Iterator begin, Iterator end, size_t count, Predicate predicate)
{
    if(count == 0)
    {
        return std::none_of(begin, end, predicate);
    }
    else
    {
        auto iter = std::find_if(begin, end, predicate);
        return (iter != end) && has_exactly_n(std::next(iter), end, count - 1, predicate);
    }
}

【讨论】:

  • 我尝试了您的代码,但无法阅读我的电子邮件。请让我知道我做错了什么。谢谢。更严肃地说,为什么不更进一步,将n 也设为编译时值(模板非类型参数)?
  • @CodyGray:如果n = 10000,编译器会递归生成 10000 个模板,还是现代编译器足够聪明,可以在模板步骤中进行尾调用消除?
  • @CodyGray 我相信std::email_client 的概念已经被推回到C++23。
  • @CodyGray 我在这里尝试过你的想法:repl.it/repls/BossyIdealDatawarehouse。正如@Kevin 所提到的,问题是为n 的每个值生成一个新函数。如果您想知道 std::vector 中是否正好有 100,000 个零,程序将无法编译。
  • @Kevin 编译器无法进行尾调用优化,因为生成的函数不同。 n 的每个值都会获得一个新函数,该函数调用 n-1 的函数。这会导致编译失败。请参阅我之前评论中的链接。
猜你喜欢
  • 2015-12-09
  • 1970-01-01
  • 1970-01-01
  • 2020-01-29
  • 1970-01-01
  • 1970-01-01
  • 2019-04-22
  • 1970-01-01
  • 2017-04-02
相关资源
最近更新 更多