web-dev-qa-db-fra.com

Devrais-je éviter d'utiliser goto ici? Si c'est le cas, comment?

Je code pour une fonction qui prend une main et vérifie les paires:

int containsPairs(vector<Card> hand)
{
    int pairs{ 0 };

    loopstart:
    for (int i = 0; i < hand.size(); i++)
    {
        Card c1 = hand[i];
        for (int j = i + 1; j < hand.size(); j++)
        {
            Card c2 = hand[j];
            if (c1.getFace() == c2.getFace())
            {
                pairs++;
                hand.erase(hand.begin() + i);
                hand.erase(hand.begin() + (j - 1));
                goto loopstart;
            }
        }
    }
    return pairs;
}

Quand il trouve la paire sur la ligne 10, je veux supprimer les cartes de la main avec laquelle il a trouvé la paire, puis redémarrer la boucle entière avec les cartes supprimées pour trouver une seconde paire, le cas échéant. Pour moi, goto était le moyen le plus intuitif de faire cela, mais dans ce cas, est-ce vrai?

26
Alex

Essaye ça:

int containsPairs(vector<int> hand)
{
    int pairs{ 0 };

    for (int i = 0; i < hand.size(); i++)
    {
        int c1 = hand[i];
        for (int j = i + 1; j < hand.size(); j++)
        {
            int c2 = hand[j];
            if (c1 == c2)
            {
                pairs++;
                hand.erase(hand.begin() + i);
                hand.erase(hand.begin() + (j - 1));
                i--;
                break;
            }
        }
    }
    return pairs;
}

C'est presque votre version, la seule différence est qu'au lieu de goto, il y a i--; break;. Cette version est plus efficace que la vôtre, car elle ne fait la double boucle qu'une seule fois.

Est-ce plus clair? Eh bien, c'est une préférence personnelle. Je ne suis pas du tout contre goto, je pense que son statut actuel "ne l'utilisez jamais" devrait être révisé. Il y a des cas où goto est la meilleure solution.


Voici une autre solution encore plus simple:

int containsPairs(vector<int> hand)
{
    int pairs{ 0 };

    for (int i = 0; i < hand.size(); i++)
    {
        int c1 = hand[i];
        for (int j = i + 1; j < hand.size(); j++)
        {
            int c2 = hand[j];
            if (c1 == c2)
            {
                pairs++;
                hand.erase(hand.begin() + j);
                break;
            }
        }
    }
    return pairs;
}

En gros, quand il trouve une paire, il enlève seulement la carte la plus éloignée et coupe la boucle. Il n’est donc pas nécessaire d’être délicat avec i.

27
geza

Un algorithme (légèrement) plus rapide évite également la variable goto

L'effacement d'un std::vector n'est jamais rapide et doit être évité. Il en va de même pour la copie d'un std::vector. En évitant les deux, vous évitez également la goto. Par exemple

size_t containsPairs(std::vector<Card> const &hand) // no copy of hand
{
    size_t num_pairs = 0;
    std::unordered_set<size_t> in_pair;

    for(size_t i=0; i!=hand.size(); ++i)
    {
        if(in_pair.count(i)) continue;
        auto c1 = hand[i];
        for(size_t j=i+1; j!=hand.size(); ++j)
        {
            if(in_pair.count(j)) continue;
            auto c2 = hand[j];
            if (c1.getFace() == c2.getFace())
            {
                ++num_pairs;
                in_pair.insert(i);
                in_pair.insert(j);
            }
        }
    }
    return num_pairs;
}

Pour les grandes mains, cet algorithme est toujours lent, puisque O (N ^ 2). Plus rapide serait le tri, après quoi les paires doivent être des cartes adjacentes, donnant un algorithme O (N logN).

Pourtant, plus vite, O(N), consiste à utiliser un unordered_set non pour les cartes par paires, mais pour toutes les autres cartes:

size_t containsPairs(std::vector<Card> const &hand) // no copy of hand
{
    size_t num_pairs = 0;
    std::unordered_set<Card> not_in_pairs;
    for(auto card:hand)
    {
        auto match = not_in_pairs.find(card));
        if(match == not_in_pairs.end())
        {
            not_in_pairs.insert(card);
        }
        else
        {
            ++num_pairs;
            not_in_pairs.erase(match);
        }   
    }
    return num_pairs;
}

