什么STL算法可以确定容器中是否只有一个项目满足谓词?

47

我需要一个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;

13
count_if 处理算法部分,但仍需要检查是否等于1。 - Kenny Ostrom
4
这个范围是有序的吗? - Galik
16
@JesperJuhl std::any_of()函数返回至少有1个元素满足谓词条件,但可能不止一个。而std::any_of()并不会返回恰好只有1个元素满足谓词条件,这正是OP所需要的。 - Remy Lebeau
9
我认为你最初使用的代码比你接受的答案清晰得多。为什么要使用库调用来编码你想要做的事情,显然只是为了简洁? - geometrian
1
@imallett 对我来说很难反驳,因为这个答案是我的,但用同样的推理,你可以说使用任何算法都是毫无意义的,因为它们与手写循环做的一样。无论我的回答如何,我不认为手写循环更清晰。这两种代码的作用是:找到一个元素然后找到另一个元素。在算法版本中,人们可以通过阅读代码从头至尾来理解,而在循环中,您必须将代码作为一个整体来考虑才能看出正在发生什么。 - 463035818_is_not_a_number
显示剩余2条评论
4个回答

78
我脑海中浮现出两个想法: 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标准算法。

13
我喜欢第二个选项的短路效应。 - NathanOliver
2
@NathanOliver 我总是尽可能明确地表达我在代码中想要做什么,这并不是为了效率,而主要是为了代码的清晰度。没有人要求整个容器的计数,所以为什么要让代码说谎呢。不幸的是,这并不总是像这个例子那样容易。 - 463035818_is_not_a_number
8
可能会有人不同意我的说法,并且会用言语表达出来,但我觉得有必要说一下。像这样的答案让我想起了标准算法库中有时会存在的美感。 - StoryTeller - Unslander Monica

15
你可以使用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的答案中已经提到。

话虽如此,原始问题中的方法也可以与上述最佳标准方法相媲美,当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;
}

5
是的,但这忽略了不计算超过2的关键要求。 - WilliamKF
3
我理解您的意思是,一旦计数达到2,继续计算谓词就没有必要了。 - WilliamKF
1
@WilliamKF 有点惭愧地同意这个观点。另一个答案展示了更好的替代方案。如果你有兴趣将你展示的算法转换为更好的版本,并将它们打包到一个模板函数中,这里有一个例子:https://wandbox.org/permlink/zej1d4L0J7RoS5PH,它会像你的代码一样进行短路处理。 - JeJo

11

formerlyknownas_463035818的答案开始,这可以泛化为查看容器是否恰好具有满足谓词的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);
    }
}

3
我试了你的代码,但是我无法阅读我的电子邮件。请告诉我我做错了什么。谢谢。更加严肃地说,为什么不进一步将n作为编译时的值(模板非类型参数)呢? - Cody Gray
@CodyGray:如果 n = 10000,编译器是否会递归生成 10000 个模板,还是现代编译器在模板化步骤中足够聪明以进行尾调用消除? - Kevin
2
我认为 std::email_client 概念已经被推迟到 C++23。 - Mark H
@CodyGray 我在这里尝试了你的想法:https://repl.it/repls/BossyIdealDatawarehouse。问题是,正如@Kevin所提到的,对于`n`的每个值都会生成一个新函数。如果您想知道std::vector<int>中是否恰好有100,000个零,则程序将无法编译。 - Mark H
@Kevin编译器无法进行尾调用优化,因为生成的函数是不同的。每个值n都会得到一个新的函数,该函数调用n-1的函数。这导致编译失败。请参见我先前评论中的链接。 - Mark H
关于使用 n 作为模板参数的想法:我认为相反的情况更为真实:递归实现可以很容易地被编译器展开和内联。因此,在这里最好保持递归实现(将 n 作为函数参数),并让编译器优化/内联递归调用(对于较小的 n)。 - Jarek C

9

使用 std::not_fn 反转谓词

作为这个问题算法的核心,它通过聚合std::find_ifstd::none_of被接受的答案中优雅地覆盖,并在失败时进行短路。扫描容器以查找一元谓词,并在满足条件时,继续扫描余下的容器来寻找该谓词的取反结果。我还会提到 C++17 中引入的 negator std::not_fn,替代了不太有用的std::not1std::not2构造。

我们可以使用std::not_fn 来实现与被接受的答案相同的谓词逻辑(std::find_if 条件性跟随 std::none_of),但是语义略有不同,将后者(std::none_of ) 替换为应用于第一步(std::find_if)所使用的一元谓词的取反结果的std::all_of。例如:

// 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在下面的评论中指出的那样。引用评论(由于评论不能保证随时间推移而存在):

仅因为大小是静态的并不意味着使用模板完全展开循环是一个好主意。在生成的汇编代码中,这需要每次迭代都执行一个分支来停止查找,因此这可能就是一个循环分支。CPU擅长运行循环(代码缓存,回路缓冲区)。编译器将根据启发式算法完全展开基于静态大小的循环,但如果a很大,则可能不会将其回滚。因此,您的第一个one_of实现已经拥有最佳的两个世界,假设使用像gcc或clang或者可能是MSVC这样的普通现代编译器。


4
尽管循环大小是静态的,但并不意味着使用模板完全展开循环是一个好主意。在生成的汇编代码中,每次迭代都需要一个分支来停止查找,因此最好使用循环分支。CPU很擅长运行循环(代码缓存、循环缓冲区)。编译器将根据启发式方法完全展开静态大小的循环,但如果"a"很大,可能不会将其重新合并。因此,假设使用像gcc、clang或者MSVC这样的正常现代编译器,你的第一个“one_of”实现已经拥有两全其美的优点。 - Peter Cordes
2
@PeterCordes 感谢您的反馈,提出了很好的观点!由于我目前在工作中没有使用现代C++功能的机会,我担心我的一些答案可能会变得盲目偏袒于使用我发现有趣的特定“新”(非遗留...)核心语言功能进行练习。如果可以的话,我将把您的评论作为我的答案结尾的引用添加进去,因为评论可能不会随着时间而持续存在。 - dfrib

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接