J'ai déjà jeté un coup d'œil à question qui parle d'algorithme pour trouver une boucle dans une liste chaînée. J'ai lu l'algorithme de recherche de cycle de Floyd solution, mentionnée à beaucoup d'endroits où nous devons prendre deux pointeurs. Un pointeur (plus lent/tortue) est augmenté de un et l'autre pointeur (plus rapide/lièvre) est augmenté de 2. Quand ils sont égaux, nous trouvons la boucle et si le pointeur plus rapide atteint null, il n'y a pas de boucle dans la liste chaînée.
Maintenant, ma question est pourquoi nous augmentons le pointeur plus rapidement de 2. Pourquoi pas autre chose? Augmenter de 2 est nécessaire ou on peut l'augmenter de X pour obtenir le résultat. Est-il nécessaire que nous trouvions une boucle si nous incrémentons le pointeur plus rapidement de 2 ou il peut y avoir le cas où nous devons incrémenter de 3 ou 5 ou x.
Il n'y a aucune raison d'utiliser le chiffre deux. Tout choix de taille de pas fonctionnera (sauf un, bien sûr).
Pour voir pourquoi cela fonctionne, regardons pourquoi l'algorithme de Floyd fonctionne en premier lieu. L'idée est de penser à la séquence x, X1, X2, ..., Xn, ... des éléments de la liste chaînée que vous visiterez si vous commencez au début de la liste et continuez à la parcourir jusqu'à la fin. Si la liste ne contient pas de cycle, toutes ces valeurs sont distinctes. Si elle contient un cycle, cette séquence se répétera sans fin.
Voici le théorème qui fait fonctionner l'algorithme de Floyd:
La liste chaînée contient un cycle si et seulement s'il y a un entier positif j tel que pour tout entier positif k, xj = xjk.
Allons le prouver; ce n'est pas si dur. Pour le cas "si", si un tel j existe, choisissez k = 2. Ensuite, nous avons cela pour certains j positifs, xj = x2j et j ≠ 2j, et donc la liste contient un cycle.
Pour l'autre sens, supposons que la liste contienne un cycle de longueur l commençant à la position s. Soit j le plus petit multiple de l supérieur à s. Alors pour tout k, si l'on considère xj et xjk, puisque j est un multiple de la longueur de boucle, on peut penser à xjk comme l'élément formé en commençant à la position j dans la liste, puis en effectuant j pas k-1 fois. Mais à chaque fois que vous effectuez j étapes, vous vous retrouvez à l'endroit où vous avez commencé dans la liste car j est un multiple de la longueur de la boucle. Par conséquent, xj = xjk.
Cette preuve vous garantit que si vous effectuez un nombre constant d'étapes à chaque itération, vous atteindrez en effet le pointeur lent. Plus précisément, si vous effectuez k étapes à chaque itération, vous finirez par trouver les points xj et xkj et détectera le cycle. Intuitivement, les utilisateurs ont tendance à choisir k = 2 pour minimiser le temps d'exécution, car vous effectuez le moins d'étapes à chaque itération.
Nous pouvons analyser le runtime de manière plus formelle comme suit. Si la liste ne contient pas de cycle, le pointeur rapide atteindra la fin de la liste après n étapes pendant O(n) fois, où n est le nombre d'éléments dans la liste. Sinon, les deux pointeurs se rencontreront une fois que le pointeur lent aura effectué j pas. Rappelez-vous que j est le plus petit multiple de l supérieur à s. Si s ≤ l, alors j = l; sinon si s> l, alors j sera à la plupart des 2s, et donc la valeur de j est O (s + l). Puisque l et s ne peuvent pas être supérieurs au nombre d'éléments dans la liste, cela signifie que j = O (n). Cependant, après le pointeur lent a fait j pas, le pointeur rapide aura fait k pas pour chacune des j pas pris par le pointeur plus lent donc il aura fait O(kj) pas. Puisque j = O (n ), le temps d'exécution net est au maximum de O (nk). Notez que cela signifie que plus nous prenons de mesures avec le pointeur rapide, plus l'algorithme prend du temps pour se terminer (bien que de manière proportionnelle). Choisir k = 2 minimise ainsi le total exécution de l'algorithme.
J'espère que cela t'aides!
Supposons que la longueur de la liste qui ne contient pas la boucle soit s
, la longueur de la boucle soit t
et le rapport de fast_pointer_speed
À slow_pointer_speed
être k
.
Laissez les deux pointeurs se rencontrer à une distance j
du début de la boucle.
Ainsi, le pointeur lent de distance parcourt = s + j
. Distance parcourue par le pointeur rapide = s + j + m * t
(Où m
est le nombre de fois que le pointeur rapide a terminé la boucle). Mais, le pointeur rapide aurait également parcouru une distance k * (s + j)
(k
fois la distance du pointeur lent).
Par conséquent, nous obtenons k * (s + j) = s + j + m * t
.
s + j = (m / k-1)t
.
Par conséquent, d'après l'équation ci-dessus, la longueur parcourue par le pointeur lent est un multiple entier de la longueur de la boucle.
Pour une meilleure efficacité, (m / k-1) = 1
(Le pointeur lent ne devrait pas avoir parcouru la boucle plus d'une fois.)
par conséquent, m = k - 1 => k = m + 1
Puisque m
est le nombre de fois où le pointeur rapide a terminé la boucle, m >= 1
. Pour une efficacité optimale, m = 1
.
donc k = 2
.
si nous prenons une valeur de k > 2
, plus la distance que les deux pointeurs devraient parcourir.
J'espère que l'explication ci-dessus vous aidera.
Considérons un cycle de taille L, ce qui signifie qu'au kième élément se trouve la boucle: xk -> xk + 1 -> ... -> xk + L-1 -> xk. Supposons qu'un pointeur soit exécuté au taux r1= 1 et l'autre à r2. Lorsque le premier pointeur atteint xk le deuxième pointeur sera déjà dans la boucle à un élément xk + s où 0 <= s <L. Après m, le pointeur supplémentaire incrémente le premier pointeur à xk + (m mod L) et le deuxième pointeur est à xk + ((m * r2+ s) mod L). Par conséquent, la condition que les deux pointeurs entrent en collision peut être formulée comme l'existence d'un m satisfaisant la congruence
m = m * r2 + s (mod L)
Cela peut être simplifié avec les étapes suivantes
m (1-r2) = s (mod L)
m (L + 1-r2) = s (mod L)
Il s'agit de la forme d'une congruence linéaire. Il a une solution m si s est divisible par pgcd (L + 1-r2, L). Ce sera certainement le cas si gcd (L + 1-r2, L) = 1. Si r2= 2 puis pgcd (L + 1-r2, L) = gcd (L-1, L) = 1 et une solution m existe toujours.
Ainsi r2= 2 a la bonne propriété que pour toute taille de cycle L, il satisfait gcd (L + 1-r2, L) = 1 et garantit ainsi que les pointeurs finiront par entrer en collision même si les deux pointeurs commencent à des emplacements différents. Autres valeurs de r2 n'ont pas cette propriété.
Si le pointeur rapide déplace les étapes 3
Et le pointeur lent à l'étape 1
, Il n'est pas garanti que les deux pointeurs se rencontrent dans des cycles contenant un nombre pair de nœuds. Cependant, si le pointeur lent se déplaçait à 2
, La réunion serait garantie.
En général, si le lièvre se déplace à H
étapes, et la tortue se déplace à T
étapes, vous êtes assuré de vous rencontrer dans un cycle ssi H = T + 1
. =
Considérez le lièvre se déplaçant par rapport à la tortue.
H - T
Nœuds par itération.Étant donné un cycle de longueur N =(H - T) * k
, où k
est n'importe quel entier positif, le lièvre sauterait tous les nœuds H - T - 1
(Encore une fois, par rapport à la tortue), et ce serait impossible pour eux de se rencontrer si la tortue était dans l'un de ces nœuds.
La seule possibilité où une réunion est garantie est quand H - T - 1 = 0
.
Par conséquent, l'augmentation du pointeur rapide de x
est autorisée, tant que le pointeur lent est augmenté de x - 1
.
S'il y a une boucle (de n nœuds), alors une fois qu'un pointeur est entré dans la boucle, il y restera pour toujours; afin que nous puissions avancer dans le temps jusqu'à ce que les deux pointeurs soient dans la boucle. A partir de là, les pointeurs peuvent être représentés par des entiers modulo n avec les valeurs initiales a et b. La condition pour qu'ils se réunissent après t étapes est alors
a + t≡b + 2t mod n qui a la solution t = a − b mod n.
Cela fonctionnera tant que la différence entre les vitesses ne partage aucun facteur premier avec n.
La seule restriction sur les vitesses est que leur différence doit être co-amorcée avec la longueur de la boucle.