Pour une hand.size() suffisamment petite, cette valeur peut ne pas être plus rapide que le code ci-dessus, en fonction de la sizeof(Card) et/ou du coût de son constructeur. Une approche similaire consiste à utiliser distribution comme suggéré dans Réponse d'Eric Duminil :

size_t containsPairs(std::vector<Card> const &hand) // no copy of hand
{
    std::unordered_map<Card,size_t> slots;
    for(auto card:hand)
    {
        slots[card]++;
    }
    size_t num_pairs = 0;
    for(auto slot:slots)
    {
        num_pairs += slot.second >> 1;
    }
    return num_pairs;
}

Bien sûr, ces méthodes peuvent être implémentées beaucoup plus simplement si Card peut être mappé de manière triviale dans un petit entier, lorsqu'aucun hachage n'est requis.

21
Walter

Pour vous amuser, voici deux autres façons, je présente une méthode légèrement plus efficace, sans pause ou goto. Je présente ensuite une méthode moins efficace qui trie d’abord.

Ces deux méthodes sont simples à lire et à comprendre.

Celles-ci sont simplement destinées à montrer des alternatives aux autres réponses. La première méthode contient des paires requiert que les valeurs de carte soient comprises entre 0 et 13 et se briseront si cela n'est pas vrai, mais est très légèrement plus efficace que les autres réponses que j'ai vues.

int containsPairs(const vector<int> &hand)
{
    int pairs{ 0 };
    std::vector<int> counts(14); //note requires 13 possible card values
    for (auto card : hand){
        if(++counts[card] == 2){
            ++pairs;
            counts[card] = 0;
        }
    }
    return pairs;
}

int containsPairs(const vector<int> &hand)
{
    int pairs{ 0 };

    std::sort(hand.begin(), hand.end());
    for (size_t i = 1;i < hand.size();++i){
        if(hand[i] == hand[i - 1]){
            ++i;
            ++pairs;
        }
    }
    return pairs;
}

Remarque: plusieurs des autres réponses traiteront 3 cartes similaires dans une main comme 2 paires. Les deux méthodes ci-dessus tiennent compte de cela et ne compteront à la place qu'une paire pour un brelan. Ils le traiteront comme 2 paires s'il y a 4 cartes similaires.

7
M2tM

goto n'est qu'un problème. Un autre gros problème est que votre méthode est inefficace.

Votre méthode

Votre méthode actuelle examine essentiellement la première carte, effectue une itération sur le reste et recherche la même valeur. Il revient ensuite à la deuxième carte et la compare au reste. C'est O(n**2).

Tri

Comment comptez-vous les paires dans la vie réelle? Vous auriez probablement trier les cartes par valeur et chercher des paires. Si vous triez efficacement, ce serait O(n*log n).

Distribution

La méthode la plus rapide consiste à préparer 13 emplacements sur une table et à répartir les cartes en fonction de leur valeur nominale. Après avoir distribué chaque carte, vous pouvez compter les cartes sur chaque emplacement et voir si un emplacement contient au moins 2 cartes. C'est O(n) et il permettrait également de détecter trois types ou quatre types.

Bien sûr, il n'y a pas beaucoup de différence entre n**2 et n lorsque n est 5. En prime, la dernière méthode serait concise, facile à écrire et goto- gratuite.

6
Eric Duminil

Si vous voulez vraiment éviter goto, vous pouvez simplement appeler la fonction récursive, où serait la ligne goto [label], en transmettant toutes les variables dont vous souhaitez enregistrer l'état en tant que paramètres. Cependant, je recommanderais de s'en tenir au goto.

3
Cpp plus 1

Personnellement, je mettrais ces deux boucles dans un lambda, au lieu de goto, je reviendrais de ce lambda avec l’indication que les boucles devraient redémarrer et appellerais le lambda en boucle. Quelque chose comme ca:

auto iterate = [&hand, &pairs]() {
             {
              ... // your two loops go here, instead of goto return true
             }
             return false;
}

while (iterate());

