Comment trouver l'élément central de la liste chaînée en un seul passage?

12

L'une des questions les plus populaires concernant les structures de données et l'algorithme, principalement posée lors d'un entretien téléphonique.

Gilles 'SO- arrête d'être méchant'
la source
5
Si les réponses présentées dans ce fil sont celles attendues par l'intervieweur, cette question ne teste pas la capacité technique, mais la capacité du candidat à esquiver comme un avocat.
G. Bach
2
Il s'agit d'une terrible question d'entrevue car elle dépend de façon critique du terme " passe " qui est vague, ambigu, subjectif. Presque toutes les bonnes réponses à cette question impliquent d'abuser de la définition afin que vous puissiez l'ignorer efficacement.
RBarryYoung
2
Eh bien, la question soulève beaucoup de discussions ici. Cela signifie que c'est une bonne question d'entrevue sur un point: cela vous fait réfléchir.
Hendrik Jan
3
Je veux donc répondre "lire tous les éléments dans un vecteur, puis accéder à l'élément à la taille de position () / 2"
Cort Ammon
1
@HendrikJan En fait, je pense que c'est une question d'entrevue complètement terrible. Premièrement, il est susceptible de conduire à des discussions sur ce que signifie exactement "en un seul passage", plutôt qu'à une discussion productive. Deuxièmement, un candidat pourrait trouver la réponse «correcte», puis la rejeter parce qu'il pensait qu'elle violait le critère «en une seule passe». Troisièmement, parce que c'est une question bien connue, c'est un meilleur test de "Connaissez-vous les questions d'entrevue populaires?" que "Êtes-vous un bon candidat pour ce travail?" N'importe lequel de ces éléments devrait suffire à poser cette question; les trois à la fois sont un désastre.
David Richerby

Réponses:

24

En trichant, et en faisant deux passes en même temps, en parallèle. Mais je ne sais pas si les recruteurs vont aimer ça.

Peut être fait sur une seule liste chaînée, avec une belle astuce. Deux pointeurs parcourent la liste, un à double vitesse. Lorsque le rapide atteint la fin, l'autre est à mi-chemin.

Hendrik Jan
la source
1
Ouais, on ne sait pas si c'est un seul passage. La question n'est pas claire sur ce point.
David Richerby
2
À propos, cela est lié au casse-tête où vous avez deux bougies, une heure allumée chacune, et vous êtes invité à mesurer 45 minutes.
Hendrik Jan
2
Est-ce vraiment différent d'itérer la liste, de compter les éléments, puis d'itérer une deuxième fois à mi-chemin? Tout ce qui est différent, c'est lorsque vous répétez la moitié supplémentaire. Comme @RBarryYoung le mentionne sur l'autre réponse similaire, ce n'est vraiment pas une seule passe, c'est une passe et demie.
2
Si la liste est longue, déplacer les deux pointeurs "en parallèle" entraînera moins de ratés de cache que d'itérer une seconde fois depuis le début.
zwol
2
Il utilise le même principe que l' algorithme de la tortue et du lièvre pour la détection de cycle.
Joshua Taylor
7

S'il ne s'agit pas d'une liste doublement liée, vous pouvez simplement compter et utiliser une liste, mais cela nécessite de doubler votre mémoire dans le pire des cas, et cela ne fonctionnera tout simplement pas si la liste est trop grande pour être stockée en mémoire.

Une solution simple, presque stupide, consiste simplement à incrémenter le nœud du milieu tous les deux nœuds

function middle(start) {
    var middle = start
    var nextnode = start
    var do_increment = false;
    while (nextnode.next != null) {
        if (do_increment) {
             middle = middle.next;
        }
        do_increment = !do_increment;
        nextnode = nextnode.next;
    }
    return middle;
}
00500005
la source
Votre deuxième option est la bonne réponse (à mon humble avis bien sûr).
Matthew Crumley
1
Cela fait en fait 1 1/2 passes sur la liste chaînée.
RBarryYoung
6

