STL fournit des fonctions de recherche binaire std :: lower_bound et std :: upper_bound, Mais j'ai tendance à ne pas les utiliser car je ne me souviens plus de ce qu'elles font, car leurs contrats me semblent totalement mystifiantes.
En regardant les noms, Je suppose que "lower_bound" pourrait être l'abréviation de "last lower bound",
c'est à dire. le dernier élément de la liste triée qui est <= la valeur donnée (le cas échéant).
De même, je suppose que "upper_bound" pourrait être un raccourci pour "first upper bound",
c'est à dire. le premier élément de la liste triée qui est> = le val donné (le cas échéant).
Mais la documentation indique qu'ils font quelque chose d'assez différent de cela -- Quelque chose qui semble être un mélange d'arrière-plans et aléatoire, pour moi .. Pour paraphraser le doc:
- lower_bound trouve le premier élément qui est> = val
- upper_bound trouve le premier élément qui est> val
Donc lower_bound ne trouve pas du tout une limite inférieure; il trouve le premier upper bound!? Et upper_bound trouve le premier strict upper bound.
Est-ce que cela a un sens ?? Comment vous en souvenez-vous?
Si vous avez plusieurs éléments dans la plage [first
, last
) dont la valeur est égale à la valeur val
que vous recherchez, la plage [l
, u
) où
l = std::lower_bound(first, last, val)
u = std::upper_bound(first, last, val)
est précisément la plage d'éléments égale à val
dans la plage [first
, last
). Donc l
et u
sont les "bornes inférieures" et "bornes supérieures" pour la plage equal. Cela a du sens si vous êtes habitué à penser en termes d'intervalles semi-ouverts.
(Notez que std::equal_range
renverra les bornes inférieure et supérieure d'une paire, en un seul appel.)
std::lower_bound
Renvoie un itérateur pointant vers le premier élément de la plage [premier, dernier] qui n'est pas inférieur à (c'est-à-dire supérieur ou égal à).
std::upper_bound
Renvoie un itérateur pointant sur le premier élément de la plage [premier, dernier) qui est supérieur à valeur.
Ainsi, en mélangeant les limites inférieure et supérieure, vous êtes en mesure de décrire exactement le début et la fin de votre plage.
Est-ce que cela a un sens ??
Oui.
Exemple:
imaginer un vecteur
std::vector<int> data = { 1, 1, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 6 };
auto lower = std::lower_bound(data.begin(), data.end(), 4);
1, 1, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 6
// ^ lower
auto upper = std::upper_bound(data.begin(), data.end(), 4);
1, 1, 2, 3, 3, 3, 3, 4, 4, 4, 5, 5, 6
// ^ upper
std::copy(lower, upper, std::ostream_iterator<int>(std::cout, " "));
tirages: 4 4 4
Dans ce cas, je pense qu'une image vaut mille mots. Supposons que nous les utilisions pour rechercher 2
dans les collections suivantes. Les flèches indiquent les itérateurs que les deux retourneraient:
Donc, si vous avez plusieurs objets avec cette valeur déjà présents dans la collection, lower_bound
vous donnera un itérateur qui fait référence au premier d'entre eux, et upper_bound
donnera un itérateur qui fait référence à l'objet immédiatement après le dernier de leur.
Ceci (entre autres choses) rend les itérateurs retournés utilisables en tant que paramètre hint
à insert
.
Par conséquent, si vous les utilisez comme indice, l'élément que vous insérez deviendra le premier premier élément avec cette valeur (si vous avez utilisé lower_bound
) ou le dernier élément avec cette valeur (si vous avez utilisé upper_bound
). Si la collection ne contenait pas d'élément avec cette valeur auparavant, vous aurez toujours un itérateur qui peut être utilisé en tant que hint
pour l'insérer à la bonne position dans la collection.
Bien sûr, vous pouvez également insérer sans indice, mais en utilisant un indice, vous avez la garantie que l'insertion se termine avec une complexité constante, à condition que le nouvel élément à insérer puisse être inséré immédiatement avant l'élément désigné par l'itérateur ces deux cas).
Considérez la séquence
1 2 3 4 5 6 6 6 7 8 9
la limite inférieure pour 6 correspond à la position du premier 6.
la limite supérieure pour 6 correspond à la position du 7.
ces positions servent de paire commune (début, fin) désignant la suite de 6 valeurs.
Exemple:
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
auto main()
-> int
{
vector<int> v = {1, 2, 3, 4, 5, 6, 6, 6, 7, 8, 9};
auto const pos1 = lower_bound( v.begin(), v.end(), 6 );
auto const pos2 = upper_bound( v.begin(), v.end(), 6 );
for( auto it = pos1; it != pos2; ++it )
{
cout << *it;
}
cout << endl;
}
J'acceptais la réponse de Brian, mais je venais de réaliser une autre manière utile de penser qui ajoute de la clarté pour moi, alors j'ajoute ceci pour référence.
Pensez à l'itérateur renvoyé comme pointant, non pas à l'élément * iter, mais simplement avant cet élément, c'est-à-dire entre cet élément et l'élément précédent de la liste, s'il en existe un. En pensant de cette façon, les contrats des deux fonctions deviennent symétriques: lower_bound trouve la position de la transition de <val à> = val, et upper_bound trouve la position de la transition de <= val à> val. En d'autres termes, lower_bound est le début de la plage d'éléments dont la comparaison est égale à val (c'est-à-dire la plage renvoyée par std :: equal_range), et upper_bound en est la fin.
Je souhaite qu'ils en parlent comme ceci (ou l'une des autres bonnes réponses données) dans la documentation; cela le rendrait beaucoup moins mystifiant!
le code source a en fait une deuxième explication que j'ai trouvée très utile pour comprendre le sens de la fonction:
lower_bound: Trouve la première position dans laquelle [val] pourrait être insérée sans changer l'ordre.
upper_bound: Trouve la dernier position dans laquelle [ val] pourrait être inséré sans modifier le classement.
ceci [premier, dernier] forme une plage dans laquelle le val peut être inséré tout en conservant la commande initiale du conteneur
lower_bound return "first" c'est-à-dire trouver la "limite inférieure de la plage"
upper_bound return "last" c'est-à-dire trouve la "limite supérieure de la plage"
Les deux fonctions sont très similaires, en ce sens qu'elles trouveront un point d'insertion dans une séquence triée qui préservera le tri. S'il n'y a aucun élément existant dans la séquence égal à l'élément de recherche, ils renverront le même itérateur.
Si vous essayez de trouver quelque chose dans la séquence, utilisez lower_bound
- il pointera directement sur l'élément s'il est trouvé.
Si vous insérez dans la séquence, utilisez upper_bound
- il conserve le classement d'origine des doublons.
Pour un tableau ou un vecteur:
std :: lower_bound: Retourne un itérateur pointant sur le premier élément de la plage
std :: upper_bound: Retourne un itérateur pointant sur le premier élément de la plage
inférieur à valeur. (pour tableau ou vecteur par ordre décroissant)
supérieur à valeur. (pour tableau ou vecteur en ordre croissant)
Oui. La question a absolument un point. Quand quelqu'un donnait son nom à ces fonctions, il ne pensait qu'à des tableaux triés contenant des éléments répétés. Si vous avez un tableau avec des éléments uniques, "std :: lower_bound ()" agit plutôt comme une recherche d'une "limite supérieure" à moins qu'il ne trouve l'élément réel.
Donc, voici ce dont je me souviens à propos de ces fonctions:
Si vous ne lisez pas le manuel après un mois ou deux depuis la dernière utilisation de ces fonctions, cela entraîne presque certainement un bogue.
Il existe déjà de bonnes réponses quant à ce que std::lower_bound
et std::upper_bound
is.
Je voudrais répondre à votre question 'comment se souvenir d'eux'?
Il est facile à comprendre/à retenir si nous établissons une analogie avec les méthodes STL begin()
et end()
de tout conteneur. begin()
renvoie l'itérateur de départ dans le conteneur, tandis que end()
renvoie l'itérateur situé juste en dehors du conteneur. Nous savons tous à quel point ils sont utiles lors d'une itération.
Désormais, sur un conteneur trié et une valeur donnée, lower_bound
et upper_bound
renverront une plage d'itérateurs pour cette valeur sur laquelle il est facile d'effectuer une itération (tout comme début et fin)
Utilisation pratique ::
Outre l'utilisation mentionnée ci-dessus sur la liste triée pour accéder à la plage d'une valeur donnée, l'une des meilleures applications de upper_bound est d'accéder aux données ayant une relation plusieurs-à-un dans une carte.
Par exemple, considérons la relation suivante: 1 -> a, 2 -> a, 3 -> a, 4 -> b, 5 -> c, 6 -> c, 7 -> c, 8 -> c , 9 -> c, 10 -> c
Les 10 mappages ci-dessus peuvent être sauvegardés dans la carte comme suit:
numeric_limits<T>::lowest() : UND
1 : a
4 : b
5 : c
11 : UND
Les valeurs peuvent être consultées par l'expression (--map.upper_bound(val))->second
.
Pour les valeurs de T allant du plus bas au 0, l'expression retournera UND
. Pour les valeurs de T allant de 1 à 3, elle retournera 'a' et ainsi de suite.
Imaginons maintenant que nous ayons des centaines de mappages de données avec une valeur et des centaines de mappages de ce type . Cette approche réduit la taille de la carte et la rend ainsi efficace.
Imaginez ce que vous feriez si vous vouliez trouver le premier élément égal à val
dans [first, last)
. Vous commencez par exclure des premiers éléments strictement inférieurs à val
, puis en arrière du dernier - 1 éléments strictement supérieurs à val
. Ensuite, la plage restante est [lower_bound, upper_bound]