Petit ajout : Je ne pense pas que ce soit le meilleur algorithme pour trouver des paires de cartes dans un deck. Il y a de bien meilleures options pour cela. Je réponds plutôt à la question omniprésente de savoir comment transférer le contrôle dans ou hors de deux boucles à la fois.

2
SergeyA

Oui, vous devriez éviter d'utiliser goto ici.

C'est une utilisation inutile de goto spécifiquement parce que l'algorithme n'en a pas besoin. En passant, j’ai tendance à ne pas utiliser goto, mais je n’en suis pas farouchement opposé, comme beaucoup. goto est un excellent outil pour rompre les boucles imbriquées ou pour quitter une fonction proprement lorsqu'une interface ne prend pas en charge RAII.

Votre approche actuelle présente quelques inconvénients:

  • Il n'y a aucune raison de rechercher à nouveau la liste depuis le début lorsque vous trouvez une paire correspondante. Vous avez déjà recherché toutes les combinaisons précédentes. La suppression de cartes ne modifie pas l'ordre relatif des cartes non retirées et ne vous fournit pas non plus de paires.
  • Il n'est pas nécessaire de supprimer les éléments du milieu de hand. Pour ce problème, supprimer du milieu d'un std::vector représentant probablement une main de 5 cartes n'est pas un problème. Cependant, si le nombre de cartes est important, cela peut être inefficace. Dans de tels problèmes, vous devriez vous demander si l'ordre des éléments est important. La réponse est non, cela n'a pas d'importance. Nous pouvons mélanger toutes les cartes qui n'ont pas encore été couplées et obtenir la même réponse.

Voici une version modifiée de votre code:

int countPairs(std::vector<Card> hand)
{
    int pairs{ 0 };

    for (decltype(hand.size()) i = 0; i < hand.size(); ++i)
    {
        // I assume getFace() has no side-effects and is a const
        // method of Card.  If getFace() does have side-effects
        // then this whole answer is flawed.
        const Card& c1 = hand[i];
        for (auto j = i + 1; j < hand.size(); ++j)
        {
            const Card& c2 = hand[j];
            if (c1.getFace() == c2.getFace())
            {
                // We found a matching card for card i however we
                // do not need to remove card i since we are
                // searching forward.  Swap the matching card
                // (card j) with the last card and pop it from the
                // back.  Even if card j is the last card, this
                // approach works fine.  Finally, break so we can
                // move on to the next card.
                pairs++;
                std::swap(c2, hand.back());
                hand.pop_back(); // Alternatively decrement a size variable
                break;
            }
        }
    }
    return pairs;
}

Vous pouvez modifier l'approche ci-dessus pour utiliser des itérateurs si vous le souhaitez. Vous pouvez également prendre une référence constante std::vector et utiliser std::reference_wrapper pour trier à nouveau le conteneur.

Pour un meilleur algorithme global, construisez un tableau de fréquence de chaque valeur faciale et de son compte correspondant.

2
twohundredping
#include <vector>
#include <unordered_map>
#include <algorithm>

std::size_t containsPairs(const std::vector<int>& hand)
{
    // boilerplate for more readability
    using card_t = std::decay_t<decltype(hand)>::value_type;
    using map_t = std::unordered_map<card_t, std::size_t>;

    // populate map and count the entrys with 2 occurences
    map_t occurrences;
    for (auto&& c : hand) { ++occurrences[c]; }
    return std::count_if( std::cbegin(occurrences), std::cend(occurrences), [](const map_t::value_type& entry){ return entry.second == 2; });
}
1
phön

Je le ferais probablement de cette façon:

Caractéristiques: 

  • 3 d'un genre n'est pas une paire
  • renvoie un vecteur de cartes dans l'ordre décroissant Face indiquant quelles faces sont des paires dans la main.

std::vector<Card> reduceToPair(std::vector<Card> hand)
{
    auto betterFace = [](auto&& cardl, auto&& cardr)
    {
        return cardl.getFace() > cardr.getFace();
    };

    std::sort(begin(hand), end(hand), betterFace);

    auto first = begin(hand);
    while (first != end(hand))
    {
        auto differentFace = [&](auto&& card)
        {
            return card.getFace() != first->getFace();
        };
        auto next = std::find_if(first + 1, end(hand), differentFace);
        auto dist = std::distance(first, next);
        if (dist == 2)
        {
            first = hand.erase(first + 1, next);
        }
        else
        {
            first = hand.erase(first, next);
        }
    }

    return hand;
}