Élaboration de la réponse d'Hendrik

S'il s'agit d'une liste doublement liée, parcourez les deux extrémités

function middle(start, end) {
  do_advance_start = false;
  while(start !== end && start && end) {
     if (do_advance_start) {
        start = start.next
     }
     else {
        end = end.prev
     }
     do_advance_start = !do_advance_start
  }
  return (start === end) ? start : null;
}

Donné [1, 2, 3] => 2

1, 3
1, 2
2, 2

Donné [1, 2] => 1

1, 2
1, 1

Donné [1] => 1

Donné [] => null

00500005
la source
Comment est-ce efficace? Vous répétez également n fois et non n / 2.
Karan Khanna
3

Créez une structure avec un pointeur capable de pointer vers les nœuds de la liste liée et avec une variable entière qui tient compte du nombre de nœuds dans la liste.

struct LL{
    struct node *ptr;
    int         count;
}start;

Maintenant, stockez l'adresse du premier nœud de la liste liée dans et initialisez . Assurez-vous que la valeur de est incrémentée de un après la création réussie d'un nouveau noeud dans la liste liée. De même, décrémentez-le de un chaque fois qu'un nœud est supprimé de la liste chaînée.s t a r t . c o u n t = 1 s t a r t . c o u n tstart.ptrstart.count=1
start.count

Utilisez pour trouver l'élément central en une seule passe.start.count

Prateek
la source
-2

Créez un tableau dynamique, où chaque élément du tableau est un pointeur vers chaque nœud de la liste dans l'ordre de déplacement, en commençant par le début. Créez un entier, initialisé à 1, qui garde une trace du nombre de nœuds que vous avez visités (qui augmente chaque fois que vous accédez à un nouveau nœud). Lorsque vous arrivez à la fin, vous savez quelle est la taille de la liste et vous disposez d'un tableau ordonné de pointeurs vers chaque nœud. Enfin, divisez la taille de la liste par 2 (et soustrayez 1 pour l'indexation basée sur 0) et récupérez le pointeur contenu dans cet index du tableau; si la taille de la liste est impaire, vous pouvez choisir quel élément renvoyer (je retournerai quand même le premier).

Voici du code Java qui fait passer le message (même si l'idée d'un tableau dynamique sera un peu chancelante). Je fournirais C / C ++ mais je suis très rouillé dans ce domaine.

public Node getMiddleNode(List<Node> nodes){

    int size = 1;
    //add code to dynamically increase size if at capacity after adding
    Node[] pointers = new Node[10];

    for (int i = 0; i < nodes.size(); i++){
        //remember to dynamically allocate more space if needed
        pointers[i] = nodes.get(i);
        size++;
    }

    return pointers[(size - 1)/2];

}
Andonaeus
la source
-3

La récursivité est-elle considérée comme plus d'un passage?

Parcourez la liste jusqu'à la fin, en passant un nombre entier par référence. Faites une copie locale de cette valeur à chaque niveau pour référence ultérieure et incrémentez le nombre de références lors du prochain appel.

Sur le dernier nœud, divisez le nombre par deux et tronquez / floor () le résultat (si vous voulez que le premier nœud soit le "milieu" quand il n'y a que deux éléments) ou arrondissez (si vous voulez que le deuxième nœud soit le milieu"). Utilisez de manière appropriée un index à zéro ou à un.

En déroulant, faites correspondre le nombre de références à la copie locale (qui est le numéro du nœud). S'il est égal, renvoyez ce nœud; sinon retourne le nœud renvoyé par l'appel récursif.
.

Il existe d'autres façons de procéder; certains d'entre eux peuvent être moins encombrants (je pensais avoir vu quelqu'un dire de le lire dans un tableau et d'utiliser la longueur du tableau pour déterminer le milieu - bravo). Mais franchement, il n'y a pas de bonnes réponses, car c'est une question d'entrevue stupide. Numéro un, qui utilise toujours des listes chaînées ( opinion à l'appui ); Deuxièmement, trouver le nœud central est un exercice académique arbitraire sans valeur dans des scénarios réels; Troisièmement, si j'avais vraiment besoin de connaître le nœud intermédiaire, ma liste chaînée exposerait un nombre de nœuds. Il est plus facile de conserver cette propriété que de perdre du temps à parcourir toute la liste chaque fois que je veux le nœud central. Et enfin, quatre, chaque enquêteur va aimer ou rejeter des réponses différentes - ce qu'un enquêteur pense être lisse, un autre dira ridicule.

Je réponds presque toujours aux questions d'entrevue avec plus de questions. Si je reçois une question comme celle-ci (je n'en ai jamais), je demanderais (1) Que stockez-vous dans cette liste chaînée, et existe-t-il une structure plus appropriée pour accéder efficacement au nœud intermédiaire s'il y a vraiment un besoin de le faire ; (2) Quelles sont mes contraintes? Je peux le rendre plus rapide si la mémoire n'est pas un problème (par exemple la réponse du tableau), mais si l'intervieweur pense que le gonflement de la mémoire est un gaspillage, je serai sonné. (3) Dans quelle langue vais-je développer? Presque toutes les langues modernes que je connais ont des classes intégrées pour traiter les listes liées qui rendent la traversée de la liste inutile - pourquoi réinventer quelque chose qui a été réglé pour l'efficacité par les développeurs du langage?

