Quel est le moyen C++ de vérifier si un élément est contenu dans un tableau/une liste, comme ce que fait l'opérateur in
en Python?
if x in arr:
print "found"
else
print "not found"
Comment la complexité temporelle de l'équivalent C++ se compare-t-elle à l'opérateur in
de Python?
La complexité temporelle de l'opérateur in
de Python varie en fonction de la structure de données avec laquelle il est appelé. Lorsque vous l'utilisez avec une liste, la complexité est linéaire (comme on pourrait s'y attendre d'un tableau non trié sans index). Lorsque vous l'utilisez pour rechercher une appartenance à un ensemble ou la présence d'un dictionnaire, la complexité de la clé est en moyenne constante (comme on pourrait s'y attendre d'une implémentation basée sur une table de hachage):
En C++, vous pouvez utiliser std::find
Pour déterminer si un élément est contenu dans un std::vector
. La complexité est dite linéaire (comme on pourrait s'y attendre d'un tableau non trié sans index). Si vous vous assurez que le vecteur est trié, vous pouvez également utiliser std::binary_search
Pour obtenir le même résultat en temps logarithmique.
Les conteneurs associatifs fournis par la bibliothèque standard (std::set
, std::unordered_set
, std::map
, ...) fournissent à la fonction membre find()
, pour cela, performer mieux que la recherche linéaire, c’est-à-dire logarithmique ou à temps constant, selon que vous ayez choisi l’alternative ordonnée ou non.
Si vous le souhaitez, vous pouvez utiliser certains modèles de magie pour écrire une fonction wrapper qui sélectionne la méthode correcte pour le conteneur concerné, par exemple, comme présenté dans cette réponse .
Vous pouvez aborder cela de deux manières:
Vous pouvez utiliser std::find
de <algorithm>
:
auto it = std::find(container.begin(), container.end(), value);
if (it != container.end())
return it;
ou vous pouvez parcourir tous les éléments de vos conteneurs avec des boucles à distance:
for(const auto& it : container)
{
if(it == value)
return it;
}
Python fait différentes choses pour in
selon le type de conteneur utilisé. En C++, vous voudriez le même mécanisme. La règle générale pour les conteneurs standard est que s'ils fournissent une find()
, il s'agira d'un meilleur algorithme que std::find()
(par exemple, find()
pour std::unordered_map
est O (1), mais std::find()
est toujours O (N)).
Donc, nous pouvons écrire quelque chose à faire cette vérification nous-mêmes. Le plus concis serait de tirer parti des if constexpr
De C++ 17 et d'utiliser quelque chose comme celui de Yakk can_apply
:
template <class C, class K>
using find_t = decltype(std::declval<C const&>().find(std::declval<K const&>()));
template <class Container, class Key>
bool in(Container const& c, Key const& key) {
if constexpr (can_apply<find_t, Container, Key>{}) {
// the specialized case
return c.find(key) != c.end();
} else {
// the general case
using std::begin; using std::end;
return std::find(begin(c), end(c), key) != end(c);
}
}
En C++ 11, nous pouvons tirer parti de l'expression SFINAE:
namespace details {
// the specialized case
template <class C, class K>
auto in_impl(C const& c, K const& key, int )
-> decltype(c.find(key), true) {
return c.find(key) != c.end();
}
// the general case
template <class C, class K>
bool in_impl(C const& c, K const& key, ...) {
using std::begin; using std::end;
return std::find(begin(c), end(c), key) != end(c);
}
}
template <class Container, class Key>
bool in(Container const& c, Key const& key) {
return details::in_impl(c, key, 0);
}
Notez que dans les deux cas, nous avons le using std::begin; using std::end;
En deux étapes pour gérer tous les conteneurs standard, les tableaux bruts et tous les conteneurs fournis/adaptés à l'utilisation.
Je suppose que l’on pourrait utiliser this thread et créer une version personnalisée de la fonction in
.
L'idée principale est d'utiliser SFINAE (l'échec de la substitution n'est pas une erreur) pour différencier les conteneurs associatifs (qui ont type_clé membre. ) depuis les conteneurs de séquence (qui n'ont pas de membre key_type).
Voici une implémentation possible:
namespace detail
{
template<typename, typename = void>
struct is_associative : std::false_type {};
template<typename T>
struct is_associative<T,
std::enable_if_t<sizeof(typename T::key_type) != 0>> : std::true_type {};
template<typename C, typename T>
auto in(const C& container, const T& value) ->
std::enable_if_t<is_associative<C>::value, bool>
{
using std::cend;
return container.find(value) != cend(container);
}
template<typename C, typename T>
auto in(const C& container, const T& value) ->
std::enable_if_t<!is_associative<C>::value, bool>
{
using std::cbegin;
using std::cend;
return std::find(cbegin(container), cend(container), value) != cend(container);
}
}
template<typename C, typename T>
auto in(const C& container, const T& value)
{
return detail::in(container, value);
}
Petit exemple d'utilisation sur [~ # ~] wandbox [~ # ~] .
Cela vous donne un infixe opérateur *in*
:
namespace notstd {
namespace ca_helper {
template<template<class...>class, class, class...>
struct can_apply:std::false_type{};
template<class...>struct voider{using type=void;};
template<class...Ts>using void_t=typename voider<Ts...>::type;
template<template<class...>class Z, class...Ts>
struct can_apply<Z,void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply = ca_helper::can_apply<Z,void,Ts...>;
namespace find_helper {
template<class C, class T>
using dot_find_r = decltype(std::declval<C>().find(std::declval<T>()));
template<class C, class T>
using can_dot_find = can_apply< dot_find_r, C, T >;
template<class C, class T>
constexpr std::enable_if_t<can_dot_find<C&, T>{},bool>
find( C&& c, T&& t ) {
using std::end;
return c.find(std::forward<T>(t)) != end(c);
}
template<class C, class T>
constexpr std::enable_if_t<!can_dot_find<C&, T>{},bool>
find( C&& c, T&& t ) {
using std::begin; using std::end;
return std::find(begin(c), end(c), std::forward<T>(t)) != end(c);
}
template<class C, class T>
constexpr bool Finder( C&& c, T&& t ) {
return find( std::forward<C>(c), std::forward<T>(t) );
}
}
template<class C, class T>
constexpr bool find( C&& c, T&& t ) {
return find_helper::Finder( std::forward<C>(c), std::forward<T>(t) );
}
struct Finder_t {
template<class C, class T>
constexpr bool operator()(C&& c, T&& t)const {
return find( std::forward<C>(c), std::forward<T>(t) );
}
constexpr Finder_t() {}
};
constexpr Finder_t Finder{};
namespace named_operator {
template<class D>struct make_operator{make_operator(){}};
template<class T, char, class O> struct half_apply { T&& lhs; };
template<class Lhs, class Op>
half_apply<Lhs, '*', Op> operator*( Lhs&& lhs, make_operator<Op> ) {
return {std::forward<Lhs>(lhs)};
}
template<class Lhs, class Op, class Rhs>
auto operator*( half_apply<Lhs, '*', Op>&& lhs, Rhs&& rhs )
-> decltype( named_invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) ) )
{
return named_invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) );
}
}
namespace in_helper {
struct in_t:notstd::named_operator::make_operator<in_t> {};
template<class T, class C>
bool named_invoke( T&& t, in_t, C&& c ) {
return ::notstd::find(std::forward<C>(c), std::forward<T>(t));
}
}
in_helper::in_t in;
}
Sur un conteneur plat, comme un tableau vectoriel ou une chaîne, il s'agit de O (n).
Sur un conteneur trié associatif, comme un std::map
, std::set
, Il s’agit de O (lg (n)).
Sur un conteneur associé non ordonné, tel que std::unordered_set
, Il s'agit de O (1).
Code de test:
std::vector<int> v{1,2,3};
if (1 *in* v)
std::cout << "yes\n";
if (7 *in* v)
std::cout << "no\n";
std::map<std::string, std::string, std::less<>> m{
{"hello", "world"}
};
if ("hello" *in* m)
std::cout << "hello world\n";
C++ 14, mais principalement pour enable_if_t
.
Que se passe-t-il?
Eh bien, can_apply
Est un peu de code qui me permet d’écrire can_dot_find
, Qui détecte (au moment de la compilation) si container.find(x)
est une expression valide.
Cela me permet d’envoyer le code de recherche à utiliser membre-find s’il existe. Si elle n'existe pas, une recherche linéaire utilisant std::find
Est utilisée à la place.
Ce qui est un peu un mensonge. Si vous définissez une fonction libre find(c, t)
dans l'espace de noms de votre conteneur, il l'utilisera plutôt que l'une des précédentes. Mais c’est cela qui me fait envie (et cela vous permet d’étendre les conteneurs tiers avec le support *in*
).
C'est cette extensibilité ADL (recherche dépendante de l'argument) (la capacité d'extension tierce) qui explique pourquoi nous avons trois fonctions différentes nommées find
, deux dans un espace de noms d'assistance et une dans notstd
. Vous êtes censé appeler notstd::find
.
Ensuite, nous voulons un in
de type python, et qu'est-ce qui est plus python comme un opérateur infixe? Pour ce faire en C++, vous devez envelopper votre nom d'opérateur dans d'autres opérateurs J'ai choisi *
, Nous avons donc un infixe *in*
Nommé opérateur.
Vous faites using notstd::in;
Pour importer l'opérateur nommé in
.
Après cela, t *in* c
Vérifie d’abord si find(t,c)
est valide. Sinon, il vérifie si c.find(t)
est valide. Si cela échoue, il effectue une recherche linéaire de c
en utilisant std::begin
std::end
Et std::find
.
Cela vous donne de très bonnes performances sur une grande variété de conteneurs std
.
La seule chose qu'il ne supporte pas, c'est
if (7 *in* {1,2,3})
en tant qu'opérateurs (autres que =
) ne peuvent pas déduire les listes d'initialiseur, je crois. Vous pourriez obtenir
if (7 *in* il(1,2,3))
travailler.
Vous pouvez utiliser std :: find depuis <algorithm>. Mais cela ne fonctionne que pour les types de données tels que, std :: map, std :: vector, etc. Notez également que cela retournera, itérateur, au premier élément trouvé égal à la valeur que vous transmettez, contrairement à l'opérateur IN dans python qui renvoie vrai/faux.