usage:

pairInfo = reduceToPair(myhand);
bool hasPairs = pairInfo.size();
if (hasPairs)
{
  auto highFace = pairInfo[0].getFace();
  if (pairInfo.size() > 1) {
    auto lowFace = pairInfo[1].getFace();
  }
}
1
Richard Hodges

Si le tri des cartes par visage est possible et autorisé, nous pouvons compter les paires en utilisant un seul passage sans rien effacer:

bool Compare_ByFace(Card const & left, Card const & right)
{
    return(left.Get_Face() < right.Get_Face());
}

size_t Count_Pairs(vector<Card> hand)
{
    size_t pairs_count{0};
    if(1 < hand.size())
    {
        sort(hand.begin(), hand.end(), &Compare_ByFace);
        auto p_card{hand.begin()};
        auto p_ref_card{p_card};
        for(;;)
        {
           ++p_card;
           if(hand.end() == p_card)
           {          
               pairs_count += static_cast< size_t >((p_card - p_ref_card) / 2);
               break;
           }
           if(p_ref_card->Get_Face() != p_card->Get_Face())
           {
               pairs_count += static_cast< size_t >((p_card - p_ref_card) / 2);
               p_ref_card = p_card;
           }
        }
    }
    return(pairs_count);
}
1
VTT

Un problème avec goto est que les étiquettes ont tendance à faire des excuses lors d'un refactoring errant. C'est fondamentalement pourquoi je ne les aime pas. Personnellement, dans votre cas, si vous devez conserver l’algorithme tel quel, j’utiliserai la variable goto dans un appel récursif:

int containsPairs(vector<Card>&/*Deliberate change to pass by reference*/hand)
{
    for (int i = 0; i < hand.size(); i++)
    {
        Card c1 = hand[i];
        for (int j = i + 1; j < hand.size(); j++)
        {
            Card c2 = hand[j];
            if (c1.getFace() == c2.getFace())
            {
                hand.erase(hand.begin() + i);
                hand.erase(hand.begin() + (j - 1));
                return 1 + containsPairs(hand); 
            }
        }
    }
    return 0;
}

La surcharge lors de la création de la pile est négligeable, cf. les manipulations std::vector. Cela peut s'avérer peu pratique en fonction du site d'appel: vous ne pouvez plus appeler la fonction avec un temporaire anonyme par exemple. Mais, en réalité, il existe de meilleures alternatives pour l'identification des paires: pourquoi ne pas commander la main de manière plus optimale?

0
Bathsheba

Comme d'autres l'ont fait remarquer, vous devriez non seulement éviter goto, mais aussi écrire votre propre code s'il existe un algorithme standard capable de faire le travail. Je suis surpris que personne n'ait suggéré unique, qui est conçu à cet effet:

bool cardCmp(const Card& a, const Card& b) {
    return a.getFace() < b.getFace();
}

size_t containsPairs(vector<Card> hand) {
    size_t init_size = hand.size();

    std::sort(hand.begin(), hand.end(), cardCmp);
    auto it = std::unique(hand.begin(), hand.end(), cardCmp);
    hand.erase(it, hand.end());

    size_t final_size = hand.size();
    return init_size - final_size;
}

(Première réponse sur StackOverflow - excuses pour les faux pas!)

0
James Raynard

Les autres réponses à ce jour concernent la restructuration fondamentale de votre code. Ils font valoir que votre code n'était pas très efficace au début et que, au moment où vous avez résolu le problème, il vous suffit de sortir d'une boucle, vous n'avez donc pas besoin de la variable goto.