James K
la source
6
Vous ne savez pas qui a voté contre quoi donc votre conclusion selon laquelle une personne a voté contre tout peut être vraie ou non, mais cela n'a certainement aucun fondement dans les faits à votre disposition. Mais je dévalise votre réponse parce que nous cherchons des explications, pas des tas de code.
David Richerby
C'est peut-être une hypothèse, mais quand je regarde un moment et moins de 30 secondes plus tard, chaque message a exactement -1, ce n'est pas déraisonnable. Même s'il s'agissait de 5 ou 6 personnes différentes, aucune n'a laissé de commentaire pourquoi. Mais merci d'avoir au moins indiqué une raison. Je ne comprends pas pourquoi une explication verbeuse est meilleure que le code - oui, c'est pour une interview téléphonique, mais je ne donne pas au OP une réponse en conserve pour régurgiter, je lui montre un moyen de le faire. IE Je pense que voter contre un article parce qu'il a du code n'est pas demandé, mais merci d'avoir dit au moins pourquoi vous l'avez fait.
James K
Bon point - il ne m'était pas venu à l'esprit que vous pouviez connaître le moment des votes (le fait de charger la page pendant leur déroulement est, je pense, le seul moyen pour les utilisateurs ordinaires comme nous de le savoir). Nous avons quelques méta-messages sur les raisons pour lesquelles nous essayons d'éviter le code réel ici: (1) (2) .
David Richerby
Merci pour les méta liens. J'ai lu la FAQ, mais je n'y ai rien vu - pas que j'aurais remarqué quelque chose comme ça, probablement pas quelque chose à quoi je m'attendais. Mais je n'ai rien trouvé non plus quand j'ai revérifié après m'être fait piquer. Je l'oublie peut-être encore, mais j'ai regardé. Le raisonnement dans les méta-messages est logique; Merci pour la réponse.
James K
-5

En utilisant 2 pointeurs. Incrémentez un à chaque itération et l'autre à chaque seconde itération. Lorsque le premier pointeur pointe vers la fin de la liste chaînée, le second pointeur pointe vers le mode intermédiaire de la liste chaînée.

b dharuni
la source
Cela reproduit simplement la réponse d'Hendrick . Veuillez ne pas répondre, sauf si vous avez quelque chose de nouveau à dire.
David Richerby