Un de mes amis a posé cette question d'entrevue -
"Il y a un flux constant de nombres provenant d'une liste infinie de nombres dont vous avez besoin pour maintenir une infrastructure de données afin de renvoyer les 100 premiers nombres les plus élevés à tout moment donné. Supposons que tous les nombres sont uniquement des nombres entiers."
C'est simple, vous devez conserver une liste triée dans l'ordre décroissant et garder une trace du nombre le plus bas de cette liste. Si le nouveau nombre obtenu est supérieur à ce nombre le plus bas, vous devez supprimer ce nombre le plus bas et insérer le nouveau numéro dans la liste triée comme requis.
Puis la question a été étendue -
"Pouvez-vous vous assurer que l'ordre d'insertion doit être O (1)? Est-ce possible?"
Pour autant que je sache, même si vous ajoutez un nouveau numéro à la liste et le triez à nouveau à l'aide de n'importe quel algorithme de tri, ce serait au mieux O(logn) pour le tri rapide (je pense). Donc mon ami a dit que ce n'était pas possible, mais il n'était pas convaincu, il a demandé de maintenir une autre structure de données plutôt qu'une liste.
J'ai pensé à un arbre binaire équilibré, mais même là, vous n'obtiendrez pas l'insertion par ordre de 1. Donc, la même question que j'ai aussi maintenant. Je voulais savoir s'il existe une telle structure de données qui peut faire une insertion dans l'ordre de 1 pour le problème ci-dessus ou si ce n'est pas possible du tout.
Disons que k est le nombre de nombres les plus élevés que vous voulez connaître (100 dans votre exemple). Ensuite, vous pouvez ajouter un nouveau numéro dans O(k)
qui est également O(1)
. Parce que O(k*g) = O(g) if k is not zero and constant
.
Gardez la liste non triée. Déterminer s'il faut ou non insérer un nouveau numéro prendra plus de temps, mais insertion sera O (1).
C'est facile. La taille de la liste des constantes, donc le temps de tri de la liste est constant. Une opération qui s'exécute en temps constant est dite O (1). Par conséquent, le tri de la liste est O(1) pour une liste de taille fixe.
Une fois que vous avez passé 100 numéros, le coût maximum que vous encourrez pour le numéro suivant est le coût pour vérifier si le numéro est dans les 100 numéros les plus élevés (étiquetons cela CheckTime) plus le coût pour entrer dans cet ensemble et éjecter le plus bas (appelons cela EnterTime), qui est le temps constant (au moins pour les nombres bornés), ou O (1).
Worst = CheckTime + EnterTime
Ensuite, si la distribution des nombres est aléatoire, le coût moyen diminue d'autant plus que vous avez de nombres. Par exemple, la chance que vous aurez à entrer le 101e nombre dans l'ensemble maximum est 100/101, les chances pour le 1000e nombre seraient 1/10 et les chances pour le nième nombre seraient 100/n. Ainsi, notre équation pour le coût moyen sera:
Average = CheckTime + EnterTime / n
Ainsi, lorsque n approche de l'infini, seul CheckTime est important:
Average = CheckTime
Si les nombres sont liés, CheckTime est constant, et donc c'est O (1) time.
Si les nombres ne sont pas liés, le temps de vérification augmentera avec plus de nombres. En théorie, cela est dû au fait que si le plus petit nombre de l'ensemble maximal devient suffisamment grand, votre temps de vérification sera plus long car vous devrez prendre en compte plus de bits. Cela donne l'impression qu'il sera légèrement supérieur au temps constant. Cependant, vous pourriez également faire valoir que la chance que le nombre suivant se trouve dans l'ensemble le plus élevé approche de zéro lorsque n approche de l'infini et donc la chance que vous aurez besoin de considérer plus de bits approche également 0, ce qui serait un argument pour O (1) temps.
Je ne suis pas positif, mais mon instinct dit que c'est O (log (log (n))) temps. Cela est dû au fait que la probabilité que le nombre le plus faible augmente soit logarithmique et que le nombre de bits que vous devez prendre en compte pour chaque vérification est également logarithmique. Je suis intéressé par les autres peuples, car je ne suis pas vraiment sûr ...
celui-ci est facile si vous savez Binary Heap Trees . Les tas binaires soutiennent l'insertion en temps constant moyen, O (1). Et vous donne un accès facile aux premiers éléments x.
Si, par la question que l'enquêteur voulait vraiment poser "pouvons-nous nous assurer que chaque numéro entrant est traité en temps constant", alors comme beaucoup l'ont déjà souligné (par exemple, voir la réponse de @ duedl0r), la solution de votre ami est déjà O (1), et il en serait ainsi même s'il avait utilisé une liste non triée, ou utilisé un tri à bulles, ou quoi que ce soit d'autre. Dans ce cas, la question n'a pas beaucoup de sens, sauf si c'était une question délicate ou si vous vous en souvenez mal.
Je suppose que la question de l'intervieweur était significative, qu'il ne demandait pas comment faire quelque chose pour être O(1), ce qui est déjà très clairement cela.
Parce que la complexité de l'algorithme de remise en question n'a de sens que lorsque la taille de l'entrée augmente indéfiniment, et la seule entrée qui peut croître ici est 100 - la taille de la liste; Je suppose que la vraie question était "pouvons-nous nous assurer que nous obtenons les dépenses N les plus importantes O(1) fois par nombre (pas O(N) comme dans votre solution d'un ami), est-ce possible? ".
La première chose qui me vient à l'esprit est de compter le tri, ce qui entraînera une complexité de O(1) fois par numéro pour le problème Top-N au prix de l'utilisation de O(m) espace, où m est la longueur de la plage de nombres entrants. Alors oui, c'est possible.
Utilisez une file d'attente à priorité min implémentée avec un tas de Fibonacci , qui a un temps d'insertion constant:
1. Insert first 100 elements into PQ
2. loop forever
n = getNextNumber();
if n > PQ.findMin() then
PQ.deleteMin()
PQ.insert(n)
La tâche est clairement de trouver un algorithme qui est O(1) dans la longueur N de la liste de nombres requise. Donc peu importe si vous avez besoin des 100 premiers nombres ou 10000 nombres , le temps d'insertion doit être O (1).
L'astuce ici est que bien que cette exigence O(1) soit mentionnée pour l'insertion de la liste, la question n'a rien dit sur l'ordre du temps de recherche dans l'espace numérique entier, mais elle tourne cela peut aussi être fait O(1). La solution est alors la suivante:
Organisez une table de hachage avec des nombres pour les clés et des paires de pointeurs de liste liés pour les valeurs. Chaque paire de pointeurs est le début et la fin d'une séquence de liste liée. Ce ne sera normalement qu'un élément puis le suivant. Chaque élément de la liste chaînée va à côté de l'élément avec le numéro le plus élevé suivant. La liste chaînée contient ainsi la séquence triée des nombres requis. Conservez un enregistrement du nombre le plus bas.
Prenez un nouveau nombre x dans le flux aléatoire.
Est-il supérieur au dernier chiffre le plus bas enregistré? Oui => Étape 4, Non => Étape 2
Frappez la table de hachage avec le nombre qui vient d'être pris. Y a-t-il une entrée? Oui => Étape 5. Non => Prenez un nouveau nombre x-1 et répétez cette étape (il s'agit d'une simple recherche linéaire descendante, restez avec moi ici, cela peut être amélioré et je vais vous expliquer comment)
Avec l'élément de liste qui vient d'être obtenu à partir de la table de hachage, insérez le nouveau numéro juste après l'élément dans la liste liée (et mettez à jour le hachage)
Prenez le plus petit nombre l enregistré (et supprimez-le de la liste de hachage /).
Frappez la table de hachage avec le nombre qui vient d'être pris. Y a-t-il une entrée? Oui => Étape 8. Non => Prenez un nouveau nombre l + 1 et répétez cette étape (il s'agit d'une simple recherche linéaire ascendante)
Avec un résultat positif, le nombre devient le nouveau nombre le plus bas. Passez à l'étape 2
Pour autoriser les valeurs en double, le hachage a réellement besoin de conserver le début et la fin de la séquence de liste liée des éléments qui sont des doublons. L'ajout ou la suppression d'un élément à une clé donnée augmente ou diminue ainsi la plage pointée.
L'insert ici est O (1). Les recherches mentionnées sont, je suppose, quelque chose comme, O (différence moyenne entre les nombres). La différence moyenne augmente avec la taille de l'espace numérique, mais diminue avec la longueur requise de la liste de nombres.
Donc, la stratégie de recherche linéaire est assez mauvaise, si l'espace numérique est grand (par exemple pour un type entier à 4 octets, 0 à 2 ^ 32-1) et N = 100. Pour contourner ce problème de performances, vous pouvez conserver des ensembles parallèles de tables de hachage, où les nombres sont arrondis à des amplitudes plus élevées (par exemple, 1s, 10s, 100s, 1000s) pour créer des clés appropriées. De cette façon, vous pouvez monter et descendre les vitesses pour effectuer les recherches requises plus rapidement. La performance devient alors un O (log numberrange), je pense, qui est constant, c'est-à-dire O(1) également.
Pour rendre cela plus clair, imaginez que vous avez le numéro 197 à portée de main. Vous frappez la table de hachage 10s, avec '190', elle est arrondie à la dizaine la plus proche. N'importe quoi? Donc, vous descendez en 10 secondes jusqu'à ce que vous atteigniez disons 120. Ensuite, vous pouvez commencer à 129 dans la table de hachage 1s, puis essayez 128, 127 jusqu'à ce que vous frappiez quelque chose. Vous avez maintenant trouvé où dans la liste chaînée insérer le numéro 197. En le mettant, vous devez également mettre à jour la table de hachage 1s avec l'entrée 197, la table de hachage 10s avec le nombre 190, 100s avec 100, etc. vous devez faire ici sont 10 fois le journal de la plage de numéros.
Je me suis peut-être trompé sur certains détails, mais comme il s'agit de l'échange de programmeurs et que le contexte était des interviews, j'espère que ce qui précède est une réponse suffisamment convaincante pour cette situation.
EDIT J'ai ajouté quelques détails supplémentaires ici pour expliquer le schéma de hachage parallèle et comment cela signifie que les mauvaises recherches linéaires que j'ai mentionnées peuvent être remplacées par un O(1) recherche. J'ai également réalisé qu'il n'est bien sûr pas nécessaire de rechercher le numéro le plus bas suivant, car vous pouvez y accéder directement en regardant dans la table de hachage avec le numéro le plus bas et en passant au suivant élément.
Une centaine de nombres sont facilement stockés dans un tableau de taille 100. Tout arbre, liste ou ensemble est excessif, compte tenu de la tâche à accomplir.
Si le nombre entrant est supérieur au plus bas (= dernier) du tableau, exécutez toutes les entrées. Une fois que vous avez trouvé le premier qui est plus petit que votre nouveau numéro (vous pouvez utiliser des recherches sophistiquées pour le faire), parcourez le reste du tableau, en poussant chaque entrée "vers le bas" d'une unité.
Puisque vous conservez la liste triée depuis le début, vous n'avez pas besoin d'exécuter d'algorithme de tri du tout. C'est O (1).
Pouvons-nous supposer que les nombres sont d'un type de données fixe, tel que Integer? Si c'est le cas, conservez un décompte de chaque numéro ajouté. Il s'agit d'une opération O(1).
Code VB.Net:
Const Capacity As Integer = 100
Dim Tally(Integer.MaxValue) As Integer ' Assume all elements = 0
Do
Value = ReadValue()
If Tally(Value) < Capacity Then Tally(Value) += 1
Loop
Lorsque vous retournez la liste, vous pouvez prendre autant de temps que vous le souhaitez. Itérez simplement à la fin de la liste et créez une nouvelle liste des 100 valeurs les plus élevées enregistrées. Il s'agit d'une opération O(n), mais ce n'est pas pertinent.
Dim List(Capacity) As Integer
Dim ListCount As Integer = 0
Dim Value As Integer = Tally.Length - 1
Dim ValueCount As Integer = 0
Do Until ListCount = List.Length OrElse Value < 0
If Tally(Value) > ValueCount Then
List(ListCount) = Value
ValueCount += 1
ListCount += 1
Else
Value -= 1
ValueCount = 0
End If
Loop
Return List
Edit: En fait, peu importe si c'est un type de données fixe. Étant donné qu'aucune limite n'est imposée sur la consommation de mémoire (ou de disque dur), vous pouvez faire en sorte que cela fonctionne pour n'importe quelle plage d'entiers positifs.
Vous pouvez utiliser un Max-Heap binaire. Vous devriez garder une trace d'un pointeur sur le nœud minimum (qui pourrait être inconnu/nul).
Vous commencez par insérer les 100 premiers nombres dans le tas. Le max sera au top. Après cela, vous garderez toujours 100 numéros dedans.
Ensuite, lorsque vous obtenez un nouveau numéro:
if(minimumNode == null)
{
minimumNode = findMinimumNode();
}
if(newNumber > minimumNode.Value)
{
heap.Remove(minimumNode);
minimumNode = null;
heap.Insert(newNumber);
}
Malheureusement, findMinimumNode
est O (n), et vous encourez ce coût une fois par insertion (mais pas pendant l'insertion :). La suppression du noeud minimum et l'insertion du nouveau noeud sont, en moyenne, O(1) car elles tendent vers le bas du tas).
Dans l'autre sens, avec un min-tas binaire, le min est en haut, ce qui est idéal pour trouver le min à des fins de comparaison, mais c'est nul quand vous devez remplacer le minimum par un nouveau nombre qui est> min. C'est parce que vous devez supprimer le noeud min (toujours O(logN)) puis insérer le nouveau noeud (O moyen (1)). Donc, vous avez toujours O(logN) qui est meilleur que Max-Heap, mais pas O (1).
Bien sûr, si N est constant, alors vous avez toujours O (1). :)