web-dev-qa-db-fra.com

Comment puis-je répéter des valeurs égales avec la bibliothèque standard?

Supposons que j'ai un vecteur de quelque chose:

std::vector<Foo> v;

Ce vecteur est trié, donc les éléments égaux sont côte à côte.

Quelle est la meilleure façon d'obtenir toutes les paires d'itérateurs représentant des plages avec des éléments égaux (en utilisant la bibliothèque standard)?

while (v-is-not-processed) {
    iterator b = <begin-of-next-range-of-equal-elements>;
    iterator e = <end-of-next-range-of-equal-elements>;

    for (iterator i=b; i!=e; ++i) {
        // Do something with i
    }
}

Je voudrais savoir comment obtenir les valeurs de b et e dans le code ci-dessus.

Ainsi, par exemple, si v contient ces nombres:

 index 0 1 2 3 4 5 6 7 8 9
 value 2 2 2 4 6 6 7 7 7 8

Ensuite, j'aimerais que b et e pointent vers les éléments de la boucle:

 iteration  b  e
 1st        0  3
 2nd        3  4
 3rd        4  6
 4th        6  9
 5th        9 10

Existe-t-il un moyen élégant de résoudre ce problème avec la bibliothèque standard?

40
geza

Il s'agit essentiellement de la gamme v3 group_by : group_by(v, std::equal_to{}). Il n'existe pas dans la bibliothèque standard C++ 17, mais nous pouvons écrire notre propre équivalent approximatif:

template <typename FwdIter, typename BinaryPred, typename ForEach>
void for_each_equal_range(FwdIter first, FwdIter last, BinaryPred is_equal, ForEach f) {
    while (first != last) {
        auto next_unequal = std::find_if_not(std::next(first), last,
            [&] (auto const& element) { return is_equal(*first, element); });

        f(first, next_unequal);
        first = next_unequal;
    }
}

Usage:

for_each_equal_range(v.begin(), v.end(), std::equal_to{}, [&] (auto first, auto last) {
    for (; first != last; ++first) {
        // Do something with each element.
    }
});
28
Justin

Vous pouvez utiliser std::upper_bound pour amener l'itérateur à la valeur "suivante". Puisque std::upper_bound renvoie un itérateur au premier élément supérieur à la valeur fournie, si vous fournissez la valeur de l'élément actuel, il vous donnera un itérateur qui sera un après la fin de la valeur actuelle. Cela vous donnerait une boucle comme

iterator it = v.begin();
while (it != v.end()) {
    iterator b = it;
    iterator e = std::upper_bound(it, v.end(), *it);

    for (iterator i=b; i!=e; ++i) {
        // do something with i
    }
    it = e; // need this so the loop starts on the next value
}
25
NathanOliver

Tu recherches std::equal_range .

Renvoie une plage contenant tous les éléments équivalents à la valeur de la plage [premier, dernier) .

Quelque chose comme ce qui suit devrait fonctionner.

auto it = v.begin();
while (it != v.end())
{
    auto [b, e] = std::equal_range(it, v.end(), *it);
    for (; b != e; ++b) { /* do something in the range[b, e) */ }
    it = e;             // need for the beginning of next std::equal_range
}

Remarque : Même si ce sera une approche intuitive, le std::equal_range obtient ses premier et second itérateurs (c'est-à-dire b et e) à l'aide de std::lower_bound et std::upper_bound , ce qui rend cette approche légèrement inefficace . Depuis, l'itérateur d'abord pourrait être facilement accessible pour le cas de l'OP, en appelant std::upper_bound pour seconde itérateur uniquement nécessaire (comme indiqué par @ NathanOliver réponse).

19
JeJo

Si vos plages de valeurs égales sont courtes, alors std::adjacent_find fonctionnerait bien:

for (auto it = v.begin(); it != v.end();) {
    auto next = std::adjacent_find(it, v.end(), std::not_equal_to<Foo>());
    for(; it != next; ++it) {

    }
}

Vous pouvez également remplacer un lambda par std::not_equal_to si vous le souhaitez.

9
Kyle

Mais même si nous n'utilisons e pour rien, cette formulation est pratique, il est plus difficile de faire une erreur. L'autre façon (pour vérifier les valeurs changeantes) est plus fastidieuse (car nous devons gérer la dernière plage spécialement [...])

Dépend de la façon dont vous interprétez 'gérer spécialement la dernière plage':

auto begin = v.begin();
// we might need some initialization for whatever on *begin...
for(Iterator i = begin + 1; ; ++i)
{
    if(i == v.end() || *i != *begin)
    {
        // handle range single element of range [begin, ???);
        if(i == v.end())
            break;
        begin = i;
        // re-initialize next range
    }
}

Aucune manipulation particulière pour la dernière plage - uniquement, nécessitant éventuellement le code d'initialisation deux fois ...

Approche en boucle imbriquée:

auto begin = v.begin();
for(;;)
{
    // initialize first/next range using *begin
    for(Iterator i = begin + 1; ; ++i)
    {
        if(i == v.end() || *i != *begin)
        {
            // handle range single element of range [begin, ???);
            if(i == v.end())
                goto LOOP_EXIT;
            begin = i;
            break;
        }
    }
}
LOOP_EXIT:
// go on
// if nothing left to do in function, we might prefer returning over going to...

Plus élégant? Certes, je suis moi-même dans le doute ... Les deux approches évitent d'itérer deux fois sur la même plage (d'abord pour trouver la fin, puis l'itération réelle), cependant. Et si nous créons notre propre bibliothèque à partir de:

template <typename Iterator, typename RangeInitializer, typename ElementHandler>
void iterateOverEqualRanges
(
    Iterator begin, Iterator end,
    RangeInitializer ri, ElementHandler eh
)
{
    // the one of the two approaches you like better
    // or your own variation of...
}

nous pourrions alors l'utiliser comme:

std::vector<...> v;
iterateOverEqualRanges
(
    v.begin(), v.end(),
    [] (auto begin) { /* ... */ },
    [] (auto current) { /* ... */ }
);

Maintenant, finalement, il ressemble à e. g. std::for_each, n'est-ce pas?

7
Aconcagua
for(auto b=v.begin(), i=b, e=v.end(); i!=e; b=i) {
    // initialise the 'Do something' code for another range
    for(; i!=e && *i==*b; ++i) {
        // Do something with i
    }
}
0
Walter