Mais je vais répondre à la question de savoir comment éviter goto sans changer fondamentalement l’algorithme. La solution (comme c'est très souvent le cas pour éviter goto) consiste à déplacer une partie de votre code dans une fonction distincte et à utiliser une return précoce:

void containsPairsImpl(vector<Card>& hand, int& pairs)
{
    for (int i = 0; i < hand.size(); i++)
    {
        Card c1 = hand[i];
        for (int j = i + 1; j < hand.size(); j++)
        {
            Card c2 = hand[j];
            if (c1.getFace() == c2.getFace())
            {
                pairs++;
                hand.erase(hand.begin() + i);
                hand.erase(hand.begin() + (j - 1));
                return;
            }
        }
    }
    hand.clear();
}

int containsPairs(vector<Card> hand)
{
    int pairs{ 0 };
    while (!hand.empty()) {
        containsPairsImpl(hand, pairs);
    }
    return pairs;
}

Notez que je passe hand et pairs en faisant référence à la fonction interne afin qu’elles puissent être mises à jour. Si vous avez beaucoup de ces variables locales, ou si vous devez diviser la fonction en plusieurs parties, cela peut devenir lourd. La solution consiste alors à utiliser une classe:

class ContainsPairsTester {
public:
    ContainsPairsTester(): m_hand{}, m_pairs{0} {}

    void computePairs(vector<Card> hand);

    int pairs() const { return m_pairs; }
private:
    vector<Card> m_hand;
    int m_pairs;

    void computePairsImpl(vector<Card> hand);
};

void ContainsPairsTester::computePairsImpl()
{
    for (int i = 0; i < m_hand.size(); i++)
    {
        Card c1 = m_hand[i];
        for (int j = i + 1; j < m_hand.size(); j++)
        {
            Card c2 = m_hand[j];
            if (c1.getFace() == c2.getFace())
            {
                m_pairs++;
                m_hand.erase(m_hand.begin() + i);
                m_hand.erase(m_hand.begin() + (j - 1));
                return;
            }
        }
    }
    m_hand.clear();
}

void ContainsPairsTester::computePairs(vector<Card> hand)
{
    m_hand = hand;
    while (!m_hand.empty()) {
        computePairsImpl();
    }
}
0
Arthur Tacca

Votre implémentation ne fonctionne pas car elle compte trois types comme une paire, quatre types comme deux.

Voici une implémentation que je suggérerais:

int containsPairs(std::vector<Card> hand)
{
    std::array<int, 14> face_count = {0};
    for (const auto& card : hand) {
        ++face_count[card.getFace()]; // the Face type must be implicitly convertible to an integral. You might need to provide this conversion or use an std::map instead of std::array.
    }
    return std::count(begin(face_count), end(face_count), 2);
}

( démo sur coliru )

Il peut être généralisé de compter non seulement les paires mais aussi n d'un type en modifiant le 2.

0
YSC

Êtes-vous autorisé à modifier l'ordre des éléments dans un vecteur? Si oui, utilisez simplement un algorithme adjacent_find dans une seule boucle. 

Ainsi, non seulement vous vous débarrasserez de goto, mais vous obtiendrez de meilleures performances (vous avez actuellement O(N^2)) et vous garantissez l'exactitude

std::sort(hand.begin(), hand.end(), 
    [](const auto &p1, const auto &p2) { return p1.getFace() < p2.getFace(); });
for (auto begin = hand.begin(); begin != hand.end(); )
{
  begin = std::adjacent_find(begin, hand.end(), 
        [](const auto &p1, const auto &p2) { return p1.getFace() == p2.getFace(); });
  if (begin != hand.end())
  {
    auto distance = std::distance(hand.begin(), begin);
    std::erase(begin, begin + 2);  // If more than 2 card may be found, use find to find to find the end of a range
    begin = hand.begin() + distance;
  }
}
0

Bien que goto ne soit pas si terrible si vous en avez besoin, ce n'est pas nécessaire ici. Etant donné que vous ne vous souciez que du nombre de paires, il n'est pas nécessaire non plus d'enregistrer ce que sont ces paires Vous pouvez simplement xor dans toute la liste.

Si vous utilisez GCC ou Clang, ce qui suit fonctionnera. En MSVC, vous pouvez utiliser __popcnt64() à la place.

int containsPairs(vector<Card> hand)
{
    size_t counter = 0;
    for ( Card const& card : hand )
        counter ^= 1ul << (unsigned) card.getFace();

    return ( hand.size() - __builtin_popcountll(counter) ) / 2u;
}
0
KevinZ